Wire admin studio SSR and search infrastructure
This commit is contained in:
@@ -7,6 +7,10 @@ Route::middleware(['web', 'throttle:60,1'])
|
||||
->get('categories', [\App\Http\Controllers\CategoryController::class, 'index'])
|
||||
->name('api.categories.index');
|
||||
|
||||
Route::middleware(['web', 'throttle:120,1'])
|
||||
->post('worlds/analytics/events', [\App\Http\Controllers\Api\WorldAnalyticsEventController::class, 'store'])
|
||||
->name('api.worlds.analytics.events.store');
|
||||
|
||||
Route::middleware(['web', 'auth'])->prefix('dashboard')->name('api.dashboard.')->group(function () {
|
||||
Route::get('activity', [DashboardController::class, 'activity'])->name('activity');
|
||||
Route::get('analytics', [DashboardController::class, 'analytics'])->name('analytics');
|
||||
@@ -139,6 +143,13 @@ Route::prefix('rank')->name('api.rank.')->middleware(['throttle:60,1'])->group(f
|
||||
// ── Studio Pro API (authenticated) ─────────────────────────────────────────────
|
||||
Route::middleware(['web', 'auth'])->prefix('studio')->name('api.studio.')->group(function () {
|
||||
Route::post('events', [\App\Http\Controllers\Studio\StudioEventsApiController::class, 'store'])->name('events.store');
|
||||
Route::get('upload-queue', [\App\Http\Controllers\Studio\StudioUploadQueueApiController::class, 'index'])->name('upload-queue.index');
|
||||
Route::post('upload-queue/batches', [\App\Http\Controllers\Studio\StudioUploadQueueApiController::class, 'store'])->name('upload-queue.store');
|
||||
Route::post('upload-queue/bulk', [\App\Http\Controllers\Studio\StudioUploadQueueApiController::class, 'bulk'])->name('upload-queue.bulk');
|
||||
Route::post('upload-queue/items/{id}/fail', [\App\Http\Controllers\Studio\StudioUploadQueueApiController::class, 'markFailed'])->whereNumber('id')->name('upload-queue.items.fail');
|
||||
Route::post('upload-queue/items/{id}/retry', [\App\Http\Controllers\Studio\StudioUploadQueueApiController::class, 'retry'])->whereNumber('id')->name('upload-queue.items.retry');
|
||||
Route::post('news/media/upload', [\App\Http\Controllers\Studio\StudioNewsMediaApiController::class, 'store'])->middleware(['throttle:20,1', 'forum.bot.protection:api_write'])->name('news.media.upload');
|
||||
Route::delete('news/media', [\App\Http\Controllers\Studio\StudioNewsMediaApiController::class, 'destroy'])->middleware(['throttle:20,1', 'forum.bot.protection:api_write'])->name('news.media.destroy');
|
||||
Route::post('worlds/media/upload', [\App\Http\Controllers\Studio\StudioWorldMediaApiController::class, 'store'])->middleware(['throttle:20,1', 'forum.bot.protection:api_write'])->name('worlds.media.upload');
|
||||
Route::delete('worlds/media', [\App\Http\Controllers\Studio\StudioWorldMediaApiController::class, 'destroy'])->middleware(['throttle:20,1', 'forum.bot.protection:api_write'])->name('worlds.media.destroy');
|
||||
Route::put('preferences', [\App\Http\Controllers\Studio\StudioPreferencesApiController::class, 'updatePreferences'])->name('preferences.settings');
|
||||
@@ -709,6 +720,21 @@ Route::middleware(['web', 'auth', 'normalize.username', 'throttle:20,1'])->group
|
||||
->name('api.artworks.comments.destroy');
|
||||
});
|
||||
|
||||
Route::middleware(['web', 'throttle:60,1'])
|
||||
->get('news/articles/{id}/comments', [\App\Http\Controllers\Api\NewsArticleCommentController::class, 'index'])
|
||||
->whereNumber('id')
|
||||
->name('api.news.comments.index');
|
||||
|
||||
Route::middleware(['web', 'auth', 'normalize.username', 'throttle:20,1'])->group(function () {
|
||||
Route::post('news/articles/{id}/comments', [\App\Http\Controllers\Api\NewsArticleCommentController::class, 'store'])
|
||||
->whereNumber('id')
|
||||
->name('api.news.comments.store');
|
||||
|
||||
Route::delete('news/articles/{id}/comments/{commentId}', [\App\Http\Controllers\Api\NewsArticleCommentController::class, 'destroy'])
|
||||
->whereNumber(['id', 'commentId'])
|
||||
->name('api.news.comments.destroy');
|
||||
});
|
||||
|
||||
// ── Reactions ─────────────────────────────────────────────────────────────────
|
||||
// GET /api/artworks/{id}/reactions list artwork reaction totals (public)
|
||||
// POST /api/artworks/{id}/reactions toggle artwork reaction (auth)
|
||||
@@ -723,6 +749,10 @@ Route::middleware(['web', 'throttle:reactions-read'])->group(function () {
|
||||
Route::get('comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'commentReactions'])
|
||||
->whereNumber('id')
|
||||
->name('api.comments.reactions.index');
|
||||
|
||||
Route::get('news/comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'newsCommentReactions'])
|
||||
->whereNumber('id')
|
||||
->name('api.news.comments.reactions.index');
|
||||
});
|
||||
|
||||
Route::middleware(['web', 'auth', 'normalize.username', 'throttle:reactions-write'])->group(function () {
|
||||
@@ -733,6 +763,10 @@ Route::middleware(['web', 'auth', 'normalize.username', 'throttle:reactions-writ
|
||||
Route::post('comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'toggleCommentReaction'])
|
||||
->whereNumber('id')
|
||||
->name('api.comments.reactions.toggle');
|
||||
|
||||
Route::post('news/comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'toggleNewsCommentReaction'])
|
||||
->whereNumber('id')
|
||||
->name('api.news.comments.reactions.toggle');
|
||||
});
|
||||
|
||||
// ── Personalised suggestions (auth required) ────────────────────────────────
|
||||
|
||||
@@ -18,14 +18,14 @@ Artisan::command('uploads:cleanup {--limit=100 : Maximum drafts to clean in one
|
||||
|
||||
// ── Scheduled tasks ────────────────────────────────────────────────────────────
|
||||
|
||||
// Recalculate trending scores every 30 minutes (staggered: 24h first, then 7d)
|
||||
// Recalculate trending scores every 30 minutes, staggered away from other hot paths.
|
||||
Schedule::command('skinbase:recalculate-trending --period=24h')
|
||||
->everyThirtyMinutes()
|
||||
->cron('6,36 * * * *')
|
||||
->name('trending-24h')
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::command('skinbase:recalculate-trending --period=7d --skip-index')
|
||||
->everyThirtyMinutes()
|
||||
->cron('19,49 * * * *')
|
||||
->name('trending-7d')
|
||||
->runInBackground()
|
||||
->withoutOverlapping();
|
||||
@@ -39,7 +39,7 @@ Schedule::command('skinbase:reset-windowed-stats --period=24h')
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::command('skinbase:reset-windowed-stats --period=7d')
|
||||
->weeklyOn(1, '03:30') // Monday 03:30
|
||||
->weeklyOn(1, '03:50') // Monday 03:50
|
||||
->name('reset-windowed-stats-7d')
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -48,11 +48,12 @@ Schedule::command('uploads:cleanup')->dailyAt('03:00');
|
||||
Schedule::command('analytics:aggregate-similar-artworks')->dailyAt('03:10');
|
||||
Schedule::command('analytics:aggregate-feed')->dailyAt('03:20');
|
||||
Schedule::command('analytics:aggregate-discovery-feedback')->dailyAt('03:25');
|
||||
Schedule::command('analytics:aggregate-tag-interactions')->dailyAt('03:35');
|
||||
|
||||
// Drain Redis artwork-stat delta queue so MySQL counters stay fresh.
|
||||
// Run every 5 minutes with overlap protection.
|
||||
// Offset this off the :00/:10 boundaries so it does not pile onto publish jobs.
|
||||
Schedule::command('skinbase:flush-redis-stats')
|
||||
->everyFiveMinutes()
|
||||
->cron('1,11,21,31,41,51 * * * *')
|
||||
->name('flush-redis-stats')
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -116,21 +117,29 @@ Schedule::command('nova-cards:publish-scheduled')
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('collections:sync-lifecycle')
|
||||
->everyTenMinutes()
|
||||
->cron('3,13,23,33,43,53 * * * *')
|
||||
->name('sync-collection-lifecycle')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('homepage:warm-guest-cache')
|
||||
->everyTenMinutes()
|
||||
->cron('5,15,25,35,45,55 * * * *')
|
||||
->name('warm-homepage-guest-cache')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// Safety-net audit for Meilisearch drift on recently touched artworks.
|
||||
// Scans the last 65 minutes to cover the previous hour plus a small buffer.
|
||||
Schedule::command('artworks:search-reconcile --repair --reverse --remove-unexpected --limit=1000 --recent-minutes=65')
|
||||
->hourlyAt(28)
|
||||
->name('artworks-search-reconcile-recent')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// ── Feed 2.0: Trending Cache Warm-up ─────────────────────────────────────────
|
||||
// Warm the post trending cache every 2 minutes (complements the 2-min TTL).
|
||||
// Warm the post trending cache every 2 minutes on odd minutes to avoid :00/:10 pileups.
|
||||
Schedule::command('posts:warm-trending')
|
||||
->everyTwoMinutes()
|
||||
->cron('1-59/2 * * * *')
|
||||
->name('warm-post-trending')
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -138,7 +147,7 @@ Schedule::command('posts:warm-trending')
|
||||
// Recalculate ranking_score + engagement_velocity every 30 minutes.
|
||||
// Also syncs V2 scores to rank_artwork_scores so list builds benefit.
|
||||
Schedule::command('nova:recalculate-rankings --sync-rank-scores')
|
||||
->everyThirtyMinutes()
|
||||
->cron('7,37 * * * *')
|
||||
->name('ranking-v2')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
@@ -146,21 +155,30 @@ Schedule::command('nova:recalculate-rankings --sync-rank-scores')
|
||||
// ── Rising Engine (Heat / Momentum) ───────────────────────────────────────────
|
||||
// Snapshot current totals each hour, then recalculate heat every 15 minutes.
|
||||
Schedule::command('nova:metrics-snapshot-hourly')
|
||||
->hourly()
|
||||
->hourlyAt(2)
|
||||
->name('metrics-snapshot-hourly')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('nova:recalculate-heat')
|
||||
->everyFifteenMinutes()
|
||||
->cron('9,24,39,54 * * * *')
|
||||
->name('recalculate-heat')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// Additional production schedules that must live here because Laravel 11's
|
||||
// active scheduler in this app is defined in routes/console.php, not Kernel.
|
||||
|
||||
// Generate static sitemap XML files that nginx can serve directly without PHP.
|
||||
// The generate command writes public/sitemap.xml + public/sitemaps/{name}.xml.
|
||||
Schedule::command('skinbase:sitemaps:generate')
|
||||
->dailyAt('22:30')
|
||||
->name('sitemaps-generate')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('skinbase:sitemaps:publish --sync')
|
||||
->everySixHours()
|
||||
->cron('8 */6 * * *')
|
||||
->name('sitemaps-publish')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
@@ -171,13 +189,15 @@ Schedule::command('skinbase:sitemaps:validate')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// Keep the old release-pipeline cleanup running so stale release artifacts are pruned.
|
||||
|
||||
Schedule::job(new \App\Jobs\Sitemaps\CleanupSitemapReleasesJob())
|
||||
->dailyAt('05:00')
|
||||
->name('sitemaps-cleanup')
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::command('collections:dispatch-maintenance')
|
||||
->hourly()
|
||||
->hourlyAt(43)
|
||||
->name('dispatch-collection-maintenance')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
@@ -202,7 +222,7 @@ Schedule::job(new \App\Jobs\RebuildTrendingNovaCardsJob())
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::job(new \App\Jobs\RecalculateRisingNovaCardsJob())
|
||||
->everyFifteenMinutes()
|
||||
->cron('12,27,42,57 * * * *')
|
||||
->name('nova-cards-rising-cache-refresh')
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -223,30 +243,30 @@ Schedule::command('health:tick')
|
||||
->withoutOverlapping();
|
||||
|
||||
Schedule::command('forum:ai-scan')
|
||||
->everyTenMinutes()
|
||||
->hourlyAt(16)
|
||||
->name('forum-ai-scan')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('forum:bot-scan')
|
||||
->everyFiveMinutes()
|
||||
->hourlyAt(22)
|
||||
->name('forum-bot-scan')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('forum:scan-posts --limit=250')
|
||||
->everyFifteenMinutes()
|
||||
->hourlyAt(17)
|
||||
->name('forum-post-scan')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('forum:firewall-scan')
|
||||
->everyFiveMinutes()
|
||||
->hourlyAt(40)
|
||||
->name('forum-firewall-scan')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
Schedule::command('horizon:snapshot')
|
||||
->everyFiveMinutes()
|
||||
->hourlyAt(45)
|
||||
->name('horizon-snapshot')
|
||||
->withoutOverlapping();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Legacy\AvatarController;
|
||||
use App\Http\Controllers\Legacy\LegacyArtworkPhotoController;
|
||||
use App\Http\Controllers\Legacy\CategoryRedirectController;
|
||||
use App\Http\Controllers\Community\LatestCommentsController;
|
||||
use App\Http\Controllers\User\FavouritesController;
|
||||
@@ -22,6 +23,13 @@ use App\Http\Controllers\Web\GalleryController;
|
||||
// ── AVATARS ───────────────────────────────────────────────────────────────────
|
||||
Route::get('/avatar/{id}/{name?}', [AvatarController::class, 'show'])->where('id', '\d+')->name('legacy.avatar');
|
||||
|
||||
// ── LEGACY THUMBNAIL / PHOTO URLS ────────────────────────────────────────────
|
||||
Route::get('/photo/{encoded}_{size}.{extension}', LegacyArtworkPhotoController::class)
|
||||
->where('encoded', '[0-9A-Za-z]+')
|
||||
->where('size', '\d+')
|
||||
->where('extension', '[A-Za-z0-9]+')
|
||||
->name('legacy.photo');
|
||||
|
||||
// ── ARTWORK (legacy comment URL) ──────────────────────────────────────────────
|
||||
//Route::match(['get','post'], '/art/{id}/comment', [ArtController::class, 'show'])->where('id', '\d+');
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Http\Controllers\Web\RssFeedController;
|
||||
use App\Http\Controllers\Web\ApplicationController;
|
||||
use App\Http\Controllers\Web\CategoryController;
|
||||
use App\Http\Controllers\News\NewsController as FrontendNewsController;
|
||||
use App\Http\Controllers\News\NewsArticleCommentController;
|
||||
use App\Http\Controllers\News\NewsRssController;
|
||||
use App\Http\Controllers\RSS\GlobalFeedController;
|
||||
use App\Http\Controllers\RSS\DiscoverFeedController;
|
||||
@@ -36,6 +37,7 @@ 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\Admin\AdminController;
|
||||
use App\Http\Controllers\Studio\StudioNewsController;
|
||||
use App\Http\Controllers\Studio\StudioController;
|
||||
use App\Http\Controllers\Studio\StudioWorldController;
|
||||
@@ -89,6 +91,8 @@ Route::prefix('discover')->name('discover.')->group(function () {
|
||||
// ── EXPLORE (/explore/*) ──────────────────────────────────────────────────────
|
||||
Route::prefix('explore')->name('explore.')->group(function () {
|
||||
Route::get('/', [ExploreController::class, 'index'])->name('index');
|
||||
Route::get('/best', [ExploreController::class, 'hallOfFame'])->name('best');
|
||||
Route::get('/top-rated', fn () => redirect()->route('explore.index', array_merge(request()->query(), ['sort' => 'top-rated']), 301))->name('top-rated');
|
||||
Route::get('/members', fn () => redirect()->route('creators.top', request()->query(), 301))->name('members.redirect');
|
||||
Route::get('/memebers', fn () => redirect()->route('creators.top', request()->query(), 301))->name('memebers.redirect');
|
||||
Route::get('/{type}', [ExploreController::class, 'byType'])
|
||||
@@ -272,6 +276,13 @@ Route::prefix('news')->name('news.')->group(function () {
|
||||
->name('author');
|
||||
Route::get('category/{slug}', [FrontendNewsController::class, 'category'])->name('category');
|
||||
Route::get('tag/{slug}', [FrontendNewsController::class, 'tag'])->name('tag');
|
||||
Route::middleware('auth')->post('{slug}/comments', [NewsArticleCommentController::class, 'store'])
|
||||
->where('slug', '[a-z0-9\-]+')
|
||||
->name('comments.store');
|
||||
Route::middleware('auth')->delete('{slug}/comments/{comment}', [NewsArticleCommentController::class, 'destroy'])
|
||||
->where('slug', '[a-z0-9\-]+')
|
||||
->whereNumber('comment')
|
||||
->name('comments.destroy');
|
||||
Route::get('{slug}', [FrontendNewsController::class, 'show'])
|
||||
->where('slug', '[a-z0-9\-]+')
|
||||
->name('show');
|
||||
@@ -297,7 +308,11 @@ Route::get('/worlds/create', function (Request $request) {
|
||||
return redirect()->route('worlds.index', $request->query(), 302);
|
||||
})
|
||||
->name('worlds.create.redirect');
|
||||
Route::get('/worlds/{world:slug}', [WorldController::class, 'show'])
|
||||
Route::get('/worlds/{world}/{year}', [WorldController::class, 'showEdition'])
|
||||
->where('world', '^(?!create$)[a-z0-9]+(?:-[a-z0-9]+)*$')
|
||||
->whereNumber('year')
|
||||
->name('worlds.editions.show');
|
||||
Route::get('/worlds/{world}', [WorldController::class, 'show'])
|
||||
->where('world', '^(?!create$)[a-z0-9]+(?:-[a-z0-9]+)*$')
|
||||
->name('worlds.show');
|
||||
Route::get('/collections/editorial', [\App\Http\Controllers\Web\CollectionDiscoveryController::class, 'editorial'])
|
||||
@@ -388,7 +403,7 @@ Route::get('/@{username}/collections/{slug}', [ProfileCollectionController::clas
|
||||
|
||||
Route::get('/@{username}/{tab}', [ProfileController::class, 'showTabByUsername'])
|
||||
->where('username', '[A-Za-z0-9_-]{3,20}')
|
||||
->where('tab', 'posts|artworks|stories|achievements|collections|about|stats|favourites|activity')
|
||||
->where('tab', 'posts|artworks|stories|achievements|worlds|collections|about|stats|favourites|activity')
|
||||
->name('profile.tab');
|
||||
|
||||
Route::get('/@{username}', [ProfileController::class, 'showByUsername'])
|
||||
@@ -448,6 +463,7 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->nam
|
||||
Route::get('/content', [StudioController::class, 'content'])->name('content');
|
||||
Route::get('/artworks', [StudioController::class, 'artworks'])->name('artworks');
|
||||
Route::get('/drafts', [StudioController::class, 'drafts'])->name('drafts');
|
||||
Route::get('/upload-queue', [StudioController::class, 'uploadQueue'])->name('upload-queue');
|
||||
Route::get('/scheduled', [StudioController::class, 'scheduled'])->name('scheduled');
|
||||
Route::get('/calendar', [StudioController::class, 'calendar'])->name('calendar');
|
||||
Route::get('/archived', [StudioController::class, 'archived'])->name('archived');
|
||||
@@ -458,6 +474,8 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->nam
|
||||
Route::get('/analytics', [StudioController::class, 'analyticsOverview'])->name('analytics');
|
||||
Route::get('/collections', [StudioController::class, 'collections'])->name('collections');
|
||||
Route::get('/stories', [StudioController::class, 'stories'])->name('stories');
|
||||
Route::get('/stories/create', [StoryController::class, 'create'])->middleware('creator.access')->name('stories.create');
|
||||
Route::get('/stories/{story}/edit', [StoryController::class, 'edit'])->middleware('creator.access')->name('stories.edit');
|
||||
Route::get('/assets', [StudioController::class, 'assets'])->name('assets');
|
||||
Route::get('/comments', [StudioController::class, 'comments'])->name('comments');
|
||||
Route::get('/activity', [StudioController::class, 'activity'])->name('activity');
|
||||
@@ -489,6 +507,7 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->nam
|
||||
Route::get('/news/{article}/preview', [StudioNewsController::class, 'preview'])->whereNumber('article')->name('news.preview');
|
||||
Route::get('/news/{article}/edit', [StudioNewsController::class, 'edit'])->whereNumber('article')->name('news.edit');
|
||||
Route::patch('/news/{article}', [StudioNewsController::class, 'update'])->whereNumber('article')->name('news.update');
|
||||
Route::delete('/news/{article}', [StudioNewsController::class, 'destroy'])->whereNumber('article')->name('news.destroy');
|
||||
Route::post('/news/{article}/publish', [StudioNewsController::class, 'publish'])->whereNumber('article')->name('news.publish');
|
||||
Route::post('/news/{article}/archive', [StudioNewsController::class, 'archive'])->whereNumber('article')->name('news.archive');
|
||||
Route::post('/news/{article}/feature', [StudioNewsController::class, 'feature'])->whereNumber('article')->name('news.feature');
|
||||
@@ -500,6 +519,11 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->nam
|
||||
Route::get('/worlds/{world}/preview', [StudioWorldController::class, 'preview'])->whereNumber('world')->name('worlds.preview');
|
||||
Route::get('/worlds/{world}/edit', [StudioWorldController::class, 'edit'])->whereNumber('world')->name('worlds.edit');
|
||||
Route::patch('/worlds/{world}', [StudioWorldController::class, 'update'])->whereNumber('world')->name('worlds.update');
|
||||
Route::post('/worlds/{world}/suggestions/add', [StudioWorldController::class, 'addSuggestion'])->whereNumber('world')->name('worlds.suggestions.add');
|
||||
Route::post('/worlds/{world}/suggestions/pin', [StudioWorldController::class, 'pinSuggestion'])->whereNumber('world')->name('worlds.suggestions.pin');
|
||||
Route::post('/worlds/{world}/suggestions/dismiss', [StudioWorldController::class, 'dismissSuggestion'])->whereNumber('world')->name('worlds.suggestions.dismiss');
|
||||
Route::post('/worlds/{world}/suggestions/not-relevant', [StudioWorldController::class, 'markSuggestionNotRelevant'])->whereNumber('world')->name('worlds.suggestions.not-relevant');
|
||||
Route::post('/worlds/{world}/suggestions/restore', [StudioWorldController::class, 'restoreSuggestion'])->whereNumber('world')->name('worlds.suggestions.restore');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/approve', [StudioWorldController::class, 'approveSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.approve');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/remove', [StudioWorldController::class, 'removeSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.remove');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/block', [StudioWorldController::class, 'blockSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.block');
|
||||
@@ -508,7 +532,10 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('studio')->nam
|
||||
Route::post('/worlds/{world}/submissions/{submission}/feature', [StudioWorldController::class, 'featureSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.feature');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/unfeature', [StudioWorldController::class, 'unfeatureSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.unfeature');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/pending', [StudioWorldController::class, 'pendingSubmission'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.pending');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/rewards/{rewardType}', [StudioWorldController::class, 'grantSubmissionReward'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.rewards.grant');
|
||||
Route::post('/worlds/{world}/submissions/{submission}/rewards/{rewardType}/revoke', [StudioWorldController::class, 'revokeSubmissionReward'])->whereNumber('world')->whereNumber('submission')->name('worlds.submissions.rewards.revoke');
|
||||
Route::post('/worlds/{world}/publish', [StudioWorldController::class, 'publish'])->whereNumber('world')->name('worlds.publish');
|
||||
Route::post('/worlds/{world}/recap/publish', [StudioWorldController::class, 'publishRecap'])->whereNumber('world')->name('worlds.recap.publish');
|
||||
Route::post('/worlds/{world}/archive', [StudioWorldController::class, 'archive'])->whereNumber('world')->name('worlds.archive');
|
||||
Route::post('/worlds/{world}/duplicate', [StudioWorldController::class, 'duplicate'])->whereNumber('world')->name('worlds.duplicate');
|
||||
Route::post('/worlds/{world}/new-edition', [StudioWorldController::class, 'newEdition'])->whereNumber('world')->name('worlds.new-edition');
|
||||
@@ -927,9 +954,71 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('messages')->n
|
||||
Route::get('/{id}', [\App\Http\Controllers\Messaging\MessagesPageController::class, 'show'])->whereNumber('id')->name('show');
|
||||
});
|
||||
|
||||
Route::middleware(['auth', 'admin.moderation'])->get('/admin/usernames/moderation', function () {
|
||||
return Inertia::render('Admin/UsernameQueue');
|
||||
})->name('admin.usernames.moderation');
|
||||
Route::middleware(['auth'])
|
||||
->prefix('admin')
|
||||
->group(function () {
|
||||
Route::any('/{path?}', function (?string $path = null) {
|
||||
$target = '/moderation' . ($path ? '/' . $path : '');
|
||||
$query = request()->getQueryString();
|
||||
|
||||
if ($query) {
|
||||
$target .= '?' . $query;
|
||||
}
|
||||
|
||||
return redirect($target, 302);
|
||||
})->where('path', '.*');
|
||||
});
|
||||
|
||||
// ── ADMIN PANEL ───────────────────────────────────────────────────────────────
|
||||
Route::middleware(['auth', 'admin.access'])
|
||||
->prefix('moderation')
|
||||
->name('admin.')
|
||||
->group(function () {
|
||||
Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
|
||||
Route::get('/users', [AdminController::class, 'users'])->name('users');
|
||||
Route::patch('/users/{user}/role', [AdminController::class, 'updateRole'])->name('users.role');
|
||||
Route::get('/stories', [AdminController::class, 'stories'])->name('stories');
|
||||
Route::get('/artworks', [AdminController::class, 'artworks'])->name('artworks');
|
||||
Route::get('/usernames/moderation', [AdminController::class, 'usernameQueue'])->name('usernames');
|
||||
Route::get('/uploads', [AdminController::class, 'uploadQueue'])->name('uploads');
|
||||
Route::get('/settings', [AdminController::class, 'settings'])->name('settings');
|
||||
|
||||
Route::middleware(['artwork.maturity.access'])
|
||||
->prefix('ai-biography')
|
||||
->name('cp.ai-biography.')
|
||||
->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'index'])->name('index');
|
||||
Route::post('/users/{user}/rebuild', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'rebuild'])->whereNumber('user')->name('rebuild');
|
||||
Route::post('/records/{biography}/approve', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'approve'])->whereNumber('biography')->name('approve');
|
||||
Route::post('/records/{biography}/flag', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'flag'])->whereNumber('biography')->name('flag');
|
||||
Route::post('/records/{biography}/hide', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'hide'])->whereNumber('biography')->name('hide');
|
||||
Route::post('/records/{biography}/show', [\App\Http\Controllers\Settings\AiBiographyAdminController::class, 'show'])->whereNumber('biography')->name('show');
|
||||
});
|
||||
|
||||
Route::prefix('artworks/featured')
|
||||
->name('artworks.featured.')
|
||||
->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'index'])->name('main');
|
||||
Route::get('/search', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'search'])->name('search');
|
||||
Route::post('/', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'store'])->name('store');
|
||||
Route::match(['put', 'patch'], '/{feature}', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'update'])->whereNumber('feature')->name('update');
|
||||
Route::post('/{feature}/toggle', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'toggle'])->whereNumber('feature')->name('toggle');
|
||||
Route::post('/{feature}/force-hero', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'toggleForceHero'])->whereNumber('feature')->name('force-hero');
|
||||
Route::delete('/{feature}', [\App\Http\Controllers\Settings\FeaturedArtworkAdminController::class, 'destroy'])->whereNumber('feature')->name('delete');
|
||||
});
|
||||
|
||||
Route::prefix('homepage/announcements')
|
||||
->name('homepage-announcements.')
|
||||
->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'index'])->name('index');
|
||||
Route::get('/create', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'create'])->name('create');
|
||||
Route::post('/', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'store'])->name('store');
|
||||
Route::post('/preview', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'preview'])->name('preview');
|
||||
Route::get('/{homepageAnnouncement}/edit', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'edit'])->whereNumber('homepageAnnouncement')->name('edit');
|
||||
Route::match(['put', 'patch'], '/{homepageAnnouncement}', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'update'])->whereNumber('homepageAnnouncement')->name('update');
|
||||
Route::delete('/{homepageAnnouncement}', [\App\Http\Controllers\Settings\HomepageAnnouncementController::class, 'destroy'])->whereNumber('homepageAnnouncement')->name('destroy');
|
||||
});
|
||||
});
|
||||
|
||||
// ── COMMUNITY ACTIVITY ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user