Files
SkinbaseNova/routes/web.php
2026-03-05 11:24:37 +01:00

582 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\User\ProfileController;
use App\Models\ContentType;
use App\Http\Controllers\User\AvatarController;
use App\Http\Controllers\Dashboard\ManageController;
use App\Http\Controllers\Dashboard\ArtworkController as DashboardArtworkController;
use App\Http\Controllers\Web\HomeController;
use App\Http\Controllers\Web\ArtController;
use App\Http\Controllers\Web\ArtworkPageController;
use App\Http\Controllers\Misc\AvatarController as LegacyAvatarController;
use App\Http\Controllers\Forum\ForumController;
use App\Http\Controllers\Community\NewsController;
use App\Http\Controllers\Web\CategoryController;
use App\Http\Controllers\Web\FeaturedArtworksController;
use App\Http\Controllers\Web\DailyUploadsController;
use App\Http\Controllers\Community\ChatController;
use App\Http\Controllers\User\TopFavouritesController;
use App\Http\Controllers\User\FavouritesController;
use App\Http\Controllers\User\TopAuthorsController;
use App\Http\Controllers\User\TodayInHistoryController;
use App\Http\Controllers\User\TodayDownloadsController;
use App\Http\Controllers\User\MonthlyCommentatorsController;
use App\Http\Controllers\User\MembersController;
use App\Http\Controllers\Community\LatestController;
use App\Http\Controllers\Community\LatestCommentsController;
use App\Http\Controllers\Community\InterviewController;
use App\Http\Controllers\User\StatisticsController;
use App\Http\Controllers\User\ReceivedCommentsController;
use App\Http\Controllers\Web\BrowseCategoriesController;
use App\Http\Controllers\Web\GalleryController;
use App\Http\Controllers\Web\BrowseGalleryController;
use App\Http\Controllers\Web\DiscoverController;
use App\Http\Controllers\Web\ExploreController;
use App\Http\Controllers\Web\BlogController;
use App\Http\Controllers\Web\PageController;
use App\Http\Controllers\Web\StoriesController;
use App\Http\Controllers\Web\StoryController;
use App\Http\Controllers\Web\StoriesTagController;
use App\Http\Controllers\Web\StoriesAuthorController;
use App\Http\Controllers\Web\FooterController;
use App\Http\Controllers\Web\StaffController;
use App\Http\Controllers\Web\RssFeedController;
use App\Http\Controllers\Web\ApplicationController;
use App\Http\Controllers\RSS\GlobalFeedController;
use App\Http\Controllers\RSS\DiscoverFeedController;
use App\Http\Controllers\RSS\ExploreFeedController;
use App\Http\Controllers\RSS\TagFeedController;
use App\Http\Controllers\RSS\CreatorFeedController;
use App\Http\Controllers\RSS\BlogFeedController;
use App\Http\Controllers\Web\StaffApplicationAdminController;
use Inertia\Inertia;
// ── DISCOVER routes (/discover/*) — DiscoverLayout ────────────────────────────
Route::prefix('discover')->name('discover.')->group(function () {
// /discover → redirect to /discover/trending (§6.2 canonical)
Route::get('/', fn () => redirect('/discover/trending', 301));
Route::get('/trending', [DiscoverController::class, 'trending'])->name('trending');
Route::get('/rising', [DiscoverController::class, 'rising'])->name('rising');
Route::get('/fresh', [DiscoverController::class, 'fresh'])->name('fresh');
Route::get('/top-rated', [DiscoverController::class, 'topRated'])->name('top-rated');
Route::get('/most-downloaded', [DiscoverController::class, 'mostDownloaded'])->name('most-downloaded');
Route::get('/on-this-day', [DiscoverController::class, 'onThisDay'])->name('on-this-day');
// Artworks from people you follow (auth required)
Route::middleware('auth')->get('/following', [DiscoverController::class, 'following'])->name('following');
// Personalised "For You" feed (auth required; guests → redirect)
Route::middleware('auth')->get('/for-you', [DiscoverController::class, 'forYou'])->name('for-you');
});
// ── EXPLORE routes (/explore/*) — ExploreLayout ───────────────────────────────
Route::prefix('explore')->name('explore.')->group(function () {
Route::get('/', [ExploreController::class, 'index'])->name('index');
Route::get('/{type}', [ExploreController::class, 'byType'])
->where('type', 'artworks|wallpapers|skins|photography|other')
->name('type');
Route::get('/{type}/{mode}', [ExploreController::class, 'byTypeMode'])
->where('type', 'artworks|wallpapers|skins|photography|other')
->where('mode', 'trending|new-hot|best|latest')
->name('type.mode');
});
// ── BLOG routes (/blog/*) — ContentLayout ─────────────────────────────────────
Route::prefix('blog')->name('blog.')->group(function () {
Route::get('/', [BlogController::class, 'index'])->name('index');
Route::get('/{slug}', [BlogController::class, 'show'])->where('slug', '[a-z0-9\-]+')
->name('show');
});
// ── PAGES (DB-driven static pages) — ContentLayout ────────────────────────────
Route::get('/pages/{slug}', [PageController::class, 'show'])->where('slug', '[a-z0-9\-]+')
->name('pages.show');
// Root-level marketing pages (About, Help, Contact)
Route::get('/about', [PageController::class, 'marketing'])->defaults('slug', 'about')->name('about');
Route::get('/help', [PageController::class, 'marketing'])->defaults('slug', 'help')->name('help');
Route::get('/contact', [PageController::class, 'marketing'])->defaults('slug', 'contact')->name('contact');
// Legal pages
Route::get('/legal/{section}', [PageController::class, 'legal'])
->where('section', 'terms|privacy|cookies')
->name('legal');
// ── FOOTER pages ─────────────────────────────────────────────────────────────
// Legacy /bug-report now redirects to the universal contact form
Route::match(['get','post'], '/bug-report', fn() => redirect('/contact', 301))->name('bug-report.redirect');
Route::get('/rss-feeds', [RssFeedController::class, 'index'])->name('rss-feeds');
Route::get('/faq', [FooterController::class, 'faq'])->name('faq');
Route::get('/rules-and-guidelines', [FooterController::class, 'rules'])->name('rules');
Route::get('/privacy-policy', [FooterController::class, 'privacyPolicy'])->name('privacy-policy');
Route::get('/terms-of-service', [FooterController::class, 'termsOfService'])->name('terms-of-service');
Route::get('/staff', [StaffController::class, 'index'])->name('staff');
// Contact form (formerly /apply)
Route::get('/contact', [ApplicationController::class, 'show'])->name('contact.show');
Route::post('/contact', [ApplicationController::class, 'submit'])->middleware('throttle:6,1')->name('contact.submit');
// Backwards-compatibility: redirect old /apply to /contact
Route::get('/apply', fn() => redirect('/contact', 301))->name('legacy.apply.redirect');
// Admin: staff application submissions
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/applications', [StaffApplicationAdminController::class, 'index'])->name('applications.index');
Route::get('/applications/{staffApplication}', [StaffApplicationAdminController::class, 'show'])->name('applications.show');
});
// RSS XML feeds (legacy .xml routes — kept for backward compatibility)
Route::get('/rss/latest-uploads.xml', [RssFeedController::class, 'latestUploads'])->name('rss.uploads');
Route::get('/rss/latest-skins.xml', [RssFeedController::class, 'latestSkins'])->name('rss.skins');
Route::get('/rss/latest-wallpapers.xml', [RssFeedController::class, 'latestWallpapers'])->name('rss.wallpapers');
Route::get('/rss/latest-photos.xml', [RssFeedController::class, 'latestPhotos'])->name('rss.photos');
// ── RSS 2.0 Nova feeds (/rss/*) ───────────────────────────────────────────────
Route::middleware('throttle:60,1')->group(function () {
// Global feed
Route::get('/rss', GlobalFeedController::class)->name('rss.global');
// Discover feeds
Route::prefix('rss/discover')->name('rss.discover.')->group(function () {
Route::get('/', [DiscoverFeedController::class, 'index'])->name('index');
Route::get('/trending', [DiscoverFeedController::class, 'trending'])->name('trending');
Route::get('/fresh', [DiscoverFeedController::class, 'fresh'])->name('fresh');
Route::get('/rising', [DiscoverFeedController::class, 'rising'])->name('rising');
});
// Explore category feeds
Route::prefix('rss/explore')->name('rss.explore.')->group(function () {
Route::get('/{type}', [ExploreFeedController::class, 'byType'])
->where('type', 'artworks|wallpapers|skins|photography|other')
->name('type');
Route::get('/{type}/{mode}', [ExploreFeedController::class, 'byTypeMode'])
->where('type', 'artworks|wallpapers|skins|photography|other')
->where('mode', 'trending|latest|best')
->name('type.mode');
});
// Tag feeds
Route::get('/rss/tag/{slug}', TagFeedController::class)
->where('slug', '[a-z0-9\-]+')
->name('rss.tag');
// Creator feeds
Route::get('/rss/creator/{username}', CreatorFeedController::class)
->where('username', '[A-Za-z0-9_\-]{3,20}')
->name('rss.creator');
// Blog feed
Route::get('/rss/blog', BlogFeedController::class)->name('rss.blog');
});
// ── 301 REDIRECTS: Legacy → Canonical URLs ────────────────────────────────────
// §6.1 — /browse → /explore
Route::get('/browse-redirect', fn () => redirect('/explore', 301))->name('legacy.browse.redirect');
// §6.1 — /wallpapers as standalone → /explore/wallpapers
Route::get('/wallpapers-redirect', fn () => redirect('/explore/wallpapers', 301))->name('legacy.wallpapers.redirect');
// ── CREATORS routes (/creators/*) ─────────────────────────────────────────────
Route::prefix('creators')->name('creators.')->group(function () {
// Top Creators → reuse existing top-authors controller
Route::get('/top', [\App\Http\Controllers\User\TopAuthorsController::class, 'index'])->name('top');
// Rising Creators → newest creators with recent uploads
Route::get('/rising', [\App\Http\Controllers\Web\DiscoverController::class, 'risingCreators'])->name('rising');
});
// ── STORIES routes (/stories/*) — Nova Stories System ────────────────────────
Route::prefix('stories')->name('stories.')->group(function () {
Route::get('/', [StoriesController::class, 'index'])->name('index');
Route::get('/tag/{tag}', [StoriesTagController::class, 'show'])
->where('tag', '[a-z0-9\-]+')
->name('tag');
Route::get('/author/{username}', [StoriesAuthorController::class, 'show'])
->where('username', '[A-Za-z0-9_\-]{1,50}')
->name('author');
Route::get('/{slug}', [StoryController::class, 'show'])
->where('slug', '[a-z0-9\-]+')
->name('show');
});
// Tags listing page
Route::get('/tags', [\App\Http\Controllers\Web\TagController::class, 'index'])->name('tags.index');
// Following redirect (convenience shortcut for authenticated users)
Route::middleware('auth')->get('/following', function () {
return redirect()->route('dashboard.following');
})->name('following.redirect');
// Legacy route set migrated from routes/legacy.php into this file.
Route::get('/', [HomeController::class, 'index'])->name('legacy.home');
Route::get('/home', [HomeController::class, 'index']);
Route::get('/art/{id}/{slug?}', [ArtworkPageController::class, 'show'])->where('id', '\\d+')->name('art.show');
Route::match(['get','post'], '/art/{id}/comment', [ArtController::class, 'show'])->where('id', '\\d+');
Route::get('/avatar/{id}/{name?}', [LegacyAvatarController::class, 'show'])->where('id', '\\d+')->name('legacy.avatar');
Route::middleware('ensure.onboarding.complete')->prefix('forum')->name('forum.')->group(function () {
Route::get('/', [ForumController::class, 'index'])->name('index');
Route::get('/thread/{thread}-{slug?}', [ForumController::class, 'showThread'])->name('thread.show');
Route::get('/{category:slug}', [ForumController::class, 'showCategory'])->name('category.show');
Route::middleware('auth')->group(function () {
Route::get('/{category:slug}/new', [ForumController::class, 'createThreadForm'])->name('thread.create');
Route::post('/{category:slug}/new', [ForumController::class, 'storeThread'])->name('thread.store');
Route::post('/thread/{thread}/reply', [ForumController::class, 'reply'])->name('thread.reply');
Route::post('/post/{post}/report', [ForumController::class, 'reportPost'])->name('post.report');
Route::get('/post/{post}/edit', [ForumController::class, 'editPostForm'])->name('post.edit');
Route::put('/post/{post}', [ForumController::class, 'updatePost'])->name('post.update');
});
Route::middleware(['auth', 'can:moderate-forum'])->group(function () {
Route::post('/thread/{thread}/lock', [ForumController::class, 'lockThread'])->name('thread.lock');
Route::post('/thread/{thread}/unlock', [ForumController::class, 'unlockThread'])->name('thread.unlock');
Route::post('/thread/{thread}/pin', [ForumController::class, 'pinThread'])->name('thread.pin');
Route::post('/thread/{thread}/unpin', [ForumController::class, 'unpinThread'])->name('thread.unpin');
});
});
Route::middleware('ensure.onboarding.complete')->get('/forum.php', function (\Illuminate\Http\Request $request) {
$threadId = (int) ($request->query('topic') ?? $request->query('tid') ?? 0);
if ($threadId < 1) {
return redirect()->route('forum.index', [], 301);
}
$thread = \App\Models\ForumThread::query()->find($threadId);
$slug = $thread?->slug ?: ('thread-' . $threadId);
return redirect()->route('forum.thread.show', ['thread' => $threadId, 'slug' => $slug], 301);
})->name('forum.legacy.redirect');
// News/Announcements listing — redirect to forum index until a dedicated page exists
Route::get('/news', function () {
return redirect()->route('forum.index', [], 301);
})->name('news.index');
Route::get('/news/{id}/{slug?}', [NewsController::class, 'show'])->where('id', '\\d+')->name('legacy.news.show');
Route::get('/categories', [CategoryController::class, 'index'])->name('legacy.categories');
Route::get('/sections', [\App\Http\Controllers\Web\SectionsController::class, 'index'])->name('sections');
// Clean SEO-friendly URL aliases
Route::get('/uploads/latest', [LatestController::class, 'index'])->name('uploads.latest');
Route::get('/uploads/daily', [DailyUploadsController::class, 'index'])->name('uploads.daily');
Route::get('/members/photos', [MembersController::class, 'photos'])->name('members.photos');
Route::get('/authors/top', [TopAuthorsController::class, 'index'])->name('authors.top');
Route::get('/comments/latest', [LatestCommentsController::class, 'index'])->name('comments.latest');
Route::get('/comments/monthly', [MonthlyCommentatorsController::class, 'index'])->name('comments.monthly');
Route::get('/downloads/today', [TodayDownloadsController::class, 'index'])->name('downloads.today');
Route::get('/category/{group}/{slug?}/{id?}', [BrowseGalleryController::class, 'legacyCategory'])->name('legacy.category');
// §6.1 — /browse → 301 to /explore (canonical)
Route::get('/browse', fn () => redirect('/explore', 301))->name('legacy.browse');
Route::get('/featured', [FeaturedArtworksController::class, 'index'])->name('legacy.featured');
Route::get('/featured-artworks', [FeaturedArtworksController::class, 'index'])->name('legacy.featured_artworks');
Route::get('/daily-uploads', [DailyUploadsController::class, 'index'])->name('legacy.daily_uploads');
Route::get('/chat', [ChatController::class, 'index'])->name('legacy.chat');
Route::post('/chat_post', [ChatController::class, 'post'])->name('legacy.chat.post');
Route::get('/browse-categories', [BrowseCategoriesController::class, 'index'])->name('browse.categories');
Route::get('/@{username}', [ProfileController::class, 'showByUsername'])
->where('username', '[A-Za-z0-9_-]{3,20}')
->name('profile.show');
Route::middleware('auth')->post('/@{username}/follow', [ProfileController::class, 'toggleFollow'])
->where('username', '[A-Za-z0-9_-]{3,20}')
->name('profile.follow');
Route::middleware('auth')->post('/@{username}/comment', [ProfileController::class, 'storeComment'])
->where('username', '[A-Za-z0-9_-]{3,20}')
->name('profile.comment');
Route::get('/user/{username}', [ProfileController::class, 'legacyByUsername'])
->where('username', '[A-Za-z0-9_-]{3,20}')
->name('legacy.user.profile');
Route::get('/profile/{id}/{username?}', [ProfileController::class, 'legacyById'])
->where('id', '\\d+')
->name('legacy.profile.id');
Route::get('/profile/{username}', [ProfileController::class, 'legacyByUsername'])
->where('username', '[A-Za-z0-9_-]{3,20}')
->name('legacy.profile');
Route::get('/top-favourites', [TopFavouritesController::class, 'index'])->name('legacy.top_favourites');
// /top-authors → 301 redirect to canonical /creators/top
Route::get('/top-authors', function () {
return redirect('/creators/top', 301);
})->name('legacy.top_authors');
Route::middleware('auth')->get('/mybuddies.php', [\App\Http\Controllers\User\MyBuddiesController::class, 'index'])->name('legacy.mybuddies.php');
Route::middleware('auth')->get('/mybuddies', [\App\Http\Controllers\User\MyBuddiesController::class, 'index'])->name('legacy.mybuddies');
Route::middleware('auth')->delete('/mybuddies/{id}', [\App\Http\Controllers\User\MyBuddiesController::class, 'destroy'])->name('legacy.mybuddies.delete');
Route::middleware('auth')->get('/buddies.php', [\App\Http\Controllers\User\BuddiesController::class, 'index'])->name('legacy.buddies.php');
Route::middleware('auth')->get('/buddies', [\App\Http\Controllers\User\BuddiesController::class, 'index'])->name('legacy.buddies');
Route::get('/favourites/{id?}/{username?}', [FavouritesController::class, 'index'])->name('legacy.favourites');
Route::post('/favourites/{userId}/delete/{artworkId}', [FavouritesController::class, 'destroy'])->name('legacy.favourites.delete');
Route::middleware('ensure.onboarding.complete')->get('/gallery/{id}/{username?}', [GalleryController::class, 'show'])->name('legacy.gallery');
Route::middleware('auth')->get('/recieved-comments', [ReceivedCommentsController::class, 'index'])->name('legacy.received_comments');
// Canonical dashboard profile route: Inertia-powered Settings page.
Route::middleware(['auth'])->get('/dashboard/profile', [ProfileController::class, 'editSettings'])->name('dashboard.profile');
// Keep legacy `/user` as a permanent redirect to the canonical dashboard path.
Route::middleware(['auth'])->match(['get','post'], '/user', function () {
return redirect()->route('dashboard.profile', [], 301);
})->name('legacy.user.redirect');
Route::get('/today-in-history', [TodayInHistoryController::class, 'index'])->name('legacy.today_in_history');
Route::get('/today-downloads', [TodayDownloadsController::class, 'index'])->name('legacy.today_downloads');
Route::get('/monthly-commentators', [MonthlyCommentatorsController::class, 'index'])->name('legacy.monthly_commentators');
Route::get('/members', [MembersController::class, 'index'])->name('legacy.members');
Route::get('/latest', [LatestController::class, 'index'])->name('legacy.latest');
Route::get('/latest-comments', [LatestCommentsController::class, 'index'])->name('legacy.latest_comments');
// /interviews → 301 redirect to canonical /stories
Route::get('/interviews', function () {
return redirect('/stories', 301);
})->name('legacy.interviews');
Route::get('/authors/top', [\App\Http\Controllers\User\TopAuthorsController::class, 'index'])->name('authors.top');
Route::middleware(['auth'])->group(function () {
Route::get('/statistics', [StatisticsController::class, 'index'])->name('legacy.statistics');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware(['auth', \App\Http\Middleware\NoIndexDashboard::class])->prefix('dashboard')->name('dashboard.')->group(function () {
Route::get('/artworks', [DashboardArtworkController::class, 'index'])->name('artworks.index');
Route::get('/artworks/{id}/edit', [DashboardArtworkController::class, 'edit'])->whereNumber('id')->name('artworks.edit');
Route::put('/artworks/{id}', [DashboardArtworkController::class, 'update'])->whereNumber('id')->name('artworks.update');
Route::delete('/artworks/{id}', [DashboardArtworkController::class, 'destroy'])->whereNumber('id')->name('artworks.destroy');
// Favorites (user's own favourites)
Route::get('/favorites', [\App\Http\Controllers\Dashboard\FavoriteController::class, 'index'])->name('favorites');
Route::delete('/favorites/{artwork}', [\App\Http\Controllers\Dashboard\FavoriteController::class, 'destroy'])->name('favorites.destroy');
// Followers / Following / Comments (dashboard)
Route::get('/followers', [\App\Http\Controllers\Dashboard\FollowerController::class, 'index'])->name('followers');
Route::get('/following', [\App\Http\Controllers\Dashboard\FollowingController::class, 'index'])->name('following');
Route::get('/comments', [\App\Http\Controllers\Dashboard\CommentController::class, 'index'])->name('comments');
// Gallery (user uploads)
Route::get('/gallery', [\App\Http\Controllers\Dashboard\DashboardGalleryController::class, 'index'])->name('gallery');
// Awards received on the user's own artworks
Route::get('/awards', [\App\Http\Controllers\Dashboard\DashboardAwardsController::class, 'index'])->name('awards');
});
// ── Studio Pro (Creator Artwork Manager) ────────────────────────────────────
use App\Http\Controllers\Studio\StudioController;
Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->name('studio.')->group(function () {
Route::get('/', [StudioController::class, 'index'])->name('index');
Route::get('/artworks', [StudioController::class, 'artworks'])->name('artworks');
Route::get('/artworks/drafts', [StudioController::class, 'drafts'])->name('drafts');
Route::get('/artworks/archived', [StudioController::class, 'archived'])->name('archived');
Route::get('/artworks/{id}/edit', [StudioController::class, 'edit'])->whereNumber('id')->name('artworks.edit');
Route::get('/artworks/{id}/analytics', [StudioController::class, 'analytics'])->whereNumber('id')->name('artworks.analytics');
Route::get('/analytics', [StudioController::class, 'analyticsOverview'])->name('analytics');
});
Route::middleware(['auth', 'normalize.username', 'ensure.onboarding.complete'])->group(function () {
// Redirect legacy `/profile` edit path to canonical dashboard profile route.
Route::get('/profile', function () {
return redirect()->route('dashboard.profile', [], 301);
})->name('legacy.profile.redirect');
// Backwards-compatible settings path used by some layouts/links
Route::get('/settings', [ProfileController::class, 'edit'])->name('settings');
// Backwards-compatible route name expected by some packages/views
Route::get('/profile/edit', [ProfileController::class, 'edit'])->name('profile.edit');
Route::match(['post','put','patch'], '/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// Password change endpoint (accepts POST or PUT from legacy and new forms)
Route::match(['post', 'put'], '/profile/password', [ProfileController::class, 'password'])->name('profile.password');
// Avatar upload (backend only) - processes and stores avatars
Route::post('/avatar/upload', [AvatarController::class, 'upload'])->middleware('throttle:20,1')->name('avatar.upload');
});
Route::middleware(['auth', 'ensure.onboarding.complete'])->group(function () {
Route::get('/upload', function () {
$contentTypes = ContentType::with(['rootCategories.children'])->get()->map(function ($ct) {
return [
'id' => $ct->id,
'name' => $ct->name,
'categories' => $ct->rootCategories->map(function ($c) {
return [
'id' => $c->id,
'name' => $c->name,
'children' => $c->children->map(function ($ch) {
return ['id' => $ch->id, 'name' => $ch->name];
})->values()->all(),
];
})->values()->all(),
];
})->values()->all();
return Inertia::render('Upload/Index', [
'draftId' => null,
'content_types' => $contentTypes,
'suggested_tags' => [],
'filesCdnUrl' => config('cdn.files_url'),
'chunkSize' => (int) config('uploads.chunk.max_bytes', 5242880),
'feature_flags' => [
'uploads_v2' => (bool) config('features.uploads_v2', false),
],
]);
})->name('upload');
Route::get('/upload/draft/{id}', function (string $id) {
$contentTypes = ContentType::with(['rootCategories.children'])->get()->map(function ($ct) {
return [
'id' => $ct->id,
'name' => $ct->name,
'categories' => $ct->rootCategories->map(function ($c) {
return [
'id' => $c->id,
'name' => $c->name,
'children' => $c->children->map(function ($ch) {
return ['id' => $ch->id, 'name' => $ch->name];
})->values()->all(),
];
})->values()->all(),
];
})->values()->all();
return Inertia::render('Upload/Index', [
'draftId' => $id,
'content_types' => $contentTypes,
'suggested_tags' => [],
'filesCdnUrl' => config('cdn.files_url'),
'chunkSize' => (int) config('uploads.chunk.max_bytes', 5242880),
'feature_flags' => [
'uploads_v2' => (bool) config('features.uploads_v2', false),
],
]);
})->whereUuid('id')->name('upload.draft');
});
require __DIR__.'/auth.php';
Route::get('/search', [\App\Http\Controllers\Web\SearchController::class, 'index'])
->name('search');
// Public instructions page used by OAuth providers for user data deletion requests
Route::view('/data-deletion', 'privacy.data-deletion')
->name('privacy.data_deletion');
Route::get('/tag/{tag:slug}', [\App\Http\Controllers\Web\TagController::class, 'show'])
->where('tag', '[a-z0-9\-]+')
->name('tags.show');
Route::view('/blank', 'blank')->name('blank');
// Bind the artwork route parameter to a model if it exists, otherwise return null
use App\Models\Artwork;
Route::bind('artwork', function ($value) {
// firstOrFail triggers ModelNotFoundException → caught by exception handler
// which renders contextual artwork-not-found or generic 404 view.
return Artwork::where('slug', $value)->firstOrFail();
});
// Universal content router: handles content-type roots, nested categories and artwork slugs.
// Keep the explicit /photography route above (if present) so the legacy controller can continue
// to serve photography's root page. This catch-all route delegates to a controller that
// will forward to the appropriate existing controller (artwork or category handlers).
// Provide a named route alias for legacy artwork URL generation used in tests.
Route::get('/{contentTypeSlug}/{categoryPath}/{artwork}', [\App\Http\Controllers\Web\BrowseGalleryController::class, 'showArtwork'])
->where('contentTypeSlug', 'photography|wallpapers|skins|other')
->where('categoryPath', '[^/]+(?:/[^/]+)*')
->name('artworks.show');
Route::get('/{contentTypeSlug}/{path?}', [\App\Http\Controllers\Web\BrowseGalleryController::class, 'content'])
->where('contentTypeSlug', 'photography|wallpapers|skins|other')
->where('path', '.*')
->name('content.route');
Route::middleware(['auth'])->group(function () {
Route::get('/manage', [ManageController::class, 'index'])->name('manage');
Route::get('/manage/edit/{id}', [ManageController::class, 'edit'])->name('manage.edit');
Route::post('/manage/update/{id}', [ManageController::class, 'update'])->name('manage.update');
Route::post('/manage/delete/{id}', [ManageController::class, 'destroy'])->name('manage.destroy');
});
// Admin routes for artworks (separated from public routes)
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('uploads/moderation', function () {
return Inertia::render('Admin/UploadQueue');
})->middleware('admin.moderation')->name('uploads.moderation');
Route::get('usernames/moderation', function () {
return Inertia::render('Admin/UsernameQueue');
})->middleware('admin.moderation')->name('usernames.moderation');
Route::resource('artworks', \App\Http\Controllers\Admin\ArtworkController::class)->except(['show']);
Route::get('reports', function () {
return view('admin.reports.queue');
})->middleware('admin.moderation')->name('reports.queue');
// ── Early Growth System admin panel (§14) ─────────────────────────────
Route::middleware('admin.moderation')->prefix('early-growth')->name('early-growth.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\EarlyGrowthAdminController::class, 'index'])->name('index');
Route::delete('/cache', [\App\Http\Controllers\Admin\EarlyGrowthAdminController::class, 'flushCache'])->name('cache.flush');
Route::get('/status', [\App\Http\Controllers\Admin\EarlyGrowthAdminController::class, 'status'])->name('status');
});
});
Route::middleware(['auth', 'ensure.onboarding.complete'])
->get('/messages/attachments/{id}', [\App\Http\Controllers\Api\Messaging\AttachmentController::class, 'show'])
->whereNumber('id')
->name('messages.attachments.show');
// ── Messages ──────────────────────────────────────────────────────────────────
Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('messages')->name('messages.')->group(function () {
Route::get('/', [\App\Http\Controllers\Messaging\MessagesPageController::class, 'index'])->name('index');
Route::get('/{id}', [\App\Http\Controllers\Messaging\MessagesPageController::class, 'show'])->whereNumber('id')->name('show');
});
// ── Community Activity Feed ───────────────────────────────────────────────────
Route::get('/community/activity', [\App\Http\Controllers\Web\CommunityActivityController::class, 'index'])
->name('community.activity');
// ── Posts / Following Feed ────────────────────────────────────────────────────
// /feed/following Inertia page for the ranked, diversified following feed
Route::middleware(['auth', 'ensure.onboarding.complete'])
->get('/feed/following', [\App\Http\Controllers\Web\Posts\FollowingFeedController::class, 'index'])
->name('feed.following');
// ── Feed 2.0: Trending Feed ───────────────────────────────────────────────────
Route::get('/feed/trending', [\App\Http\Controllers\Web\Posts\TrendingFeedController::class, 'index'])
->name('feed.trending');
// ── Feed 2.0: Hashtag Feed ────────────────────────────────────────────────────
Route::get('/tags/{tag}', [\App\Http\Controllers\Web\Posts\HashtagFeedController::class, 'index'])
->where('tag', '[A-Za-z][A-Za-z0-9_]{1,63}')
->name('feed.hashtag');
// ── Feed 2.0: Saved Posts ─────────────────────────────────────────────────────
Route::middleware(['auth'])
->get('/feed/saved', [\App\Http\Controllers\Web\Posts\SavedFeedController::class, 'index'])
->name('feed.saved');
// ── Feed 2.0: Post Search ─────────────────────────────────────────────────────
Route::get('/feed/search', [\App\Http\Controllers\Web\Posts\SearchFeedController::class, 'index'])
->name('feed.search');
// ── Fallback: Generic 404 ─────────────────────────────────────────────────────
// Must be last. Catches any URL not matched by a registered route.
Route::fallback(function (\Illuminate\Http\Request $request) {
return app(\App\Http\Controllers\Web\ErrorController::class)->handleNotFound($request);
})->name('404.fallback');