publiclyVisible() ->latest('starts_at') ->paginate(12) ->withQueryString(); $challenges->getCollection()->transform(fn (AcademyChallenge $challenge): array => $this->access->challengePayload($challenge, $request->user(), true)); $seo = app(SeoFactory::class) ->collectionListing( 'Academy Challenges — Skinbase', 'Join Academy creative briefs, review accepted submissions, and explore current and archived AI Academy challenges.', route('academy.challenges.index'), ) ->toArray(); $seo = SeoDataBuilder::fromArray($seo) ->breadcrumbs([ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Challenges', 'url' => route('academy.challenges.index')], ]) ->build() ->toArray(); return Inertia::render('Academy/List', [ 'pageType' => 'challenges', 'title' => 'Academy challenges', 'description' => 'Creative briefs for wallpapers, worlds, covers, and prompt-driven visual experiments.', 'seo' => $seo, 'items' => $challenges, 'filters' => [], 'categories' => [], 'pricingUrl' => route('academy.pricing'), 'analytics' => [ 'enabled' => true, 'contentType' => null, 'contentId' => null, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_challenges_index', 'isPremium' => false, 'isGuest' => $request->user() === null, 'isSubscriber' => $request->user()?->hasAcademyCreatorAccess() || $request->user()?->hasAcademyProAccess(), ], ])->rootView('academy'); } public function show(Request $request, string $slug): Response { abort_unless((bool) config('academy.enabled', true), 404); abort_unless((bool) config('academy.challenges_enabled', true), 404); $challenge = AcademyChallenge::query() ->with(['submissions' => fn ($query) => $query->approved()->with(['user:id,name,username', 'artwork'])]) ->publiclyVisible() ->where('slug', $slug) ->firstOrFail(); $payload = $this->access->challengePayload($challenge, $request->user(), true); $payload['submissions'] = $challenge->submissions->map(fn ($submission): array => [ 'id' => (int) $submission->id, 'user' => $submission->user ? [ 'id' => (int) $submission->user->id, 'name' => (string) $submission->user->name, 'username' => (string) ($submission->user->username ?? ''), ] : null, 'artwork' => $submission->artwork ? [ 'id' => (int) $submission->artwork->id, 'title' => (string) ($submission->artwork->title ?? 'Untitled artwork'), 'thumb_url' => $submission->artwork->thumbUrl('md'), ] : null, 'submitted_at' => $submission->submitted_at?->toISOString(), ])->values()->all(); $canonical = route('academy.challenges.show', ['slug' => $challenge->slug]); $description = Str::limit((string) ($challenge->excerpt ?? $challenge->description ?? ''), 160, '...'); $seo = SeoDataBuilder::fromArray( app(SeoFactory::class)->collectionPage( $challenge->title . ' — Skinbase Academy', $description, $canonical, $challenge->cover_image, )->toArray() ) ->breadcrumbs([ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Challenges', 'url' => route('academy.challenges.index')], ['name' => (string) $challenge->title, 'url' => $canonical], ]) ->addJsonLd($this->challengeStructuredData($payload, $canonical, $description)) ->build() ->toArray(); $interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::CHALLENGE, (int) $challenge->id); return Inertia::render('Academy/Show', [ 'pageType' => 'challenge', 'item' => $payload, 'seo' => $seo, 'pricingUrl' => route('academy.pricing'), 'submitUrl' => $request->user() ? route('academy.challenges.submit', ['slug' => $challenge->slug]) : null, 'interaction' => $interaction, 'interactionRoutes' => [ 'like' => route('academy.interactions.like'), 'save' => route('academy.interactions.save'), ], 'loginUrl' => route('login'), 'analytics' => [ 'enabled' => true, 'contentType' => AcademyAnalyticsContentType::CHALLENGE, 'contentId' => (int) $challenge->id, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_challenge_show', 'isPremium' => (string) ($challenge->access_level ?? 'free') !== 'free', 'isGuest' => $request->user() === null, 'isSubscriber' => $request->user()?->hasAcademyCreatorAccess() || $request->user()?->hasAcademyProAccess(), 'isLocked' => (bool) ($payload['locked'] ?? false), ], ])->rootView('academy'); } /** * @param array $payload * @return array */ private function challengeStructuredData(array $payload, string $canonical, string $description): array { $image = trim((string) ($payload['cover_image'] ?? '')); $imageUrl = $image !== '' && preg_match('/^https?:\/\//i', $image) === 1 ? $image : ($image !== '' ? url($image) : null); $requiredTags = array_values((array) ($payload['required_tags'] ?? [])); $status = strtolower(trim((string) ($payload['status'] ?? ''))); $eventStatus = match ($status) { 'scheduled' => 'https://schema.org/EventScheduled', 'active', 'voting' => 'https://schema.org/EventInProgress', 'completed', 'archived' => 'https://schema.org/EventCompleted', default => null, }; return array_filter([ '@context' => 'https://schema.org', '@type' => 'Event', 'name' => (string) ($payload['title'] ?? 'Skinbase Academy challenge'), 'description' => $description, 'url' => $canonical, 'image' => $imageUrl, 'startDate' => $payload['starts_at'] ?? null, 'endDate' => $payload['ends_at'] ?? null, 'eventStatus' => $eventStatus, 'eventAttendanceMode' => 'https://schema.org/OnlineEventAttendanceMode', 'location' => [ '@type' => 'VirtualLocation', 'url' => $canonical, ], 'organizer' => [ '@type' => 'Organization', 'name' => config('seo.site_name', 'Skinbase'), 'url' => url('/'), ], 'keywords' => $requiredTags !== [] ? $requiredTags : null, 'isAccessibleForFree' => (string) ($payload['access_level'] ?? 'free') === 'free', ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []); } }