where('user_id', $user->id) ->when($presetType !== null, fn ($q) => $q->where('preset_type', $presetType)) ->orderByDesc('is_default') ->orderBy('name') ->get(); } public function create(User $user, array $data): NovaCardCreatorPreset { $type = (string) Arr::get($data, 'preset_type', NovaCardCreatorPreset::TYPE_STYLE); // Enforce per-type limit. $typeCount = NovaCardCreatorPreset::query() ->where('user_id', $user->id) ->where('preset_type', $type) ->count(); if ($typeCount >= self::MAX_PER_TYPE) { abort(422, 'Maximum number of ' . $type . ' presets reached (' . self::MAX_PER_TYPE . ').'); } // Enforce total limit. $totalCount = NovaCardCreatorPreset::query()->where('user_id', $user->id)->count(); if ($totalCount >= self::MAX_TOTAL) { abort(422, 'Maximum total presets reached (' . self::MAX_TOTAL . ').'); } $preset = NovaCardCreatorPreset::query()->create([ 'user_id' => $user->id, 'name' => (string) Arr::get($data, 'name', 'My preset'), 'preset_type' => $type, 'config_json' => $this->sanitizeConfig((array) Arr::get($data, 'config_json', []), $type), 'is_default' => false, ]); if ((bool) Arr::get($data, 'is_default', false)) { $this->setDefault($user, $preset); } return $preset->refresh(); } public function update(User $user, NovaCardCreatorPreset $preset, array $data): NovaCardCreatorPreset { $this->authorizeOwner($user, $preset); $preset->fill([ 'name' => (string) Arr::get($data, 'name', $preset->name), 'config_json' => $this->sanitizeConfig( (array) Arr::get($data, 'config_json', $preset->config_json ?? []), $preset->preset_type, ), ]); $preset->save(); if (array_key_exists('is_default', $data) && (bool) $data['is_default']) { $this->setDefault($user, $preset); } return $preset->refresh(); } public function delete(User $user, NovaCardCreatorPreset $preset): void { $this->authorizeOwner($user, $preset); $preset->delete(); } public function setDefault(User $user, NovaCardCreatorPreset|int $preset): void { if (is_int($preset)) { $preset = NovaCardCreatorPreset::query()->findOrFail($preset); } $this->authorizeOwner($user, $preset); // Clear existing defaults of the same type before setting new one. NovaCardCreatorPreset::query() ->where('user_id', $user->id) ->where('preset_type', $preset->preset_type) ->where('id', '!=', $preset->id) ->update(['is_default' => false]); $preset->update(['is_default' => true]); } /** * Capture a preset from a published or saved card project JSON. * Extracts only the fields relevant to the given preset type. */ public function captureFromCard(User $user, NovaCard $card, string $presetName, string $presetType): NovaCardCreatorPreset { $project = is_array($card->project_json) ? $card->project_json : []; $config = $this->extractFromProject($project, $presetType); return $this->create($user, [ 'name' => $presetName, 'preset_type' => $presetType, 'config_json' => $config, ]); } /** * Apply a preset config on top of the given project patch array. * Returns the merged patch to be passed into the draft autosave flow. */ public function applyToProjectPatch(NovaCardCreatorPreset $preset, array|NovaCard $currentProject): array { if ($currentProject instanceof NovaCard) { $currentProject = is_array($currentProject->project_json) ? $currentProject->project_json : []; } $config = $preset->config_json ?? []; return match ($preset->preset_type) { NovaCardCreatorPreset::TYPE_TYPOGRAPHY => [ 'typography' => array_merge( (array) Arr::get($currentProject, 'typography', []), (array) (Arr::get($config, 'typography', $config)), ), ], NovaCardCreatorPreset::TYPE_LAYOUT => [ 'layout' => array_merge( (array) Arr::get($currentProject, 'layout', []), (array) (Arr::get($config, 'layout', $config)), ), ], NovaCardCreatorPreset::TYPE_BACKGROUND => [ 'background' => array_merge( (array) Arr::get($currentProject, 'background', []), (array) (Arr::get($config, 'background', $config)), ), ], NovaCardCreatorPreset::TYPE_STYLE => array_filter([ 'typography' => isset($config['typography']) ? array_merge((array) Arr::get($currentProject, 'typography', []), (array) $config['typography']) : null, 'background' => isset($config['background']) ? array_merge((array) Arr::get($currentProject, 'background', []), (array) $config['background']) : null, 'effects' => isset($config['effects']) ? array_merge((array) Arr::get($currentProject, 'effects', []), (array) $config['effects']) : null, ]), NovaCardCreatorPreset::TYPE_STARTER => $config, default => [], }; } private function extractFromProject(array $project, string $presetType): array { return match ($presetType) { NovaCardCreatorPreset::TYPE_TYPOGRAPHY => array_intersect_key( (array) Arr::get($project, 'typography', []), array_flip(['font_preset', 'text_color', 'accent_color', 'quote_size', 'author_size', 'letter_spacing', 'line_height', 'shadow_preset', 'quote_mark_preset', 'text_panel_style', 'text_glow', 'text_stroke']), ), NovaCardCreatorPreset::TYPE_LAYOUT => array_intersect_key( (array) Arr::get($project, 'layout', []), array_flip(['layout', 'position', 'alignment', 'padding', 'max_width']), ), NovaCardCreatorPreset::TYPE_BACKGROUND => array_intersect_key( (array) Arr::get($project, 'background', []), array_flip(['type', 'gradient_preset', 'gradient_colors', 'solid_color', 'overlay_style', 'blur_level', 'opacity', 'brightness', 'contrast', 'texture_overlay', 'gradient_direction']), ), NovaCardCreatorPreset::TYPE_STYLE => [ 'typography' => array_intersect_key( (array) Arr::get($project, 'typography', []), array_flip(['font_preset', 'text_color', 'accent_color', 'shadow_preset', 'quote_mark_preset', 'text_panel_style']), ), 'background' => array_intersect_key( (array) Arr::get($project, 'background', []), array_flip(['gradient_preset', 'gradient_colors', 'overlay_style']), ), 'effects' => (array) Arr::get($project, 'effects', []), ], NovaCardCreatorPreset::TYPE_STARTER => array_intersect_key($project, array_flip([ 'template', 'layout', 'typography', 'background', 'decorations', 'effects', 'frame', ])), default => [], }; } private function sanitizeConfig(array $config, string $type): array { // Strip deeply nested unknowns and limit total size. $encoded = json_encode($config); if ($encoded === false || mb_strlen($encoded) > 32_768) { abort(422, 'Preset config is too large.'); } return $config; } private function authorizeOwner(User $user, NovaCardCreatorPreset $preset): void { if ((int) $preset->user_id !== (int) $user->id) { abort(403); } } public function toArray(NovaCardCreatorPreset $preset): array { return [ 'id' => (int) $preset->id, 'name' => (string) $preset->name, 'preset_type' => (string) $preset->preset_type, 'config_json' => $preset->config_json ?? [], 'is_default' => (bool) $preset->is_default, 'created_at' => optional($preset->created_at)?->toISOString(), ]; } }