optimizations
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use App\Services\NovaCards\NovaCardAiAssistService;
|
||||
use App\Services\NovaCards\NovaCardPresenter;
|
||||
use App\Services\NovaCards\NovaCardRelatedCardsService;
|
||||
use App\Services\NovaCards\NovaCardRisingService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NovaCardDiscoveryController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NovaCardPresenter $presenter,
|
||||
private readonly NovaCardRisingService $rising,
|
||||
private readonly NovaCardRelatedCardsService $related,
|
||||
private readonly NovaCardAiAssistService $aiAssist,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/cards/rising
|
||||
* Returns recently published cards gaining traction fast.
|
||||
*/
|
||||
public function rising(Request $request): JsonResponse
|
||||
{
|
||||
$limit = min((int) $request->query('limit', 18), 36);
|
||||
$cards = $this->rising->risingCards($limit);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->cards($cards->all(), false, $request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/cards/{id}/related
|
||||
* Returns related cards for a given card.
|
||||
*/
|
||||
public function related(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = NovaCard::query()->publiclyVisible()->findOrFail($id);
|
||||
$limit = min((int) $request->query('limit', 8), 16);
|
||||
$relatedCards = $this->related->related($card, $limit);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->cards($relatedCards->all(), false, $request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/cards/{id}/ai-suggest
|
||||
* Returns AI-assist suggestions for the given draft card.
|
||||
* The creator must own the card.
|
||||
*/
|
||||
public function suggest(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = NovaCard::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($id);
|
||||
|
||||
$suggestions = $this->aiAssist->allSuggestions($card);
|
||||
|
||||
return response()->json([
|
||||
'data' => $suggestions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
161
app/Http/Controllers/Api/NovaCards/NovaCardDraftController.php
Normal file
161
app/Http/Controllers/Api/NovaCards/NovaCardDraftController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Events\NovaCards\NovaCardBackgroundUploaded;
|
||||
use App\Events\NovaCards\NovaCardPublished;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\NovaCards\SaveNovaCardDraftRequest;
|
||||
use App\Http\Requests\NovaCards\StoreNovaCardDraftRequest;
|
||||
use App\Http\Requests\NovaCards\UploadNovaCardBackgroundRequest;
|
||||
use App\Models\NovaCard;
|
||||
use App\Services\NovaCards\NovaCardBackgroundService;
|
||||
use App\Services\NovaCards\NovaCardDraftService;
|
||||
use App\Services\NovaCards\NovaCardPresenter;
|
||||
use App\Services\NovaCards\NovaCardPublishService;
|
||||
use App\Services\NovaCards\NovaCardRenderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NovaCardDraftController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NovaCardDraftService $drafts,
|
||||
private readonly NovaCardBackgroundService $backgrounds,
|
||||
private readonly NovaCardRenderService $renders,
|
||||
private readonly NovaCardPublishService $publishes,
|
||||
private readonly NovaCardPresenter $presenter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function store(StoreNovaCardDraftRequest $request): JsonResponse
|
||||
{
|
||||
$card = $this->drafts->createDraft($request->user(), $request->validated());
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(SaveNovaCardDraftRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
$card = $this->drafts->autosave($card, $request->validated());
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function autosave(SaveNovaCardDraftRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
$card = $this->drafts->autosave($card, $request->validated());
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
'meta' => [
|
||||
'saved_at' => now()->toISOString(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function background(UploadNovaCardBackgroundRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
$background = $this->backgrounds->storeUploadedBackground($request->user(), $request->file('background'));
|
||||
|
||||
$card = $this->drafts->autosave($card, [
|
||||
'background_type' => 'upload',
|
||||
'background_image_id' => $background->id,
|
||||
'project_json' => [
|
||||
'background' => [
|
||||
'type' => 'upload',
|
||||
'background_image_id' => $background->id,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
event(new NovaCardBackgroundUploaded($card, $background));
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
'background' => [
|
||||
'id' => (int) $background->id,
|
||||
'processed_url' => $background->processedUrl(),
|
||||
'width' => (int) $background->width,
|
||||
'height' => (int) $background->height,
|
||||
],
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
public function render(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
$result = $this->renders->render($card->loadMissing('backgroundImage'));
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card->fresh()->loadMissing(['user.profile', 'category', 'template', 'backgroundImage', 'tags']), true, $request->user()),
|
||||
'render' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function publish(SaveNovaCardDraftRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
|
||||
if ($request->validated() !== []) {
|
||||
$card = $this->drafts->autosave($card, $request->validated());
|
||||
}
|
||||
|
||||
if (trim((string) $card->title) === '' || trim((string) $card->quote_text) === '') {
|
||||
return response()->json([
|
||||
'message' => 'Title and quote text are required before publishing.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$card = $this->publishes->publishNow($card->loadMissing('backgroundImage'));
|
||||
event(new NovaCardPublished($card));
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presenter->card($card, true, $request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->editableCard($request, $id);
|
||||
|
||||
if ($card->status === NovaCard::STATUS_PUBLISHED && in_array($card->visibility, [NovaCard::VISIBILITY_PUBLIC, NovaCard::VISIBILITY_UNLISTED], true)) {
|
||||
return response()->json([
|
||||
'message' => 'Published cards cannot be deleted from the draft API.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$card->delete();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function editableCard(Request $request, int $id): NovaCard
|
||||
{
|
||||
return NovaCard::query()
|
||||
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Jobs\UpdateNovaCardStatsJob;
|
||||
use App\Events\NovaCards\NovaCardDownloaded;
|
||||
use App\Events\NovaCards\NovaCardShared;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NovaCardEngagementController extends Controller
|
||||
{
|
||||
public function share(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->card($request, $id);
|
||||
$card->increment('shares_count');
|
||||
$card->refresh();
|
||||
UpdateNovaCardStatsJob::dispatch($card->id);
|
||||
|
||||
event(new NovaCardShared($card, $request->user()?->id));
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'shares_count' => (int) $card->shares_count,
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->card($request, $id);
|
||||
abort_unless($card->allow_download && $card->previewUrl() !== null, 404);
|
||||
|
||||
$card->increment('downloads_count');
|
||||
$card->refresh();
|
||||
UpdateNovaCardStatsJob::dispatch($card->id);
|
||||
|
||||
event(new NovaCardDownloaded($card, $request->user()?->id));
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'downloads_count' => (int) $card->downloads_count,
|
||||
'download_url' => $card->previewUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function card(Request $request, int $id): NovaCard
|
||||
{
|
||||
$card = NovaCard::query()
|
||||
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
|
||||
->published()
|
||||
->findOrFail($id);
|
||||
|
||||
abort_unless($card->canBeViewedBy($request->user()), 404);
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\NovaCardExport;
|
||||
use App\Services\NovaCards\NovaCardExportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NovaCardExportController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NovaCardExportService $exports,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an export for the given card.
|
||||
*
|
||||
* POST /api/cards/{id}/export
|
||||
*/
|
||||
public function store(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = NovaCard::query()
|
||||
->where(function ($q) use ($request): void {
|
||||
// Owner can export any status; others can only export published cards.
|
||||
$q->where('user_id', $request->user()->id)
|
||||
->orWhere(function ($inner) use ($request): void {
|
||||
$inner->where('status', NovaCard::STATUS_PUBLISHED)
|
||||
->where('visibility', NovaCard::VISIBILITY_PUBLIC)
|
||||
->where('allow_export', true);
|
||||
});
|
||||
})
|
||||
->findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'export_type' => ['required', 'string', 'in:' . implode(',', array_keys(NovaCardExportService::EXPORT_SPECS))],
|
||||
'options' => ['sometimes', 'array'],
|
||||
]);
|
||||
|
||||
$export = $this->exports->requestExport(
|
||||
$request->user(),
|
||||
$card,
|
||||
$data['export_type'],
|
||||
(array) ($data['options'] ?? []),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->exports->getStatus($export),
|
||||
], $export->wasRecentlyCreated ? Response::HTTP_ACCEPTED : Response::HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll export status.
|
||||
*
|
||||
* GET /api/cards/exports/{exportId}
|
||||
*/
|
||||
public function show(Request $request, int $exportId): JsonResponse
|
||||
{
|
||||
$export = NovaCardExport::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($exportId);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->exports->getStatus($export),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Jobs\UpdateNovaCardStatsJob;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\NovaCardChallenge;
|
||||
use App\Models\NovaCardReaction;
|
||||
use App\Models\NovaCardVersion;
|
||||
use App\Services\NovaCards\NovaCardChallengeService;
|
||||
use App\Services\NovaCards\NovaCardCollectionService;
|
||||
use App\Services\NovaCards\NovaCardDraftService;
|
||||
use App\Services\NovaCards\NovaCardLineageService;
|
||||
use App\Services\NovaCards\NovaCardPresenter;
|
||||
use App\Services\NovaCards\NovaCardReactionService;
|
||||
use App\Services\NovaCards\NovaCardVersionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NovaCardInteractionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NovaCardReactionService $reactions,
|
||||
private readonly NovaCardCollectionService $collections,
|
||||
private readonly NovaCardDraftService $drafts,
|
||||
private readonly NovaCardVersionService $versions,
|
||||
private readonly NovaCardChallengeService $challenges,
|
||||
private readonly NovaCardLineageService $lineage,
|
||||
private readonly NovaCardPresenter $presenter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function lineage(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$card = $this->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);
|
||||
}
|
||||
}
|
||||
131
app/Http/Controllers/Api/NovaCards/NovaCardPresetController.php
Normal file
131
app/Http/Controllers/Api/NovaCards/NovaCardPresetController.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\NovaCards;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\NovaCardCreatorPreset;
|
||||
use App\Services\NovaCards\NovaCardCreatorPresetService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NovaCardPresetController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NovaCardCreatorPresetService $presets,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->query('type');
|
||||
$items = $this->presets->listForUser(
|
||||
$request->user(),
|
||||
is_string($type) && in_array($type, NovaCardCreatorPreset::TYPES, true) ? $type : null,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $items->map(fn ($p) => $this->presets->toArray($p))->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'preset_type' => ['required', 'string', 'in:' . implode(',', NovaCardCreatorPreset::TYPES)],
|
||||
'config_json' => ['required', 'array'],
|
||||
'is_default' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$preset = $this->presets->create($request->user(), $data);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presets->toArray($preset),
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$preset = NovaCardCreatorPreset::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['sometimes', 'string', 'max:120'],
|
||||
'config_json' => ['sometimes', 'array'],
|
||||
'is_default' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$preset = $this->presets->update($request->user(), $preset, $data);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presets->toArray($preset),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$preset = NovaCardCreatorPreset::query()->findOrFail($id);
|
||||
|
||||
$this->presets->delete($request->user(), $preset);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a preset from an existing published card.
|
||||
*/
|
||||
public function captureFromCard(Request $request, int $cardId): JsonResponse
|
||||
{
|
||||
$card = NovaCard::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($cardId);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'preset_type' => ['required', 'string', 'in:' . implode(',', NovaCardCreatorPreset::TYPES)],
|
||||
]);
|
||||
|
||||
$preset = $this->presets->captureFromCard(
|
||||
$request->user(),
|
||||
$card,
|
||||
$data['name'],
|
||||
$data['preset_type'],
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $this->presets->toArray($preset),
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a saved preset to a draft card, returning a project_json patch.
|
||||
*/
|
||||
public function applyToCard(Request $request, int $presetId, int $cardId): JsonResponse
|
||||
{
|
||||
$preset = NovaCardCreatorPreset::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->findOrFail($presetId);
|
||||
|
||||
$card = NovaCard::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereIn('status', [NovaCard::STATUS_DRAFT, NovaCard::STATUS_PUBLISHED])
|
||||
->findOrFail($cardId);
|
||||
|
||||
$currentProject = is_array($card->project_json) ? $card->project_json : [];
|
||||
$patch = $this->presets->applyToProjectPatch($preset, $currentProject);
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'preset' => $this->presets->toArray($preset),
|
||||
'project_patch' => $patch,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user