resolveTemplate(Arr::get($attributes, 'template_id')); $category = $this->resolveCategory(Arr::get($attributes, 'category_id')); $title = trim((string) Arr::get($attributes, 'title', 'Untitled card')); $quote = trim((string) Arr::get($attributes, 'quote_text', 'Your next quote starts here.')); $project = $this->normalizer->upgradeToV2(null, $template, $attributes); $topLevel = $this->normalizer->syncTopLevelAttributes($project); $originalCardId = Arr::get($attributes, 'original_card_id'); $rootCardId = Arr::get($attributes, 'root_card_id', $originalCardId); $card = NovaCard::query()->create([ 'user_id' => $user->id, 'category_id' => $category?->id, 'template_id' => $template?->id, 'title' => Str::limit($topLevel['title'] ?: $title, (int) config('nova_cards.validation.title_max', 120), ''), 'slug' => $this->generateUniqueSlug($title), 'quote_text' => Str::limit($topLevel['quote_text'] ?: $quote, (int) config('nova_cards.validation.quote_max', 420), ''), 'quote_author' => $topLevel['quote_author'], 'quote_source' => $topLevel['quote_source'], 'description' => Arr::get($attributes, 'description'), 'format' => $this->resolveFormat((string) Arr::get($attributes, 'format', NovaCard::FORMAT_SQUARE)), 'project_json' => $project, 'schema_version' => (int) $topLevel['schema_version'], 'visibility' => NovaCard::VISIBILITY_PRIVATE, 'status' => NovaCard::STATUS_DRAFT, 'moderation_status' => NovaCard::MOD_PENDING, 'background_type' => $topLevel['background_type'], 'background_image_id' => $topLevel['background_image_id'], 'allow_download' => true, 'allow_remix' => (bool) Arr::get($attributes, 'allow_remix', true), 'allow_background_reuse' => (bool) Arr::get($attributes, 'allow_background_reuse', false), 'allow_export' => (bool) Arr::get($attributes, 'allow_export', true), 'style_family' => Arr::get($attributes, 'style_family'), 'palette_family' => Arr::get($attributes, 'palette_family'), 'original_card_id' => $originalCardId, 'root_card_id' => $rootCardId, ]); $this->tagService->syncTags($card, Arr::wrap(Arr::get($attributes, 'tags', []))); $this->versions->snapshot($card->fresh(['template']), $user, $originalCardId ? 'Remix draft created' : 'Initial draft', true); event(new NovaCardCreated($card->fresh()->load(['category', 'template', 'tags']))); if ($card->template_id !== null) { event(new NovaCardTemplateSelected($card->fresh()->load(['category', 'template', 'tags']), null, (int) $card->template_id)); } return $card->load(['category', 'template', 'tags']); } public function autosave(NovaCard $card, array $payload): NovaCard { $currentProject = $this->normalizer->upgradeToV2(is_array($card->project_json) ? $card->project_json : [], $card->template, [], $card); $template = $this->resolveTemplateId($payload, $card) ? NovaCardTemplate::query()->find($this->resolveTemplateId($payload, $card)) : $card->template; $projectPatch = is_array(Arr::get($payload, 'project_json')) ? Arr::get($payload, 'project_json') : []; $normalizedProject = $this->normalizer->upgradeToV2( array_replace_recursive($currentProject, $projectPatch), $template, array_merge($payload, [ 'title' => Arr::get($payload, 'title', $card->title), 'quote_text' => Arr::get($payload, 'quote_text', $card->quote_text), 'quote_author' => Arr::get($payload, 'quote_author', $card->quote_author), 'quote_source' => Arr::get($payload, 'quote_source', $card->quote_source), 'background_type' => Arr::get($payload, 'background_type', $card->background_type), 'background_image_id' => Arr::get($payload, 'background_image_id', $card->background_image_id), 'original_card_id' => $card->original_card_id, 'root_card_id' => $card->root_card_id, ]), $card, ); $topLevel = $this->normalizer->syncTopLevelAttributes($normalizedProject); if (array_key_exists('title', $payload)) { $topLevel['title'] = trim((string) $payload['title']); } if (array_key_exists('quote_text', $payload)) { $topLevel['quote_text'] = trim((string) $payload['quote_text']); } if (array_key_exists('quote_author', $payload)) { $topLevel['quote_author'] = (($value = trim((string) $payload['quote_author'])) !== '') ? $value : null; } if (array_key_exists('quote_source', $payload)) { $topLevel['quote_source'] = (($value = trim((string) $payload['quote_source'])) !== '') ? $value : null; } $previousTemplateId = $card->template_id ? (int) $card->template_id : null; $card->fill([ 'title' => Str::limit($topLevel['title'], (int) config('nova_cards.validation.title_max', 120), ''), 'quote_text' => Str::limit($topLevel['quote_text'], (int) config('nova_cards.validation.quote_max', 420), ''), 'quote_author' => $topLevel['quote_author'], 'quote_source' => $topLevel['quote_source'], 'description' => Arr::get($payload, 'description', $card->description), 'format' => $this->resolveFormat((string) Arr::get($payload, 'format', $card->format)), 'project_json' => $normalizedProject, 'schema_version' => (int) $topLevel['schema_version'], 'background_type' => $topLevel['background_type'], 'background_image_id' => $topLevel['background_image_id'], 'template_id' => $this->resolveTemplateId($payload, $card), 'category_id' => $this->resolveCategoryId($payload, $card), 'visibility' => Arr::get($payload, 'visibility', $card->visibility), 'allow_download' => (bool) Arr::get($payload, 'allow_download', $card->allow_download), 'allow_remix' => (bool) Arr::get($payload, 'allow_remix', $card->allow_remix), 'allow_background_reuse' => (bool) Arr::get($payload, 'allow_background_reuse', $card->allow_background_reuse ?? false), 'allow_export' => (bool) Arr::get($payload, 'allow_export', $card->allow_export ?? true), 'style_family' => Arr::get($payload, 'style_family', $card->style_family), 'palette_family' => Arr::get($payload, 'palette_family', $card->palette_family), 'editor_mode_last_used' => Arr::get($payload, 'editor_mode_last_used', $card->editor_mode_last_used), ]); if ($card->isDirty('title')) { $card->slug = $this->generateUniqueSlug($card->title, $card->id); } $card->save(); $changes = $card->getChanges(); if (array_key_exists('tags', $payload)) { $this->tagService->syncTags($card, Arr::wrap(Arr::get($payload, 'tags', []))); $changes['tags'] = true; } if ($changes !== []) { $fresh = $card->fresh()->load(['category', 'template', 'tags']); $this->versions->snapshot($fresh->loadMissing('template'), $fresh->user, Arr::get($payload, 'version_label')); event(new NovaCardAutosaved($fresh, array_keys($changes))); $currentTemplateId = $fresh->template_id ? (int) $fresh->template_id : null; if ($currentTemplateId !== $previousTemplateId && $currentTemplateId !== null) { event(new NovaCardTemplateSelected($fresh, $previousTemplateId, $currentTemplateId)); } } return $card->refresh()->load(['category', 'template', 'tags']); } public function createRemix(User $user, NovaCard $source, array $attributes = []): NovaCard { $baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template')); $payload = array_merge([ 'title' => 'Remix of ' . $source->title, 'quote_text' => $source->quote_text, 'quote_author' => $source->quote_author, 'quote_source' => $source->quote_source, 'description' => $source->description, 'format' => $source->format, 'background_type' => $source->background_type, 'background_image_id' => $source->background_image_id, 'template_id' => $source->template_id, 'category_id' => $source->category_id, 'tags' => $source->tags->pluck('name')->all(), 'project_json' => $baseProject, 'original_card_id' => $source->id, 'root_card_id' => $source->root_card_id ?: $source->id, ], $attributes); return $this->createDraft($user, $payload); } public function createDuplicate(User $user, NovaCard $source, array $attributes = []): NovaCard { abort_unless($source->isOwnedBy($user), 403); $baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template')); $payload = array_merge([ 'title' => 'Copy of ' . $source->title, 'quote_text' => $source->quote_text, 'quote_author' => $source->quote_author, 'quote_source' => $source->quote_source, 'description' => $source->description, 'format' => $source->format, 'background_type' => $source->background_type, 'background_image_id' => $source->background_image_id, 'template_id' => $source->template_id, 'category_id' => $source->category_id, 'tags' => $source->tags->pluck('name')->all(), 'project_json' => $baseProject, 'allow_remix' => $source->allow_remix, ], $attributes); return $this->createDraft($user, $payload); } private function buildProjectPayload(?NovaCardTemplate $template, array $attributes): array { return $this->normalizer->normalize(null, $template, $attributes); } private function generateUniqueSlug(string $title, ?int $ignoreId = null): string { $base = Str::slug($title); if ($base === '') { $base = 'nova-card'; } $slug = $base; $suffix = 2; while (NovaCard::query() ->when($ignoreId !== null, fn ($query) => $query->where('id', '!=', $ignoreId)) ->where('slug', $slug) ->exists()) { $slug = $base . '-' . $suffix; $suffix++; } return $slug; } private function resolveTemplate(mixed $templateId): ?NovaCardTemplate { if ($templateId) { return NovaCardTemplate::query()->find($templateId); } return NovaCardTemplate::query()->where('active', true)->orderBy('order_num')->first(); } private function resolveCategory(mixed $categoryId): ?NovaCardCategory { if ($categoryId) { return NovaCardCategory::query()->find($categoryId); } return NovaCardCategory::query()->where('active', true)->orderBy('order_num')->first(); } private function resolveFormat(string $format): string { $formats = array_keys((array) config('nova_cards.formats', [])); return in_array($format, $formats, true) ? $format : NovaCard::FORMAT_SQUARE; } private function resolveTemplateId(array $payload, NovaCard $card): ?int { if (! array_key_exists('template_id', $payload)) { return $card->template_id; } return $this->resolveTemplate(Arr::get($payload, 'template_id'))?->id; } private function resolveCategoryId(array $payload, NovaCard $card): ?int { if (! array_key_exists('category_id', $payload)) { return $card->category_id; } return $this->resolveCategory(Arr::get($payload, 'category_id'))?->id; } }