diff --git a/app/Console/Commands/ForumConvertPosts.php b/app/Console/Commands/ForumConvertPosts.php new file mode 100644 index 00000000..854e53eb --- /dev/null +++ b/app/Console/Commands/ForumConvertPosts.php @@ -0,0 +1,78 @@ +option('dry-run'); + $chunk = (int)$this->option('chunk'); + $limit = $this->option('limit') ? (int)$this->option('limit') : null; + + $query = ForumPost::query()->orderBy('id'); + $total = $limit ? min($query->count(), $limit) : $query->count(); + + $this->info('Converting forum posts (dry-run='.($dry ? 'yes' : 'no').')'); + $this->info("Total posts to consider: {$total}"); + + $bar = $this->output->createProgressBar($total); + $bar->start(); + + $converter = new BbcodeConverter(); + $processed = 0; + $changed = 0; + + try { + $query->chunkById($chunk, function ($posts) use (&$bar, &$processed, &$changed, $dry, $limit, $converter) { + foreach ($posts as $post) { + if ($limit !== null && $processed >= $limit) { + throw new \RuntimeException('limit_reached'); + } + $bar->advance(); + $processed++; + + $old = $post->content ?? ''; + $new = $converter->convert($old); + + if ($old === $new) { + continue; + } + + $changed++; + if ($dry) { + $this->line('[dry] would update post ' . $post->id); + continue; + } + + $post->content = $new; + $post->save(); + } + }); + } catch (\RuntimeException $e) { + if ($e->getMessage() !== 'limit_reached') { + throw $e; + } + // intentionally stop chunking when limit reached + } + + $bar->finish(); + $this->line(''); + + $this->info("Processed: {$processed} posts. Changed: {$changed} posts."); + + if ($this->option('report')) { + $this->info('Conversion complete'); + } + + return 0; + } +} diff --git a/app/Console/Commands/ForumMigrateOld.php b/app/Console/Commands/ForumMigrateOld.php new file mode 100644 index 00000000..18e0ead6 --- /dev/null +++ b/app/Console/Commands/ForumMigrateOld.php @@ -0,0 +1,388 @@ +logPath = storage_path('logs/forum_migration.log'); + } + + public function handle(): int + { + $this->info('Starting forum migration'); + $this->log('Starting forum migration'); + + $dry = $this->option('dry-run'); + $only = $this->option('only'); + $chunk = (int)$this->option('chunk'); + + try { + if (!$only || $only === 'categories') { + $this->migrateCategories($dry); + } + + if (!$only || $only === 'threads') { + $this->migrateThreads($dry, $chunk); + } + + if (!$only || $only === 'posts') { + $this->migratePosts($dry, $chunk); + } + + if (!$only || $only === 'gallery') { + $this->migrateGallery($dry, $chunk); + } + + if ($this->option('report')) { + $this->generateReport(); + } + + $this->info('Forum migration finished'); + $this->log('Forum migration finished'); + return 0; + } catch (Exception $e) { + $this->error('Migration failed: ' . $e->getMessage()); + $this->log('Migration failed: ' . $e->getMessage()); + return 1; + } + } + + protected function migrateCategories(bool $dry) + { + $this->info('Migrating categories'); + $legacy = DB::connection('legacy'); + + $roots = $legacy->table('forum_topics') + ->select('root_id') + ->distinct() + ->where('root_id', '>', 0) + ->pluck('root_id'); + + $this->info('Found ' . $roots->count() . ' legacy root ids'); + + foreach ($roots as $rootId) { + $row = $legacy->table('forum_topics')->where('topic_id', $rootId)->first(); + $name = $row->topic ?? 'Category ' . $rootId; + $slug = Str::slug(substr($name, 0, 150)); + + $this->line("-> root {$rootId}: {$name}"); + + if ($dry) { + $this->log("[dry] create category {$name} ({$slug})"); + continue; + } + + ForumCategory::updateOrCreate( + ['id' => $rootId], + ['name' => $name, 'slug' => $slug] + ); + } + + $this->info('Categories migrated'); + } + + protected function migrateThreads(bool $dry, int $chunk) + { + $this->info('Migrating threads'); + $legacy = DB::connection('legacy'); + + $query = $legacy->table('forum_topics')->orderBy('topic_id'); + + $total = $query->count(); + $this->info("Total threads to process: {$total}"); + + $bar = $this->output->createProgressBar($total); + $bar->start(); + + // chunk by legacy primary key `topic_id` + $query->chunkById($chunk, function ($rows) use ($dry, $bar) { + foreach ($rows as $r) { + $bar->advance(); + + $data = [ + 'id' => $r->topic_id, + 'category_id' => $this->resolveCategoryId($r->root_id ?? null, $r->topic_id), + // resolve user id or assign to system user (1) when missing or not found + 'user_id' => $this->resolveUserId($r->user_id ?? null), + 'title' => $r->topic, + 'slug' => $this->uniqueSlug(Str::slug(substr($r->topic,0,200)) ?: 'thread-'.$r->topic_id, $r->topic_id), + 'content' => $r->preview ?? '', + 'views' => $r->views ?? 0, + 'is_locked' => isset($r->open) ? !((bool)$r->open) : false, + 'is_pinned' => false, + 'visibility' => $this->mapPrivilegeToVisibility($r->privilege ?? 0), + 'last_post_at' => $this->normalizeDate($r->last_update ?? null), + ]; + + if ($dry) { + $this->log('[dry] thread: ' . $r->topic_id . ' - ' . $r->topic); + continue; + } + + ForumThread::updateOrCreate(['id' => $data['id']], $data); + } + }, 'topic_id'); + + $bar->finish(); + $this->line(''); + $this->info('Threads migrated'); + } + + protected function migratePosts(bool $dry, int $chunk) + { + $this->info('Migrating posts'); + $legacy = DB::connection('legacy'); + + $query = $legacy->table('forum_posts')->orderBy('post_id'); + $total = $query->count(); + $this->info("Total posts to process: {$total}"); + + $bar = $this->output->createProgressBar($total); + $bar->start(); + + // legacy forum_posts uses `post_id` as primary key + $query->chunkById($chunk, function ($rows) use ($dry, $bar) { + foreach ($rows as $r) { + $bar->advance(); + + $data = [ + 'id' => $r->post_id, + 'thread_id' => $r->topic_id, + 'user_id' => $r->user_id ?? null, + 'content' => $this->convertLegacyMessage($r->message ?? ''), + 'is_edited' => isset($r->isupdated) ? (bool)$r->isupdated : false, + 'edited_at' => $r->updated ?? null, + ]; + + if ($dry) { + $this->log('[dry] post: ' . $r->post_id); + continue; + } + + ForumPost::updateOrCreate(['id' => $data['id']], $data); + } + }, 'post_id'); + + $bar->finish(); + $this->line(''); + $this->info('Posts migrated'); + } + + protected function mapPrivilegeToVisibility($priv) + { + // legacy privilege: 0 public, 1 members, 4 staff? adjust mapping conservatively + if ($priv >= 4) return 'staff'; + if ($priv >= 1) return 'members'; + return 'public'; + } + + protected function normalizeDate($val) + { + if (empty($val)) return null; + $s = trim((string)$val); + // legacy sometimes contains sentinel invalid dates like -0001-11-30 or zero dates + if (strpos($s, '-0001') !== false) return null; + if (strpos($s, '0000-00-00') !== false) return null; + if (strtotime($s) === false) return null; + return date('Y-m-d H:i:s', strtotime($s)); + } + + protected function uniqueSlug(string $base, int $id) + { + $slug = $base; + $i = 0; + while (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) { + $i++; + $slug = $base . '-' . $id; + // if somehow still exists, append counter + if (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) { + $slug = $base . '-' . $id . '-' . $i; + } + } + return $slug; + } + + protected function resolveCategoryId($rootId, $topicId) + { + // prefer explicit rootId + if (!empty($rootId)) { + // ensure category exists + if (ForumCategory::where('id', $rootId)->exists()) return $rootId; + } + + // if this topic itself is a category + if (ForumCategory::where('id', $topicId)->exists()) return $topicId; + + // fallback: use first available category + $first = ForumCategory::first(); + if ($first) return $first->id; + + // as last resort, create Uncategorized + $cat = ForumCategory::create(['name' => 'Uncategorized', 'slug' => 'uncategorized']); + return $cat->id; + } + + protected function resolveUserId($userId) + { + if (empty($userId)) { + return 1; + } + + // check users table in default connection + if (\DB::table('users')->where('id', $userId)->exists()) { + return $userId; + } + + return 1; // fallback system user + } + + protected function convertLegacyMessage($msg) + { + $converter = new BbcodeConverter(); + return $converter->convert($msg); + } + + protected function generateReport() + { + $this->info('Generating migration report'); + $legacy = DB::connection('legacy'); + + $legacyCounts = [ + 'categories' => $legacy->table('forum_topics')->where('root_id','>',0)->distinct('root_id')->count('root_id'), + 'threads' => $legacy->table('forum_topics')->count(), + 'posts' => $legacy->table('forum_posts')->count(), + ]; + + $newCounts = [ + 'categories' => ForumCategory::count(), + 'threads' => ForumThread::count(), + 'posts' => ForumPost::count(), + 'attachments' => \DB::table('forum_attachments')->count(), + ]; + + $this->info('Legacy counts: ' . json_encode($legacyCounts)); + $this->info('New counts: ' . json_encode($newCounts)); + $this->log('Report: legacy=' . json_encode($legacyCounts) . ' new=' . json_encode($newCounts)); + } + + protected function log(string $msg) + { + $line = '[' . date('c') . '] ' . $msg . "\n"; + file_put_contents($this->logPath, $line, FILE_APPEND | LOCK_EX); + } + + protected function migrateGallery(bool $dry, int $chunk) + { + $this->info('Migrating gallery (forum_topics_gallery → forum_attachments)'); + $legacy = DB::connection('legacy'); + + if (!$legacy->getSchemaBuilder()->hasTable('forum_topics_gallery')) { + $this->info('No legacy forum_topics_gallery table found, skipping'); + return; + } + + $query = $legacy->table('forum_topics_gallery')->orderBy('id'); + $total = $query->count(); + $this->info("Total gallery items to process: {$total}"); + + $bar = $this->output->createProgressBar($total); + $bar->start(); + + $query->chunkById($chunk, function ($rows) use ($dry, $bar) { + foreach ($rows as $r) { + $bar->advance(); + + // expected legacy fields: id, name, category (topic id), folder, datum, description + $topicId = $r->category ?? ($r->topic_id ?? null); + $fileName = $r->name ?? null; + if (empty($topicId) || empty($fileName)) { + $this->log('Skipping gallery row with missing topic or name: ' . json_encode($r)); + continue; + } + + $nid = floor($topicId / 100); + $relativePath = "files/news/{$nid}/{$topicId}/{$fileName}"; + $publicPath = public_path($relativePath); + + $fileSize = null; + $mimeType = null; + $width = null; + $height = null; + + if (file_exists($publicPath)) { + $fileSize = filesize($publicPath); + $img = @getimagesize($publicPath); + if ($img !== false) { + $width = $img[0]; + $height = $img[1]; + $mimeType = $img['mime'] ?? null; + } else { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $publicPath); + finfo_close($finfo); + } + } + + // find legacy first post id for this topic + $legacy = DB::connection('legacy'); + $firstPostId = $legacy->table('forum_posts') + ->where('topic_id', $topicId) + ->orderBy('post_date') + ->value('post_id'); + + // map to new forum_posts id (we preserved ids when migrating) + $postId = null; + if ($firstPostId && \App\Models\ForumPost::where('id', $firstPostId)->exists()) { + $postId = $firstPostId; + } else { + // fallback: find any post in new DB for thread + $post = \App\Models\ForumPost::where('thread_id', $topicId)->orderBy('created_at')->first(); + if ($post) $postId = $post->id; + } + + if (empty($postId)) { + $this->log('No target post found for gallery item: topic ' . $topicId . ' file ' . $fileName); + continue; + } + + if ($dry) { + $this->log("[dry] attach {$relativePath} -> post {$postId}"); + continue; + } + + \App\Models\ForumAttachment::create([ + 'post_id' => $postId, + 'file_path' => $relativePath, + 'file_size' => $fileSize ?? 0, + 'mime_type' => $mimeType, + 'width' => $width, + 'height' => $height, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + }, 'id'); + + $bar->finish(); + $this->line(''); + $this->info('Gallery migrated'); + } +} diff --git a/app/Http/Controllers/ArtworkController.php b/app/Http/Controllers/ArtworkController.php index f7e8267a..408898bd 100644 --- a/app/Http/Controllers/ArtworkController.php +++ b/app/Http/Controllers/ArtworkController.php @@ -1,13 +1,12 @@ first(); } + // When the URL can represent a nested category path (e.g. /skins/audio/winamp), + // prefer category rendering over artwork slug collisions so same-level groups + // behave consistently. + if (! empty($artworkSlug)) { + $combinedPath = trim($categoryPath . '/' . $artworkSlug, '/'); + $resolvedCategory = Category::findByPath($contentTypeSlug, $combinedPath); + if ($resolvedCategory) { + return app(BrowseGalleryController::class)->content(request(), $contentTypeSlug, $combinedPath); + } + } + // If no artwork was found, treat the request as a category path. - // The route places the artwork slug in the last segment, so include it - // when forwarding to CategoryPageController to support arbitrary-depth paths + // The route places the artwork slug in the last segment, so include it. + // Delegate to BrowseGalleryController to render the same modern gallery + // layout used by routes like /skins/audio. if (! $foundArtwork) { $combinedPath = $categoryPath; if ($artworkSlug) { $combinedPath = trim($categoryPath . '/' . $artworkSlug, '/'); } - return app(CategoryPageController::class)->show(request(), $contentTypeSlug, $combinedPath); + return app(BrowseGalleryController::class)->content(request(), $contentTypeSlug, $combinedPath); } if (! $foundArtwork->is_public || ! $foundArtwork->is_approved || $foundArtwork->trashed()) { diff --git a/app/Http/Controllers/Community/ChatController.php b/app/Http/Controllers/Community/ChatController.php index 7cfff4bd..d8919bb1 100644 --- a/app/Http/Controllers/Community/ChatController.php +++ b/app/Http/Controllers/Community/ChatController.php @@ -41,4 +41,40 @@ class ChatController extends Controller return view('community.chat', compact('page_title', 'adHtml', 'chatHtml', 'smileys')); } + + /** + * Handle legacy AJAX chat posts from old JS. + */ + public function post(Request $request) + { + $message = $request->input('message') ?? $request->input('chat_txt') ?? null; + if (empty($message)) { + return response()->json(['ok' => false, 'error' => 'empty_message'], 400); + } + + // Ensure legacy $_SESSION keys exist for Chat class (best-effort sync from Laravel session/auth) + if (empty($_SESSION['web_login']['user_id'])) { + $webLogin = session('web_login'); + if ($webLogin && isset($webLogin['user_id'])) { + $_SESSION['web_login'] = $webLogin; + } elseif (auth()->check()) { + $user = auth()->user(); + $_SESSION['web_login'] = [ + 'user_id' => $user->id, + 'username' => $user->username ?? $user->name ?? null, + 'status' => true, + ]; + } + } + + $chat = new \App\Chat(); + try { + $chat->StoreMessage($message); + $chat->UpdateChatFile('cron/chat_log.txt', 50); + } catch (\Throwable $e) { + return response()->json(['ok' => false, 'error' => 'store_failed', 'message' => $e->getMessage()], 500); + } + + return response()->json(['ok' => true]); + } } diff --git a/app/Http/Controllers/Community/ForumController.php b/app/Http/Controllers/Community/ForumController.php index 5ec82a4f..badca315 100644 --- a/app/Http/Controllers/Community/ForumController.php +++ b/app/Http/Controllers/Community/ForumController.php @@ -18,13 +18,76 @@ class ForumController extends Controller public function index() { $data = $this->legacy->forumIndex(); + + if (empty($data['topics']) || count($data['topics']) === 0) { + try { + $categories = \App\Models\ForumCategory::query() + ->withCount(['threads as num_subtopics']) + ->orderBy('position') + ->orderBy('id') + ->get(); + + $topics = $categories->map(function ($category) { + $threadIds = \App\Models\ForumThread::where('category_id', $category->id)->pluck('id'); + + return (object) [ + 'topic_id' => $category->id, + 'topic' => $category->name, + 'discuss' => null, + 'last_update' => \App\Models\ForumThread::where('category_id', $category->id)->max('last_post_at'), + 'num_posts' => $threadIds->isEmpty() ? 0 : \App\Models\ForumPost::whereIn('thread_id', $threadIds)->count(), + 'num_subtopics' => (int) ($category->num_subtopics ?? 0), + ]; + }); + + $data['topics'] = $topics; + } catch (\Throwable $e) { + // keep legacy response + } + } + return view('community.forum.index', $data); } - public function topic(Request $request, $topic_id) + public function topic(Request $request, $topic_id, $slug = null) { + // Redirect to canonical slug when possible + try { + $thread = \App\Models\ForumThread::find((int) $topic_id); + if ($thread && !empty($thread->slug)) { + $correct = $thread->slug; + if ($slug !== $correct) { + $qs = $request->getQueryString(); + $url = route('legacy.forum.topic', ['topic_id' => $topic_id, 'slug' => $correct]); + if ($qs) $url .= '?' . $qs; + return redirect($url, 301); + } + } + } catch (\Throwable $e) { + // ignore + } + + $data = $this->legacy->forumTopic((int) $topic_id, (int) $request->query('page', 1)); + if (! $data) { + // fallback to new forum tables if migration already ran + try { + $thread = \App\Models\ForumThread::with(['posts.user'])->find((int) $topic_id); + if ($thread) { + $posts = \App\Models\ForumPost::where('thread_id', $thread->id)->orderBy('created_at')->get(); + $data = [ + 'type' => 'posts', + 'thread' => $thread, + 'posts' => $posts, + 'page_title' => $thread->title ?? 'Forum', + ]; + } + } catch (\Throwable $e) { + // ignore and fall through to placeholder + } + } + if (! $data) { return view('shared.placeholder'); } diff --git a/app/Http/Controllers/Community/NewsController.php b/app/Http/Controllers/Community/NewsController.php index cd21418f..b9d089a5 100644 --- a/app/Http/Controllers/Community/NewsController.php +++ b/app/Http/Controllers/Community/NewsController.php @@ -26,6 +26,19 @@ class NewsController extends Controller return redirect('/'); } + // redirect to canonical slug for SEO if available + try { + $correct = \Illuminate\Support\Str::slug($news->headline ?? 'news-' . $id); + if ($slug !== $correct) { + $qs = $request->getQueryString(); + $url = route('legacy.news.show', ['id' => $id, 'slug' => $correct]); + if ($qs) $url .= '?' . $qs; + return redirect($url, 301); + } + } catch (\Throwable $e) { + // ignore + } + try { $comments = DB::table('news_comment as c') ->leftJoin('users as u', 'c.user_id', '=', 'u.user_id') diff --git a/app/Http/Controllers/Web/ArtController.php b/app/Http/Controllers/Web/ArtController.php index c0fd545f..f9991fa2 100644 --- a/app/Http/Controllers/Web/ArtController.php +++ b/app/Http/Controllers/Web/ArtController.php @@ -18,6 +18,30 @@ class ArtController extends Controller public function show(Request $request, $id, $slug = null) { + // canonicalize to new artwork route when possible + try { + $art = \App\Models\Artwork::find((int)$id); + if ($art && !empty($art->slug)) { + if ($slug !== $art->slug) { + // attempt to derive contentType and category for route + $category = $art->categories()->with('contentType')->first(); + if ($category && $category->contentType) { + $contentTypeSlug = $category->contentType->slug ?? 'other'; + $categoryPath = $category->slug ?? $category->category_name ?? 'other'; + return redirect(route('artworks.show', [ + 'contentTypeSlug' => $contentTypeSlug, + 'categoryPath' => $categoryPath, + 'artwork' => $art->slug, + ]), 301); + } elseif (!empty($art->slug)) { + // fallback: redirect to artwork slug only (may be handled by router) + return redirect('/' . $art->slug, 301); + } + } + } + } catch (\Throwable $e) { + // ignore and continue rendering legacy view + } if ($request->isMethod('post') && $request->input('action') === 'store_comment') { if (auth()->check()) { try { diff --git a/app/Http/Controllers/Web/BrowseGalleryController.php b/app/Http/Controllers/Web/BrowseGalleryController.php index 1660ed4e..942576fc 100644 --- a/app/Http/Controllers/Web/BrowseGalleryController.php +++ b/app/Http/Controllers/Web/BrowseGalleryController.php @@ -4,10 +4,10 @@ namespace App\Http\Controllers\Web; use App\Models\Category; use App\Models\ContentType; +use App\Models\Artwork; use App\Services\ArtworkService; use Illuminate\Http\Request; use Illuminate\Support\Collection; -use App\Http\Controllers\ArtworkController as ArtworkControllerAlias; class BrowseGalleryController extends \App\Http\Controllers\Controller { @@ -120,13 +120,32 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller ]); } - public function showArtwork(Request $request, string $contentTypeSlug, string $categoryPath, string $artwork) + public function showArtwork(...$params) { - return app(\App\Http\Controllers\ArtController::class)->show( - $request, - strtolower($contentTypeSlug), - trim($categoryPath, '/'), - $artwork + $req = request(); + $pathSegments = array_values(array_filter(explode('/', trim($req->path(), '/')))); + + $contentTypeSlug = $params[0] ?? ($pathSegments[0] ?? null); + $categoryPath = $params[1] ?? null; + $artwork = $params[2] ?? null; + + // If artwork wasn't provided (some route invocations supply fewer args), + // derive it from the request path's last segment. + if ($artwork === null) { + $artwork = end($pathSegments) ?: null; + } + + $contentTypeSlug = strtolower((string) $contentTypeSlug); + $categoryPath = $categoryPath !== null ? trim((string) $categoryPath, '/') : (isset($pathSegments[1]) ? implode('/', array_slice($pathSegments, 1, max(0, count($pathSegments) - 2))) : ''); + + // Normalize artwork param if route-model binding returned an Artwork model + $artworkSlug = $artwork instanceof Artwork ? (string) $artwork->slug : (string) $artwork; + + return app(\App\Http\Controllers\ArtworkController::class)->show( + $req, + $contentTypeSlug, + $categoryPath, + $artworkSlug ); } diff --git a/app/Http/Controllers/Web/GalleryController.php b/app/Http/Controllers/Web/GalleryController.php index 87945079..16cd160d 100644 --- a/app/Http/Controllers/Web/GalleryController.php +++ b/app/Http/Controllers/Web/GalleryController.php @@ -17,6 +17,19 @@ class GalleryController extends Controller abort(404); } + // canonicalize username in URL when possible + try { + $correctName = $user->name ?? $user->uname ?? null; + if ($username && $correctName && $username !== $correctName) { + $qs = $request->getQueryString(); + $url = route('legacy.gallery', ['id' => $user->id, 'username' => $correctName]); + if ($qs) $url .= '?' . $qs; + return redirect($url, 301); + } + } catch (\Throwable $e) { + // ignore + } + $page = max(1, (int) $request->query('page', 1)); $hits = 20; diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php index 19f02566..d09f983a 100644 --- a/app/Http/Controllers/Web/HomeController.php +++ b/app/Http/Controllers/Web/HomeController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Services\ArtworkService; use Illuminate\Support\Facades\DB; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\Log; class HomeController extends Controller { @@ -36,34 +38,49 @@ class HomeController extends Controller $latestUploads = $this->artworks->getLatestArtworks(20); // Forum news (root forum section id 2876) - $forumNews = DB::table('forum_topics as t1') - ->leftJoin('users as u', 't1.user_id', '=', 'u.user_id') - ->select('t1.topic_id', 't1.topic', 'u.uname', 't1.post_date', 't1.preview') - ->where('t1.root_id', 2876) - ->where('t1.privilege', '<', 4) - ->orderBy('t1.post_date', 'desc') - ->limit(8) - ->get(); + try { + $forumNews = DB::table('forum_topics as t1') + ->leftJoin('users as u', 't1.user_id', '=', 'u.user_id') + ->select('t1.topic_id', 't1.topic', 'u.uname', 't1.post_date', 't1.preview') + ->where('t1.root_id', 2876) + ->where('t1.privilege', '<', 4) + ->orderBy('t1.post_date', 'desc') + ->limit(8) + ->get(); + } catch (QueryException $e) { + Log::warning('Forum topics table missing or DB error when loading forum news', ['exception' => $e->getMessage()]); + $forumNews = collect(); + } // Our news (latest site news) - $ourNews = DB::table('news as t1') - ->join('news_categories as c', 't1.category_id', '=', 'c.category_id') - ->join('users as u', 't1.user_id', '=', 'u.user_id') - ->selectRaw('t1.news_id, t1.headline, t1.user_id, t1.picture, t1.preview, u.uname, t1.create_date, t1.views, c.category_name, (SELECT COUNT(*) FROM news_comments WHERE news_id = t1.news_id) AS num_comments') - ->orderBy('t1.create_date', 'desc') - ->limit(5) - ->get(); + try { + $ourNews = DB::table('news as t1') + ->join('news_categories as c', 't1.category_id', '=', 'c.category_id') + ->join('users as u', 't1.user_id', '=', 'u.user_id') + ->selectRaw('t1.news_id, t1.headline, t1.user_id, t1.picture, t1.preview, u.uname, t1.create_date, t1.views, c.category_name, (SELECT COUNT(*) FROM news_comments WHERE news_id = t1.news_id) AS num_comments') + ->orderBy('t1.create_date', 'desc') + ->limit(5) + ->get(); + } catch (QueryException $e) { + Log::warning('News table missing or DB error when loading our news', ['exception' => $e->getMessage()]); + $ourNews = collect(); + } // Latest forum activity (exclude rootless and news root) - $latestForumActivity = DB::table('forum_topics as t1') - ->selectRaw('t1.topic_id, t1.topic, (SELECT COUNT(*) FROM forum_posts WHERE topic_id = t1.topic_id) AS numPosts') - ->where('t1.root_id', '<>', 0) - ->where('t1.root_id', '<>', 2876) - ->where('t1.privilege', '<', 4) - ->orderBy('t1.last_update', 'desc') - ->orderBy('t1.post_date', 'desc') - ->limit(10) - ->get(); + try { + $latestForumActivity = DB::table('forum_topics as t1') + ->selectRaw('t1.topic_id, t1.topic, (SELECT COUNT(*) FROM forum_posts WHERE topic_id = t1.topic_id) AS numPosts') + ->where('t1.root_id', '<>', 0) + ->where('t1.root_id', '<>', 2876) + ->where('t1.privilege', '<', 4) + ->orderBy('t1.last_update', 'desc') + ->orderBy('t1.post_date', 'desc') + ->limit(10) + ->get(); + } catch (QueryException $e) { + Log::warning('Forum topics table missing or DB error when loading latest forum activity', ['exception' => $e->getMessage()]); + $latestForumActivity = collect(); + } return view('web.home', compact( 'page_title', diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..bff8757d --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,18 @@ + + */ + protected $except = [ + 'chat_post', + 'chat_post/*', + ]; +} diff --git a/app/Models/ForumAttachment.php b/app/Models/ForumAttachment.php new file mode 100644 index 00000000..30537604 --- /dev/null +++ b/app/Models/ForumAttachment.php @@ -0,0 +1,21 @@ +belongsTo(ForumPost::class, 'post_id'); + } +} diff --git a/app/Models/ForumCategory.php b/app/Models/ForumCategory.php new file mode 100644 index 00000000..8b06c75f --- /dev/null +++ b/app/Models/ForumCategory.php @@ -0,0 +1,26 @@ +belongsTo(ForumCategory::class, 'parent_id'); + } + + public function threads() + { + return $this->hasMany(ForumThread::class, 'category_id'); + } +} diff --git a/app/Models/ForumPost.php b/app/Models/ForumPost.php new file mode 100644 index 00000000..ce989c09 --- /dev/null +++ b/app/Models/ForumPost.php @@ -0,0 +1,33 @@ + 'datetime', + ]; + + public function thread() + { + return $this->belongsTo(ForumThread::class, 'thread_id'); + } + + public function attachments() + { + return $this->hasMany(ForumAttachment::class, 'post_id'); + } +} diff --git a/app/Models/ForumThread.php b/app/Models/ForumThread.php new file mode 100644 index 00000000..0685bcc1 --- /dev/null +++ b/app/Models/ForumThread.php @@ -0,0 +1,33 @@ + 'datetime', + ]; + + public function category() + { + return $this->belongsTo(ForumCategory::class, 'category_id'); + } + + public function posts() + { + return $this->hasMany(ForumPost::class, 'thread_id'); + } +} diff --git a/app/Services/BbcodeConverter.php b/app/Services/BbcodeConverter.php new file mode 100644 index 00000000..11252640 --- /dev/null +++ b/app/Services/BbcodeConverter.php @@ -0,0 +1,103 @@ +' . htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''; + return "__CODEBLOCK_{$idx}__"; + }, $text); + + // Escape remaining text to avoid XSS + $text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + // Basic tags + $simple = [ + '/\[b\](.*?)\[\/b\]/is' => '$1', + '/\[i\](.*?)\[\/i\]/is' => '$1', + '/\[u\](.*?)\[\/u\]/is' => '$1', + '/\[s\](.*?)\[\/s\]/is' => '$1', + ]; + + foreach ($simple as $pat => $rep) { + $text = preg_replace($pat, $rep, $text); + } + + // [url=link]text[/url] and [url]link[/url] + $text = preg_replace_callback('/\[url=(.*?)\](.*?)\[\/url\]/is', function ($m) { + $url = $this->sanitizeUrl(html_entity_decode($m[1])); + $label = $m[2]; + return '' . $label . ''; + }, $text); + $text = preg_replace_callback('/\[url\](.*?)\[\/url\]/is', function ($m) { + $url = $this->sanitizeUrl(html_entity_decode($m[1])); + return '' . $url . ''; + }, $text); + + // [img]url[/img] + $text = preg_replace_callback('/\[img\](.*?)\[\/img\]/is', function ($m) { + $src = $this->sanitizeUrl(html_entity_decode($m[1])); + return ''; + }, $text); + + // [quote]...[/quote] + $text = preg_replace('/\[quote\](.*?)\[\/quote\]/is', '
$1
', $text); + + // [list] and [*] + // Convert [list]...[*]item[*]...[/list] to + $text = preg_replace_callback('/\[list\](.*?)\[\/list\]/is', function ($m) { + $items = preg_split('/\[\*\]/', $m[1]); + $out = ''; + foreach ($items as $it) { + $it = trim($it); + if ($it === '') continue; + $out .= '
  • ' . $it . '
  • '; + } + return ''; + }, $text); + + // sizes and colors: simple inline styles + $text = preg_replace('/\[size=(\d+)\](.*?)\[\/size\]/is', '$2', $text); + $text = preg_replace('/\[color=(#[0-9a-fA-F]{3,6}|[a-zA-Z]+)\](.*?)\[\/color\]/is', '$2', $text); + + // Preserve line breaks + $text = nl2br($text); + + // Restore code blocks + if (!empty($codeBlocks)) { + foreach ($codeBlocks as $i => $html) { + $text = str_replace('__CODEBLOCK_' . $i . '__', $html, $text); + } + } + + return $text; + } + + protected function sanitizeUrl($url) + { + $url = trim($url); + // allow relative paths + if (strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0 || strpos($url, '/') === 0) { + return htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + // fallback: prefix with http:// if looks like domain + if (preg_match('/^[A-Za-z0-9\-\.]+(\:[0-9]+)?(\/.*)?$/', $url)) { + return 'http://' . htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + return '#'; + } +} diff --git a/database/migrations/2026_02_17_161455_create_forum_categories_table.php b/database/migrations/2026_02_17_161455_create_forum_categories_table.php new file mode 100644 index 00000000..538e6fe8 --- /dev/null +++ b/database/migrations/2026_02_17_161455_create_forum_categories_table.php @@ -0,0 +1,38 @@ +id(); + + $table->string('name', 150); + $table->string('slug', 150)->unique(); + + $table->foreignId('parent_id') + ->nullable() + ->constrained('forum_categories') + ->nullOnDelete(); + + $table->integer('position')->default(0); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('forum_categories'); + } +}; diff --git a/database/migrations/2026_02_17_161456_create_forum_posts_table.php b/database/migrations/2026_02_17_161456_create_forum_posts_table.php new file mode 100644 index 00000000..6f63f119 --- /dev/null +++ b/database/migrations/2026_02_17_161456_create_forum_posts_table.php @@ -0,0 +1,41 @@ +id(); + + $table->foreignId('thread_id')->constrained('forum_threads')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + + $table->longText('content'); + + $table->boolean('is_edited')->default(false); + $table->timestamp('edited_at')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->index(['thread_id','created_at']); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('forum_posts'); + } +}; diff --git a/database/migrations/2026_02_17_161456_create_forum_threads_table.php b/database/migrations/2026_02_17_161456_create_forum_threads_table.php new file mode 100644 index 00000000..61013fed --- /dev/null +++ b/database/migrations/2026_02_17_161456_create_forum_threads_table.php @@ -0,0 +1,48 @@ +id(); + + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + + $table->string('title'); + $table->string('slug')->unique(); + + $table->longText('content'); + + $table->unsignedInteger('views')->default(0); + + $table->boolean('is_locked')->default(false); + $table->boolean('is_pinned')->default(false); + + $table->enum('visibility', ['public','members','staff'])->default('public'); + + $table->timestamp('last_post_at')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->index(['category_id','last_post_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('forum_threads'); + } +}; diff --git a/database/migrations/2026_02_17_161458_create_forum_attachments_table.php b/database/migrations/2026_02_17_161458_create_forum_attachments_table.php new file mode 100644 index 00000000..4f774444 --- /dev/null +++ b/database/migrations/2026_02_17_161458_create_forum_attachments_table.php @@ -0,0 +1,40 @@ +id(); + + $table->foreignId('post_id')->constrained('forum_posts')->cascadeOnDelete(); + + $table->string('file_path'); + $table->unsignedBigInteger('file_size'); + + $table->string('mime_type', 100)->nullable(); + + $table->unsignedInteger('width')->nullable(); + $table->unsignedInteger('height')->nullable(); + + $table->timestamps(); + + $table->index('post_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('forum_attachments'); + } +}; diff --git a/database/migrations/2026_02_17_163000_fix_forum_threads_category_fk.php b/database/migrations/2026_02_17_163000_fix_forum_threads_category_fk.php new file mode 100644 index 00000000..254f16d6 --- /dev/null +++ b/database/migrations/2026_02_17_163000_fix_forum_threads_category_fk.php @@ -0,0 +1,34 @@ +dropForeign(['category_id']); + } catch (\Exception $e) { + // ignore if not exists + } + + $table->foreign('category_id')->references('id')->on('forum_categories')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::table('forum_threads', function (Blueprint $table) { + try { + $table->dropForeign(['category_id']); + } catch (\Exception $e) { + } + + $table->foreign('category_id')->references('id')->on('categories')->cascadeOnDelete(); + }); + } +}; diff --git a/public/js/legacy-gallery-init.js b/public/js/legacy-gallery-init.js index fb3e761b..d3b5166f 100644 --- a/public/js/legacy-gallery-init.js +++ b/public/js/legacy-gallery-init.js @@ -230,12 +230,25 @@ var colCount = 1; try { var cols = window.getComputedStyle(grid).getPropertyValue('grid-template-columns'); - if (cols) colCount = Math.max(1, cols.trim().split(/\s+/).length); + if (cols) { + var parts = cols.trim().split(/\s+/).filter(function (p) { return p.length > 0; }); + colCount = Math.max(1, parts.length); + } } catch (e) { colCount = 1; } - var refIndex = Math.max(0, cards.length - colCount); + if (cards.length === 0) { + if (trigger.parentNode) trigger.parentNode.removeChild(trigger); + grid.appendChild(trigger); + return; + } + + // Compute the first index of the last row, then step back one row to + // place the trigger at the start of the penultimate row. + var lastRowStart = Math.floor((cards.length - 1) / colCount) * colCount; + var penultimateRowStart = Math.max(0, lastRowStart - colCount); + var refIndex = penultimateRowStart; var ref = cards[refIndex] || null; if (trigger.parentNode) trigger.parentNode.removeChild(trigger); diff --git a/public/legacy/js/legacy-gallery-init.js b/public/legacy/js/legacy-gallery-init.js index fb3e761b..d3b5166f 100644 --- a/public/legacy/js/legacy-gallery-init.js +++ b/public/legacy/js/legacy-gallery-init.js @@ -230,12 +230,25 @@ var colCount = 1; try { var cols = window.getComputedStyle(grid).getPropertyValue('grid-template-columns'); - if (cols) colCount = Math.max(1, cols.trim().split(/\s+/).length); + if (cols) { + var parts = cols.trim().split(/\s+/).filter(function (p) { return p.length > 0; }); + colCount = Math.max(1, parts.length); + } } catch (e) { colCount = 1; } - var refIndex = Math.max(0, cards.length - colCount); + if (cards.length === 0) { + if (trigger.parentNode) trigger.parentNode.removeChild(trigger); + grid.appendChild(trigger); + return; + } + + // Compute the first index of the last row, then step back one row to + // place the trigger at the start of the penultimate row. + var lastRowStart = Math.floor((cards.length - 1) / colCount) * colCount; + var penultimateRowStart = Math.max(0, lastRowStart - colCount); + var refIndex = penultimateRowStart; var ref = cards[refIndex] || null; if (trigger.parentNode) trigger.parentNode.removeChild(trigger); diff --git a/resources/views/community/chat.blade.php b/resources/views/community/chat.blade.php index 7f4d9e0f..ac942e2a 100644 --- a/resources/views/community/chat.blade.php +++ b/resources/views/community/chat.blade.php @@ -30,4 +30,4 @@ -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/community/forum/index.blade.php b/resources/views/community/forum/index.blade.php new file mode 100644 index 00000000..e0c748a3 --- /dev/null +++ b/resources/views/community/forum/index.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.nova') + +@php + use Carbon\Carbon; + use Illuminate\Support\Str; +@endphp + +@section('content') +
    +
    +

    Forum

    +

    Browse forum sections and latest activity.

    +
    + +
    +
    Forum Sections
    + +
    + + + + + + + + + + + @forelse (($topics ?? []) as $topic) + @php + $topicId = (int) ($topic->topic_id ?? $topic->id ?? 0); + $topicTitle = $topic->topic ?? $topic->title ?? $topic->name ?? 'Untitled'; + $topicSlug = Str::slug($topicTitle); + $topicUrl = $topicId > 0 ? route('legacy.forum.topic', ['topic_id' => $topicId, 'slug' => $topicSlug]) : '#'; + @endphp + + + + + + + @empty + + + + @endforelse + +
    SectionPostsTopicsLast Update
    + {{ $topicTitle }} + @if (!empty($topic->discuss)) +
    {!! Str::limit(strip_tags((string) $topic->discuss), 180) !!}
    + @endif +
    {{ $topic->num_posts ?? 0 }}{{ $topic->num_subtopics ?? 0 }} + @if (!empty($topic->last_update)) + {{ Carbon::parse($topic->last_update)->format('d.m.Y H:i') }} + @else + - + @endif +
    No forum sections available.
    +
    +
    +
    +@endsection diff --git a/resources/views/community/forum/posts.blade.php b/resources/views/community/forum/posts.blade.php new file mode 100644 index 00000000..37364a58 --- /dev/null +++ b/resources/views/community/forum/posts.blade.php @@ -0,0 +1,69 @@ +@extends('layouts.nova') + +@php + use Carbon\Carbon; + use Illuminate\Support\Str; +@endphp + +@section('content') + @php + $headerTitle = data_get($topic ?? null, 'topic') + ?? data_get($topic ?? null, 'title') + ?? data_get($thread ?? null, 'title') + ?? 'Thread'; + $headerDesc = data_get($topic ?? null, 'discuss') + ?? data_get($thread ?? null, 'content'); + @endphp + +
    +
    + ← Back to forum +

    {{ $headerTitle }}

    + @if (!empty($headerDesc)) +

    {!! Str::limit(strip_tags((string) $headerDesc), 260) !!}

    + @endif +
    + +
    + @forelse (($posts ?? []) as $post) + @php + $authorName = $post->uname ?? data_get($post, 'user.name') ?? 'Anonymous'; + $authorId = $post->user_id ?? data_get($post, 'user.id'); + $postBody = $post->message ?? $post->content ?? ''; + $postedAt = $post->post_date ?? $post->created_at ?? null; + @endphp + +
    +
    +
    {{ $authorName }}
    +
    + @if (!empty($postedAt)) + {{ Carbon::parse($postedAt)->format('d.m.Y H:i') }} + @endif +
    +
    + +
    +
    + {!! $postBody !!} +
    + + @if (!empty($authorId)) +
    + User ID: {{ $authorId }} +
    + @endif +
    +
    + @empty +
    + No posts yet. +
    + @endforelse +
    + + @if (isset($posts) && method_exists($posts, 'links')) +
    {{ $posts->withQueryString()->links() }}
    + @endif +
    +@endsection diff --git a/resources/views/community/forum/topic.blade.php b/resources/views/community/forum/topic.blade.php new file mode 100644 index 00000000..169c66b7 --- /dev/null +++ b/resources/views/community/forum/topic.blade.php @@ -0,0 +1,69 @@ +@extends('layouts.nova') + +@php + use Carbon\Carbon; + use Illuminate\Support\Str; +@endphp + +@section('content') +
    +
    + ← Back to forum +

    {{ $topic->topic ?? $topic->title ?? 'Topic' }}

    + @if (!empty($topic->discuss)) +

    {!! Str::limit(strip_tags((string) $topic->discuss), 220) !!}

    + @endif +
    + +
    +
    Threads
    +
    + + + + + + + + + + + @forelse (($subtopics ?? []) as $sub) + @php + $id = (int) ($sub->topic_id ?? $sub->id ?? 0); + $title = $sub->topic ?? $sub->title ?? 'Untitled'; + @endphp + + + + + + + @empty + + + + @endforelse + +
    ThreadPostsByLast Update
    + {{ $title }} + @if (!empty($sub->discuss)) +
    {!! Str::limit(strip_tags((string) $sub->discuss), 180) !!}
    + @endif +
    {{ $sub->num_posts ?? 0 }}{{ $sub->uname ?? 'Unknown' }} + @if (!empty($sub->last_update)) + {{ Carbon::parse($sub->last_update)->format('d.m.Y H:i') }} + @elseif (!empty($sub->post_date)) + {{ Carbon::parse($sub->post_date)->format('d.m.Y H:i') }} + @else + - + @endif +
    No threads in this section yet.
    +
    +
    + + @if (isset($subtopics) && method_exists($subtopics, 'links')) +
    {{ $subtopics->withQueryString()->links() }}
    + @endif +
    +@endsection diff --git a/resources/views/gallery/index.blade.php b/resources/views/gallery/index.blade.php index 396d623b..e0509b59 100644 --- a/resources/views/gallery/index.blade.php +++ b/resources/views/gallery/index.blade.php @@ -101,7 +101,7 @@
    - @if ($artworks instanceof \Illuminate\Contracts\Pagination\Paginator) + @if ($artworks instanceof \Illuminate\Contracts\Pagination\Paginator || $artworks instanceof \Illuminate\Contracts\Pagination\CursorPaginator) {{ method_exists($artworks, 'withQueryString') ? $artworks->withQueryString()->links() : $artworks->links() }} @endif
    @@ -132,7 +132,39 @@ [data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); } } [data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; } - [data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; } + /* Keep pagination visible when JS enhances the gallery so users + have a clear navigation control (numeric links for length-aware + paginators, prev/next for cursor paginators). Make it compact. */ + [data-nova-gallery].is-enhanced [data-gallery-pagination] { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1.5rem; + } + [data-nova-gallery].is-enhanced [data-gallery-pagination] ul { + display: inline-flex; + gap: 0.25rem; + align-items: center; + padding: 0; + margin: 0; + list-style: none; + } + [data-nova-gallery].is-enhanced [data-gallery-pagination] li a, + [data-nova-gallery].is-enhanced [data-gallery-pagination] li span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.25rem; + height: 2.25rem; + border-radius: 0.5rem; + padding: 0 0.5rem; + background: rgba(255,255,255,0.03); + color: #e6eef8; + border: 1px solid rgba(255,255,255,0.04); + text-decoration: none; + font-size: 0.875rem; + } [data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; } .nova-skeleton-card { border-radius: 1rem; diff --git a/resources/views/layouts/legacy.blade.php b/resources/views/layouts/legacy.blade.php new file mode 100644 index 00000000..fc5e8c9c --- /dev/null +++ b/resources/views/layouts/legacy.blade.php @@ -0,0 +1,51 @@ +@include('layouts._legacy') + + + + {{ $page_title ?? 'Skinbase' }} + + + + + + @isset($page_canonical) + + @endisset + + + + + @vite(['resources/css/app.css','resources/scss/nova.scss','resources/js/nova.js']) + @stack('head') + + + +
    +@include('layouts.nova.toolbar') + +
    +
    + @hasSection('sidebar') +
    +
    + @yield('content') +
    + +
    + @else +
    + @yield('content') +
    + @endif +
    +
    + +@include('layouts.nova.footer') + +@stack('toolbar') +@stack('scripts') + + + diff --git a/resources/views/user/buddies.blade.php b/resources/views/user/buddies.blade.php index a587eec7..6b15b258 100644 --- a/resources/views/user/buddies.blade.php +++ b/resources/views/user/buddies.blade.php @@ -41,4 +41,4 @@ @endif -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/user/favourites.blade.php b/resources/views/user/favourites.blade.php index 4268c3cf..24db32fd 100644 --- a/resources/views/user/favourites.blade.php +++ b/resources/views/user/favourites.blade.php @@ -70,4 +70,4 @@ @endif -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/art.blade.php b/resources/views/web/art.blade.php index a5f60d28..71bf8d58 100644 --- a/resources/views/web/art.blade.php +++ b/resources/views/web/art.blade.php @@ -115,4 +115,4 @@ -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/categories.blade.php b/resources/views/web/categories.blade.php index 5ef92eed..ea446d24 100644 --- a/resources/views/web/categories.blade.php +++ b/resources/views/web/categories.blade.php @@ -49,4 +49,4 @@ @endforelse -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/daily-uploads.blade.php b/resources/views/web/daily-uploads.blade.php index 32e63220..ce2fe954 100644 --- a/resources/views/web/daily-uploads.blade.php +++ b/resources/views/web/daily-uploads.blade.php @@ -45,4 +45,4 @@ @endpush -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/featured-artworks.blade.php b/resources/views/web/featured-artworks.blade.php index 128e6ee3..c55dfb13 100644 --- a/resources/views/web/featured-artworks.blade.php +++ b/resources/views/web/featured-artworks.blade.php @@ -46,4 +46,4 @@ @if($artworks){{ $artworks->withQueryString()->links('pagination::bootstrap-4') }}@endif -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/gallery.blade.php b/resources/views/web/gallery.blade.php index 05d360fd..327e5830 100644 --- a/resources/views/web/gallery.blade.php +++ b/resources/views/web/gallery.blade.php @@ -57,4 +57,4 @@ @endif -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/web/home/featured.blade.php b/resources/views/web/home/featured.blade.php index bf66caba..dabb5620 100644 --- a/resources/views/web/home/featured.blade.php +++ b/resources/views/web/home/featured.blade.php @@ -1,46 +1,36 @@ -{{-- Featured row (migrated from legacy/home/featured.blade.php) --}} -