with(['user.profile', 'category', 'template', 'backgroundImage', 'tags']) ->latest('updated_at') ->paginate(24) ->withQueryString(); $featuredCreators = User::query() ->whereHas('novaCards', fn ($query) => $query->publiclyVisible()) ->withCount([ 'novaCards as public_cards_count' => fn ($query) => $query->publiclyVisible(), 'novaCards as featured_cards_count' => fn ($query) => $query->publiclyVisible()->where('featured', true), ]) ->withSum([ 'novaCards as total_views_count' => fn ($query) => $query->publiclyVisible(), ], 'views_count') ->orderByDesc('nova_featured_creator') ->orderByDesc('featured_cards_count') ->orderByDesc('public_cards_count') ->orderBy('username') ->limit(8) ->get(); $reportCounts = Report::query() ->selectRaw('status, COUNT(*) as aggregate') ->whereIn('target_type', $this->reportTargets->novaCardTargetTypes()) ->groupBy('status') ->pluck('aggregate', 'status'); return Inertia::render('Collection/NovaCardsAdminIndex', [ 'cards' => $this->presenter->paginator($cards, false, $request->user()), 'featuredCreators' => $featuredCreators->map(fn (User $creator): array => [ 'id' => (int) $creator->id, 'username' => (string) $creator->username, 'name' => $creator->name, 'display_name' => $creator->name ?: '@' . $creator->username, 'public_url' => route('cards.creator', ['username' => $creator->username]), 'nova_featured_creator' => (bool) $creator->nova_featured_creator, 'public_cards_count' => (int) ($creator->public_cards_count ?? 0), 'featured_cards_count' => (int) ($creator->featured_cards_count ?? 0), 'total_views_count' => (int) ($creator->total_views_count ?? 0), ])->values()->all(), 'categories' => NovaCardCategory::query()->orderBy('order_num')->orderBy('name')->get()->map(fn (NovaCardCategory $category): array => [ 'id' => (int) $category->id, 'slug' => (string) $category->slug, 'name' => (string) $category->name, 'description' => $category->description, 'active' => (bool) $category->active, 'order_num' => (int) $category->order_num, 'cards_count' => (int) $category->cards()->count(), ])->values()->all(), 'stats' => [ 'pending' => NovaCard::query()->where('moderation_status', NovaCard::MOD_PENDING)->count(), 'flagged' => NovaCard::query()->where('moderation_status', NovaCard::MOD_FLAGGED)->count(), 'featured' => NovaCard::query()->where('featured', true)->count(), 'published' => NovaCard::query()->where('status', NovaCard::STATUS_PUBLISHED)->count(), 'remixable' => NovaCard::query()->where('allow_remix', true)->count(), 'challenges' => NovaCardChallenge::query()->count(), ], 'reportingQueue' => [ 'enabled' => true, 'pending' => (int) ($reportCounts['open'] ?? 0), 'label' => 'Nova Cards report queue', 'description' => 'Review open, investigating, and resolved reports for cards, challenge prompts, and challenge entries.', 'statuses' => [ 'open' => (int) ($reportCounts['open'] ?? 0), 'reviewing' => (int) ($reportCounts['reviewing'] ?? 0), 'closed' => (int) ($reportCounts['closed'] ?? 0), ], ], 'endpoints' => [ 'updateCardPattern' => route('cp.cards.update', ['card' => '__CARD__']), 'updateCreatorPattern' => route('cp.cards.creators.update', ['user' => '__CREATOR__']), 'templates' => route('cp.cards.templates.index'), 'assetPacks' => route('cp.cards.asset-packs.index'), 'challenges' => route('cp.cards.challenges.index'), 'collections' => route('cp.cards.collections.index'), 'storeCategory' => route('cp.cards.categories.store'), 'updateCategoryPattern' => route('cp.cards.categories.update', ['category' => '__CATEGORY__']), 'reportsQueue' => route('api.admin.reports.queue', ['group' => 'nova_cards']), 'updateReportPattern' => route('api.admin.reports.update', ['report' => '__REPORT__']), 'moderateReportTargetPattern' => route('api.admin.reports.moderate-target', ['report' => '__REPORT__']), ], 'moderationDispositionOptions' => [ NovaCard::MOD_PENDING => $this->moderation->dispositionOptions(NovaCard::MOD_PENDING), NovaCard::MOD_APPROVED => $this->moderation->dispositionOptions(NovaCard::MOD_APPROVED), NovaCard::MOD_FLAGGED => $this->moderation->dispositionOptions(NovaCard::MOD_FLAGGED), NovaCard::MOD_REJECTED => $this->moderation->dispositionOptions(NovaCard::MOD_REJECTED), ], 'editorOptions' => $this->presenter->options(), ])->rootView('collections'); } public function templates(Request $request): Response { return Inertia::render('Collection/NovaCardsTemplateAdmin', [ 'templates' => NovaCardTemplate::query()->orderBy('order_num')->orderBy('name')->get()->map(fn (NovaCardTemplate $template): array => [ 'id' => (int) $template->id, 'slug' => (string) $template->slug, 'name' => (string) $template->name, 'description' => $template->description, 'preview_image' => $template->preview_image, 'config_json' => $template->config_json, 'supported_formats' => $template->supported_formats, 'active' => (bool) $template->active, 'official' => (bool) $template->official, 'order_num' => (int) $template->order_num, ])->values()->all(), 'editorOptions' => $this->presenter->options(), 'endpoints' => [ 'store' => route('cp.cards.templates.store'), 'updatePattern' => route('cp.cards.templates.update', ['template' => '__TEMPLATE__']), 'cards' => route('cp.cards.index'), ], ])->rootView('collections'); } public function assetPacks(Request $request): Response { return Inertia::render('Collection/NovaCardsAssetPackAdmin', [ 'packs' => NovaCardAssetPack::query()->orderBy('type')->orderBy('order_num')->orderBy('name')->get()->map(fn (NovaCardAssetPack $pack): array => [ 'id' => (int) $pack->id, 'slug' => (string) $pack->slug, 'name' => (string) $pack->name, 'description' => $pack->description, 'type' => (string) $pack->type, 'preview_image' => $pack->preview_image, 'manifest_json' => $pack->manifest_json, 'official' => (bool) $pack->official, 'active' => (bool) $pack->active, 'order_num' => (int) $pack->order_num, ])->values()->all(), 'endpoints' => [ 'store' => route('cp.cards.asset-packs.store'), 'updatePattern' => route('cp.cards.asset-packs.update', ['assetPack' => '__PACK__']), 'cards' => route('cp.cards.index'), ], ])->rootView('collections'); } public function challenges(Request $request): Response { return Inertia::render('Collection/NovaCardsChallengeAdmin', [ 'challenges' => NovaCardChallenge::query()->with('winnerCard')->orderByDesc('featured')->orderByDesc('starts_at')->get()->map(fn (NovaCardChallenge $challenge): array => [ 'id' => (int) $challenge->id, 'slug' => (string) $challenge->slug, 'title' => (string) $challenge->title, 'description' => $challenge->description, 'prompt' => $challenge->prompt, 'rules_json' => $challenge->rules_json, 'status' => (string) $challenge->status, 'official' => (bool) $challenge->official, 'featured' => (bool) $challenge->featured, 'winner_card_id' => $challenge->winner_card_id ? (int) $challenge->winner_card_id : null, 'entries_count' => (int) $challenge->entries_count, 'starts_at' => optional($challenge->starts_at)?->format('Y-m-d\TH:i'), 'ends_at' => optional($challenge->ends_at)?->format('Y-m-d\TH:i'), ])->values()->all(), 'cards' => NovaCard::query()->published()->latest('published_at')->limit(100)->get()->map(fn (NovaCard $card): array => [ 'id' => (int) $card->id, 'title' => (string) $card->title, ])->values()->all(), 'endpoints' => [ 'store' => route('cp.cards.challenges.store'), 'updatePattern' => route('cp.cards.challenges.update', ['challenge' => '__CHALLENGE__']), 'cards' => route('cp.cards.index'), ], ])->rootView('collections'); } public function collections(Request $request): Response { return Inertia::render('Collection/NovaCardsCollectionAdmin', [ 'collections' => NovaCardCollection::query() ->with(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']) ->orderByDesc('featured') ->orderByDesc('official') ->orderByDesc('updated_at') ->get() ->map(fn (NovaCardCollection $collection): array => $this->presenter->collection($collection, $request->user(), true)) ->values() ->all(), 'cards' => NovaCard::query() ->published() ->latest('published_at') ->limit(200) ->get() ->map(fn (NovaCard $card): array => [ 'id' => (int) $card->id, 'title' => (string) $card->title, 'slug' => (string) $card->slug, 'creator' => $card->user?->username, ]) ->values() ->all(), 'admins' => \App\Models\User::query() ->whereIn('role', ['admin', 'moderator']) ->orderBy('username') ->limit(50) ->get(['id', 'username', 'name']) ->map(fn ($user): array => [ 'id' => (int) $user->id, 'username' => (string) $user->username, 'name' => $user->name, ]) ->values() ->all(), 'endpoints' => [ 'store' => route('cp.cards.collections.store'), 'updatePattern' => route('cp.cards.collections.update', ['collection' => '__COLLECTION__']), 'attachCardPattern' => route('cp.cards.collections.cards.store', ['collection' => '__COLLECTION__']), 'detachCardPattern' => route('cp.cards.collections.cards.destroy', ['collection' => '__COLLECTION__', 'card' => '__CARD__']), 'cards' => route('cp.cards.index'), ], ])->rootView('collections'); } public function storeTemplate(AdminStoreNovaCardTemplateRequest $request): JsonResponse { $template = NovaCardTemplate::query()->create($request->validated()); return response()->json([ 'template' => [ 'id' => (int) $template->id, 'slug' => (string) $template->slug, 'name' => (string) $template->name, 'description' => $template->description, 'preview_image' => $template->preview_image, 'config_json' => $template->config_json, 'supported_formats' => $template->supported_formats, 'active' => (bool) $template->active, 'official' => (bool) $template->official, 'order_num' => (int) $template->order_num, ], ]); } public function updateTemplate(AdminStoreNovaCardTemplateRequest $request, NovaCardTemplate $template): JsonResponse { $template->update($request->validated()); return response()->json([ 'template' => [ 'id' => (int) $template->id, 'slug' => (string) $template->slug, 'name' => (string) $template->name, 'description' => $template->description, 'preview_image' => $template->preview_image, 'config_json' => $template->config_json, 'supported_formats' => $template->supported_formats, 'active' => (bool) $template->active, 'official' => (bool) $template->official, 'order_num' => (int) $template->order_num, ], ]); } public function storeAssetPack(AdminStoreNovaCardAssetPackRequest $request): JsonResponse { $pack = NovaCardAssetPack::query()->create($request->validated()); return response()->json([ 'pack' => [ 'id' => (int) $pack->id, 'slug' => (string) $pack->slug, 'name' => (string) $pack->name, 'description' => $pack->description, 'type' => (string) $pack->type, 'preview_image' => $pack->preview_image, 'manifest_json' => $pack->manifest_json, 'official' => (bool) $pack->official, 'active' => (bool) $pack->active, 'order_num' => (int) $pack->order_num, ], ]); } public function updateAssetPack(AdminStoreNovaCardAssetPackRequest $request, NovaCardAssetPack $assetPack): JsonResponse { $assetPack->update($request->validated()); return response()->json([ 'pack' => [ 'id' => (int) $assetPack->id, 'slug' => (string) $assetPack->slug, 'name' => (string) $assetPack->name, 'description' => $assetPack->description, 'type' => (string) $assetPack->type, 'preview_image' => $assetPack->preview_image, 'manifest_json' => $assetPack->manifest_json, 'official' => (bool) $assetPack->official, 'active' => (bool) $assetPack->active, 'order_num' => (int) $assetPack->order_num, ], ]); } public function storeChallenge(AdminStoreNovaCardChallengeRequest $request): JsonResponse { $challenge = NovaCardChallenge::query()->create($request->validated() + [ 'user_id' => $request->user()->id, ]); return response()->json([ 'challenge' => [ 'id' => (int) $challenge->id, 'slug' => (string) $challenge->slug, 'title' => (string) $challenge->title, 'description' => $challenge->description, 'prompt' => $challenge->prompt, 'rules_json' => $challenge->rules_json, 'status' => (string) $challenge->status, 'official' => (bool) $challenge->official, 'featured' => (bool) $challenge->featured, 'winner_card_id' => $challenge->winner_card_id ? (int) $challenge->winner_card_id : null, 'entries_count' => (int) $challenge->entries_count, 'starts_at' => optional($challenge->starts_at)?->format('Y-m-d\TH:i'), 'ends_at' => optional($challenge->ends_at)?->format('Y-m-d\TH:i'), ], ]); } public function updateChallenge(AdminStoreNovaCardChallengeRequest $request, NovaCardChallenge $challenge): JsonResponse { $challenge->update($request->validated()); return response()->json([ 'challenge' => [ 'id' => (int) $challenge->id, 'slug' => (string) $challenge->slug, 'title' => (string) $challenge->title, 'description' => $challenge->description, 'prompt' => $challenge->prompt, 'rules_json' => $challenge->rules_json, 'status' => (string) $challenge->status, 'official' => (bool) $challenge->official, 'featured' => (bool) $challenge->featured, 'winner_card_id' => $challenge->winner_card_id ? (int) $challenge->winner_card_id : null, 'entries_count' => (int) $challenge->entries_count, 'starts_at' => optional($challenge->starts_at)?->format('Y-m-d\TH:i'), 'ends_at' => optional($challenge->ends_at)?->format('Y-m-d\TH:i'), ], ]); } public function storeCategory(AdminStoreNovaCardCategoryRequest $request): JsonResponse { $category = NovaCardCategory::query()->create($request->validated()); return response()->json([ 'category' => [ 'id' => (int) $category->id, 'slug' => (string) $category->slug, 'name' => (string) $category->name, 'description' => $category->description, 'active' => (bool) $category->active, 'order_num' => (int) $category->order_num, ], ]); } public function updateCategory(AdminStoreNovaCardCategoryRequest $request, NovaCardCategory $category): JsonResponse { $category->update($request->validated()); return response()->json([ 'category' => [ 'id' => (int) $category->id, 'slug' => (string) $category->slug, 'name' => (string) $category->name, 'description' => $category->description, 'active' => (bool) $category->active, 'order_num' => (int) $category->order_num, ], ]); } public function storeCollection(Request $request): JsonResponse { $payload = $request->validate([ 'user_id' => ['required', 'integer', 'exists:users,id'], 'slug' => ['nullable', 'string', 'max:140'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'description' => ['nullable', 'string', 'max:1000'], 'visibility' => ['required', 'in:private,public'], 'official' => ['nullable', 'boolean'], 'featured' => ['nullable', 'boolean'], ]); $collection = $this->collections->createManagedCollection($payload); return response()->json([ 'collection' => $this->presenter->collection($collection->fresh(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']), $request->user(), true), ]); } public function updateCollection(Request $request, NovaCardCollection $collection): JsonResponse { $payload = $request->validate([ 'user_id' => ['required', 'integer', 'exists:users,id'], 'slug' => ['required', 'string', 'max:140'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'description' => ['nullable', 'string', 'max:1000'], 'visibility' => ['required', 'in:private,public'], 'official' => ['nullable', 'boolean'], 'featured' => ['nullable', 'boolean'], ]); $collection->update([ 'user_id' => (int) $payload['user_id'], 'slug' => $payload['slug'], 'name' => $payload['name'], 'description' => $payload['description'] ?? null, 'visibility' => $payload['visibility'], 'official' => (bool) ($payload['official'] ?? false), 'featured' => (bool) ($payload['featured'] ?? false), ]); return response()->json([ 'collection' => $this->presenter->collection($collection->fresh(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']), $request->user(), true), ]); } public function storeCollectionCard(Request $request, NovaCardCollection $collection): JsonResponse { $payload = $request->validate([ 'card_id' => ['required', 'integer', 'exists:nova_cards,id'], 'note' => ['nullable', 'string', 'max:1000'], ]); $card = NovaCard::query()->findOrFail((int) $payload['card_id']); $this->collections->addCardToCollection($collection, $card, $payload['note'] ?? null); return response()->json([ 'collection' => $this->presenter->collection($collection->fresh(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']), $request->user(), true), ]); } public function destroyCollectionCard(Request $request, NovaCardCollection $collection, NovaCard $card): JsonResponse { $this->collections->removeCardFromCollection($collection, $card); return response()->json([ 'collection' => $this->presenter->collection($collection->fresh(['user', 'items.card.user.profile', 'items.card.category', 'items.card.template', 'items.card.backgroundImage', 'items.card.tags']), $request->user(), true), ]); } public function updateCard(AdminUpdateNovaCardRequest $request, NovaCard $card): JsonResponse { $attributes = $request->validated(); $requestedDisposition = $attributes['disposition'] ?? null; $hasModerationChange = array_key_exists('moderation_status', $attributes) && $attributes['moderation_status'] !== $card->moderation_status; $hasDispositionChange = array_key_exists('disposition', $attributes) && $requestedDisposition !== (($this->moderation->latestOverride($card) ?? [])['disposition'] ?? null); if (isset($attributes['featured']) && (! isset($attributes['status']) || $attributes['status'] !== NovaCard::STATUS_PUBLISHED)) { $attributes['featured'] = (bool) $attributes['featured'] && $card->status === NovaCard::STATUS_PUBLISHED; } unset($attributes['disposition']); $card->update($attributes); if ($hasModerationChange || $hasDispositionChange) { $card = $this->moderation->recordStaffOverride( $card->fresh(), (string) ($attributes['moderation_status'] ?? $card->moderation_status), $request->user(), 'admin_card_update', [ 'disposition' => $requestedDisposition, ], ); } return response()->json([ 'card' => $this->presenter->card($card->fresh()->loadMissing(['user.profile', 'category', 'template', 'backgroundImage', 'tags']), false, $request->user()), ]); } public function updateCreator(Request $request, User $user): JsonResponse { $attributes = $request->validate([ 'nova_featured_creator' => ['required', 'boolean'], ]); $user->update($attributes); $publicCardsCount = $user->novaCards()->publiclyVisible()->count(); $featuredCardsCount = $user->novaCards()->publiclyVisible()->where('featured', true)->count(); $totalViewsCount = (int) ($user->novaCards()->publiclyVisible()->sum('views_count') ?? 0); return response()->json([ 'creator' => [ 'id' => (int) $user->id, 'username' => (string) $user->username, 'name' => $user->name, 'display_name' => $user->name ?: '@' . $user->username, 'public_url' => route('cards.creator', ['username' => $user->username]), 'nova_featured_creator' => (bool) $user->nova_featured_creator, 'public_cards_count' => $publicCardsCount, 'featured_cards_count' => $featuredCardsCount, 'total_views_count' => $totalViewsCount, ], ]); } }