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(); } }