290 lines
16 KiB
PHP
290 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\NovaCards;
|
|
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardTemplate;
|
|
use Illuminate\Support\Arr;
|
|
|
|
class NovaCardProjectNormalizer
|
|
{
|
|
public function normalize(?array $project, ?NovaCardTemplate $template = null, array $attributes = [], ?NovaCard $card = null): array
|
|
{
|
|
return $this->upgradeToV3($project, $template, $attributes, $card);
|
|
}
|
|
|
|
public function isLegacyProject(?array $project): bool
|
|
{
|
|
return (int) Arr::get($project, 'schema_version', 1) < 3;
|
|
}
|
|
|
|
public function upgradeToV3(?array $project, ?NovaCardTemplate $template = null, array $attributes = [], ?NovaCard $card = null): array
|
|
{
|
|
// First normalize to v2 as the base, then layer on v3 additions.
|
|
$v2 = $this->upgradeToV2($project, $template, $attributes, $card);
|
|
|
|
// v3: enrich background with v3-specific fields.
|
|
$v3Background = array_merge($v2['background'], [
|
|
'brightness' => (int) Arr::get($project, 'background.brightness', Arr::get($attributes, 'brightness', 0)),
|
|
'contrast' => (int) Arr::get($project, 'background.contrast', Arr::get($attributes, 'contrast', 0)),
|
|
'texture_overlay' => (string) Arr::get($project, 'background.texture_overlay', Arr::get($attributes, 'texture_overlay', '')),
|
|
'gradient_direction' => (string) Arr::get($project, 'background.gradient_direction', Arr::get($attributes, 'gradient_direction', 'to-bottom')),
|
|
]);
|
|
|
|
// v3: enrich typography with v3-specific fields.
|
|
$v3Typography = array_merge($v2['typography'], [
|
|
'quote_mark_preset' => (string) Arr::get($project, 'typography.quote_mark_preset', Arr::get($attributes, 'quote_mark_preset', 'none')),
|
|
'text_panel_style' => (string) Arr::get($project, 'typography.text_panel_style', Arr::get($attributes, 'text_panel_style', 'none')),
|
|
'text_glow' => (bool) Arr::get($project, 'typography.text_glow', Arr::get($attributes, 'text_glow', false)),
|
|
'text_stroke' => (bool) Arr::get($project, 'typography.text_stroke', Arr::get($attributes, 'text_stroke', false)),
|
|
]);
|
|
|
|
// v3: canvas safe zones and layout anchors.
|
|
$v3Canvas = [
|
|
'snap_guides' => (bool) Arr::get($project, 'canvas.snap_guides', true),
|
|
'safe_zones' => (bool) Arr::get($project, 'canvas.safe_zones', true),
|
|
];
|
|
|
|
// v3: frame pack reference.
|
|
$v3Frame = [
|
|
'frame_preset' => (string) Arr::get($project, 'frame.frame_preset', Arr::get($attributes, 'frame_preset', 'none')),
|
|
'frame_color' => (string) Arr::get($project, 'frame.frame_color', Arr::get($attributes, 'frame_color', '')),
|
|
];
|
|
|
|
// v3: effects layer (glow overlays, vignette, etc.)
|
|
$v3Effects = [
|
|
'vignette' => (bool) Arr::get($project, 'effects.vignette', Arr::get($attributes, 'vignette', false)),
|
|
'vignette_strength' => (string) Arr::get($project, 'effects.vignette_strength', 'soft'),
|
|
'color_grade' => (string) Arr::get($project, 'effects.color_grade', Arr::get($attributes, 'color_grade', 'none')),
|
|
'glow_overlay' => (bool) Arr::get($project, 'effects.glow_overlay', Arr::get($attributes, 'glow_overlay', false)),
|
|
];
|
|
|
|
// v3: export preferences (what the creator last chose).
|
|
$v3ExportPrefs = [
|
|
'preferred_format' => (string) Arr::get($project, 'export_preferences.preferred_format', Arr::get($attributes, 'export_preferred_format', 'preview')),
|
|
'include_watermark' => (bool) Arr::get($project, 'export_preferences.include_watermark', true),
|
|
];
|
|
|
|
// v3: source/attribution context.
|
|
$v3SourceContext = [
|
|
'original_card_id' => Arr::get($v2, 'meta.remix.original_card_id'),
|
|
'root_card_id' => Arr::get($v2, 'meta.remix.root_card_id'),
|
|
'original_creator_id' => Arr::get($attributes, 'original_creator_id', $card?->original_creator_id),
|
|
'preset_id' => Arr::get($attributes, 'preset_id', Arr::get($project, 'source_context.preset_id')),
|
|
];
|
|
|
|
return array_merge($v2, [
|
|
'schema_version' => 3,
|
|
'meta' => array_merge($v2['meta'], [
|
|
'editor' => 'nova-cards-v3',
|
|
]),
|
|
'canvas' => $v3Canvas,
|
|
'background' => $v3Background,
|
|
'typography' => $v3Typography,
|
|
'frame' => $v3Frame,
|
|
'effects' => $v3Effects,
|
|
'export_preferences' => $v3ExportPrefs,
|
|
'source_context' => $v3SourceContext,
|
|
]);
|
|
}
|
|
|
|
public function upgradeToV2(?array $project, ?NovaCardTemplate $template = null, array $attributes = [], ?NovaCard $card = null): array
|
|
{
|
|
$project = is_array($project) ? $project : [];
|
|
$templateConfig = is_array($template?->config_json) ? $template->config_json : [];
|
|
$existingContent = is_array(Arr::get($project, 'content')) ? Arr::get($project, 'content') : [];
|
|
|
|
$title = trim((string) Arr::get($attributes, 'title', Arr::get($existingContent, 'title', $card?->title ?? 'Untitled card')));
|
|
$quoteText = trim((string) Arr::get($attributes, 'quote_text', Arr::get($existingContent, 'quote_text', $card?->quote_text ?? 'Your next quote starts here.')));
|
|
$quoteAuthor = trim((string) Arr::get($attributes, 'quote_author', Arr::get($existingContent, 'quote_author', $card?->quote_author ?? '')));
|
|
$quoteSource = trim((string) Arr::get($attributes, 'quote_source', Arr::get($existingContent, 'quote_source', $card?->quote_source ?? '')));
|
|
$textBlocks = $this->normalizeTextBlocks(Arr::wrap(Arr::get($project, 'text_blocks', [])), [
|
|
'title' => $title,
|
|
'quote_text' => $quoteText,
|
|
'quote_author' => $quoteAuthor,
|
|
'quote_source' => $quoteSource,
|
|
]);
|
|
|
|
[$syncedTitle, $syncedQuote, $syncedAuthor, $syncedSource] = $this->syncLegacyContent($textBlocks, [
|
|
'title' => $title,
|
|
'quote_text' => $quoteText,
|
|
'quote_author' => $quoteAuthor,
|
|
'quote_source' => $quoteSource,
|
|
]);
|
|
|
|
if (array_key_exists('title', $attributes)) {
|
|
$syncedTitle = $title;
|
|
}
|
|
|
|
if (array_key_exists('quote_text', $attributes)) {
|
|
$syncedQuote = $quoteText;
|
|
}
|
|
|
|
if (array_key_exists('quote_author', $attributes)) {
|
|
$syncedAuthor = $quoteAuthor;
|
|
}
|
|
|
|
if (array_key_exists('quote_source', $attributes)) {
|
|
$syncedSource = $quoteSource;
|
|
}
|
|
|
|
$gradientKey = (string) Arr::get($project, 'background.gradient_preset', Arr::get($attributes, 'gradient_preset', Arr::get($templateConfig, 'gradient_preset', 'midnight-nova')));
|
|
$fontKey = (string) Arr::get($project, 'typography.font_preset', Arr::get($attributes, 'font_preset', Arr::get($templateConfig, 'font_preset', 'modern-sans')));
|
|
$sourceLineEnabled = collect($textBlocks)->contains(fn (array $block): bool => ($block['type'] ?? null) === 'source' && (bool) ($block['enabled'] ?? true) && trim((string) ($block['text'] ?? '')) !== '');
|
|
$authorLineEnabled = collect($textBlocks)->contains(fn (array $block): bool => ($block['type'] ?? null) === 'author' && (bool) ($block['enabled'] ?? true) && trim((string) ($block['text'] ?? '')) !== '');
|
|
|
|
return [
|
|
'schema_version' => 2,
|
|
'template' => [
|
|
'id' => $template?->id ?? Arr::get($project, 'template.id'),
|
|
'slug' => $template?->slug ?? Arr::get($project, 'template.slug'),
|
|
],
|
|
'meta' => [
|
|
'editor' => 'nova-cards-v2',
|
|
'remix' => [
|
|
'original_card_id' => Arr::get($attributes, 'original_card_id', $card?->original_card_id),
|
|
'root_card_id' => Arr::get($attributes, 'root_card_id', $card?->root_card_id),
|
|
],
|
|
],
|
|
'content' => [
|
|
'title' => $syncedTitle,
|
|
'quote_text' => $syncedQuote,
|
|
'quote_author' => $syncedAuthor,
|
|
'quote_source' => $syncedSource,
|
|
],
|
|
'text_blocks' => $textBlocks,
|
|
'layout' => [
|
|
'layout' => (string) Arr::get($project, 'layout.layout', Arr::get($attributes, 'layout', Arr::get($templateConfig, 'layout', 'quote_heavy'))),
|
|
'position' => (string) Arr::get($project, 'layout.position', Arr::get($attributes, 'position', 'center')),
|
|
'alignment' => (string) Arr::get($project, 'layout.alignment', Arr::get($attributes, 'alignment', Arr::get($templateConfig, 'text_align', 'center'))),
|
|
'padding' => (string) Arr::get($project, 'layout.padding', Arr::get($attributes, 'padding', 'comfortable')),
|
|
'max_width' => (string) Arr::get($project, 'layout.max_width', Arr::get($attributes, 'max_width', 'balanced')),
|
|
],
|
|
'typography' => [
|
|
'font_preset' => $fontKey,
|
|
'text_color' => (string) Arr::get($project, 'typography.text_color', Arr::get($attributes, 'text_color', Arr::get($templateConfig, 'text_color', '#ffffff'))),
|
|
'accent_color' => (string) Arr::get($project, 'typography.accent_color', Arr::get($attributes, 'accent_color', '#e0f2fe')),
|
|
'quote_size' => (int) Arr::get($project, 'typography.quote_size', Arr::get($attributes, 'quote_size', 72)),
|
|
'author_size' => (int) Arr::get($project, 'typography.author_size', Arr::get($attributes, 'author_size', 28)),
|
|
'letter_spacing' => (int) Arr::get($project, 'typography.letter_spacing', Arr::get($attributes, 'letter_spacing', 0)),
|
|
'line_height' => (float) Arr::get($project, 'typography.line_height', Arr::get($attributes, 'line_height', 1.2)),
|
|
'shadow_preset' => (string) Arr::get($project, 'typography.shadow_preset', Arr::get($attributes, 'shadow_preset', 'soft')),
|
|
'author_line_enabled' => $authorLineEnabled,
|
|
'source_line_enabled' => $sourceLineEnabled,
|
|
],
|
|
'background' => [
|
|
'type' => (string) Arr::get($project, 'background.type', Arr::get($attributes, 'background_type', $card?->background_type ?? 'gradient')),
|
|
'gradient_preset' => $gradientKey,
|
|
'gradient_colors' => array_values(Arr::wrap(Arr::get($project, 'background.gradient_colors', Arr::get($attributes, 'gradient_colors', Arr::get(config('nova_cards.gradient_presets'), $gradientKey . '.colors', ['#0f172a', '#1d4ed8']))))),
|
|
'solid_color' => (string) Arr::get($project, 'background.solid_color', Arr::get($attributes, 'solid_color', '#111827')),
|
|
'background_image_id' => Arr::get($attributes, 'background_image_id', Arr::get($project, 'background.background_image_id', $card?->background_image_id)),
|
|
'overlay_style' => (string) Arr::get($project, 'background.overlay_style', Arr::get($attributes, 'overlay_style', Arr::get($templateConfig, 'overlay_style', 'dark-soft'))),
|
|
'focal_position' => (string) Arr::get($project, 'background.focal_position', Arr::get($attributes, 'focal_position', 'center')),
|
|
'blur_level' => (int) Arr::get($project, 'background.blur_level', Arr::get($attributes, 'blur_level', 0)),
|
|
'opacity' => (int) Arr::get($project, 'background.opacity', Arr::get($attributes, 'opacity', 50)),
|
|
],
|
|
'decorations' => array_values(Arr::wrap(Arr::get($project, 'decorations', Arr::get($attributes, 'decorations', [])))),
|
|
'assets' => [
|
|
'pack_ids' => array_values(array_filter(
|
|
array_map('intval', Arr::wrap(Arr::get($project, 'assets.pack_ids', Arr::get($attributes, 'asset_pack_ids', []))))
|
|
)),
|
|
'template_pack_ids' => array_values(array_filter(
|
|
array_map('intval', Arr::wrap(Arr::get($project, 'assets.template_pack_ids', Arr::get($attributes, 'template_pack_ids', []))))
|
|
)),
|
|
'items' => array_values(Arr::wrap(Arr::get($project, 'assets.items', []))),
|
|
],
|
|
];
|
|
}
|
|
|
|
public function normalizeForCard(NovaCard $card): array
|
|
{
|
|
return $this->normalize($card->project_json, $card->template, [
|
|
'title' => $card->title,
|
|
'quote_text' => $card->quote_text,
|
|
'quote_author' => $card->quote_author,
|
|
'quote_source' => $card->quote_source,
|
|
'background_type' => $card->background_type,
|
|
'background_image_id' => $card->background_image_id,
|
|
'original_card_id' => $card->original_card_id,
|
|
'root_card_id' => $card->root_card_id,
|
|
], $card);
|
|
}
|
|
|
|
public function syncTopLevelAttributes(array $project): array
|
|
{
|
|
[$title, $quoteText, $quoteAuthor, $quoteSource] = $this->syncLegacyContent(Arr::wrap(Arr::get($project, 'text_blocks', [])), Arr::get($project, 'content', []));
|
|
|
|
return [
|
|
'schema_version' => (int) Arr::get($project, 'schema_version', 3),
|
|
'title' => $title,
|
|
'quote_text' => $quoteText,
|
|
'quote_author' => $quoteAuthor !== '' ? $quoteAuthor : null,
|
|
'quote_source' => $quoteSource !== '' ? $quoteSource : null,
|
|
'background_type' => (string) Arr::get($project, 'background.type', 'gradient'),
|
|
'background_image_id' => Arr::get($project, 'background.background_image_id'),
|
|
];
|
|
}
|
|
|
|
private function normalizeTextBlocks(array $blocks, array $fallback): array
|
|
{
|
|
$normalized = collect($blocks)
|
|
->map(function ($block, int $index): array {
|
|
$block = is_array($block) ? $block : [];
|
|
|
|
return [
|
|
'key' => (string) Arr::get($block, 'key', 'block-' . ($index + 1)),
|
|
'type' => (string) Arr::get($block, 'type', 'body'),
|
|
'text' => (string) Arr::get($block, 'text', ''),
|
|
'enabled' => ! array_key_exists('enabled', $block) || (bool) Arr::get($block, 'enabled', true),
|
|
'style' => is_array(Arr::get($block, 'style')) ? Arr::get($block, 'style') : [],
|
|
];
|
|
})
|
|
->filter(fn (array $block): bool => $block['type'] !== '' && $block['key'] !== '')
|
|
->values();
|
|
|
|
if ($normalized->isEmpty()) {
|
|
$normalized = collect([
|
|
['key' => 'title', 'type' => 'title', 'text' => (string) ($fallback['title'] ?? ''), 'enabled' => true, 'style' => ['role' => 'eyebrow']],
|
|
['key' => 'quote', 'type' => 'quote', 'text' => (string) ($fallback['quote_text'] ?? ''), 'enabled' => true, 'style' => ['role' => 'headline']],
|
|
['key' => 'author', 'type' => 'author', 'text' => (string) ($fallback['quote_author'] ?? ''), 'enabled' => (string) ($fallback['quote_author'] ?? '') !== '', 'style' => ['role' => 'byline']],
|
|
['key' => 'source', 'type' => 'source', 'text' => (string) ($fallback['quote_source'] ?? ''), 'enabled' => (string) ($fallback['quote_source'] ?? '') !== '', 'style' => ['role' => 'caption']],
|
|
]);
|
|
}
|
|
|
|
return $normalized->take((int) config('nova_cards.validation.max_text_blocks', 8))->values()->all();
|
|
}
|
|
|
|
private function syncLegacyContent(array $blocks, array $fallback): array
|
|
{
|
|
$title = trim((string) ($fallback['title'] ?? 'Untitled card'));
|
|
$quoteText = trim((string) ($fallback['quote_text'] ?? 'Your next quote starts here.'));
|
|
$quoteAuthor = trim((string) ($fallback['quote_author'] ?? ''));
|
|
$quoteSource = trim((string) ($fallback['quote_source'] ?? ''));
|
|
|
|
foreach ($blocks as $block) {
|
|
if (! is_array($block) || ! ($block['enabled'] ?? true)) {
|
|
continue;
|
|
}
|
|
|
|
$text = trim((string) ($block['text'] ?? ''));
|
|
if ($text === '') {
|
|
continue;
|
|
}
|
|
|
|
$type = (string) ($block['type'] ?? '');
|
|
if ($type === 'title') {
|
|
$title = $text;
|
|
} elseif ($type === 'quote') {
|
|
$quoteText = $text;
|
|
} elseif ($type === 'author') {
|
|
$quoteAuthor = $text;
|
|
} elseif ($type === 'source') {
|
|
$quoteSource = $text;
|
|
}
|
|
}
|
|
|
|
return [$title !== '' ? $title : 'Untitled card', $quoteText !== '' ? $quoteText : 'Your next quote starts here.', $quoteAuthor, $quoteSource];
|
|
}
|
|
} |