Files
SkinbaseNova/database/seeders/NovaCardDemoSeeder.php
2026-03-28 19:15:39 +01:00

358 lines
19 KiB
PHP

<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\NovaCard;
use App\Models\NovaCardAsset;
use App\Models\NovaCardAssetPack;
use App\Models\NovaCardCategory;
use App\Models\NovaCardChallenge;
use App\Models\NovaCardChallengeEntry;
use App\Models\NovaCardCollection;
use App\Models\NovaCardCollectionItem;
use App\Models\NovaCardTag;
use App\Models\NovaCardTemplate;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class NovaCardDemoSeeder extends Seeder
{
public function run(): void
{
if (! (bool) config('nova_cards.seed_demo_cards.enabled', false)) {
return;
}
$userConfig = (array) config('nova_cards.seed_demo_cards.user', []);
$user = User::query()->firstOrCreate(
['email' => (string) Arr::get($userConfig, 'email', 'nova-cards-demo@skinbase.test')],
[
'username' => (string) Arr::get($userConfig, 'username', 'nova.cards'),
'name' => (string) Arr::get($userConfig, 'name', 'Nova Cards'),
'password' => (string) Arr::get($userConfig, 'password', 'password'),
'role' => 'user',
]
);
$cards = [
[
'slug' => 'official-spark',
'title' => 'Official Spark',
'quote_text' => 'Small moments of focus turn into visible momentum.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Launch Collection',
'description' => 'An official Nova Cards demo card for featured browse surfaces.',
'category_slug' => 'motivation',
'template_slug' => 'neon-nova',
'format' => NovaCard::FORMAT_SQUARE,
'featured' => true,
'tags' => ['focus', 'launch'],
'project_json' => [
'layout' => ['layout' => 'quote_heavy', 'position' => 'center', 'alignment' => 'center', 'padding' => 'comfortable', 'max_width' => 'balanced'],
'typography' => ['font_preset' => 'bold-poster', 'text_color' => '#e0f2fe', 'accent_color' => '#ffffff', 'quote_size' => 80, 'author_size' => 24, 'letter_spacing' => 1, 'line_height' => 1.05, 'shadow_preset' => 'strong'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'midnight-nova', 'gradient_colors' => ['#0f172a', '#1d4ed8'], 'solid_color' => '#111827', 'overlay_style' => 'dark-strong', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 65],
],
],
[
'slug' => 'soft-breath',
'title' => 'Soft Breath',
'quote_text' => 'Rest is not a pause from growth. It is part of it.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Healing Notes',
'description' => 'A calm demo card showing the softer side of Nova Cards.',
'category_slug' => 'healing',
'template_slug' => 'soft-pastel',
'format' => NovaCard::FORMAT_PORTRAIT,
'featured' => false,
'tags' => ['healing', 'calm'],
'project_json' => [
'layout' => ['layout' => 'centered', 'position' => 'center', 'alignment' => 'center', 'padding' => 'airy', 'max_width' => 'compact'],
'typography' => ['font_preset' => 'soft-handwritten', 'text_color' => '#1f2937', 'accent_color' => '#6d28d9', 'quote_size' => 72, 'author_size' => 24, 'letter_spacing' => 0, 'line_height' => 1.2, 'shadow_preset' => 'none'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'soft-pastel', 'gradient_colors' => ['#f9a8d4', '#c4b5fd'], 'solid_color' => '#f5d0fe', 'overlay_style' => 'light-soft', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 35],
],
],
[
'slug' => 'night-echo',
'title' => 'Night Echo',
'quote_text' => 'Not every quiet room is empty. Some are full of answers.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Dark Mood Study',
'description' => 'A darker official demo card for mood-oriented discovery blocks.',
'category_slug' => 'dark-mood',
'template_slug' => 'cinematic-dark',
'format' => NovaCard::FORMAT_LANDSCAPE,
'featured' => false,
'tags' => ['night', 'mood'],
'project_json' => [
'layout' => ['layout' => 'minimal', 'position' => 'lower-middle', 'alignment' => 'left', 'padding' => 'comfortable', 'max_width' => 'wide'],
'typography' => ['font_preset' => 'modern-sans', 'text_color' => '#f8fafc', 'accent_color' => '#93c5fd', 'quote_size' => 68, 'author_size' => 20, 'letter_spacing' => 1, 'line_height' => 1.2, 'shadow_preset' => 'soft'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'deep-cinema', 'gradient_colors' => ['#020617', '#334155'], 'solid_color' => '#020617', 'overlay_style' => 'dark-strong', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 80],
],
],
[
'slug' => 'editorial-glow',
'title' => 'Editorial Glow',
'quote_text' => 'Design with restraint, then let one accent do the speaking.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Editorial Kit',
'description' => 'A crisp editorial-format demo card for official collections.',
'category_slug' => 'motivation',
'template_slug' => 'golden-serif',
'format' => NovaCard::FORMAT_PORTRAIT,
'featured' => true,
'tags' => ['editorial', 'gold'],
'project_json' => [
'layout' => ['layout' => 'author_emphasis', 'position' => 'center', 'alignment' => 'center', 'padding' => 'comfortable', 'max_width' => 'compact'],
'typography' => ['font_preset' => 'elegant-serif', 'text_color' => '#fffbeb', 'accent_color' => '#fbbf24', 'quote_size' => 76, 'author_size' => 24, 'letter_spacing' => 1, 'line_height' => 1.15, 'shadow_preset' => 'soft'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'amber-glow', 'gradient_colors' => ['#451a03', '#b45309'], 'solid_color' => '#451a03', 'overlay_style' => 'dark-soft', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 70],
],
],
[
'slug' => 'story-bloom',
'title' => 'Story Bloom',
'quote_text' => 'If the layout breathes, the words can reach further.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Story Vertical Pack',
'description' => 'A vertical story-oriented demo card for public browsing and challenges.',
'category_slug' => 'healing',
'template_slug' => 'story-vertical',
'format' => NovaCard::FORMAT_STORY,
'featured' => false,
'tags' => ['story', 'vertical'],
'project_json' => [
'layout' => ['layout' => 'centered', 'position' => 'center', 'alignment' => 'center', 'padding' => 'airy', 'max_width' => 'balanced'],
'typography' => ['font_preset' => 'modern-sans', 'text_color' => '#ecfdf5', 'accent_color' => '#6ee7b7', 'quote_size' => 82, 'author_size' => 24, 'letter_spacing' => 0, 'line_height' => 1.18, 'shadow_preset' => 'soft'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'emerald-bloom', 'gradient_colors' => ['#064e3b', '#10b981'], 'solid_color' => '#022c22', 'overlay_style' => 'dark-soft', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 65],
],
],
[
'slug' => 'remix-launch-variant',
'title' => 'Remix Launch Variant',
'quote_text' => 'Take the spark and give it a new rhythm.',
'quote_author' => 'Skinbase Nova',
'quote_source' => 'Remix Lab',
'description' => 'A seeded remix showing lineage in demo content.',
'category_slug' => 'motivation',
'template_slug' => 'bold-statement',
'format' => NovaCard::FORMAT_SQUARE,
'featured' => false,
'tags' => ['remix', 'launch'],
'project_json' => [
'layout' => ['layout' => 'quote_heavy', 'position' => 'center', 'alignment' => 'left', 'padding' => 'comfortable', 'max_width' => 'wide'],
'typography' => ['font_preset' => 'bold-poster', 'text_color' => '#ffffff', 'accent_color' => '#38bdf8', 'quote_size' => 80, 'author_size' => 22, 'letter_spacing' => 1, 'line_height' => 1.05, 'shadow_preset' => 'strong'],
'background' => ['type' => 'gradient', 'gradient_preset' => 'midnight-nova', 'gradient_colors' => ['#082f49', '#1d4ed8'], 'solid_color' => '#0f172a', 'overlay_style' => 'dark-strong', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 75],
],
],
];
$seededCards = collect();
foreach ($cards as $index => $definition) {
$category = NovaCardCategory::query()->where('slug', $definition['category_slug'])->first();
$template = NovaCardTemplate::query()->where('slug', $definition['template_slug'])->first();
$card = NovaCard::query()->updateOrCreate(
['slug' => $definition['slug']],
[
'uuid' => NovaCard::query()->where('slug', $definition['slug'])->value('uuid') ?: (string) Str::uuid(),
'user_id' => $user->id,
'category_id' => $category?->id,
'template_id' => $template?->id,
'title' => $definition['title'],
'quote_text' => $definition['quote_text'],
'quote_author' => $definition['quote_author'],
'quote_source' => $definition['quote_source'],
'description' => $definition['description'],
'format' => $definition['format'],
'project_json' => [
'content' => [
'title' => $definition['title'],
'quote_text' => $definition['quote_text'],
'quote_author' => $definition['quote_author'],
'quote_source' => $definition['quote_source'],
],
...$definition['project_json'],
'decorations' => [],
],
'render_version' => 1,
'background_type' => 'gradient',
'visibility' => NovaCard::VISIBILITY_PUBLIC,
'status' => NovaCard::STATUS_PUBLISHED,
'moderation_status' => NovaCard::MOD_APPROVED,
'featured' => (bool) $definition['featured'],
'allow_download' => true,
'views_count' => 25 - ($index * 4),
'shares_count' => 6 - $index,
'downloads_count' => 3 - min($index, 2),
'likes_count' => max(1, 8 - $index),
'favorites_count' => max(0, 5 - $index),
'saves_count' => max(0, 4 - $index),
'published_at' => now()->subDays($index + 1),
]
);
$tagIds = collect($definition['tags'])
->map(function (string $tag): int {
$model = NovaCardTag::query()->firstOrCreate(
['slug' => Str::slug($tag)],
['name' => Str::title(str_replace('-', ' ', $tag))]
);
return (int) $model->id;
})
->all();
$card->tags()->sync($tagIds);
$seededCards->push($card->fresh());
}
$source = $seededCards->firstWhere('slug', 'official-spark');
$remix = $seededCards->firstWhere('slug', 'remix-launch-variant');
if ($source && $remix) {
$remix->forceFill([
'original_card_id' => $source->id,
'root_card_id' => $source->id,
'remixes_count' => 0,
])->save();
$source->forceFill([
'remixes_count' => 1,
])->save();
}
foreach (array_merge(
(array) config('nova_cards.asset_packs', []),
(array) config('nova_cards.template_packs', [])
) as $index => $packConfig) {
$pack = NovaCardAssetPack::query()->updateOrCreate(
['slug' => (string) ($packConfig['slug'] ?? ('pack-' . $index))],
[
'name' => (string) ($packConfig['name'] ?? 'Nova Card Pack'),
'description' => $packConfig['description'] ?? null,
'type' => (string) ($packConfig['type'] ?? NovaCardAssetPack::TYPE_ASSET),
'manifest_json' => $packConfig['manifest_json'] ?? [],
'official' => (bool) ($packConfig['official'] ?? true),
'active' => (bool) ($packConfig['active'] ?? true),
'order_num' => $index,
]
);
foreach ((array) data_get($packConfig, 'manifest_json.items', []) as $itemIndex => $item) {
NovaCardAsset::query()->updateOrCreate(
['asset_pack_id' => $pack->id, 'asset_key' => (string) ($item['key'] ?? ('asset-' . $itemIndex))],
[
'label' => (string) ($item['label'] ?? 'Nova Asset'),
'type' => (string) ($item['type'] ?? 'glyph'),
'preview_image' => $item['preview_image'] ?? null,
'data_json' => $item,
'official' => true,
'active' => true,
'order_num' => $itemIndex,
]
);
}
}
$collection = NovaCardCollection::query()->updateOrCreate(
['user_id' => $user->id, 'slug' => 'editorial-favorites'],
[
'name' => 'Editorial Favorites',
'description' => 'Officially curated Nova Cards spotlighting launch visuals, remixes, and story-first layouts.',
'visibility' => NovaCardCollection::VISIBILITY_PUBLIC,
'official' => true,
'featured' => true,
'cards_count' => 0,
]
);
$challengeCollection = NovaCardCollection::query()->updateOrCreate(
['user_id' => $user->id, 'slug' => 'challenge-winners'],
[
'name' => 'Challenge Winners',
'description' => 'Official challenge highlights and standout seeded examples.',
'visibility' => NovaCardCollection::VISIBILITY_PUBLIC,
'official' => true,
'featured' => false,
'cards_count' => 0,
]
);
foreach ($seededCards->take(4)->values() as $position => $card) {
NovaCardCollectionItem::query()->updateOrCreate(
['collection_id' => $collection->id, 'card_id' => $card->id],
['sort_order' => $position + 1, 'note' => $position === 0 ? 'Launch anchor card for the collection.' : null],
);
}
$collection->forceFill(['cards_count' => NovaCardCollectionItem::query()->where('collection_id', $collection->id)->count()])->save();
foreach ($seededCards->slice(2, 4)->values() as $position => $card) {
NovaCardCollectionItem::query()->updateOrCreate(
['collection_id' => $challengeCollection->id, 'card_id' => $card->id],
['sort_order' => $position + 1, 'note' => $position === 0 ? 'Seeded challenge highlight.' : null],
);
}
$challengeCollection->forceFill(['cards_count' => NovaCardCollectionItem::query()->where('collection_id', $challengeCollection->id)->count()])->save();
$challenge = NovaCardChallenge::query()->updateOrCreate(
['slug' => 'launch-remix-run'],
[
'user_id' => $user->id,
'title' => 'Launch Remix Run',
'description' => 'A seeded official challenge for remix-ready launch cards.',
'prompt' => 'Remix one official card and push the layout into a fresh direction.',
'status' => NovaCardChallenge::STATUS_ACTIVE,
'official' => true,
'featured' => true,
'starts_at' => now()->subDays(2),
'ends_at' => now()->addDays(10),
]
);
$secondaryChallenge = NovaCardChallenge::query()->updateOrCreate(
['slug' => 'minimal-poster-week'],
[
'user_id' => $user->id,
'title' => 'Minimal Poster Week',
'description' => 'A seeded editorial challenge focused on restraint and poster typography.',
'prompt' => 'Build a bold quote poster with minimal decoration and strong hierarchy.',
'status' => NovaCardChallenge::STATUS_COMPLETED,
'official' => true,
'featured' => false,
'starts_at' => now()->subDays(14),
'ends_at' => now()->subDays(3),
]
);
foreach ($seededCards->take(3)->values() as $position => $card) {
NovaCardChallengeEntry::query()->updateOrCreate(
['challenge_id' => $challenge->id, 'card_id' => $card->id],
['user_id' => $user->id, 'status' => $position === 0 ? 'featured' : 'active', 'note' => $position === 0 ? 'Seeded featured entry.' : null],
);
}
foreach ($seededCards->slice(3, 3)->values() as $position => $card) {
NovaCardChallengeEntry::query()->updateOrCreate(
['challenge_id' => $secondaryChallenge->id, 'card_id' => $card->id],
['user_id' => $user->id, 'status' => $position === 0 ? 'winner' : 'active', 'note' => $position === 0 ? 'Seeded winner entry.' : null],
);
}
$challenge->forceFill([
'entries_count' => NovaCardChallengeEntry::query()->where('challenge_id', $challenge->id)->count(),
'winner_card_id' => $seededCards->first()?->id,
])->save();
$secondaryChallenge->forceFill([
'entries_count' => NovaCardChallengeEntry::query()->where('challenge_id', $secondaryChallenge->id)->count(),
'winner_card_id' => $seededCards->get(3)?->id,
])->save();
}
}