with(['category', 'template', 'backgroundImage', 'tags', 'user.profile']) ->where('user_id', $request->user()->id) ->latest('updated_at') ->paginate(18) ->withQueryString(); $baseQuery = NovaCard::query()->where('user_id', $request->user()->id); return Inertia::render('Studio/StudioCardsIndex', [ 'cards' => $this->presenter->paginator($cards, false, $request->user()), 'stats' => [ 'all' => (clone $baseQuery)->count(), 'drafts' => (clone $baseQuery)->where('status', NovaCard::STATUS_DRAFT)->count(), 'processing' => (clone $baseQuery)->where('status', NovaCard::STATUS_PROCESSING)->count(), 'published' => (clone $baseQuery)->where('status', NovaCard::STATUS_PUBLISHED)->count(), ], 'editorOptions' => $this->presenter->options(), 'endpoints' => [ 'create' => route('studio.cards.create'), 'editPattern' => route('studio.cards.edit', ['id' => '__CARD__']), 'previewPattern' => route('studio.cards.preview', ['id' => '__CARD__']), 'analyticsPattern' => route('studio.cards.analytics', ['id' => '__CARD__']), 'draftStore' => route('api.cards.drafts.store'), ], ]); } public function create(Request $request): Response { $options = $this->presenter->optionsWithPresets($this->presenter->options(), $request->user()); return Inertia::render('Studio/StudioCardEditor', [ 'card' => null, 'previewMode' => false, 'mobileSteps' => $this->mobileSteps(), 'editorOptions' => $options, 'endpoints' => $this->editorEndpoints(), ]); } public function edit(Request $request, int $id): Response { $card = $this->ownedCard($request, $id); $options = $this->presenter->optionsWithPresets($this->presenter->options(), $request->user()); return Inertia::render('Studio/StudioCardEditor', [ 'card' => $this->presenter->card($card, true, $request->user()), 'versions' => $this->versionPayloads($card), 'previewMode' => false, 'mobileSteps' => $this->mobileSteps(), 'editorOptions' => $options, 'endpoints' => $this->editorEndpoints(), ]); } public function preview(Request $request, int $id): Response { $card = $this->ownedCard($request, $id); $options = $this->presenter->optionsWithPresets($this->presenter->options(), $request->user()); return Inertia::render('Studio/StudioCardEditor', [ 'card' => $this->presenter->card($card, true, $request->user()), 'versions' => $this->versionPayloads($card), 'previewMode' => true, 'mobileSteps' => $this->mobileSteps(), 'editorOptions' => $options, 'endpoints' => $this->editorEndpoints(), ]); } public function analytics(Request $request, int $id): Response { $card = $this->ownedCard($request, $id); return Inertia::render('Studio/StudioCardAnalytics', [ 'card' => $this->presenter->card($card, false, $request->user()), 'analytics' => [ 'views' => (int) $card->views_count, 'likes' => (int) $card->likes_count, 'favorites' => (int) $card->favorites_count, 'saves' => (int) $card->saves_count, 'remixes' => (int) $card->remixes_count, 'comments' => (int) $card->comments_count, 'challenge_entries' => (int) $card->challenge_entries_count, 'shares' => (int) $card->shares_count, 'downloads' => (int) $card->downloads_count, 'trending_score' => (float) $card->trending_score, 'last_engaged_at' => optional($card->last_engaged_at)?->toDayDateTimeString(), ], ]); } public function remix(Request $request, int $id): RedirectResponse { $source = NovaCard::query()->published()->findOrFail($id); abort_unless($source->canBeViewedBy($request->user()), 404); $card = app(\App\Services\NovaCards\NovaCardDraftService::class)->createRemix($request->user(), $source->loadMissing('tags')); return redirect()->route('studio.cards.edit', ['id' => $card->id]); } private function mobileSteps(): array { return [ ['key' => 'format', 'label' => 'Format', 'description' => 'Choose the canvas shape and basic direction.'], ['key' => 'background', 'label' => 'Template & Background', 'description' => 'Pick the visual foundation for the card.'], ['key' => 'content', 'label' => 'Text', 'description' => 'Write the quote, author, and source.'], ['key' => 'style', 'label' => 'Style', 'description' => 'Fine-tune typography and layout.'], ['key' => 'preview', 'label' => 'Preview', 'description' => 'Check the live composition before publish.'], ['key' => 'publish', 'label' => 'Publish', 'description' => 'Review metadata and release settings.'], ]; } private function editorEndpoints(): array { return [ 'draftStore' => route('api.cards.drafts.store'), 'draftShowPattern' => route('api.cards.drafts.show', ['id' => '__CARD__']), 'draftUpdatePattern' => route('api.cards.drafts.update', ['id' => '__CARD__']), 'draftAutosavePattern' => route('api.cards.drafts.autosave', ['id' => '__CARD__']), 'draftBackgroundPattern' => route('api.cards.drafts.background', ['id' => '__CARD__']), 'draftRenderPattern' => route('api.cards.drafts.render', ['id' => '__CARD__']), 'draftPublishPattern' => route('api.cards.drafts.publish', ['id' => '__CARD__']), 'draftDeletePattern' => route('api.cards.drafts.destroy', ['id' => '__CARD__']), 'draftVersionsPattern' => route('api.cards.drafts.versions', ['id' => '__CARD__']), 'draftRestorePattern' => route('api.cards.drafts.restore', ['id' => '__CARD__', 'versionId' => '__VERSION__']), 'remixPattern' => route('api.cards.remix', ['id' => '__CARD__']), 'duplicatePattern' => route('api.cards.duplicate', ['id' => '__CARD__']), 'collectionsIndex' => route('api.cards.collections.index'), 'collectionsStore' => route('api.cards.collections.store'), 'savePattern' => route('api.cards.save', ['id' => '__CARD__']), 'likePattern' => route('api.cards.like', ['id' => '__CARD__']), 'favoritePattern' => route('api.cards.favorite', ['id' => '__CARD__']), 'challengeSubmitPattern' => route('api.cards.challenges.submit', ['challengeId' => '__CHALLENGE__', 'id' => '__CARD__']), 'studioCards' => route('studio.cards.index'), 'studioAnalyticsPattern' => route('studio.cards.analytics', ['id' => '__CARD__']), // v3 endpoints 'presetsIndex' => route('api.cards.presets.index'), 'presetsStore' => route('api.cards.presets.store'), 'presetUpdatePattern' => route('api.cards.presets.update', ['id' => '__PRESET__']), 'presetDestroyPattern' => route('api.cards.presets.destroy', ['id' => '__PRESET__']), 'presetApplyPattern' => route('api.cards.presets.apply', ['presetId' => '__PRESET__', 'cardId' => '__CARD__']), 'capturePresetPattern' => route('api.cards.presets.capture', ['cardId' => '__CARD__']), 'aiSuggestPattern' => route('api.cards.ai-suggest', ['id' => '__CARD__']), 'exportPattern' => route('api.cards.export.store', ['id' => '__CARD__']), 'exportStatusPattern' => route('api.cards.exports.show', ['exportId' => '__EXPORT__']), ]; } private function versionPayloads(NovaCard $card): array { return $card->versions()->latest('version_number')->get()->map(fn ($version): array => [ 'id' => (int) $version->id, 'version_number' => (int) $version->version_number, 'label' => $version->label, 'created_at' => $version->created_at?->toISOString(), 'snapshot_json' => is_array($version->snapshot_json) ? $version->snapshot_json : [], ])->values()->all(); } private function ownedCard(Request $request, int $id): NovaCard { return NovaCard::query() ->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'versions']) ->where('user_id', $request->user()->id) ->findOrFail($id); } }