A lot of things.

This commit is contained in:
2026-05-27 21:24:38 +02:00
parent b361f07954
commit d02baa89d6
43 changed files with 1340 additions and 231 deletions

View File

@@ -4,9 +4,12 @@ namespace App\Auth;
use App\Services\XenforoService;
use App\XenForoDataTypes\XenForoData;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable;
class XenForoUser extends XenForoData implements Authenticatable {
class XenForoUser extends XenForoData implements Authenticatable, Authorizable {
use \Illuminate\Foundation\Auth\Access\Authorizable;
public ?array $permissions = null;
public function getAuthIdentifierName(): string
@@ -64,7 +67,7 @@ class XenForoUser extends XenForoData implements Authenticatable {
return null;
}
public function can(string $permissionGroup, string $permissionName): bool
public function _can(string $permissionGroup, string $permissionName): bool
{
if( !$this->permissions ){
$this->permissions = $this->services->getPermissions($this->data->user_id, $this->data->permission_combination_id);

View File

@@ -2,6 +2,9 @@
namespace App\Helpers;
use App\Models\Entry;
use App\Services\XenforoApiService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class EntryHelpers {
@@ -56,4 +59,33 @@ class EntryHelpers {
default => $fields['entry_title'],
};
}
public static function getLatestComments(Entry $entry, int $limit = 20): array {
if( !$entry->comments_thread_id ){
return [];
}
$cacheKey = "entry_comments_{$entry->id}";
return Cache::remember($cacheKey, now()->addDays(1), function () use ($entry, $limit) {
$service = app(XenforoApiService::class);
// Get thread infos and pagination.
$paginationInfos = $service->getThreadPosts($entry->comments_thread_id, 1);
$lastPage = $paginationInfos['pagination']['last_page'] ?? 1;
// Get last threads
$lastPageData = $lastPage > 1 ? $service->getThreadPosts($entry->comments_thread_id, $lastPage) : $paginationInfos;
$posts = $lastPageData['posts'] ?? [];
if( count( $posts ) < $limit && $lastPage > 1 ){
$previousPageData = $service->getThreadPosts($entry->comments_thread_id, $lastPage - 1 );
$posts = array_merge( $posts, $previousPageData['posts'] ?? [] );
}
return collect( $posts )->slice(-$limit)->reverse()->values()->toArray();
});
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Helpers\EntryHelpers;
use App\Models\Entry;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -25,7 +26,9 @@ class EntryController extends Controller
if( $entry->type !== $section )
abort(404);
return view('entries.show', compact('entry', 'section'));
$comments = EntryHelpers::getLatestComments( $entry );
return view('entries.show', compact('entry', 'section', 'comments' ) );
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Models\Entry;
use Illuminate\Http\Request;
class RedirectController extends Controller
{
public function entryReportRedirect( Request $request )
{
$id = $request->input('id');
$entry = Entry::findOrFail($id);
return redirect()->route('entries.show', ['section' => $entry->type, 'entry' => $entry])->with('success', "Your report has been sent.");
}
}

View File

@@ -102,164 +102,19 @@ class SubmissionController extends Controller
public function update(StoreEntryRequest $request, string $section, Entry $entry)
{
if( $entry->type !== $section ) {
abort(404);
try {
$entry = $this->services->editEntry($request, $section, $entry);
return match ($entry->state) {
'published' => redirect()->route('entries.show', ['section' => $section, 'entry' => $entry->slug])->with('success', "Your entry has been published."),
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
};
} catch ( SubmissionException $e ) {
return back()->withInput()->withErrors(['error' => $e->getMessage()]);
} catch ( \Exception $e ) {
return back()->withInput()->withErrors(['error' => 'Unknown error: '.$e->getMessage()]);
}
$gameId = null;
if( !$request->input('game_id') ){
if( $request->input('new-game-title') && $request->input('new-game-platform') && $request->input('new-game-genre') ){
$platform = Platform::find($request->input('new-game-platform'));
$genre = Genre::find($request->input('new-game-genre'));
$game = Game::create([
'name' => $request->input('new-game-title'),
'slug' => Str::slug($request->input('new-game-title')),
'platform_id' => $platform->id,
'genre_id' => $genre->id,
]);
$gameId = $game->id;
}
} else {
$gameId = $request->input('game_id');
}
$mainImage = $entry->main_image;
if ( $request->hasFile('main-image') ) {
if ( $mainImage ) {
Storage::disk('public')->delete($mainImage);
}
$mainImage = $request->file('main-image')->store('entries/main_images', 'public');
} elseif ( $request->input('remove_main_image') === '1' ) {
if ( $mainImage ) {
Storage::disk('public')->delete($mainImage);
}
$mainImage = null;
}
$staffCredits = collect($request->input('credits', []))
->filter(fn($item) => isset($item['name']) || isset($item['description']))
->map(function ($item) {
$name = trim($item['name'] ?? '');
$description = trim($item['description'] ?? '');
if ($name === '' && $description === '') {
return null;
}
return trim($name . ($name !== '' && $description !== '' ? ' — ' : '') . $description);
})
->filter()
->implode("\n");
$fields = [
'type' => $section,
'title' => $request->input('entry_title'),
'slug' => $request->input('slug') ?? Str::slug($request->input('entry_title', '')),
'description' => $request->input('description'),
'main_image' => $mainImage,
'state' => $request->input('submit-state', 'draft'),
'game_id' => $gameId,
'status_id' => $request->input('status'),
'version' => $request->input('version'),
'release_date' => $request->input('release-date'),
'staff_credits' => $staffCredits ?: null,
'relevant_link' => $request->input('release_site'),
'youtube_link' => $request->input('youtube_video'),
];
$entry->update($fields);
$entry->hashes()->delete();
foreach ( $request->input('hashes', []) as $hash ) {
if( !isset($hash['filename'], $hash['crc32'], $hash['sha1'], $hash['verified']) ) {
continue;
}
EntryHash::create([
'entry_id' => $entry->id,
'filename' => $hash['filename'],
'hash_crc32' => $hash['crc32'],
'hash_sha1' => $hash['sha1'],
'verified' => $hash['verified'],
]);
}
$authorIds = [];
foreach ( $request->input('authors', []) as $authorId ) {
$author = Author::find($authorId);
if( $author ) {
$authorIds[] = $author->id;
}
}
foreach( $request->input('new-authors', []) as $authorName ) {
$authorName = trim($authorName);
if ($authorName === '') continue;
$author = Author::firstOrCreate(
['slug' => Str::slug($authorName)],
['name' => $authorName],
);
$authorIds[] = $author->id;
}
$entry->authors()->sync(array_values(array_unique($authorIds)));
if( section_must_be( 'romhacks', $section ) ){
$entry->modifications()->sync($request->input('modifications', []));
} else {
$entry->modifications()->sync([]);
}
$entry->languages()->sync($request->input('languages', []));
$existingFileUuids = $request->input('existing_file_ids', []);
if (!is_array($existingFileUuids)) {
$existingFileUuids = [];
}
$entry->files()->whereNotIn('file_uuid', $existingFileUuids)->delete();
foreach ( $request->input('file_ids', []) as $file_uuid ) {
$fileData = Cache::pull("uploaded_file_{$file_uuid}");
if( ! $fileData ) {
continue;
}
EntryFile::create([
'entry_id' => $entry->id,
'file_uuid' => $fileData['uuid'],
'filename' => $fileData['filename'],
'filepath' => $fileData['filepath'],
'favorite_server' => $fileData['favorite_server'],
'favorite_at' => \DateTimeImmutable::createFromTimestamp( $fileData['favorite_at'] ),
'filesize' => $fileData['filesize'],
'state' => 'public'
]);
}
$existingGalleryIds = $request->input('existing_gallery_ids', []);
if (!is_array($existingGalleryIds)) {
$existingGalleryIds = [];
}
$entry->gallery()->whereNotIn('id', $existingGalleryIds)->get()->each(function ($gallery) {
if ($gallery->image) {
Storage::disk('public')->delete($gallery->image);
}
$gallery->delete();
});
foreach ( $request->file('gallery', [] ) as $galleryFile ){
if( !$galleryFile->isValid() ){
continue;
}
$path = $galleryFile->store('entries/gallery/' . $entry->id, 'public');
EntryGallery::create([
'entry_id' => $entry->id,
'image' => $path
]);
}
return match( $entry->state ){
'published' => redirect()->route('entries.show', [ 'section' => $section, 'entry' => $entry->slug ])->with('success', "Your entry has been published."),
'pending' => redirect()->route('home')->with('success', "Your entry has been submitted and is pending review."),
default => redirect()->route('home')->with('success', "Your entry has been saved as a draft.")
};
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Entry;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class WebhookController extends Controller
{
public function XenForoNewPost(Request $request)
{
if( $request->header('XF-Webhook-Secret') !== env('WEBHOOK_SECRET') )
abort(403);
$threadId = $request->input('data.thread_id');
if( $threadId ){
$entry = Entry::where('comments_thread_id', $threadId)->first();
if( $entry ){
Cache::forget("entry_comments_{$entry->id}");
}
}
return response()->json(['success' => true]);
}
}

View File

@@ -24,7 +24,7 @@ class CheckXenForoPermissions
foreach ($permissions as $permissionStr) {
[$group, $permission] = explode('.', $permissionStr);
if( !\Auth::user()->can($group, $permission) )
if( !\Auth::user()->_can($group, $permission) )
return $this->deny($request, $permission);
}

View File

@@ -14,8 +14,11 @@ class StoreEntryRequest extends FormRequest
*/
public function authorize(): bool
{
// TODO: Change it by role.
return true;
$entry = $this->route('entry');
if( $entry )
return $this->user()->can('update', $entry);
return $this->user()->can('create', '\App\Models\Entry');
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests;
use App\Services\TemporaryFileService;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
@@ -12,7 +13,7 @@ class TemporaryFileUploadRequest extends FormRequest
*/
public function authorize(): bool
{
return true;
return $this->user()->can('create', TemporaryFileService::class );
}
/**

View File

@@ -32,6 +32,7 @@ class Entry extends Model
'youtube_link',
'user_id',
'complete_title',
'comments_thread_id',
];
/**

View File

@@ -3,6 +3,8 @@
namespace App\Providers;
use App\Auth\XenForoGuard;
use App\Policies\TempFilePolicy;
use App\Services\TemporaryFileService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -24,5 +26,7 @@ class AppServiceProvider extends ServiceProvider
\Auth::extend('xenforo', function ($app, $name, array $config) {
return new XenForoGuard($app['request']);
});
\Gate::policy(TemporaryFileService::class, TempFilePolicy::class );
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services;
use App\Exceptions\SubmissionException;
use App\Helpers\EntryHelpers;
use App\Http\Requests\StoreEntryRequest;
use App\Jobs\CreateXenForoCommentsThread;
use App\Models\Author;
use App\Models\Entry;
use App\Models\EntryFile;
@@ -36,6 +37,12 @@ class SubmissionsService {
*/
private ?string $section = null;
/**
* Entry for edit.
* @var Entry|null
*/
private ?Entry $entry = null;
/**
* @return list<FSFileData>
*/
@@ -174,6 +181,9 @@ class SubmissionsService {
$this->Step12b_MoveMainImage( $entry );
$this->Step12c_SaveGalleryImages( $entry );
// Step 13: Try to create the comments section.
$this->Step13_CreateCommentsThread( $entry );
return $entry;
}
@@ -190,6 +200,13 @@ class SubmissionsService {
return $this->request->input('game_id');
// Need to create a game.
$game = $this->createGameFromFormFields();
return $game->id;
}
private function createGameFromFormFields(): Game
{
if( !$this->request->input('new-game-title') || !$this->request->input('new-game-platform') || !$this->request->input('new-game-genre') )
throw new SubmissionException( "New game informations is missing" );
@@ -201,14 +218,12 @@ class SubmissionsService {
$gameSlug = EntryHelpers::uniqueSlug( $this->request->input('new-game-title'), Game::class );
$game = Game::create([
return Game::create([
'name' => trim( $this->request->input('new-game-title') ),
'slug' => $gameSlug,
'platform_id' => $platform->id,
'genre_id' => $genre->id,
]);
return $game->id;
}
/**
@@ -228,7 +243,7 @@ class SubmissionsService {
if( section_must_be( 'translations', $this->section ) ) {
$fields['languages_string'] = Language::whereIn('id', $this->request->input('languages', []))->pluck('name')->implode(', ');
}
if( section_must_be(['romhacks', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) {
if( section_must_be(['romhacks', 'translations', 'homebrew', 'lua-scripts', 'tutorials'], $this->section ) ) {
// TODO: Add single platform ID compatibility.
$fields['platform_name'] = Game::find( $gameId )->platform->name;
}
@@ -242,12 +257,15 @@ class SubmissionsService {
* @return void
* @throws SubmissionException
*/
private function Step7_SaveEntryFiles( int $entryId ): void
private function Step7_SaveEntryFiles( int $entryId, ?array $uuidData = null ): void
{
foreach ( $this->request->input('files_uuid', [] ) as $uuid ) {
if( !$uuidData )
$uuidData = $this->request->input('files_uuid', [] );
foreach ( $uuidData as $uuid ) {
$fileData = Cache::pull("uploaded_file_{$uuid}");
if( !$fileData )
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry." );
throw new SubmissionException( "File {$uuid} has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry." );
EntryFile::create([
'entry_id' => $entryId,
@@ -293,6 +311,8 @@ class SubmissionsService {
*/
private function Step9_SaveAuthors( Entry $entry ): void
{
// TODO: Code fragment to be replaced by edit version.
// Existing authors.
foreach ( $this->request->input('authors', [] ) as $authorId ) {
$author = Author::find( $authorId );
@@ -323,6 +343,9 @@ class SubmissionsService {
*/
private function Step10_SaveRomhacksModifications( Entry $entry ): void
{
// TODO: Replace by edit version
foreach ( $this->request->input('modifications', [] ) as $modificationId ) {
$modification = Modification::find( $modificationId );
if( !$modification )
@@ -339,6 +362,8 @@ class SubmissionsService {
*/
private function Step11_SaveLanguages( Entry $entry ): void
{
// TODO: Replace by edit version.
foreach ( $this->request->input('languages', [] ) as $languageId ) {
$language = Language::find( $languageId );
if( !$language )
@@ -385,4 +410,325 @@ class SubmissionsService {
}
}
public function editEntry( StoreEntryRequest $request, string $section, Entry $entry ): Entry
{
// STEP 1: Prepare basic fields and keep in save some others fields.
$this->request = $request;
$this->section = $section;
$this->entry = $entry;
$user_id = 0; // TODO: Replace that.
$oldMainImage = $entry->main_image;
$galleryPaths = [];
$entry = DB::transaction( function() use ( $user_id, &$galleryPaths ){
// STEP 2: Create game if different.
$gameId = null;
if( section_must_be( ['romhacks', 'translations' ], $this->section ) ){
$gameId = $this->eStep2_VerifyCreateAndEditGameId();
}
// STEP 3: Recreate complete title and refresh slug if needed.
$completeTitle = $this->Step3_BuildCompleteTitle( $gameId );
if( $completeTitle !== $this->entry->complete_title ) {
$this->entry->complete_title = $completeTitle;
$this->entry->slug = EntryHelpers::uniqueSlug( $completeTitle, Entry::class, $this->entry->id );
}
// STEP 4: Regenerate entry title.
if( section_must_be( 'translations', $this->section ) &&
!$this->request->input('entry_title') ){
$this->entry->title = Game::find($gameId)->name;
} else {
$this->entry->title = $this->request->input('entry_title');
}
// STEP 5: Update entry fields.
$fields = [
'type' => $this->section,
'title' => $this->entry->title, // Useless, I know.
'slug' => $this->entry->slug,
'description' => $this->request->input('description'),
'main_image' => $this->request->input('main-image'),
'state' => $this->request->input('submit-state'),
'game_id' => $gameId,
'status_id' => $this->request->input('status'),
'version' => $this->request->input('version'),
'release_date' => $this->request->input('release-date'),
'staff_credits' => $this->request->input('staff_credits'),
'relevant_link' => $this->request->input('release_site'),
'youtube_link' => $this->request->input('youtube_video'),
'user_id' => $user_id,
'complete_title' => $completeTitle,
];
$this->entry->update( $fields );
// STEP 6: Update entry files.
$this->eStep6_UpdateEntryFiles( $this->entry->id );
// STEP 7: Update hashes.
$this->eStep7_UpdateHashes( $this->entry->id );
// STEP 8: Update Authors.
$this->eStep8_UpdateAuthors();
// STEP 9: Update romhacks modifications.
if( section_must_be( 'romhacks', $this->section ) ) {
$this->eStep9_UpdateRomhacksModifications();
}
// STEP 10: Update Languages.
$this->eStep10_UpdateLanguages();
// STEP 11: Prepare new gallery images and prepare deletion of others ones.
$galleryPaths = $this->eStep11a_UpdateGalleryImages();
// STEP 13: Try to create comments area if it doesn't exist.
$this->Step13_CreateCommentsThread( $this->entry );
return $this->entry;
});
// STEP 11 : Update main image if needed.
$this->eStep11b_UpdateMainImage( $oldMainImage );
// STEP 11 : Update gallery storage.
$this->eStep11c_UpdateGalleryImages( $galleryPaths );
return $entry;
}
/**
* @throws SubmissionException
*/
private function eStep2_VerifyCreateAndEditGameId(): int
{
// Already existing game.
if( $this->request->input('game_id') ){
if( $this->entry->game_id == $this->request->input('game_id') ){
return $this->entry->game_id; // No changes.
} else { // Change in game but already exist.
$game = Game::find( $this->request->input('game_id') );
if( !$game )
throw new SubmissionException( "Game {$this->request->input('game_id')} does not exist." );
$this->entry->game_id = $game->id;
return $this->entry->game_id;
}
}
// Need to create a game.
$game = $this->createGameFromFormFields();
$this->entry->game_id = $game->id;
return $this->entry->game_id;
}
/**
* @throws SubmissionException
*/
private function eStep6_UpdateEntryFiles(int $entryId ): void
{
$requestUuids = $this->request->input('files_uuid', []);
$existingUuids = EntryFile::where( 'entry_id', $entryId )->pluck('file_uuid')->toArray();
$needDeletion = array_diff( $existingUuids, $requestUuids );
if( !empty( $needDeletion ) ){
EntryFile::where('entry_id', $entryId)->whereIn('file_uuid', $needDeletion)->delete();
}
$needAddition = array_diff( $requestUuids, $existingUuids );
if( !empty( $needAddition ) ){
$this->Step7_SaveEntryFiles( $this->entry->id, $needAddition ); // Same code.
}
}
private function eStep7_UpdateHashes(int $entryId): void
{
$requestHashes = collect( $this->request->input('hashes', [] ) )
->filter( fn($h) => isset( $h['filename'], $h['hash_crc32'], $h['hash_sha1'], $h['verified'] ) )
->keyBy( 'hash_sha1' )
->toArray();
;
$existingHashes = EntryHash::where( 'entry_id', $entryId )->get()->keyBy( 'hash_sha1' );
$hashsToDelete = array_diff( $existingHashes->keys()->toArray(), array_keys( $requestHashes ) );
if( !empty( $hashsToDelete ) ){
EntryHash::where( 'entry_id', $entryId )->whereIn('hash_sha1', $hashsToDelete)->delete();
}
foreach( $requestHashes as $sha1 => $hash ){
if( $existingHashes->has( $sha1 ) ){
$existingHashes->get( $sha1 )->update([
'filename' => $hash['filename'],
'hash_crc32' => $hash['hash_crc32'],
'hash_sha1' => $hash['hash_sha1'],
'verified' => $hash['verified'],
]);
} else {
EntryHash::create([
'entry_id' => $entryId,
'filename' => $hash['filename'],
'hash_crc32' => $hash['hash_crc32'],
'hash_sha1' => $hash['hash_sha1'],
'verified' => $hash['verified'],
]);
}
}
}
/**
* @return void
* @throws SubmissionException
*/
private function eStep8_UpdateAuthors(): void
{
$syncAuthorsId = [];
$requestAuthorsId = $this->request->input('authors', [] );
if( !empty( $requestAuthorsId ) ){
$valid = Author::whereIn( 'id', $requestAuthorsId )->pluck('id')->toArray();
if( count( $valid ) !== count( $requestAuthorsId ) ){
throw new SubmissionException( "One of the authors doesn't exist." );
}
$syncAuthorsId = array_merge( $syncAuthorsId, $requestAuthorsId );
}
foreach ( $this->request->input('new-authors', [] ) as $authorName ) {
$authorName = trim($authorName);
if ($authorName === '')
continue;
$author = Author::firstOrCreate(
['slug' => EntryHelpers::uniqueSlug($authorName, Author::class)],
['name' => $authorName]
);
$syncAuthorsId[] = $author->id;
}
$this->entry->authors()->sync( $syncAuthorsId );
}
/**
* @return void
* @throws SubmissionException
*/
private function eStep9_UpdateRomhacksModifications(): void
{
$requestModifications = $this->request->input('modifications', [] );
if( !empty( $requestModifications ) ){
$valid = Modification::whereIn( 'id', $requestModifications )->pluck('id')->toArray();
if( count( $valid ) !== count( $requestModifications ) ){
throw new SubmissionException( "One of the modifications doesn't exist." );
}
}
$this->entry->modifications()->sync( $requestModifications );
}
/**
* @return void
* @throws SubmissionException
*/
private function eStep10_UpdateLanguages(): void
{
$requestLanguages = $this->request->input('languages', [] );
if( !empty( $requestLanguages ) ){
$valid = Language::whereIn( 'id', $requestLanguages )->pluck('id')->toArray();
if( count( $valid ) !== count( $requestLanguages ) ){
throw new SubmissionException( "One of the languages doesn't exist." );
}
}
$this->entry->languages()->sync( $requestLanguages );
}
private function eStep11a_UpdateGalleryImages(): array
{
$requestGallery = $this->request->input('gallery', [] );
$existingGalleryPaths = $this->entry->gallery->pluck('image')->toArray();
$needDeletion = array_diff( $existingGalleryPaths, $requestGallery );
if( !empty( $needDeletion ) ){
EntryGallery::where('entry_id', $this->entry->id)->whereIn('image', $needDeletion )->delete();
}
$needAddition = array_diff( $requestGallery, $existingGalleryPaths );
$images = [];
foreach( $needAddition as $imagePath ){
$images[] = EntryGallery::create([
'entry_id' => $this->entry->id,
'image' => $imagePath,
]);
}
return [ 'addition' => $images, 'deletion' => $needDeletion ];
}
private function eStep11b_UpdateMainImage( ?string $oldMainImagePath ): void
{
$currentMainImagePath = $this->entry->main_image;
if( $currentMainImagePath === $oldMainImagePath )
return;
$newPath = 'entries/main-images/' . basename( $currentMainImagePath );
if( !Storage::disk('public')->move( $currentMainImagePath, $newPath ) ){
$this->entry->update(['main_image' => $oldMainImagePath]);
return;
}
$this->entry->update(['main_image' => $newPath]);
if( $oldMainImagePath && Storage::disk('public')->exists($oldMainImagePath) )
Storage::disk('public')->delete($oldMainImagePath);
}
private function eStep11c_UpdateGalleryImages( array $pathsChanges ): void
{
foreach ( $pathsChanges['deletion'] as $deletePath ){
if( Storage::disk('public')->exists($deletePath) )
Storage::disk('public')->delete($deletePath);
}
foreach ( $pathsChanges['addition'] as $galleryItem ){
$newPath = 'entries/gallery-images/' . $this->entry->id . '/' . basename( $galleryItem->image );
if( !Storage::disk('public')->move( $galleryItem->image, $newPath ) ){
continue;
}
$galleryItem->update(['image' => $newPath]);
}
}
private function Step13_CreateCommentsThread( Entry $entry ): void
{
if( !$entry->comments_thread_id )
CreateXenForoCommentsThread::dispatch( $entry );
// app(XenforoApiService::class)->createCommentsThread( $entry );
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Entry;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
@@ -71,4 +72,40 @@ class XenforoApiService {
});
}
public function createCommentsThread( Entry $entry ): bool
{
if( !$entry->comments_thread_id || $entry->comments_thread_id <= 0 ){
$data = [
'node_id' => config('xenforo.comments_node_id'),
'title' => $entry->complete_title,
'message' => $entry->description,
'prefix_id' => config('xenforo.comments_prefixes')[$entry->type] ?? 1,
'custom_fields' => [ 'entry_id' => $entry->id ],
'discussion_open' => true,
];
// TODO: Flag must be removed.
$response = $this->post("threads?api_bypass_permissions=true", config('xenforo.bot_user_id'), $data );
if( $response['success'] === true ){
$commentsThreadId = $response['thread']['thread_id'];
$entry->update(['comments_thread_id' => $commentsThreadId]);
return true;
}
}
return false;
}
/**
* @throws ConnectionException
*/
public function getThreadPosts(int $threadId, int $page = 1 ): array
{
$response = $this->get("threads/{$threadId}/posts?page=$page");
if( !isset( $response['posts'] ) || $response['posts'] === [] )
return [ 'posts' => [], 'pagination' => null ];
return $response;
}
}

View File

@@ -3,6 +3,7 @@
namespace App\View\Components;
use App\Auth\XenForoUser;
use App\Services\XenforoService;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
@@ -13,11 +14,13 @@ class XenForoAvatar extends Component
* Create a new component instance.
*/
public function __construct(
public ?XenForoUser $user = null,
public null|int|XenForoUser $user = null,
)
{
if( $this->user === null )
$this->user = \Auth::user();
else if( is_int( $this->user ) )
$this->user = app(XenforoService::class)->getXfUser( $this->user );
}
/**

View File

@@ -11,3 +11,9 @@ if( !function_exists( 'xfCsrfToken') ){
return app(\App\Services\XenforoService::class)->getCSRFToken();
}
}
if( !function_exists( 'xfStyleVariationUrl' ) ){
function xfStyleVariationUrl( string $variation ): string {
return config('app.forum_url') . '/misc/style-variation?variation=' . $variation . "&t=" . xfCsrfToken();
}
}

View File

@@ -8,10 +8,11 @@ return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
api: __DIR__.'/../routes/api.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->encryptCookies(except: ['xf_session','xf_user','xf_csrf']);
$middleware->encryptCookies(except: ['xf_session','xf_user','xf_csrf','theme','entries_per_page']);
$middleware->alias([
'xf.auth' => \App\Http\Middleware\CheckXenForoPermissions::class,
]);

View File

@@ -45,6 +45,7 @@ return [
],
'xenforo' => [
'driver' => 'xenforo',
'provider' => 'users',
]
],

View File

@@ -82,7 +82,7 @@ return [
[
'name' => 'Contact Us',
'icon' => 'at-sign',
'route' => 'home'
'xf_route' => 'misc/contact'
],
[
'name' => 'Legal pages',

18
config/xenforo.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
return [
'bot_user_id' => 1,
'comments_node_id' => 4,
'comments_prefixes' => [
'translations' => 1,
'romhacks' => 2,
'homebrew' => 3,
'utilities' => 4,
'documents' => 5,
'lua-scripts' => 6,
'tutorials' => 7,
'news' => 8
],
];

View File

@@ -71,6 +71,32 @@ ul {
--menu-user-avatar-bg: #555;
}
.\$light-mode {
/* RHPZ color */
--rhpz-orange: #ff7300;
--rhpz-orange-hover: #e56700;
/* Background colors */
--bg: #f0f0f0;
--bg2: #ffffff;
--bg3: #e8e8e8;
--bg4: #dcdcdc;
/* Text */
--text: #454545;
--text2: #737373;
--text3: #111111;
/* Elements */
--border: #d0d0d0;
--error: #e57373;
--info: #1976d2;
--success: #81c784;
--success2: #388e3c;
}
/* File: resources/css/components/cards.css */
/* STAT CARDS */
@@ -1695,6 +1721,165 @@ ul {
}
/* File: resources/css/components/settings.css */
.\$settings-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 240px;
background-color: var(--bg2);
border: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 2000;
}
.\$settings-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background-color: var(--bg3);
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
}
.\$settings-section {
padding: 12px 16px;
}
.\$settings-section-title {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--text2);
margin-bottom: 10px;
}
.\$settings-separator {
border-top: 1px solid var(--border);
}
.\$settings-themes {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.\$settings-theme-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text);
transition: transform 0.15s, border-color 0.15s;
padding: 0;
&:hover {
transform: scale(1.15);
}
.\$active {
border-color: var(--text);
transform: scale(1.1);
}
}
.\$settings-theme-toggle {
width: 100%;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 12px;
cursor: pointer;
font-family: var(--typography);
font-size: 0.88rem;
transition: background-color 0.1s;
text-align: left;
&:hover {
background-color: var(--bg4);
}
}
.\$settings-theme-toggle-inner {
display: flex;
align-items: center;
gap: 8px;
}
.\$settings-theme-toggle-badge {
margin-left: auto;
background-color: var(--rhpz-orange);
color: #111;
font-size: 0.65rem;
font-weight: 700;
padding: 2px 6px;
}
.\$settings-perpage {
display: flex;
gap: 6px;
}
.\$settings-perpage-btn {
flex: 1;
padding: 6px 4px;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text2);
font-size: 0.85rem;
cursor: pointer;
font-family: var(--typography);
transition: all 0.1s;
&:hover {
background-color: var(--bg4);
color: var(--text);
}
.\$active {
background-color: var(--rhpz-orange);
border-color: var(--rhpz-orange);
color: var(--text3);
font-weight: 600;
}
}
.\$settings-link {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
color: var(--text);
text-decoration: none;
font-size: 0.88rem;
transition: background-color 0.1s;
border: 1px solid transparent;
&:hover {
background-color: var(--bg3);
border-color: var(--border);
text-decoration: none;
}
}
.\$settings-link--danger {
color: var(--error);
&:hover {
background-color: rgba(229, 115, 115, 0.08);
border-color: rgba(229, 115, 115, 0.3);
}
}
/* File: resources/css/layout/content.css */
#main-wrapper {
flex-grow: 1;

10
package-lock.json generated
View File

@@ -6,6 +6,7 @@
"": {
"dependencies": {
"easymde": "^2.21.0",
"js-cookie": "^3.0.7",
"lucide": "^1.14.0"
},
"devDependencies": {
@@ -971,6 +972,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz",
"integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/laravel-vite-plugin": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz",

View File

@@ -15,6 +15,7 @@
},
"dependencies": {
"easymde": "^2.21.0",
"js-cookie": "^3.0.7",
"lucide": "^1.14.0"
}
}

View File

@@ -15,6 +15,7 @@
@import './components/database.css';
@import './components/hovercard.css';
@import './components/notifications.css';
@import './components/settings.css';
@import './components/easymde.css';

View File

@@ -29,3 +29,29 @@
--menu-size: 260px;
--menu-user-avatar-bg: #555;
}
.light-mode {
/* RHPZ color */
--rhpz-orange: #ff7300;
--rhpz-orange-hover: #e56700;
/* Background colors */
--bg: #f0f0f0;
--bg2: #ffffff;
--bg3: #e8e8e8;
--bg4: #dcdcdc;
/* Text */
--text: #454545;
--text2: #737373;
--text3: #111111;
/* Elements */
--border: #d0d0d0;
--error: #e57373;
--info: #1976d2;
--success: #81c784;
--success2: #388e3c;
}

View File

@@ -64,6 +64,13 @@
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.block-success {
background-color: var(--success);
border: 1px solid var(--success);
color: var(--text);
padding: 20px;
margin-bottom: 20px;
}
.block-error {
background-color: var(--error);
border: 1px solid var(--error);

View File

@@ -0,0 +1,156 @@
.settings-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 240px;
background-color: var(--bg2);
border: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 2000;
}
.settings-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background-color: var(--bg3);
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
}
.settings-section {
padding: 12px 16px;
}
.settings-section-title {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--text2);
margin-bottom: 10px;
}
.settings-separator {
border-top: 1px solid var(--border);
}
.settings-themes {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.settings-theme-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text);
transition: transform 0.15s, border-color 0.15s;
padding: 0;
&:hover {
transform: scale(1.15);
}
.active {
border-color: var(--text);
transform: scale(1.1);
}
}
.settings-theme-toggle {
width: 100%;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 12px;
cursor: pointer;
font-family: var(--typography);
font-size: 0.88rem;
transition: background-color 0.1s;
text-align: left;
&:hover {
background-color: var(--bg4);
}
}
.settings-theme-toggle-inner {
display: flex;
align-items: center;
gap: 8px;
}
.settings-theme-toggle-badge {
margin-left: auto;
background-color: var(--rhpz-orange);
color: #111;
font-size: 0.65rem;
font-weight: 700;
padding: 2px 6px;
}
.settings-perpage {
display: flex;
gap: 6px;
}
.settings-perpage-btn {
flex: 1;
padding: 6px 4px;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text2);
font-size: 0.85rem;
cursor: pointer;
font-family: var(--typography);
transition: all 0.1s;
&:hover {
background-color: var(--bg4);
color: var(--text);
}
.active {
background-color: var(--rhpz-orange);
border-color: var(--rhpz-orange);
color: var(--text3);
font-weight: 600;
}
}
.settings-link {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
color: var(--text);
text-decoration: none;
font-size: 0.88rem;
transition: background-color 0.1s;
border: 1px solid transparent;
&:hover {
background-color: var(--bg3);
border-color: var(--border);
text-decoration: none;
}
}
.settings-link--danger {
color: var(--error);
&:hover {
background-color: rgba(229, 115, 115, 0.08);
border-color: rgba(229, 115, 115, 0.3);
}
}

View File

@@ -1,4 +1,4 @@
#entry-container {
#entry-container, #comments-section, #reviews-section {
background-color: var(--bg2);
border: 1px solid var(--border);
display: flex;
@@ -128,3 +128,112 @@
text-align: center;
color: var( --text2 );
}
.comment-block {
display: flex;
gap: 16px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
&:last-child {
border-bottom: none;
}
.comment-avatar {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
background-color: var(--bg4);
border: 1px solid var(--border);
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.comment-content {
flex: 1;
min-width: 0;
.comment-meta {
font-size: 0.88rem;
color: var(--text2);
margin-bottom: 6px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.comment-author {
font-weight: 600;
color: var(--text);
text-decoration: none;
transition: color 0.2s;
&:hover {
color: var(--rhpz-orange);
}
}
.comment-date {
color: var(--text2);
}
.comment-separator {
color: var(--border);
user-select: none;
}
}
.comment-body {
font-size: 0.95rem;
color: var(--text);
line-height: 1.5;
word-wrap: break-word;
p {
margin-bottom: 10px;
&:last-child { margin-bottom: 0; }
}
a {
color: var(--rhpz-orange);
&:hover {
color: var(--rhpz-orange-hover);
text-decoration: underline;
}
}
blockquote, .bbCodeBlock-blockquote {
background-color: var(--bg);
border-left: 3px solid var(--info);
padding: 12px 16px;
margin: 12px 0;
font-style: italic;
color: var(--text2);
}
code {
font-family: monospace;
background-color: var(--bg3);
border: 1px solid var(--border);
padding: 2px 5px;
font-size: 0.9rem;
}
}
}
}
.comments-empty {
text-align: center;
padding: 40px 20px;
color: var(--text2);
font-style: italic;
background-color: var(--bg);
border: 1px dashed var(--border);
}

View File

@@ -72,8 +72,9 @@ export function GalleryManager() {
break;
const IMG = GalleryImage();
IMG.getOldImage( PATH );
IMG.serverFilePath = PATH;
this.images.push(IMG);
this.images[this.images.length - 1].getOldImage( PATH );
}
},

View File

@@ -2,6 +2,12 @@ export function MainImageManager() {
return {
/**
* Used for gallery managament and indexation.
* @type {string}
*/
key: crypto.randomUUID(),
/**
* If an image has been uploaded or not.
* @type {boolean}

View File

@@ -6,7 +6,16 @@ import { calculate as calculateHashes } from "./hashes.js";
import hovercard from "./hovercard.js";
import notifications from "./notifications.js";
import conversations from "./conversations.js";
import settings from "./settings.js";
/**
* Get config defined in meta.blade.php
* @param {string} key
* @return {string|null}
*/
window.getConfig = function( key ){
return document.querySelector('meta[name="config-' + key + '"]').getAttribute('content') ?? null;
}
// Lucide icons.
createIcons({ icons });
@@ -31,3 +40,6 @@ Alpine.store('notifications', notifications() );
// Conversations
Alpine.store('conversations', conversations() );
// Settings
Alpine.store('settings', settings() );

83
resources/js/settings.js Normal file
View File

@@ -0,0 +1,83 @@
import Cookies from 'js-cookie';
export default function settings() {
return {
/**
* @type {boolean}
*/
start: false,
/**
* Two keys, default and alternate.
* @type {Object}
*/
xfUrls: {},
/**
* @type {number[]}
*/
entriesPerPage: [ 12, 30, 48 ],
/**
* @type {string}
*/
currentTheme: Cookies.get("theme") ?? 'default',
/**
* @type {number}
*/
currentEntriesPerPage: Cookies.get("entries_per_page") ?? 30,
/**
*
* @param {string} newTheme default|alternate
*/
themeChanged( newTheme ){
if( newTheme !== "default" && newTheme !== "alternate" )
return;
if( newTheme === this.currentTheme )
return;
this.currentTheme = newTheme;
document.documentElement.classList.toggle('light-mode', this.currentTheme === 'alternate');
Cookies.set('theme', this.currentTheme, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
this.syncXF();
},
/**
*
* @return {Promise<void>}
*/
async syncXF(){
await fetch(this.xfUrls[this.currentTheme ?? 'default'], { method: "GET", credentials: "include", mode: "no-cors" });
},
/**
*
*/
toggleTheme(){
this.themeChanged(this.currentTheme === 'default' ? 'alternate' : 'default');
},
/**
*
* @param n
*/
entriesPerPageChanged( n ){
if( !this.entriesPerPage.includes(n) )
return;
this.entriesPerPage = n;
Cookies.set('entries_per_page', this.entriesPerPage, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
if( window.Livewire ){
Livewire.dispatch('entriesPerPageChanged', {n});
}
},
open(){ this.start = !this.start; },
close(){ this.start = false; },
}
}

View File

@@ -8,7 +8,7 @@
@endif
</div>
<div class="entry-card-info">
<div class="entry-card-title">{{ $entry->title }}</div>
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ] ) }}" class="entry-card-title">{{ $entry->title }}</a>
<div class="entry-card-author">
@forelse( $entry->authors as $author)
@if($loop->first)By @endif
@@ -24,13 +24,13 @@
@foreach( $entry->modifications as $modif )
<span class="badge orange">{{ $modif->name }}</span>
@endforeach
@if( $entry->status_id )
<span class="badge">{{ $entry->status->name }}</span>
@endif
@foreach( $entry->languages as $lang )
<span class="badge">{{ $lang->name }}</span>
@endforeach
@endif
@if( $entry->status_id )
<span class="badge">{{ $entry->status->name }}</span>
@endif
@foreach( $entry->languages as $lang )
<span class="badge">{{ $lang->name }}</span>
@endforeach
</div>
<div class="entry-card-meta">
<span><i data-lucide="download" size="12"></i> x</span>

View File

@@ -14,7 +14,7 @@
</div>
</div>
<div class="form-gallery form-group level" style="flex:4;">
<template x-for="(image,i) in images" :key="image.serverFilePath">
<template x-for="(image,i) in images" :key="image.key">
<div class="gallery-item">
<div class="form-image-preview-wrap">
<img :src="image.preview" :alt="image.name">
@@ -27,7 +27,7 @@
</div>
</div>
<template x-for="(image, i) in images" :key="image.serverFilePath">
<template x-for="(image, i) in images" :key="image.key">
<input type="hidden" name="gallery[]" :value="image.serverFilePath">
</template>

View File

@@ -1,7 +1,7 @@
<?php /** @var \App\Models\Language $language */ ?>
<div class="languages-selector form-group level" x-data="{
search: '',
selected: {{ JS::from( (array) $selected ) }},
selected: @js((array) $selected),
toggle(value){
const i = this.selected.indexOf(value);
i === -1 ? this.selected.push(value) : this.selected.splice(i,1);

View File

@@ -0,0 +1,72 @@
<div
x-data
x-show="$store.settings.start"
x-cloak
class="settings-dropdown"
@keydown.escape.window="$store.settings.close()"
>
<div class="settings-header">
<span>Settings</span>
</div>
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="sun-moon" size="14"></i>
Theme
</div>
<button type="button" class="settings-theme-toggle" @click="$store.settings.toggleTheme()">
<template x-if="$store.settings.currentTheme === 'default'">
<span class="settings-theme-toggle-inner">
<i data-lucide="moon" size="15"></i>
Dark
<span class="settings-theme-toggle-badge">ON</span>
</span>
</template>
<template x-if="$store.settings.currentTheme === 'alternate'">
<span class="settings-theme-toggle-inner">
<i data-lucide="sun" size="15"></i>
Light
<span class="settings-theme-toggle-badge">ON</span>
</span>
</template>
</button>
</div>
<div class="settings-separator"></div>
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="layout-grid" size="14"></i>
Entries per page
</div>
<div class="settings-perpage">
<template x-for="n in $store.settings.entriesPerPage" :key="n">
<button
type="button"
class="settings-perpage-btn"
:class="{ 'active': $store.settings.currentEntriesPerPage == n }"
@click="$store.settings.entriesPerPageChanged(n)"
x-text="n"
></button>
</template>
</div>
<div class="settings-separator"></div>
@auth
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="user-cog" size="14"></i>
Account
</div>
<a href="{{ xfRoute('account/account-details') }}" class="settings-link">
<i data-lucide="settings-2" size="14"></i>
XenForo settings
<i data-lucide="external-link" size="12" style="margin-left:auto"></i>
</a>
</div>
@endauth
</div>
</div>

View File

@@ -1,4 +1,4 @@
@php $topbarModSeparator = false; $topBarAdminSeparator = false; @endphp
@php $topbarModSeparator = false; $topbarAdminSeparator = false; @endphp
<header id="topbar">
<button class="mobile-toggle">
<i data-lucide="menu"></i>
@@ -43,11 +43,11 @@
@endif
{{-- Users --}}
@if( !\Auth::guest() && \Auth::user()->can('romhackplaza', 'canSubmitEntry') )
@can('create','\App\Models\Entry')
<a href="#" class="btn">
<i data-lucide="hard-drive-upload" size="18"></i>
</a>
@endif
@endcan
@if( !\Auth::guest() )
<div x-data x-init="$store.notifications.unviewed = {{ \Auth::user()->alerts_unviewed }}" style="position:relative">
<button type="button" class="btn" :class="{ 'active': $store.notifications.start }" @click="$store.notifications.open($el)" @click.outside="$store.notifications.close()">
@@ -76,9 +76,19 @@
@include('components.conversations')
</div>
@endif
<button class="btn">
<i data-lucide="settings" size="18"></i>
</button>
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }">
<button
type="button"
class="btn"
:class="{ 'active': $store.settings.start }"
@click="$store.settings.open()"
@click.outside="$store.settings.close()"
>
<i data-lucide="settings" size="18"></i>
</button>
@include('components.settings-dropdown')
</div>
</div>
</header>

View File

@@ -0,0 +1,38 @@
<div class="grid-c2" style="margin-top:1%;">
<div id="reviews-section">
<div class="entry-content">
<x-entry-section-title label="Reviews" icon="star" />
</div>
</div>
<div id="comments-section">
<div class="entry-content">
<x-entry-section-title label="Last comments" icon="message-circle" />
@forelse( $comments as $comment )
@if($comment['user_id'] === config('xenforo.bot_user_id') && $comment['position'] == 0)
@continue
@else
<div class="comment-block">
<x-xen-foro-avatar :user="$comment['user_id']" />
<div class="comment-content">
<div class="comment-meta">
<span class="comment-author">{{ $comment['User']['username'] }}</span>
<span class="comment-separator"></span>
<span class="comment-date">{{ date('Y-m-d', $comment['post_date']) }}</span>
</div>
<div class="comment-body">
{!! $comment['message'] !!}
</div>
</div>
</div>
@endif
@empty
<span class="whisper">Be the first to post a comment</span>
@endforelse
<a href="{{ xfRoute("threads/{$entry->comments_thread_id}/latest") }}" class="btn primary" style="margin-top: 1%;">
<i data-lucide="pen"></i> Post a comment
</a>
</div>
</div>
</div>

View File

@@ -59,9 +59,19 @@
<button class="btn primary" onclick="Livewire.dispatch('entryOpenFilesModal', { entryId: {{ $entry->id }} })">
<i data-lucide="download"></i> Download
</button>
@can('update',$entry)
<a href="{{ route('submit.edit', ['section' => $entry->type, 'entry' => $entry ] ) }}" class="btn primary">
<i data-lucide="edit"></i> Edit
</a>
@endcan
<button class="btn">
<i data-lucide="message-square"></i> Comments
</button>
@auth
<a href="{{ xfRoute("romhackplaza_entry/{$entry->id}/report") }}" class="btn">
<i data-lucide="flag"></i> Report / Claim Ownership
</a>
@endauth
</div>
</div>
</div>
@@ -94,5 +104,6 @@
@endif
</div>
</article>
@include('entries.comments')
@livewire('entry-files-modal')
@endsection

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{{ \Illuminate\Support\Facades\Cookie::get('theme', 'default') === 'alternate' ? 'light-mode' : '' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@include('meta')
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
@stack('styles')
@@ -15,14 +16,14 @@
<main id="main-wrapper">
@include('components.topbar')
@if(session('success'))
<x-success-block success-type="custom" :message="session('success')" />
@endif
@if(session('error'))
<x-error-block error-type="custom" :message="session('error')" />
@endif
<div id="content">
@if(session('success'))
<x-success-block success-type="custom" :message="session('success')" />
@endif
@if(session('error'))
<x-error-block error-type="custom" :message="session('error')" />
@endif
@yield('content')
</div>
</main>

View File

@@ -0,0 +1 @@
<meta name="config-session-domain" content="{{ config('session.domain') }}">

View File

@@ -84,6 +84,12 @@
@if( section_must_be( 'translations', $section ) )
<x-form-field-title name="Languages" required="true" />
<x-languages-selector :selected="$oldLanguages" />
@error('languages')
<x-form-error-text message="{{ $message }}" />
@enderror
@error('languages.*')
<x-form-error-text message="{{ $message }}" />
@enderror
@endif
<div class="form-group" x-ref="descriptionField">

37
routes/api.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
// FileServerController
use App\Http\Controllers\WebhookController;
Route::name('fs.')->controller(\App\Http\Controllers\FileServerController::class)->group(function () {
Route::post('/fs/upload-chunk/{section}', 'uploadChunk' )->name('uploadchunk')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials' ])
->middleware('xf.auth:romhackplaza.canSubmitEntry');
;
Route::get( '/fs/download/{entry_id}/{file:file_uuid}', 'download' )->name('download')
->where(['entry_id' => '[0-9]+']);
});
// WebhookController
Route::name('webhook.')->controller(WebhookController::class)->group(function () {
Route::post('/webhook/xenforo-new-post', 'XenForoNewPost' )->name('xenforo_new_post');
});
// TemporaryFileController
Route::name('tempfile.')->controller(\App\Http\Controllers\TemporaryFileController::class)->group(function () {
Route::post('/tempfile/upload', 'upload' )->name('upload')
->middleware(['xf.auth']);
});
// DynamicLoadController
Route::get( '/dynamic/hovercard/{user_id}', [ \App\Http\Controllers\DynamicLoadController::class, 'hovercard' ] )
->where(['user_id' => '[0-9]+'])
->name('dynamic.hovercard')
->middleware('throttle:60,1')
;
Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/dynamic/')->group(function(){
Route::get('/notifications', 'getNotifications' )->name('notifications');
Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread');
Route::get('/conversations', 'getConversations' )->name('conversations');
});

View File

@@ -1,6 +1,8 @@
<?php
use App\Http\Controllers\EntryController;
use App\Http\Controllers\WebhookController;
use Illuminate\Routing\RedirectController;
use Illuminate\Support\Facades\Route;
// HomeController.
@@ -13,54 +15,28 @@ Route::name('entries.')->controller(EntryController::class)->group(function () {
Route::get('/{section}/{entry:slug}', 'show' )->name('show')->where(
[
'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials',
'entry' => '[a-zA-Z0-9\-]+'
'entry' => '[a-zA-Z0-9\-_]+'
]
);
});
// SubmissionController.
Route::name('submit.')->prefix('/submit')->controller(\App\Http\Controllers\SubmissionController::class)->middleware('xf.auth:romhackplaza.canSubmitEntry')->group(function () {
Route::name('submit.')->prefix('/submit')->controller(\App\Http\Controllers\SubmissionController::class)->middleware(['xf.auth', 'can:create,\App\Models\Entry'])->group(function () {
Route::get('/{section}', 'create' )->name('create')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials' ]);
Route::post('/{section}', 'store' )->name('store')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials' ]);
});
Route::name('submit.')->prefix('/edit')->controller(\App\Http\Controllers\SubmissionController::class)->middleware('xf.auth:romhackplaza.canSubmitEntry')->group(function () {
Route::name('submit.')->prefix('/edit')->controller(\App\Http\Controllers\SubmissionController::class)->middleware(['xf.auth', 'can:update,entry'])->group(function () {
Route::get('/{section}/{entry:id}', 'edit' )->name('edit')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]);
Route::post('/{section}/{entry:id}', 'update' )->name('update')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials', 'entry' => '[0-9\-]+' ]);
});
/* API ROUTES */
// FileServerController
Route::name('fs.')->controller(\App\Http\Controllers\FileServerController::class)->group(function () {
Route::post('/api/fs/upload-chunk/{section}', 'uploadChunk' )->name('uploadchunk')
->where([ 'section' => 'translations|romhacks|homebrew|utilities|documents|lua-scripts|tutorials' ])
->middleware('xf.auth:romhackplaza.canSubmitEntry');
;
Route::get( '/api/fs/download/{entry_id}/{file:file_uuid}', 'download' )->name('download')
->where(['entry_id' => '[0-9]+']);
});
// TemporaryFileController
Route::name('tempfile.')->controller(\App\Http\Controllers\TemporaryFileController::class)->group(function () {
Route::post('/api/tempfile/upload', 'upload' )->name('upload')
->middleware('xf.auth:romhackplaza.canSubmitTempFile');
});
// DynamicLoadController
Route::get( '/api/dynamic/hovercard/{user_id}', [ \App\Http\Controllers\DynamicLoadController::class, 'hovercard' ] )
->where(['user_id' => '[0-9]+'])
->name('dynamic.hovercard')
->middleware('throttle:60,1')
;
Route::middleware('xf.auth')->controller(\App\Http\Controllers\DynamicLoadController::class)->name('dynamic.')->prefix('/api/dynamic/')->group(function(){
Route::get('/notifications', 'getNotifications' )->name('notifications');
Route::post('/notifications/mark-all-read', 'markAllRead' )->name('markallread');
Route::get('/conversations', 'getConversations' )->name('conversations');
// RedirectController
Route::name('redirect.')->controller(\App\Http\Controllers\RedirectController::class)->group(function () {
Route::get('/entry/report_redirect', 'entryReportRedirect' )->name('entry_report');
});