A lot of things.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
17
app/Http/Controllers/RedirectController.php
Normal file
17
app/Http/Controllers/RedirectController.php
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
29
app/Http/Controllers/WebhookController.php
Normal file
29
app/Http/Controllers/WebhookController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ class Entry extends Model
|
||||
'youtube_link',
|
||||
'user_id',
|
||||
'complete_title',
|
||||
'comments_thread_id',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -45,6 +45,7 @@ return [
|
||||
],
|
||||
'xenforo' => [
|
||||
'driver' => 'xenforo',
|
||||
'provider' => 'users',
|
||||
]
|
||||
],
|
||||
|
||||
|
||||
@@ -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
18
config/xenforo.php
Normal 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
|
||||
],
|
||||
];
|
||||
185
extra.less
185
extra.less
@@ -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
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"easymde": "^2.21.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"lucide": "^1.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@import './components/database.css';
|
||||
@import './components/hovercard.css';
|
||||
@import './components/notifications.css';
|
||||
@import './components/settings.css';
|
||||
|
||||
@import './components/easymde.css';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
156
resources/css/components/settings.css
Normal file
156
resources/css/components/settings.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
83
resources/js/settings.js
Normal 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; },
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
72
resources/views/components/settings-dropdown.blade.php
Normal file
72
resources/views/components/settings-dropdown.blade.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
38
resources/views/entries/comments.blade.php
Normal file
38
resources/views/entries/comments.blade.php
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
resources/views/meta.blade.php
Normal file
1
resources/views/meta.blade.php
Normal file
@@ -0,0 +1 @@
|
||||
<meta name="config-session-domain" content="{{ config('session.domain') }}">
|
||||
@@ -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
37
routes/api.php
Normal 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');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user