236 lines
9.0 KiB
PHP
236 lines
9.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\NovaCards;
|
|
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardCreatorPreset;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class NovaCardCreatorPresetService
|
|
{
|
|
/** Maximum presets per user across all types. */
|
|
public const MAX_TOTAL = 30;
|
|
|
|
/** Maximum presets per type per user. */
|
|
public const MAX_PER_TYPE = 8;
|
|
|
|
public function listForUser(User $user, ?string $presetType = null): Collection
|
|
{
|
|
return NovaCardCreatorPreset::query()
|
|
->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(),
|
|
];
|
|
}
|
|
}
|