visibleCard($request, $id); return response()->json([ 'data' => $this->lineage->resolve($card, $request->user()), ]); } public function like(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $state = $this->reactions->setReaction($request->user(), $card, NovaCardReaction::TYPE_LIKE, true); return response()->json(['ok' => true, ...$state]); } public function unlike(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $state = $this->reactions->setReaction($request->user(), $card, NovaCardReaction::TYPE_LIKE, false); return response()->json(['ok' => true, ...$state]); } public function favorite(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $state = $this->reactions->setReaction($request->user(), $card, NovaCardReaction::TYPE_FAVORITE, true); return response()->json(['ok' => true, ...$state]); } public function unfavorite(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $state = $this->reactions->setReaction($request->user(), $card, NovaCardReaction::TYPE_FAVORITE, false); return response()->json(['ok' => true, ...$state]); } public function collections(Request $request): JsonResponse { return response()->json([ 'data' => $this->collections->listCollections($request->user()), ]); } public function storeCollection(Request $request): JsonResponse { $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'slug' => ['nullable', 'string', 'max:140'], 'description' => ['nullable', 'string', 'max:500'], 'visibility' => ['nullable', 'in:private,public'], ]); $collection = $this->collections->createCollection($request->user(), $payload); return response()->json([ 'collection' => [ 'id' => (int) $collection->id, 'slug' => (string) $collection->slug, 'name' => (string) $collection->name, 'description' => $collection->description, 'visibility' => (string) $collection->visibility, 'cards_count' => (int) $collection->cards_count, ], ], Response::HTTP_CREATED); } public function updateCollection(Request $request, int $id): JsonResponse { $collection = $this->ownedCollection($request, $id); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'slug' => ['nullable', 'string', 'max:140'], 'description' => ['nullable', 'string', 'max:500'], 'visibility' => ['nullable', 'in:private,public'], ]); $collection->update([ 'name' => $payload['name'], 'slug' => $payload['slug'] ?: $collection->slug, 'description' => $payload['description'] ?? null, 'visibility' => $payload['visibility'] ?? $collection->visibility, ]); 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 storeCollectionItem(Request $request, int $id): JsonResponse { $collection = $this->ownedCollection($request, $id); $payload = $request->validate([ 'card_id' => ['required', 'integer'], 'note' => ['nullable', 'string', 'max:500'], 'sort_order' => ['nullable', 'integer', 'min:0'], ]); $card = $this->visibleCard($request, (int) $payload['card_id']); $this->collections->addCardToCollection($collection, $card, $payload['note'] ?? null, $payload['sort_order'] ?? 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), ], Response::HTTP_CREATED); } public function destroyCollectionItem(Request $request, int $id, int $cardId): JsonResponse { $collection = $this->ownedCollection($request, $id); $card = NovaCard::query()->findOrFail($cardId); $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 challenges(Request $request): JsonResponse { return response()->json([ 'data' => $this->presenter->options()['challenge_feed'] ?? [], ]); } public function assets(Request $request): JsonResponse { return response()->json([ 'data' => $this->presenter->options()['asset_packs'] ?? [], ]); } public function templates(Request $request): JsonResponse { return response()->json([ 'packs' => $this->presenter->options()['template_packs'] ?? [], 'templates' => $this->presenter->options()['templates'] ?? [], ]); } public function save(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $payload = $request->validate([ 'collection_id' => ['nullable', 'integer'], 'note' => ['nullable', 'string', 'max:500'], ]); $collection = $this->collections->saveCard($request->user(), $card, $payload['collection_id'] ?? null, $payload['note'] ?? null); return response()->json([ 'ok' => true, 'saves_count' => (int) $card->fresh()->saves_count, 'collection' => [ 'id' => (int) $collection->id, 'name' => (string) $collection->name, 'slug' => (string) $collection->slug, ], ]); } public function unsave(Request $request, int $id): JsonResponse { $card = $this->visibleCard($request, $id); $payload = $request->validate([ 'collection_id' => ['nullable', 'integer'], ]); $this->collections->unsaveCard($request->user(), $card, $payload['collection_id'] ?? null); return response()->json([ 'ok' => true, 'saves_count' => (int) $card->fresh()->saves_count, ]); } public function remix(Request $request, int $id): JsonResponse { $source = $this->visibleCard($request, $id); abort_unless($source->allow_remix, 422, 'This card does not allow remixes.'); $card = $this->drafts->createRemix($request->user(), $source); $source->increment('remixes_count'); UpdateNovaCardStatsJob::dispatch($source->id); return response()->json([ 'data' => $this->presenter->card($card, true, $request->user()), ], Response::HTTP_CREATED); } public function duplicate(Request $request, int $id): JsonResponse { $source = $this->ownedCard($request, $id); $card = $this->drafts->createDuplicate($request->user(), $source->loadMissing('tags')); return response()->json([ 'data' => $this->presenter->card($card, true, $request->user()), ], Response::HTTP_CREATED); } public function versions(Request $request, int $id): JsonResponse { $card = $this->ownedCard($request, $id); $versions = $card->versions() ->latest('version_number') ->get() ->map(fn (NovaCardVersion $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(); return response()->json(['data' => $versions]); } public function restoreVersion(Request $request, int $id, int $versionId): JsonResponse { $card = $this->ownedCard($request, $id); $version = $card->versions()->findOrFail($versionId); $card = $this->versions->restore($card, $version, $request->user()); return response()->json([ 'data' => $this->presenter->card($card, true, $request->user()), ]); } public function submitChallenge(Request $request, int $challengeId, int $id): JsonResponse { $payload = $request->validate([ 'note' => ['nullable', 'string', 'max:500'], ]); return $this->submitChallengeWithPayload($request, $challengeId, $id, $payload['note'] ?? null); } public function submitChallengeByChallenge(Request $request, int $id): JsonResponse { $payload = $request->validate([ 'card_id' => ['required', 'integer'], 'note' => ['nullable', 'string', 'max:500'], ]); return $this->submitChallengeWithPayload($request, $id, (int) $payload['card_id'], $payload['note'] ?? null); } private function submitChallengeWithPayload(Request $request, int $challengeId, int $cardId, ?string $note = null): JsonResponse { $card = $this->ownedCard($request, $cardId); abort_unless($card->status === NovaCard::STATUS_PUBLISHED, 422, 'Publish the card before entering a challenge.'); $challenge = NovaCardChallenge::query() ->where('status', NovaCardChallenge::STATUS_ACTIVE) ->findOrFail($challengeId); $entry = $this->challenges->submit($request->user(), $challenge, $card, $note); UpdateNovaCardStatsJob::dispatch($card->id); return response()->json([ 'entry' => [ 'id' => (int) $entry->id, 'challenge_id' => (int) $entry->challenge_id, 'card_id' => (int) $entry->card_id, 'status' => (string) $entry->status, ], 'challenge_entries_count' => (int) $card->fresh()->challenge_entries_count, ]); } private function visibleCard(Request $request, int $id): NovaCard { $card = NovaCard::query() ->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags', 'originalCard', 'rootCard']) ->published() ->findOrFail($id); abort_unless($card->canBeViewedBy($request->user()), 404); return $card; } 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); } private function ownedCollection(Request $request, int $id): \App\Models\NovaCardCollection { return \App\Models\NovaCardCollection::query() ->where('user_id', $request->user()->id) ->findOrFail($id); } }