, warnings: list, page_count: int}} */ public function generateFromWorld(World $world, ?User $actor = null, int $pages = 7, bool $force = false, bool $publish = false, bool $dryRun = false): array { $pageCount = max(5, min(10, $pages)); $existing = WorldWebStory::query()->where('world_id', $world->id)->orderByDesc('id')->first(); if ($existing && ! $force && ! $dryRun) { throw ValidationException::withMessages([ 'world' => ['A web story already exists for this world. Use --force to rebuild it.'], ]); } $selectedArtworks = $this->candidateArtworks($world)->take(max(3, $pageCount - 3))->values(); $storyAttributes = [ 'world_id' => $world->id, 'slug' => $existing?->slug ?: $this->uniqueSlug($world->slug, $existing?->id), 'title' => $existing?->title ?: (string) $world->title, 'subtitle' => $world->tagline, 'excerpt' => $world->summary ?: $world->tagline, 'description' => $world->description ?: $world->summary, 'seo_title' => trim((string) ($world->seo_title ?: ($world->title . ' – Skinbase Web Story'))), 'seo_description' => trim((string) ($world->seo_description ?: $world->summary ?: $world->description ?: '')), 'status' => WorldWebStory::STATUS_DRAFT, 'active' => true, 'noindex' => false, 'featured' => false, 'updated_by' => $actor?->id, ]; if (! $existing) { $storyAttributes['created_by'] = $actor?->id; } $pagePayloads = $this->buildPagePayloads($world, $selectedArtworks, $pageCount); if ($dryRun) { $story = $existing ?? new WorldWebStory($storyAttributes); $story->fill($storyAttributes); $story->setRelation('orderedPages', collect($pagePayloads)->map(fn (array $page): WorldWebStoryPage => new WorldWebStoryPage($page))); $this->assets->buildAssets($story, force: $force, dryRun: true); $validation = $this->validation->validate($story); return [ 'story' => $story, 'created' => ! $existing, 'validation' => $validation, ]; } $story = DB::transaction(function () use ($existing, $storyAttributes, $pagePayloads): WorldWebStory { $story = $existing ?? new WorldWebStory(); $story->fill($storyAttributes); $story->save(); $story->pages()->delete(); foreach ($pagePayloads as $pagePayload) { $story->pages()->create($pagePayload); } return $story->fresh(['orderedPages', 'world']); }); $this->assets->buildAssets($story, force: $force); $story->refresh()->load('orderedPages', 'world'); if ($publish) { $this->validation->assertPublishable($story); $story->forceFill([ 'status' => WorldWebStory::STATUS_PUBLISHED, 'published_at' => now(), ])->save(); } return [ 'story' => $story->fresh(['orderedPages', 'world']), 'created' => ! $existing, 'validation' => $this->validation->validate($story), ]; } /** * @return Collection */ private function candidateArtworks(World $world): Collection { $relationIds = $world->worldRelations() ->where('related_type', 'artwork') ->orderByDesc('is_featured') ->orderBy('sort_order') ->pluck('related_id') ->map(fn ($id): int => (int) $id) ->filter() ->values(); $artworks = collect(); if ($relationIds->isNotEmpty()) { $artworks = Artwork::query() ->whereIn('id', $relationIds) ->get() ->sortBy(fn (Artwork $artwork): int => $relationIds->search((int) $artwork->id)) ->values(); } if ($artworks->count() < 3) { $submissionArtworks = WorldSubmission::query() ->with('artwork.user') ->where('world_id', $world->id) ->where('status', WorldSubmission::STATUS_LIVE) ->orderByDesc('is_featured') ->orderByDesc('featured_at') ->orderByDesc('id') ->get() ->pluck('artwork') ->filter(fn ($artwork): bool => $artwork instanceof Artwork); $artworks = $artworks->concat($submissionArtworks)->unique(fn (Artwork $artwork): int => (int) $artwork->id)->values(); } return $artworks; } /** * @param Collection $artworks * @return list> */ private function buildPagePayloads(World $world, Collection $artworks, int $pageCount): array { $primaryArtwork = $artworks->get(0); $secondaryArtwork = $artworks->get(1) ?: $primaryArtwork; $tertiaryArtwork = $artworks->get(2) ?: $secondaryArtwork; $pages = [ [ 'position' => 1, 'layout' => WorldWebStoryPage::LAYOUT_COVER, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => (string) $world->title, 'body' => Str::limit((string) ($world->tagline ?: $world->summary ?: 'A cinematic Skinbase World.'), 160, ''), 'caption' => 'Skinbase World', 'alt_text' => (string) $world->title, 'text_position' => 'bottom', 'overlay_strength' => 45, 'animation' => 'fade-in', 'active' => true, ], [ 'position' => 2, 'layout' => WorldWebStoryPage::LAYOUT_MOOD, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => 'Step into ' . $world->title, 'body' => Str::limit((string) ($world->summary ?: $world->description ?: 'Curated visuals, featured creators, and a clear editorial mood.'), 170, ''), 'caption' => 'World intro', 'alt_text' => 'Intro for ' . $world->title, 'text_position' => 'bottom', 'overlay_strength' => 35, 'animation' => 'fly-in-bottom', 'active' => true, ], ]; if ($primaryArtwork instanceof Artwork) { $pages[] = [ 'position' => count($pages) + 1, 'layout' => WorldWebStoryPage::LAYOUT_ARTWORK, 'artwork_id' => $primaryArtwork->id, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => (string) ($primaryArtwork->title ?: 'Featured artwork'), 'body' => Str::limit('A featured visual from ' . $world->title . ' by ' . ($primaryArtwork->user?->name ?: $primaryArtwork->user?->username ?: 'a Skinbase creator') . '.', 160, ''), 'caption' => 'Featured artwork', 'alt_text' => (string) ($primaryArtwork->title ?: 'Featured artwork'), 'text_position' => 'bottom', 'overlay_strength' => 35, 'animation' => 'pan-left', 'active' => true, ]; } if ($secondaryArtwork instanceof Artwork) { $pages[] = [ 'position' => count($pages) + 1, 'layout' => WorldWebStoryPage::LAYOUT_CREATOR, 'artwork_id' => $secondaryArtwork->id, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => 'Creator spotlight', 'body' => Str::limit(($secondaryArtwork->user?->name ?: $secondaryArtwork->user?->username ?: 'A featured creator') . ' helps define the mood of ' . $world->title . '.', 160, ''), 'caption' => 'Creator spotlight', 'alt_text' => (string) ($secondaryArtwork->title ?: 'Creator spotlight artwork'), 'text_position' => 'bottom', 'overlay_strength' => 40, 'animation' => 'fade-in', 'active' => true, ]; } if ($tertiaryArtwork instanceof Artwork) { $pages[] = [ 'position' => count($pages) + 1, 'layout' => WorldWebStoryPage::LAYOUT_COLLECTION, 'artwork_id' => $tertiaryArtwork->id, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => 'More from this World', 'body' => Str::limit('Explore more wallpapers, digital art, and creator picks collected inside ' . $world->title . '.', 155, ''), 'caption' => 'Community picks', 'alt_text' => (string) ($tertiaryArtwork->title ?: 'World picks'), 'text_position' => 'bottom', 'overlay_strength' => 35, 'animation' => 'pan-right', 'active' => true, ]; } while (count($pages) < max(5, $pageCount - 1)) { $pages[] = [ 'position' => count($pages) + 1, 'layout' => WorldWebStoryPage::LAYOUT_MOOD, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => 'Inside the theme', 'body' => Str::limit('A short visual pause that keeps the story connected to ' . $world->title . '.', 150, ''), 'caption' => 'World mood', 'alt_text' => 'Mood page for ' . $world->title, 'text_position' => 'bottom', 'overlay_strength' => 35, 'animation' => 'fade-in', 'active' => true, ]; } $pages[] = [ 'position' => count($pages) + 1, 'layout' => WorldWebStoryPage::LAYOUT_CTA, 'background_type' => WorldWebStoryPage::BACKGROUND_IMAGE, 'headline' => 'Explore ' . $world->title, 'body' => Str::limit('Open the full World page for the complete artwork grid, featured picks, and related creator content.', 160, ''), 'caption' => 'Continue on Skinbase', 'cta_label' => 'View World', 'cta_url' => $world->publicUrl(), 'alt_text' => 'Explore ' . $world->title . ' on Skinbase', 'text_position' => 'bottom', 'overlay_strength' => 45, 'animation' => 'pulse', 'active' => true, ]; return collect($pages) ->take($pageCount) ->values() ->map(fn (array $page, int $index): array => array_merge($page, [ 'position' => $index + 1, ])) ->all(); } private function uniqueSlug(string $base, ?int $ignoreId = null): string { $candidate = Str::slug($base) ?: 'web-story'; $slug = $candidate; $suffix = 2; while (WorldWebStory::query()->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId))->where('slug', $slug)->exists()) { $slug = $candidate . '-' . $suffix; $suffix++; } return $slug; } }