diff --git a/.env.example b/.env.example index c765ce9f..fa38c0fe 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,15 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost +SECURITY_REPORT_ENABLED=true +SECURITY_REPORT_NOTIFY_EMAIL= +SECURITY_REPORT_SCAN_NPM=true +SECURITY_REPORT_SCAN_COMPOSER=true +SECURITY_REPORT_STORE_RAW=true +SECURITY_REPORT_MAX_RAW_KB=512 +SECURITY_REPORT_FAIL_ON_HIGH=false +SECURITY_REPORT_FAIL_ON_CRITICAL=false + APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US diff --git a/app/Console/Commands/BuildSitemapsCommand.php b/app/Console/Commands/BuildSitemapsCommand.php index ffa4b3fb..40dd1a04 100644 --- a/app/Console/Commands/BuildSitemapsCommand.php +++ b/app/Console/Commands/BuildSitemapsCommand.php @@ -49,10 +49,10 @@ final class BuildSitemapsCommand extends Command $t = microtime(true); $this->line(' Building sitemap index…'); $index = $build->buildIndex(force: true, persist: false, families: $families); - $disk->put('sitemap.xml', $index['content']); + $disk->put('sitemaps/sitemap.xml', $index['content']); $written++; $this->line(sprintf( - ' ✔ sitemap.xml %d entries %.3fs', + ' ✔ sitemaps/sitemap.xml %d entries %.3fs', $index['url_count'], microtime(true) - $t, )); @@ -112,6 +112,36 @@ final class BuildSitemapsCommand extends Command )); } + foreach ($build->enabledGroupIndexes() as $groupName => $groupFamilies) { + foreach ($groupFamilies as $family) { + if (! in_array($family, $families, true)) { + continue 2; + } + } + + $t = microtime(true); + $this->line(sprintf(' Building grouped sitemap %s…', $groupName)); + + $built = $build->buildNamed($groupName, force: true, persist: false); + + if ($built === null) { + $this->line(sprintf(' – %s.xml SKIPPED (group builder returned null)', $groupName)); + $failed++; + + continue; + } + + $disk->put('sitemaps/' . $groupName . '.xml', $built['content']); + $written++; + + $this->line(sprintf( + ' ✔ %s %d entries %.3fs', + $groupName . '.xml', + $built['url_count'] ?? 0, + microtime(true) - $t, + )); + } + // ── Summary ─────────────────────────────────────────────────────── $this->newLine(); $this->info(sprintf( @@ -295,4 +325,4 @@ final class BuildSitemapsCommand extends Command return array_values(array_filter($enabled, fn (string $family): bool => in_array($family, $only, true))); } -} \ No newline at end of file +} diff --git a/app/Console/Commands/GenerateSitemapsCommand.php b/app/Console/Commands/GenerateSitemapsCommand.php index aeb5d04b..d613c35e 100644 --- a/app/Console/Commands/GenerateSitemapsCommand.php +++ b/app/Console/Commands/GenerateSitemapsCommand.php @@ -96,6 +96,35 @@ final class GenerateSitemapsCommand extends Command )); } + foreach ($build->enabledGroupIndexes() as $groupName => $groupFamilies) { + foreach ($groupFamilies as $family) { + if (! in_array($family, $families, true)) { + continue 2; + } + } + + $t = microtime(true); + $built = $build->buildNamed($groupName, force: true, persist: false); + + if ($built === null) { + $this->line(sprintf(' – %s.xml SKIPPED (group builder returned null)', $groupName)); + $failed++; + + continue; + } + + $path = 'sitemaps/' . $groupName . '.xml'; + $disk->put($path, $built['content']); + $written++; + + $this->line(sprintf( + ' ✔ %s %d entries %.3fs', + $groupName . '.xml', + $built['url_count'] ?? 0, + microtime(true) - $t, + )); + } + // ── Summary ─────────────────────────────────────────────────────── $this->newLine(); $this->info(sprintf( diff --git a/app/Console/Commands/SecurityReportScanCommand.php b/app/Console/Commands/SecurityReportScanCommand.php new file mode 100644 index 00000000..40566712 --- /dev/null +++ b/app/Console/Commands/SecurityReportScanCommand.php @@ -0,0 +1,83 @@ +warn('Security report scanning is disabled.'); + + return self::INVALID; + } + + $this->info('Running security report scan...'); + + $report = $scanner->scan( + (string) $this->option('triggered-by'), + $this->option('user-id') !== null ? (int) $this->option('user-id') : null, + ); + + if ($report->status === 'failed') { + $this->error('Security scan failed: ' . (string) ($report->error_message ?? 'Unknown error')); + + return self::FAILURE; + } + + $this->table( + ['Status', 'Critical', 'High', 'Medium', 'Low', 'Unknown', 'Composer outdated', 'npm outdated'], + [[ + $report->status, + $report->total_critical, + $report->total_high, + $report->total_medium, + $report->total_low, + $report->total_unknown, + $report->composer_outdated_count, + $report->npm_outdated_count, + ]], + ); + + if ((bool) $this->option('notify') && $report->hasDangerFindings()) { + $this->sendDangerNotification($report); + $this->info('Danger notification sent.'); + } + + if ($report->hasCriticalFindings() && (bool) config('security-report.fail_on.critical', false)) { + return self::FAILURE; + } + + if ($report->hasHighFindings() && (bool) config('security-report.fail_on.high', false)) { + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function sendDangerNotification(\App\Models\SecurityReport $report): void + { + $email = trim((string) config('security-report.notify_email', '')); + + if ($email === '') { + return; + } + + Notification::route('mail', $email) + ->notify(new SecurityReportDangerNotification($report)); + } +} diff --git a/app/Http/Controllers/Academy/AcademyBillingController.php b/app/Http/Controllers/Academy/AcademyBillingController.php index 514baec7..a0202a3f 100644 --- a/app/Http/Controllers/Academy/AcademyBillingController.php +++ b/app/Http/Controllers/Academy/AcademyBillingController.php @@ -363,8 +363,17 @@ final class AcademyBillingController extends Controller /** @var User|null $user */ $user = $request->user(); $currentTier = $this->access->currentTier($user); + $seo = \app(SeoFactory::class) + ->simplePage( + 'Academy Subscription Confirmed — Skinbase', + 'Payment confirmation for your Skinbase Academy subscription.', + \route('academy.billing.success'), + false, + ) + ->toArray(); return \Inertia\Inertia::render('Academy/Billing/Success', [ + 'seo' => $seo, 'message' => 'Payment is being confirmed. Your access will update automatically.', 'currentTier' => $currentTier, 'isSubscribed' => $user instanceof User ? $this->access->hasActiveAcademySubscription($user) : false, @@ -381,8 +390,17 @@ final class AcademyBillingController extends Controller public function cancel(): \Inertia\Response { \abort_unless((bool) \config('academy.enabled', true), 404); + $seo = \app(SeoFactory::class) + ->simplePage( + 'Academy Billing Canceled — Skinbase', + 'Checkout was canceled before starting a Skinbase Academy subscription.', + \route('academy.billing.cancel'), + false, + ) + ->toArray(); return \Inertia\Inertia::render('Academy/Billing/Cancel', [ + 'seo' => $seo, 'message' => 'Checkout was canceled. No payment was made.', 'links' => [ 'pricing' => \route('academy.pricing'), @@ -495,10 +513,19 @@ final class AcademyBillingController extends Controller /** @var User $user */ $user = $request->user(); $subscription = $this->academySubscription($user); + $seo = \app(SeoFactory::class) + ->simplePage( + 'Academy Subscription Account — Skinbase', + 'Manage your Skinbase Academy subscription and billing access.', + \route('academy.billing.account'), + false, + ) + ->toArray(); $activePlan = $this->activePlan($user); return \Inertia\Inertia::render('Academy/Billing/Account', [ + 'seo' => $seo, 'currentTier' => $this->access->currentTier($user), 'isSubscribed' => $this->access->hasActiveAcademySubscription($user), 'activePlan' => $activePlan ? [ diff --git a/app/Http/Controllers/Academy/AcademyChallengeController.php b/app/Http/Controllers/Academy/AcademyChallengeController.php index 95709855..8648fa83 100644 --- a/app/Http/Controllers/Academy/AcademyChallengeController.php +++ b/app/Http/Controllers/Academy/AcademyChallengeController.php @@ -10,6 +10,7 @@ use App\Services\Academy\AcademyAccessService; use App\Services\Academy\AcademyInteractionService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Http\Request; use Illuminate\Support\Str; use Inertia\Inertia; @@ -44,6 +45,13 @@ final class AcademyChallengeController extends Controller 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', @@ -94,12 +102,24 @@ final class AcademyChallengeController extends Controller 'submitted_at' => $submission->submitted_at?->toISOString(), ])->values()->all(); - $seo = app(SeoFactory::class)->collectionPage( - $challenge->title . ' — Skinbase Academy', - Str::limit((string) ($challenge->excerpt ?? $challenge->description ?? ''), 160, '...'), - route('academy.challenges.show', ['slug' => $challenge->slug]), - $challenge->cover_image, - )->toArray(); + $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); @@ -128,4 +148,46 @@ final class AcademyChallengeController extends Controller ], ])->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 !== []); + } } diff --git a/app/Http/Controllers/Academy/AcademyChallengeSubmissionController.php b/app/Http/Controllers/Academy/AcademyChallengeSubmissionController.php index 1669ac7b..2398ccb1 100644 --- a/app/Http/Controllers/Academy/AcademyChallengeSubmissionController.php +++ b/app/Http/Controllers/Academy/AcademyChallengeSubmissionController.php @@ -35,6 +35,8 @@ final class AcademyChallengeSubmissionController extends Controller 'Submit to ' . $challenge->title . ' — Skinbase Academy', 'Attach one of your artworks to this Academy challenge submission.', route('academy.challenges.submit', ['slug' => $challenge->slug]), + null, + false, )->toArray(); return Inertia::render('Academy/ChallengeSubmit', [ diff --git a/app/Http/Controllers/Academy/AcademyCourseController.php b/app/Http/Controllers/Academy/AcademyCourseController.php index c5c3f539..d9f4fa5f 100644 --- a/app/Http/Controllers/Academy/AcademyCourseController.php +++ b/app/Http/Controllers/Academy/AcademyCourseController.php @@ -14,6 +14,7 @@ use App\Services\Academy\AcademyCourseNavigationService; use App\Services\Academy\AcademyCourseProgressService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; @@ -37,6 +38,7 @@ final class AcademyCourseController extends Controller 'difficulty' => ['nullable', 'string', 'max:40'], 'access' => ['nullable', 'string', 'max:40'], ]); + $hasActiveFilters = filled($filters['difficulty'] ?? null) || filled($filters['access'] ?? null); $query = AcademyCourse::query()->published()->ordered(); @@ -77,6 +79,13 @@ final class AcademyCourseController extends Controller ) ->toArray(); + if ($hasActiveFilters) { + $seo = SeoDataBuilder::fromArray($seo) + ->indexable(false) + ->build() + ->toArray(); + } + return Inertia::render('Academy/CoursesIndex', [ 'seo' => $seo, 'title' => 'Academy courses', diff --git a/app/Http/Controllers/Academy/AcademyCourseLessonController.php b/app/Http/Controllers/Academy/AcademyCourseLessonController.php index 0c29ee50..c328c81b 100644 --- a/app/Http/Controllers/Academy/AcademyCourseLessonController.php +++ b/app/Http/Controllers/Academy/AcademyCourseLessonController.php @@ -13,6 +13,7 @@ use App\Services\Academy\AcademyCourseNavigationService; use App\Services\Academy\AcademyCourseProgressService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Support\Str; use Illuminate\Http\Request; use Inertia\Inertia; @@ -52,7 +53,7 @@ final class AcademyCourseLessonController extends Controller ->all(); $payload = $this->access->courseLessonPayload($courseLesson, $request->user(), true); - $canonical = route('academy.courses.lessons.show', ['course' => $course->slug, 'lesson' => $lesson->slug]); + $canonical = route('academy.lessons.show', ['slug' => $lesson->slug]); $description = Str::limit(trim((string) ($lesson->seo_description ?? $lesson->excerpt ?? 'Skinbase Academy course lesson.')), 160, '...'); $seo = app(SeoFactory::class)->academyLessonPage( (string) ($lesson->seo_title ?? ($lesson->title . ' — ' . $course->title)), @@ -70,6 +71,10 @@ final class AcademyCourseLessonController extends Controller $lesson->updated_at?->toAtomString(), (string) $course->title, )->toArray(); + $seo = SeoDataBuilder::fromArray($seo) + ->indexable(false) + ->build() + ->toArray(); $interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::LESSON, (int) $lesson->id); diff --git a/app/Http/Controllers/Academy/AcademyLessonController.php b/app/Http/Controllers/Academy/AcademyLessonController.php index 489865a3..6bd4854f 100644 --- a/app/Http/Controllers/Academy/AcademyLessonController.php +++ b/app/Http/Controllers/Academy/AcademyLessonController.php @@ -13,6 +13,7 @@ use App\Services\Academy\AcademyCacheService; use App\Services\Academy\AcademyInteractionService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -37,6 +38,9 @@ final class AcademyLessonController extends Controller 'category' => ['nullable', 'string', 'max:140'], 'difficulty' => ['nullable', 'string', 'max:40'], ]); + $hasActiveFilters = filled($filters['q'] ?? null) + || filled($filters['category'] ?? null) + || filled($filters['difficulty'] ?? null); $query = AcademyLesson::query() ->with('category') @@ -78,6 +82,13 @@ final class AcademyLessonController extends Controller ) ->toArray(); + if ($hasActiveFilters) { + $seo = SeoDataBuilder::fromArray($seo) + ->indexable(false) + ->build() + ->toArray(); + } + return Inertia::render('Academy/List', [ 'pageType' => 'lessons', 'title' => 'Academy lessons', diff --git a/app/Http/Controllers/Academy/AcademyPromptController.php b/app/Http/Controllers/Academy/AcademyPromptController.php index bb242d69..6a1ea727 100644 --- a/app/Http/Controllers/Academy/AcademyPromptController.php +++ b/app/Http/Controllers/Academy/AcademyPromptController.php @@ -13,6 +13,7 @@ use App\Services\Academy\AcademyInteractionService; use App\Services\Academy\AcademyPopularityService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Support\Str; @@ -41,6 +42,10 @@ final class AcademyPromptController extends Controller 'difficulty' => ['nullable', 'string', 'max:40'], 'tag' => ['nullable', 'string', 'max:60'], ]); + $hasActiveFilters = filled($filters['q'] ?? null) + || filled($filters['category'] ?? null) + || filled($filters['difficulty'] ?? null) + || filled($filters['tag'] ?? null); $query = AcademyPromptTemplate::query() ->with('category') @@ -87,6 +92,13 @@ final class AcademyPromptController extends Controller ) ->toArray(); + if ($hasActiveFilters) { + $seo = SeoDataBuilder::fromArray($seo) + ->indexable(false) + ->build() + ->toArray(); + } + return Inertia::render('Academy/List', [ 'pageType' => 'prompts', 'promptView' => 'library', @@ -203,6 +215,13 @@ final class AcademyPromptController extends Controller ) ->toArray(); + if ($selectedPeriod['value'] !== '30d') { + $seo = SeoDataBuilder::fromArray($seo) + ->indexable(false) + ->build() + ->toArray(); + } + return Inertia::render('Academy/List', [ 'pageType' => 'prompts', 'promptView' => 'popular', diff --git a/app/Http/Controllers/Academy/AcademyPromptPackController.php b/app/Http/Controllers/Academy/AcademyPromptPackController.php index 2ffec7a1..1765b423 100644 --- a/app/Http/Controllers/Academy/AcademyPromptPackController.php +++ b/app/Http/Controllers/Academy/AcademyPromptPackController.php @@ -10,6 +10,7 @@ use App\Services\Academy\AcademyAccessService; use App\Services\Academy\AcademyInteractionService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; +use App\Support\Seo\SeoDataBuilder; use Illuminate\Http\Request; use Illuminate\Support\Str; use Inertia\Inertia; @@ -44,6 +45,13 @@ final class AcademyPromptPackController extends Controller route('academy.packs.index'), ) ->toArray(); + $seo = SeoDataBuilder::fromArray($seo) + ->breadcrumbs([ + ['name' => 'Academy', 'url' => route('academy.index')], + ['name' => 'Prompt Packs', 'url' => route('academy.packs.index')], + ]) + ->build() + ->toArray(); return Inertia::render('Academy/List', [ 'pageType' => 'packs', @@ -79,12 +87,24 @@ final class AcademyPromptPackController extends Controller ->firstOrFail(); $payload = $this->access->packPayload($pack, $request->user(), true); - $seo = app(SeoFactory::class)->collectionPage( - $pack->title . ' — Skinbase Academy', - Str::limit((string) ($pack->excerpt ?? $pack->description ?? ''), 160, '...'), - route('academy.packs.show', ['slug' => $pack->slug]), - $pack->cover_image, - )->toArray(); + $canonical = route('academy.packs.show', ['slug' => $pack->slug]); + $description = Str::limit((string) ($pack->excerpt ?? $pack->description ?? ''), 160, '...'); + $seo = SeoDataBuilder::fromArray( + app(SeoFactory::class)->collectionPage( + $pack->title . ' — Skinbase Academy', + $description, + $canonical, + $pack->cover_image, + )->toArray() + ) + ->breadcrumbs([ + ['name' => 'Academy', 'url' => route('academy.index')], + ['name' => 'Prompt Packs', 'url' => route('academy.packs.index')], + ['name' => (string) $pack->title, 'url' => $canonical], + ]) + ->addJsonLd($this->packStructuredData($payload, $canonical, $description)) + ->build() + ->toArray(); $interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::PROMPT_PACK, (int) $pack->id); @@ -112,4 +132,58 @@ final class AcademyPromptPackController extends Controller ], ])->rootView('academy'); } + + /** + * @param array $payload + * @return array + */ + private function packStructuredData(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); + $keywords = array_values((array) ($payload['tags'] ?? [])); + $isFree = (string) ($payload['access_level'] ?? 'free') === 'free'; + $promptEntries = collect((array) ($payload['prompts'] ?? [])) + ->map(function (array $prompt): ?array { + $title = trim((string) ($prompt['title'] ?? '')); + $slug = trim((string) ($prompt['slug'] ?? '')); + + if ($title === '' || $slug === '') { + return null; + } + + return [ + '@type' => 'ListItem', + 'position' => null, + 'item' => [ + '@type' => 'CreativeWork', + 'name' => $title, + 'url' => route('academy.prompts.show', ['slug' => $slug]), + ], + ]; + }) + ->filter() + ->values() + ->map(function (array $item, int $index): array { + $item['position'] = $index + 1; + + return $item; + }) + ->all(); + + return array_filter([ + '@context' => 'https://schema.org', + '@type' => ['CreativeWork', 'LearningResource'], + 'name' => (string) ($payload['title'] ?? 'Skinbase Academy prompt pack'), + 'description' => $description, + 'url' => $canonical, + 'image' => $imageUrl, + 'keywords' => $keywords !== [] ? $keywords : null, + 'isAccessibleForFree' => $isFree, + 'hasPart' => $promptEntries !== [] ? [ + '@type' => 'ItemList', + 'itemListElement' => $promptEntries, + ] : null, + ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []); + } } diff --git a/app/Http/Controllers/Admin/SecurityReportController.php b/app/Http/Controllers/Admin/SecurityReportController.php new file mode 100644 index 00000000..33abbc5a --- /dev/null +++ b/app/Http/Controllers/Admin/SecurityReportController.php @@ -0,0 +1,130 @@ +latest('id')->with('user:id,name,username')->first(); + $reports = SecurityReport::query() + ->with('user:id,name,username') + ->latest('id') + ->paginate(20) + ->through(fn (SecurityReport $report): array => $this->mapListItem($report)); + + return Inertia::render('Admin/System/SecurityReportIndex', [ + 'latest' => $latest ? $this->mapDetail($latest) : null, + 'reports' => $reports, + 'canRunScan' => true, + ])->rootView('moderation'); + } + + public function show(SecurityReport $securityReport): Response + { + abort_unless((bool) config('security-report.enabled', true), 404); + + $securityReport->load('user:id,name,username'); + + return Inertia::render('Admin/System/SecurityReportShow', [ + 'report' => $this->mapDetail($securityReport), + ])->rootView('moderation'); + } + + public function run(Request $request): RedirectResponse + { + abort_unless((bool) config('security-report.enabled', true), 404); + + RunSecurityReportScanJob::dispatch($request->user()?->id); + + return redirect() + ->route('admin.system.security-report.index') + ->with('success', 'Security scan has been queued.'); + } + + /** + * @return array + */ + private function mapListItem(SecurityReport $report): array + { + return [ + 'id' => (int) $report->id, + 'status' => (string) $report->status, + 'risk_label' => (string) $report->risk_label, + 'finished_at' => optional($report->finished_at)?->toIso8601String(), + 'total_critical' => (int) $report->total_critical, + 'total_high' => (int) $report->total_high, + 'total_medium' => (int) $report->total_medium, + 'total_low' => (int) $report->total_low, + 'composer_outdated_count' => (int) $report->composer_outdated_count, + 'npm_outdated_count' => (int) $report->npm_outdated_count, + 'show_url' => route('admin.system.security-report.show', ['securityReport' => $report]), + 'triggered_by' => (string) ($report->triggered_by ?? ''), + 'user' => $report->user ? [ + 'id' => (int) $report->user->id, + 'name' => (string) $report->user->name, + 'username' => (string) ($report->user->username ?? ''), + ] : null, + ]; + } + + /** + * @return array + */ + private function mapDetail(SecurityReport $report): array + { + return [ + 'id' => (int) $report->id, + 'status' => (string) $report->status, + 'risk_label' => (string) $report->risk_label, + 'started_at' => optional($report->started_at)?->toIso8601String(), + 'finished_at' => optional($report->finished_at)?->toIso8601String(), + 'composer_critical' => (int) $report->composer_critical, + 'composer_high' => (int) $report->composer_high, + 'composer_medium' => (int) $report->composer_medium, + 'composer_low' => (int) $report->composer_low, + 'composer_unknown' => (int) $report->composer_unknown, + 'npm_critical' => (int) $report->npm_critical, + 'npm_high' => (int) $report->npm_high, + 'npm_moderate' => (int) $report->npm_moderate, + 'npm_low' => (int) $report->npm_low, + 'npm_info' => (int) $report->npm_info, + 'npm_unknown' => (int) $report->npm_unknown, + 'total_critical' => (int) $report->total_critical, + 'total_high' => (int) $report->total_high, + 'total_medium' => (int) $report->total_medium, + 'total_low' => (int) $report->total_low, + 'total_unknown' => (int) $report->total_unknown, + 'composer_outdated_count' => (int) $report->composer_outdated_count, + 'npm_outdated_count' => (int) $report->npm_outdated_count, + 'summary' => $report->summary ?? [], + 'triggered_by' => (string) ($report->triggered_by ?? ''), + 'error_message' => (string) ($report->error_message ?? ''), + 'show_url' => route('admin.system.security-report.show', ['securityReport' => $report]), + 'index_url' => route('admin.system.security-report.index'), + 'composer_audit' => $report->composer_audit, + 'composer_outdated' => $report->composer_outdated, + 'npm_audit' => $report->npm_audit, + 'npm_outdated' => $report->npm_outdated, + 'composer_advisories' => $report->composerAdvisories(), + 'npm_vulnerabilities' => $report->npmVulnerabilities(), + 'user' => $report->user ? [ + 'id' => (int) $report->user->id, + 'name' => (string) $report->user->name, + 'username' => (string) ($report->user->username ?? ''), + ] : null, + ]; + } +} diff --git a/app/Http/Controllers/Api/Messaging/MessageSearchController.php b/app/Http/Controllers/Api/Messaging/MessageSearchController.php index caa42524..5569b0c2 100644 --- a/app/Http/Controllers/Api/Messaging/MessageSearchController.php +++ b/app/Http/Controllers/Api/Messaging/MessageSearchController.php @@ -48,10 +48,7 @@ class MessageSearchController extends Controller $estimated = 0; try { - $client = new Client( - config('scout.meilisearch.host'), - config('scout.meilisearch.key') - ); + $client = app(Client::class); $prefix = (string) config('scout.prefix', ''); $indexName = $prefix . (string) config('messaging.search.index', 'messages'); diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php index 5546f6fa..53d01364 100644 --- a/app/Http/Controllers/SitemapController.php +++ b/app/Http/Controllers/SitemapController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Services\Sitemaps\PublishedSitemapResolver; +use App\Services\Sitemaps\SitemapBuildService; use App\Services\Sitemaps\SitemapXmlRenderer; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -14,34 +15,38 @@ final class SitemapController extends Controller { public function __construct( private readonly PublishedSitemapResolver $published, + private readonly SitemapBuildService $build, private readonly SitemapXmlRenderer $renderer, ) { } public function index(): Response|BinaryFileResponse { - // 1. Static file written by the build/generate commands. - // On production nginx serves this directly via try_files without reaching PHP. - // On dev / misconfigured servers we stream it with sendfile — no RAM load. - $path = public_path('sitemap.xml'); - if (file_exists($path)) { - return $this->xmlFileResponse($path); - } - - // 2. Published release (release management pipeline fallback). + // 1. Published release (release management pipeline fallback). $published = $this->published->resolveIndex(); if ($published !== null) { return $this->renderer->xmlResponse($published['content']); } + // 2. Live-build fallback when no published sitemap is available. + if ((bool) config('sitemaps.delivery.fallback_to_live_build', true)) { + $built = $this->build->buildIndex(force: true, persist: false); + + return $this->renderer->xmlResponse($built['content']); + } + throw new NotFoundHttpException(); } public function show(string $name): Response|BinaryFileResponse { + if ($name === 'sitemap') { + return $this->index(); + } + // 1. Static file. $path = public_path('sitemaps/' . $name . '.xml'); - if (file_exists($path)) { + if ((bool) config('sitemaps.pre_generated.enabled', true) && file_exists($path)) { return $this->xmlFileResponse($path); } @@ -51,6 +56,13 @@ final class SitemapController extends Controller return $this->renderer->xmlResponse($published['content']); } + if ((bool) config('sitemaps.delivery.fallback_to_live_build', true)) { + $built = $this->build->buildNamed($name, force: true, persist: false); + if ($built !== null) { + return $this->renderer->xmlResponse($built['content']); + } + } + throw new NotFoundHttpException(); } @@ -61,4 +73,4 @@ final class SitemapController extends Controller 'Cache-Control' => 'public, max-age=' . max(60, (int) config('sitemaps.cache_ttl_seconds', 900)), ]); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Web/SearchController.php b/app/Http/Controllers/Web/SearchController.php index c7132b3c..ebfb4f50 100644 --- a/app/Http/Controllers/Web/SearchController.php +++ b/app/Http/Controllers/Web/SearchController.php @@ -11,6 +11,7 @@ use App\Services\GroupDiscoveryService; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Support\Arr; use Illuminate\View\View; use cPad\Plugins\News\Models\NewsArticle; @@ -19,6 +20,12 @@ final class SearchController extends Controller { private const ALLOWED_SORTS = ['latest', 'popular', 'likes', 'downloads']; + /** Reject requests with an absurd number of query params before touching search/DB. */ + private const MAX_QUERY_PARAMS = 15; + + /** Reject requests with an absurdly long query string before touching search/DB. */ + private const MAX_QUERY_STRING_LENGTH = 500; + public function __construct( private readonly ArtworkSearchService $search, private readonly GroupDiscoveryService $groups, @@ -26,6 +33,8 @@ final class SearchController extends Controller public function index(Request $request): View|RedirectResponse { + $this->rejectMalformedQuery($request); + $canonicalQuery = $this->canonicalQueryParameters($request); $canonicalUrl = $this->canonicalSearchUrl($request, $canonicalQuery); @@ -110,6 +119,21 @@ final class SearchController extends Controller ]); } + /** + * Bail out before any search/DB work for junk requests — e.g. scripted + * floods that repeat/nest query params (group=all&page=..&sort=.. etc.). + */ + private function rejectMalformedQuery(Request $request): void + { + $query = $request->query(); + + if (count($query) > self::MAX_QUERY_PARAMS + || strlen((string) $request->getQueryString()) > self::MAX_QUERY_STRING_LENGTH + ) { + abort(Response::HTTP_BAD_REQUEST); + } + } + /** * @return array */ diff --git a/app/Jobs/RunSecurityReportScanJob.php b/app/Jobs/RunSecurityReportScanJob.php new file mode 100644 index 00000000..8cb72802 --- /dev/null +++ b/app/Jobs/RunSecurityReportScanJob.php @@ -0,0 +1,47 @@ +scan('manual', $this->userId); + + if (! $report->hasDangerFindings()) { + return; + } + + $email = trim((string) config('security-report.notify_email', '')); + if ($email === '') { + return; + } + + Notification::route('mail', $email) + ->notify(new SecurityReportDangerNotification($report)); + } +} diff --git a/app/Models/Artwork.php b/app/Models/Artwork.php index 6c9dbf9f..14227620 100644 --- a/app/Models/Artwork.php +++ b/app/Models/Artwork.php @@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Storage; use Laravel\Scout\Searchable; use Laravel\Scout\SearchableScope; @@ -73,6 +72,8 @@ class Artwork extends Model 'has_missing_thumbnails', 'missing_thumbnail_variants_json', 'thumbnails_checked_at', + 'featured_thumbnail_variants_json', + 'featured_thumbnails_checked_at', 'file_size', 'mime_type', 'width', @@ -154,6 +155,8 @@ class Artwork extends Model 'published_at' => 'datetime', 'missing_thumbnail_variants_json' => 'array', 'thumbnails_checked_at' => 'datetime', + 'featured_thumbnail_variants_json' => 'array', + 'featured_thumbnails_checked_at' => 'datetime', 'published_as_type' => 'string', 'published_as_id' => 'integer', 'publish_at' => 'datetime', @@ -271,18 +274,64 @@ class Artwork extends Model ?? 'https://files.skinbase.org/default/missing_xl.webp'; } + /** + * Audit state of `featured_thumbnail_variants_json`, distinguishing "never checked" + * from "checked, nothing found": + * + * - `null` → not audited yet (FeaturedArtworkThumbnailGenerator has never + * run plan()/generate() for this artwork). + * - `[]` → audited, but no featured variant exists in object storage. + * - non-empty string[] → audited; these variant names are known to exist. + * + * A value that fails to decode as an array (e.g. legacy/malformed data) is treated + * the same as `null` — "not audited" — rather than throwing or reporting variants + * that were never actually confirmed to exist. + * + * @return array{status: 'not_audited'|'no_variants'|'available', variants: list} + */ + public function featuredThumbnailAuditState(): array + { + $raw = $this->featured_thumbnail_variants_json; + + if (! is_array($raw)) { + return ['status' => 'not_audited', 'variants' => []]; + } + + $variants = array_values(array_filter($raw, 'is_string')); + + if ($variants === []) { + return ['status' => 'no_variants', 'variants' => []]; + } + + return ['status' => 'available', 'variants' => $variants]; + } + + /** + * Whether a dedicated featured-hero variant is known to exist in object storage. + * + * This reads precomputed state from `featured_thumbnail_variants_json` (see + * `featuredThumbnailAuditState()`) rather than checking the remote disk directly — + * remote existence checks must only happen from `FeaturedArtworkThumbnailGenerator` + * (admin commands / queued jobs), never during public homepage rendering, since a + * live `Storage::exists()` per variant is a synchronous network round trip. + */ public function hasFeaturedThumbnail(?string $variant = null): bool { if (empty($this->hash)) { return false; } + $known = $this->featuredThumbnailAuditState()['variants']; + + if ($known === []) { + return false; + } + $helper = app(ArtworkFeaturedImagePath::class); $variants = $variant !== null ? [$helper->normalizeVariant($variant)] : $helper->variantNames(); - $disk = Storage::disk((string) config('uploads.object_storage.disk', 's3')); foreach ($variants as $variantName) { - if ($disk->exists($helper->objectPath($this, $variantName))) { + if (in_array($variantName, $known, true)) { return true; } } diff --git a/app/Models/SecurityReport.php b/app/Models/SecurityReport.php new file mode 100644 index 00000000..c1a85ddb --- /dev/null +++ b/app/Models/SecurityReport.php @@ -0,0 +1,160 @@ + 'datetime', + 'finished_at' => 'datetime', + 'summary' => 'array', + 'composer_audit' => 'array', + 'composer_outdated' => 'array', + 'npm_audit' => 'array', + 'npm_outdated' => 'array', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function hasCriticalFindings(): bool + { + return $this->total_critical > 0; + } + + public function hasHighFindings(): bool + { + return $this->total_high > 0; + } + + public function hasDangerFindings(): bool + { + return $this->hasCriticalFindings() || $this->hasHighFindings(); + } + + public function getRiskLabelAttribute(): string + { + if ($this->total_critical > 0) { + return 'Critical'; + } + + if ($this->total_high > 0) { + return 'High'; + } + + if ($this->total_medium > 0) { + return 'Medium'; + } + + if ($this->total_low > 0) { + return 'Low'; + } + + return 'Clean'; + } + + /** + * @return array> + */ + public function composerAdvisories(): array + { + $advisories = $this->composer_audit['advisories'] ?? []; + $items = []; + + foreach ($advisories as $package => $packageAdvisories) { + if (! is_array($packageAdvisories)) { + continue; + } + + foreach ($packageAdvisories as $advisory) { + if (! is_array($advisory)) { + continue; + } + + $items[] = [ + 'package' => (string) $package, + 'severity' => strtolower((string) ($advisory['severity'] ?? 'unknown')), + 'title' => (string) ($advisory['title'] ?? $advisory['advisoryId'] ?? 'Unknown advisory'), + 'cve' => (string) ($advisory['cve'] ?? $advisory['link'] ?? ''), + 'affected_versions' => (string) ($advisory['affectedVersions'] ?? $advisory['affected_versions'] ?? ''), + 'reported_at' => (string) ($advisory['reportedAt'] ?? ''), + 'link' => (string) ($advisory['link'] ?? ''), + ]; + } + } + + return $items; + } + + /** + * @return array> + */ + public function npmVulnerabilities(): array + { + $vulnerabilities = $this->npm_audit['vulnerabilities'] ?? []; + $items = []; + + foreach ($vulnerabilities as $package => $vulnerability) { + if (! is_array($vulnerability)) { + continue; + } + + $via = collect((array) ($vulnerability['via'] ?? [])) + ->first(fn (mixed $item): bool => is_array($item)); + + $items[] = [ + 'package' => (string) $package, + 'severity' => strtolower((string) ($vulnerability['severity'] ?? 'unknown')), + 'title' => is_array($via) ? (string) ($via['title'] ?? 'Unknown advisory') : 'Unknown advisory', + 'cve' => is_array($via) ? (string) ($via['cve'] ?? '') : '', + 'range' => (string) ($vulnerability['range'] ?? ''), + 'fix_available' => is_array($vulnerability['fixAvailable'] ?? null) ? (string) (($vulnerability['fixAvailable']['name'] ?? '') . '@' . ($vulnerability['fixAvailable']['version'] ?? '')) : ((bool) ($vulnerability['fixAvailable'] ?? false) ? 'Yes' : ''), + 'url' => is_array($via) ? (string) ($via['url'] ?? '') : '', + ]; + } + + return $items; + } +} diff --git a/app/Notifications/SecurityReportDangerNotification.php b/app/Notifications/SecurityReportDangerNotification.php new file mode 100644 index 00000000..345fb506 --- /dev/null +++ b/app/Notifications/SecurityReportDangerNotification.php @@ -0,0 +1,40 @@ +subject('Skinbase Security Report Alert') + ->greeting('Security report alert') + ->line('High or critical dependency vulnerabilities were detected in the latest private security scan.') + ->line('Status: ' . $this->report->status) + ->line('Critical: ' . $this->report->total_critical) + ->line('High: ' . $this->report->total_high) + ->line('Medium: ' . $this->report->total_medium) + ->line('Low: ' . $this->report->total_low) + ->action('Open Security Report', url('/moderation/system/security-report/' . $this->report->id)) + ->line('This report is private and intended for administrators only.'); + } +} diff --git a/app/Observers/ArtworkFeatureObserver.php b/app/Observers/ArtworkFeatureObserver.php index 297d22c1..9b1d725b 100644 --- a/app/Observers/ArtworkFeatureObserver.php +++ b/app/Observers/ArtworkFeatureObserver.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Observers; +use App\Jobs\GenerateFeaturedArtworkThumbnailsJob; use App\Models\Artwork; use App\Models\ArtworkFeature; use App\Services\HomepageService; @@ -21,12 +22,14 @@ final class ArtworkFeatureObserver public function created(ArtworkFeature $feature): void { $this->homepage->clearFeaturedAndMedalCaches(); + $this->queueFeaturedThumbnailRefresh($feature); $this->queueCreatorRebuild($feature); } public function updated(ArtworkFeature $feature): void { $this->homepage->clearFeaturedAndMedalCaches(); + $this->queueFeaturedThumbnailRefresh($feature); $this->queueCreatorRebuild($feature); } @@ -39,6 +42,7 @@ final class ArtworkFeatureObserver public function restored(ArtworkFeature $feature): void { $this->homepage->clearFeaturedAndMedalCaches(); + $this->queueFeaturedThumbnailRefresh($feature); $this->queueCreatorRebuild($feature); } @@ -48,6 +52,31 @@ final class ArtworkFeatureObserver $this->queueCreatorRebuild($feature); } + /** + * Ensure the featured hero variants (and their DB-persisted existence state) + * are ready before the next homepage guest-cache warm cycle picks this + * artwork up as the hero winner, so the public request never has to check + * the remote disk itself. + */ + private function queueFeaturedThumbnailRefresh(ArtworkFeature $feature): void + { + $artworkId = (int) $feature->artwork_id; + + if ($artworkId <= 0) { + return; + } + + $artwork = $feature->relationLoaded('artwork') + ? $feature->artwork + : Artwork::withTrashed()->find($artworkId); + + if (! $artwork instanceof Artwork || empty($artwork->hash) || empty($artwork->file_ext)) { + return; + } + + GenerateFeaturedArtworkThumbnailsJob::dispatch($artworkId); + } + private function queueCreatorRebuild(ArtworkFeature $feature): void { $artwork = $feature->relationLoaded('artwork') diff --git a/app/Observers/ArtworkObserver.php b/app/Observers/ArtworkObserver.php index c08715d3..631230a1 100644 --- a/app/Observers/ArtworkObserver.php +++ b/app/Observers/ArtworkObserver.php @@ -6,6 +6,7 @@ namespace App\Observers; use App\Events\Achievements\AchievementCheckRequested; use App\Models\Artwork; +use App\Jobs\GenerateFeaturedArtworkThumbnailsJob; use App\Jobs\RecComputeSimilarByTagsJob; use App\Jobs\RecComputeSimilarHybridJob; use App\Jobs\Posts\AutoUploadPostJob; @@ -15,6 +16,7 @@ use App\Services\Profile\CreatorJourneyService; use App\Services\UserStatsService; use App\Services\XPService; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; /** * Syncs artwork documents to Meilisearch on every relevant model event. @@ -61,6 +63,25 @@ class ArtworkObserver $this->indexer->update($artwork); + // A changed hash/extension means any previously persisted + // featured_thumbnail_variants_json now refers to the WRONG object paths (they + // were computed from the old hash) — the variant names would still read as + // "available" while pointing at objects that don't exist under the new hash, + // which would surface as a broken hero image. Reset the audit state immediately + // (via a direct query, not a model save, to avoid re-entering this observer) so + // hasFeaturedThumbnail() safely reports "not audited" until the queued job below + // re-verifies and repopulates it; the public homepage never blocks on that check. + if ($artwork->wasChanged(['hash', 'file_ext'])) { + DB::table('artworks')->where('id', $artwork->id)->update([ + 'featured_thumbnail_variants_json' => null, + 'featured_thumbnails_checked_at' => null, + ]); + + if (! empty($artwork->hash) && ! empty($artwork->file_ext) && $artwork->features()->exists()) { + GenerateFeaturedArtworkThumbnailsJob::dispatch((int) $artwork->id, true); + } + } + // §7.5 On-demand: recompute similarity when tags/categories could have changed. // The pivot sync happens outside this observer, so we dispatch on every // meaningful update and let the job be idempotent (cheap if nothing changed). @@ -146,7 +167,7 @@ class ArtworkObserver private function shouldClearFeaturedCaches(Artwork $artwork): bool { - if (! $artwork->wasChanged(['published_at', 'is_public', 'is_approved', 'deleted_at', 'has_missing_thumbnails'])) { + if (! $artwork->wasChanged(['published_at', 'is_public', 'is_approved', 'deleted_at', 'has_missing_thumbnails', 'hash', 'file_ext'])) { return false; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fc2398e5..ade2d09d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -87,6 +87,21 @@ class AppServiceProvider extends ServiceProvider $app->make(NullSubjectDetector::class), ]); }); + + // Override Scout's default Meilisearch client binding: Scout registers it + // with no HTTP timeout, so a slow/overloaded Meilisearch leaves PHP-FPM + // workers blocked forever in curl_exec(), draining the whole pool under + // a search traffic spike. Fail fast instead. + $this->app->singleton(\Meilisearch\Client::class, function ($app) { + $config = $app['config']->get('scout.meilisearch'); + + $httpClient = new \GuzzleHttp\Client([ + 'connect_timeout' => 2, + 'timeout' => 5, + ]); + + return new \Meilisearch\Client($config['host'], $config['key'], $httpClient); + }); } /** @@ -111,6 +126,8 @@ class AppServiceProvider extends ServiceProvider $this->configureUploadRateLimiters(); $this->configureMessagingRateLimiters(); $this->configureDownloadRateLimiter(); + $this->configureSearchRateLimiter(); + $this->configureVectorSearchRateLimiter(); $this->configureArtworkRateLimiters(); $this->configureNovaCardRateLimiters(); $this->configureReactionRateLimiters(); @@ -468,6 +485,36 @@ class AppServiceProvider extends ServiceProvider }); } + private function configureSearchRateLimiter(): void + { + RateLimiter::for('search', function (Request $request): array { + $userId = $request->user()?->id; + + // Search fans out to Meilisearch + DB queries per request, so IP + // limits are kept tight to blunt scripted floods of /search traffic. + return [ + Limit::perMinute(20)->by('search:user:' . ($userId ?? 'guest')), + Limit::perMinute(30)->by('search:ip:' . $request->ip()), + ]; + }); + } + + private function configureVectorSearchRateLimiter(): void + { + RateLimiter::for('vector-search', function (Request $request): array { + $userId = $request->user()?->id; + + // Each hit here can trigger synchronous outbound HTTP to the vision + // vector gateway (image download + similarity search), so keep the + // per-IP allowance tight — unlike cached list endpoints, a flood of + // distinct artwork IDs can't be absorbed by cache alone. + return [ + Limit::perMinute(30)->by('vector-search:user:' . ($userId ?? 'guest')), + Limit::perMinute(20)->by('vector-search:ip:' . $request->ip()), + ]; + }); + } + private function configureArtworkRateLimiters(): void { RateLimiter::for('artwork-awards', function (Request $request): array { diff --git a/app/Services/Images/FeaturedArtworkThumbnailGenerator.php b/app/Services/Images/FeaturedArtworkThumbnailGenerator.php index 61b4236f..2de8f460 100644 --- a/app/Services/Images/FeaturedArtworkThumbnailGenerator.php +++ b/app/Services/Images/FeaturedArtworkThumbnailGenerator.php @@ -7,8 +7,10 @@ namespace App\Services\Images; use App\Models\Artwork; use App\Services\Cdn\ArtworkCdnPurgeService; use App\Services\ArtworkOriginalFileLocator; +use App\Services\HomepageService; use App\Services\Uploads\UploadStorageService; use App\Support\ArtworkFeaturedImagePath; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Intervention\Image\Drivers\Gd\Driver as GdDriver; @@ -28,6 +30,7 @@ final class FeaturedArtworkThumbnailGenerator private readonly ArtworkOriginalFileLocator $locator, private readonly UploadStorageService $storage, private readonly ArtworkCdnPurgeService $cdnPurge, + private readonly HomepageService $homepage, ) { try { $this->manager = extension_loaded('gd') @@ -59,6 +62,8 @@ final class FeaturedArtworkThumbnailGenerator $missing[] = $variant; } + $this->persistVariantState($artwork, $existing); + return [ 'existing' => $existing, 'missing' => $missing, @@ -66,6 +71,52 @@ final class FeaturedArtworkThumbnailGenerator ]; } + /** + * Record which featured variants are known to exist so the public homepage can read + * this state instead of checking the remote disk during a request. + * + * saveQuietly() intentionally bypasses model observers (ArtworkObserver / + * ArtworkFeatureObserver never fire for this write), so cache invalidation for the + * homepage hero cannot be delegated to them here — it has to happen inline, and only + * when the persisted availability actually changed for an artwork that is currently + * an active feature (otherwise routine --all/--missing-only audits over thousands of + * non-featured artworks would repeatedly invalidate the guest payload cache for no + * reason and cause a stampede). + * + * @param list $existingVariants + */ + private function persistVariantState(Artwork $artwork, array $existingVariants): void + { + $existingVariants = array_values(array_unique($existingVariants)); + sort($existingVariants); + + $previousVariants = (array) ($artwork->featured_thumbnail_variants_json ?? []); + sort($previousVariants); + + $changed = $previousVariants !== $existingVariants; + + $artwork->forceFill([ + 'featured_thumbnail_variants_json' => $existingVariants, + 'featured_thumbnails_checked_at' => now(), + ])->saveQuietly(); + + if ($changed && $this->isActivelyFeatured($artwork)) { + $this->homepage->clearFeaturedAndMedalCaches(); + } + } + + private function isActivelyFeatured(Artwork $artwork): bool + { + return DB::table('artwork_features') + ->where('artwork_id', $artwork->id) + ->where('is_active', true) + ->whereNull('deleted_at') + ->where(function ($query): void { + $query->whereNull('expires_at')->orWhere('expires_at', '>', now()); + }) + ->exists(); + } + /** * @return array{existing:list,missing:list,target_variants:list,generated:int,skipped:int,generated_variants:list,generated_paths:list,failed:array} */ @@ -129,6 +180,10 @@ final class FeaturedArtworkThumbnailGenerator ]); } + if ($generatedVariants !== []) { + $this->persistVariantState($artwork, array_values(array_unique([...$plan['existing'], ...$generatedVariants]))); + } + return $plan + [ 'generated' => count($generatedVariants), 'skipped' => max(0, count($targetVariants) - count($generatedVariants) - count($failed)) + count($plan['existing']), diff --git a/app/Services/SecurityReport/SecurityReportScanner.php b/app/Services/SecurityReport/SecurityReportScanner.php new file mode 100644 index 00000000..a36efa70 --- /dev/null +++ b/app/Services/SecurityReport/SecurityReportScanner.php @@ -0,0 +1,364 @@ +create([ + 'status' => 'running', + 'started_at' => now(), + 'triggered_by' => $triggeredBy, + 'user_id' => $userId, + ]); + + try { + $composerAudit = null; + $composerOutdated = null; + $npmAudit = null; + $npmOutdated = null; + + if ((bool) config('security-report.scan.composer', true)) { + $composerAudit = $this->runJsonCommand((array) config('security-report.commands.composer_audit', [])); + $composerOutdated = $this->runJsonCommand((array) config('security-report.commands.composer_outdated', [])); + } + + if ((bool) config('security-report.scan.npm', true)) { + $npmAudit = $this->runJsonCommand((array) config('security-report.commands.npm_audit', [])); + $npmOutdated = $this->runJsonCommand((array) config('security-report.commands.npm_outdated', [])); + } + + $normalized = $this->summarizePayloads($composerAudit, $composerOutdated, $npmAudit, $npmOutdated); + $status = ($normalized['total_critical'] > 0 || $normalized['total_high'] > 0 || $normalized['total_medium'] > 0 || $normalized['total_low'] > 0) + ? 'completed_with_findings' + : 'completed'; + + $report->update(array_merge($normalized, [ + 'status' => $status, + 'finished_at' => now(), + 'composer_audit' => $this->shouldStoreRaw() ? $this->limitRaw($composerAudit) : null, + 'composer_outdated' => $this->shouldStoreRaw() ? $this->limitRaw($composerOutdated) : null, + 'npm_audit' => $this->shouldStoreRaw() ? $this->limitRaw($npmAudit) : null, + 'npm_outdated' => $this->shouldStoreRaw() ? $this->limitRaw($npmOutdated) : null, + ])); + + return $report->fresh(); + } catch (Throwable $exception) { + Log::error('Security report scan failed', [ + 'message' => $exception->getMessage(), + ]); + + $report->update([ + 'status' => 'failed', + 'finished_at' => now(), + 'error_message' => $this->sanitizeText($exception->getMessage()), + ]); + + return $report->fresh(); + } + } + + /** + * @param array|null $composerAudit + * @param array|null $composerOutdated + * @param array|null $npmAudit + * @param array|null $npmOutdated + * @return array + */ + public function summarizePayloads(?array $composerAudit, ?array $composerOutdated, ?array $npmAudit, ?array $npmOutdated): array + { + $composerCounts = $this->summarizeComposerAudit($composerAudit); + $npmCounts = $this->summarizeNpmAudit($npmAudit); + $composerOutdatedCount = $this->countComposerOutdated($composerOutdated); + $npmOutdatedCount = $this->countNpmOutdated($npmOutdated); + + $totalCritical = $composerCounts['critical'] + $npmCounts['critical']; + $totalHigh = $composerCounts['high'] + $npmCounts['high']; + $totalMedium = $composerCounts['medium'] + $npmCounts['moderate']; + $totalLow = $composerCounts['low'] + $npmCounts['low']; + $totalUnknown = $composerCounts['unknown'] + $npmCounts['unknown'] + $npmCounts['info']; + + return [ + 'composer_critical' => $composerCounts['critical'], + 'composer_high' => $composerCounts['high'], + 'composer_medium' => $composerCounts['medium'], + 'composer_low' => $composerCounts['low'], + 'composer_unknown' => $composerCounts['unknown'], + 'npm_critical' => $npmCounts['critical'], + 'npm_high' => $npmCounts['high'], + 'npm_moderate' => $npmCounts['moderate'], + 'npm_low' => $npmCounts['low'], + 'npm_info' => $npmCounts['info'], + 'npm_unknown' => $npmCounts['unknown'], + 'total_critical' => $totalCritical, + 'total_high' => $totalHigh, + 'total_medium' => $totalMedium, + 'total_low' => $totalLow, + 'total_unknown' => $totalUnknown, + 'composer_outdated_count' => $composerOutdatedCount, + 'npm_outdated_count' => $npmOutdatedCount, + 'summary' => [ + 'total' => [ + 'critical' => $totalCritical, + 'high' => $totalHigh, + 'medium' => $totalMedium, + 'low' => $totalLow, + 'unknown' => $totalUnknown, + ], + 'composer' => $composerCounts, + 'npm' => $npmCounts, + 'outdated' => [ + 'composer' => $composerOutdatedCount, + 'npm' => $npmOutdatedCount, + ], + ], + ]; + } + + /** + * @param array|null $audit + * @return array{critical:int,high:int,medium:int,low:int,unknown:int} + */ + public function summarizeComposerAudit(?array $audit): array + { + $counts = [ + 'critical' => 0, + 'high' => 0, + 'medium' => 0, + 'low' => 0, + 'unknown' => 0, + ]; + + if (! is_array($audit)) { + return $counts; + } + + $advisories = $audit['advisories'] ?? []; + + foreach ($advisories as $packageAdvisories) { + if (! is_array($packageAdvisories)) { + continue; + } + + foreach ($packageAdvisories as $advisory) { + if (! is_array($advisory)) { + continue; + } + + $severity = strtolower((string) ($advisory['severity'] ?? 'unknown')); + if (array_key_exists($severity, $counts)) { + $counts[$severity]++; + } else { + $counts['unknown']++; + } + } + } + + return $counts; + } + + /** + * @param array|null $audit + * @return array{critical:int,high:int,moderate:int,low:int,info:int,unknown:int} + */ + public function summarizeNpmAudit(?array $audit): array + { + $counts = [ + 'critical' => 0, + 'high' => 0, + 'moderate' => 0, + 'low' => 0, + 'info' => 0, + 'unknown' => 0, + ]; + + if (! is_array($audit)) { + return $counts; + } + + if (isset($audit['metadata']['vulnerabilities']) && is_array($audit['metadata']['vulnerabilities'])) { + $vulnerabilities = $audit['metadata']['vulnerabilities']; + + $counts['critical'] = (int) ($vulnerabilities['critical'] ?? 0); + $counts['high'] = (int) ($vulnerabilities['high'] ?? 0); + $counts['moderate'] = (int) ($vulnerabilities['moderate'] ?? 0); + $counts['low'] = (int) ($vulnerabilities['low'] ?? 0); + $counts['info'] = (int) ($vulnerabilities['info'] ?? 0); + + return $counts; + } + + $vulnerabilities = $audit['vulnerabilities'] ?? []; + + foreach ($vulnerabilities as $vulnerability) { + if (! is_array($vulnerability)) { + continue; + } + + $severity = strtolower((string) ($vulnerability['severity'] ?? 'unknown')); + if (array_key_exists($severity, $counts)) { + $counts[$severity]++; + } else { + $counts['unknown']++; + } + } + + return $counts; + } + + /** + * @param array $command + * @return array|null + */ + private function runJsonCommand(array $command): ?array + { + if ($command === []) { + return null; + } + + $process = new Process( + $command, + base_path(), + null, + null, + (float) config('security-report.timeout_seconds', 180), + ); + + $process->run(); + + $output = trim($process->getOutput()); + $errorOutput = trim($process->getErrorOutput()); + + if ($output === '') { + return [ + '_status' => $errorOutput !== '' ? 'no_json_output' : 'empty_output', + '_exit_code' => $process->getExitCode(), + '_error' => $errorOutput !== '' ? $this->sanitizeText(mb_substr($errorOutput, 0, 5000)) : null, + ]; + } + + $json = json_decode($output, true); + + if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) { + return [ + '_status' => 'invalid_json', + '_exit_code' => $process->getExitCode(), + '_json_error' => json_last_error_msg(), + '_output_preview' => $this->sanitizeText(mb_substr($output, 0, 5000)), + '_error_preview' => $this->sanitizeText(mb_substr($errorOutput, 0, 5000)), + ]; + } + + $json['_exit_code'] = $process->getExitCode(); + + return $this->sanitizeArray($json); + } + + /** + * @param array|null $outdated + */ + private function countComposerOutdated(?array $outdated): int + { + if (! is_array($outdated)) { + return 0; + } + + return isset($outdated['installed']) && is_array($outdated['installed']) + ? count($outdated['installed']) + : 0; + } + + /** + * @param array|null $outdated + */ + private function countNpmOutdated(?array $outdated): int + { + if (! is_array($outdated)) { + return 0; + } + + return count(array_filter( + $outdated, + static fn (mixed $value, mixed $key): bool => is_array($value) && ! str_starts_with((string) $key, '_'), + ARRAY_FILTER_USE_BOTH, + )); + } + + private function shouldStoreRaw(): bool + { + return (bool) config('security-report.store_raw', true); + } + + /** + * @param array|null $data + * @return array|null + */ + private function limitRaw(?array $data): ?array + { + if ($data === null) { + return null; + } + + $sanitized = $this->sanitizeArray($data); + $maxBytes = max(64, (int) config('security-report.max_raw_kb', 512)) * 1024; + $json = json_encode($sanitized, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); + + if (! is_string($json)) { + return [ + '_status' => 'raw_encode_failed', + ]; + } + + if (strlen($json) <= $maxBytes) { + return $sanitized; + } + + return [ + '_status' => 'truncated', + '_max_kb' => (int) config('security-report.max_raw_kb', 512), + '_preview' => mb_substr($json, 0, $maxBytes), + ]; + } + + /** + * @param array $data + * @return array + */ + private function sanitizeArray(array $data): array + { + $sanitized = []; + + foreach ($data as $key => $value) { + if (is_array($value)) { + $sanitized[$key] = $this->sanitizeArray($value); + continue; + } + + if (is_string($value)) { + $sanitized[$key] = $this->sanitizeText($value); + continue; + } + + $sanitized[$key] = $value; + } + + return $sanitized; + } + + private function sanitizeText(string $value): string + { + $normalized = str_replace(["\r\n", "\r"], "\n", $value); + $normalized = str_replace(base_path(), '[project-root]', $normalized); + $normalized = preg_replace('/[A-Z]:\\\\[^\s"\']+/', '[path]', $normalized) ?? $normalized; + + return trim($normalized); + } +} diff --git a/app/Services/Sitemaps/Builders/AcademyPagesSitemapBuilder.php b/app/Services/Sitemaps/Builders/AcademyPagesSitemapBuilder.php new file mode 100644 index 00000000..83c30124 --- /dev/null +++ b/app/Services/Sitemaps/Builders/AcademyPagesSitemapBuilder.php @@ -0,0 +1,38 @@ +urls->staticRoute('/academy'), + $this->urls->staticRoute('/academy/pricing'), + ]; + } + + public function lastModified(): ?DateTimeInterface + { + return null; + } +} diff --git a/app/Services/Sitemaps/Builders/AcademyPromptsSitemapBuilder.php b/app/Services/Sitemaps/Builders/AcademyPromptsSitemapBuilder.php index 0a356e2f..c10b5c50 100644 --- a/app/Services/Sitemaps/Builders/AcademyPromptsSitemapBuilder.php +++ b/app/Services/Sitemaps/Builders/AcademyPromptsSitemapBuilder.php @@ -27,7 +27,10 @@ final class AcademyPromptsSitemapBuilder extends AbstractSitemapBuilder return []; } - $items = [$this->urls->staticRoute('/academy/prompts')]; + $items = [ + $this->urls->staticRoute('/academy/prompts'), + $this->urls->staticRoute('/academy/prompts/popular'), + ]; $details = AcademyPromptTemplate::query() ->active() @@ -45,4 +48,4 @@ final class AcademyPromptsSitemapBuilder extends AbstractSitemapBuilder { return $this->dateTime(AcademyPromptTemplate::query()->active()->published()->max('updated_at')); } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/Builders/StaticPagesSitemapBuilder.php b/app/Services/Sitemaps/Builders/StaticPagesSitemapBuilder.php index ab3a766d..96915d1b 100644 --- a/app/Services/Sitemaps/Builders/StaticPagesSitemapBuilder.php +++ b/app/Services/Sitemaps/Builders/StaticPagesSitemapBuilder.php @@ -24,8 +24,6 @@ final class StaticPagesSitemapBuilder extends AbstractSitemapBuilder { $items = [ $this->urls->staticRoute('/'), - $this->urls->staticRoute('/academy'), - $this->urls->staticRoute('/academy/pricing'), $this->urls->staticRoute('/web-stories'), $this->urls->staticRoute('/faq'), $this->urls->staticRoute('/rules-and-guidelines'), @@ -61,4 +59,4 @@ final class StaticPagesSitemapBuilder extends AbstractSitemapBuilder { return $this->dateTime(Page::query()->published()->max('updated_at')); } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/PublishedSitemapResolver.php b/app/Services/Sitemaps/PublishedSitemapResolver.php index fa5e4667..839fd78b 100644 --- a/app/Services/Sitemaps/PublishedSitemapResolver.php +++ b/app/Services/Sitemaps/PublishedSitemapResolver.php @@ -102,6 +102,14 @@ final class PublishedSitemapResolver } } + foreach ((array) ($manifest['groups'] ?? []) as $groupName => $group) { + $entryName = (string) ($group['entry_name'] ?? ''); + + if ($requestedName === $groupName && $entryName !== '') { + return $entryName; + } + } + return null; } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/SitemapBuildService.php b/app/Services/Sitemaps/SitemapBuildService.php index d25dcee3..59eb28eb 100644 --- a/app/Services/Sitemaps/SitemapBuildService.php +++ b/app/Services/Sitemaps/SitemapBuildService.php @@ -42,6 +42,23 @@ final class SitemapBuildService */ public function buildNamed(string $name, bool $force = false, bool $persist = true): ?array { + $groupFamilies = $this->groupFamilies($name); + if ($groupFamilies !== null) { + $built = $this->cache->remember( + $name, + fn (): string => $this->renderer->renderIndex($this->index->itemsForFamilies($groupFamilies)), + $force, + $persist, + ); + + return $built + [ + 'type' => SitemapTarget::TYPE_INDEX, + 'url_count' => count($this->index->itemsForFamilies($groupFamilies)), + 'shard_count' => 0, + 'name' => $name, + ]; + } + $target = $this->shards->resolve($this->registry, $name); if ($target === null) { @@ -111,6 +128,24 @@ final class SitemapBuildService )); } + /** + * @return list|null + */ + public function groupFamilies(string $name): ?array + { + $families = $this->index->activeGroupIndexes($this->enabledFamilies())[$name] ?? null; + + return is_array($families) && $families !== [] ? $families : null; + } + + /** + * @return array> + */ + public function enabledGroupIndexes(): array + { + return $this->index->activeGroupIndexes($this->enabledFamilies()); + } + private function renderTarget(SitemapTarget $target): string { if ($target->type === SitemapTarget::TYPE_INDEX) { @@ -140,4 +175,4 @@ final class SitemapBuildService return count($target->builder->items()); } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/SitemapIndexService.php b/app/Services/Sitemaps/SitemapIndexService.php index 071bd399..2370a404 100644 --- a/app/Services/Sitemaps/SitemapIndexService.php +++ b/app/Services/Sitemaps/SitemapIndexService.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace App\Services\Sitemaps; +use DateTimeImmutable; +use DateTimeInterface; + final class SitemapIndexService { public function __construct( @@ -18,8 +21,25 @@ final class SitemapIndexService public function items(?array $families = null): array { $items = []; + $selectedFamilies = $families ?? (array) config('sitemaps.enabled', []); + $groupedFamilies = []; + + foreach ($this->activeGroupIndexes($selectedFamilies) as $groupName => $groupFamilies) { + $items[] = new SitemapIndexItem( + url('/sitemaps/' . $groupName . '.xml'), + $this->lastModifiedForFamilies($groupFamilies), + ); + + foreach ($groupFamilies as $family) { + $groupedFamilies[$family] = true; + } + } + + foreach ($selectedFamilies as $name) { + if (isset($groupedFamilies[(string) $name])) { + continue; + } - foreach ($families ?? (array) config('sitemaps.enabled', []) as $name) { $builder = $this->registry->get((string) $name); if ($builder === null) { @@ -35,6 +55,30 @@ final class SitemapIndexService return $items; } + /** + * @param list $families + * @return list + */ + public function itemsForFamilies(array $families): array + { + $items = []; + + foreach ($families as $family) { + $builder = $this->registry->get($family); + + if ($builder === null) { + continue; + } + + $items[] = new SitemapIndexItem( + url('/sitemaps/' . $this->shards->rootEntryName($builder) . '.xml'), + $builder->lastModified(), + ); + } + + return $items; + } + /** * @return list */ @@ -58,4 +102,69 @@ final class SitemapIndexService $builder->lastModified(), )]; } -} \ No newline at end of file + + /** + * @param list $selectedFamilies + * @return array> + */ + public function activeGroupIndexes(array $selectedFamilies): array + { + $selectedLookup = array_fill_keys($selectedFamilies, true); + $groups = []; + + foreach ((array) config('sitemaps.group_indexes', []) as $groupName => $groupFamilies) { + if (! is_string($groupName) || $groupName === '') { + continue; + } + + $validFamilies = array_values(array_filter( + (array) $groupFamilies, + fn (mixed $family): bool => is_string($family) && $family !== '' && $this->registry->get($family) !== null, + )); + + if ($validFamilies === []) { + continue; + } + + foreach ($validFamilies as $family) { + if (! isset($selectedLookup[$family])) { + continue 2; + } + } + + $groups[$groupName] = $validFamilies; + } + + return $groups; + } + + /** + * @param list $families + */ + private function lastModifiedForFamilies(array $families): ?DateTimeInterface + { + $latest = null; + + foreach ($families as $family) { + $builder = $this->registry->get($family); + + if ($builder === null) { + continue; + } + + $lastModified = $builder->lastModified(); + + if ($lastModified !== null) { + $candidate = $lastModified instanceof DateTimeInterface + ? $lastModified + : new DateTimeImmutable((string) $lastModified); + + if ($latest === null || $candidate->getTimestamp() > $latest->getTimestamp()) { + $latest = $candidate; + } + } + } + + return $latest; + } +} diff --git a/app/Services/Sitemaps/SitemapPublishService.php b/app/Services/Sitemaps/SitemapPublishService.php index 728ae18d..4013f83f 100644 --- a/app/Services/Sitemaps/SitemapPublishService.php +++ b/app/Services/Sitemaps/SitemapPublishService.php @@ -130,6 +130,7 @@ final class SitemapPublishService $releaseId ??= $this->releases->generateReleaseId(); $familyManifest = []; + $groupManifest = []; $documents = [ SitemapCacheService::INDEX_DOCUMENT => $this->releases->documentRelativePath(SitemapCacheService::INDEX_DOCUMENT), ]; @@ -180,15 +181,43 @@ final class SitemapPublishService ]; } + foreach ($this->build->enabledGroupIndexes() as $groupName => $groupFamilies) { + foreach ($groupFamilies as $family) { + if (! in_array($family, $selectedFamilies, true)) { + continue 2; + } + } + + $built = $this->build->buildNamed($groupName, true, false); + + if ($built === null) { + throw new \RuntimeException('Failed to build sitemap group [' . $groupName . '].'); + } + + $this->releases->putDocument($releaseId, $groupName, (string) $built['content']); + $documents[$groupName] = $this->releases->documentRelativePath($groupName); + + $groupManifest[$groupName] = [ + 'name' => $groupName, + 'entry_name' => $groupName, + 'families' => $groupFamilies, + 'documents' => [$groupName], + 'url_count' => (int) $built['url_count'], + 'type' => SitemapTarget::TYPE_INDEX, + ]; + } + $manifest = [ 'release_id' => $releaseId, 'status' => 'built', 'built_at' => now()->toAtomString(), 'published_at' => null, 'families' => $familyManifest, + 'groups' => $groupManifest, 'documents' => $documents, 'totals' => [ 'families' => count($familyManifest), + 'groups' => count($groupManifest), 'documents' => count($documents), 'urls' => $totalUrls, ], @@ -204,4 +233,4 @@ final class SitemapPublishService return $manifest; } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/SitemapRegistry.php b/app/Services/Sitemaps/SitemapRegistry.php index 50cc2ff3..d41f0772 100644 --- a/app/Services/Sitemaps/SitemapRegistry.php +++ b/app/Services/Sitemaps/SitemapRegistry.php @@ -8,6 +8,7 @@ use App\Services\Sitemaps\Builders\ArtworksSitemapBuilder; use App\Services\Sitemaps\Builders\AcademyChallengesSitemapBuilder; use App\Services\Sitemaps\Builders\AcademyCoursesSitemapBuilder; use App\Services\Sitemaps\Builders\AcademyLessonsSitemapBuilder; +use App\Services\Sitemaps\Builders\AcademyPagesSitemapBuilder; use App\Services\Sitemaps\Builders\AcademyPacksSitemapBuilder; use App\Services\Sitemaps\Builders\AcademyPromptsSitemapBuilder; use App\Services\Sitemaps\Builders\CardsSitemapBuilder; @@ -33,6 +34,7 @@ final class SitemapRegistry public function __construct( ArtworksSitemapBuilder $artworks, + AcademyPagesSitemapBuilder $academyPages, AcademyCoursesSitemapBuilder $academyCourses, AcademyLessonsSitemapBuilder $academyLessons, AcademyPromptsSitemapBuilder $academyPrompts, @@ -54,6 +56,7 @@ final class SitemapRegistry ) { $this->builders = [ $artworks->name() => $artworks, + $academyPages->name() => $academyPages, $academyCourses->name() => $academyCourses, $academyLessons->name() => $academyLessons, $academyPrompts->name() => $academyPrompts, @@ -87,4 +90,4 @@ final class SitemapRegistry { return $this->builders[$name] ?? null; } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/SitemapReleaseValidator.php b/app/Services/Sitemaps/SitemapReleaseValidator.php index ce71305f..ee944b70 100644 --- a/app/Services/Sitemaps/SitemapReleaseValidator.php +++ b/app/Services/Sitemaps/SitemapReleaseValidator.php @@ -32,6 +32,7 @@ final class SitemapReleaseValidator $errors = []; $families = (array) ($manifest['families'] ?? []); + $groups = (array) ($manifest['groups'] ?? []); $documents = (array) ($manifest['documents'] ?? []); $rootContent = $this->releases->getDocument($releaseId, SitemapCacheService::INDEX_DOCUMENT); @@ -41,10 +42,9 @@ final class SitemapReleaseValidator $errors[] = 'Root sitemap.xml is missing or invalid.'; } else { $rootLocs = $this->extractLocs($rootXml, 'sitemap'); - $expectedRootLocs = array_map( - fn (string $entryName): string => url('/sitemaps/' . $entryName . '.xml'), - array_values(array_map(static fn (array $family): string => (string) ($family['entry_name'] ?? ''), $families)), - ); + $expectedRoot = $this->build->buildIndex(true, false, array_keys($families)); + $expectedRootXml = $this->loadXml((string) $expectedRoot['content']); + $expectedRootLocs = $expectedRootXml ? $this->extractLocs($expectedRootXml, 'sitemap') : []; if ($rootLocs !== $expectedRootLocs) { $errors[] = 'Root sitemap index does not match the manifest family entries.'; @@ -147,13 +147,48 @@ final class SitemapReleaseValidator } } + $groupReports = []; + + foreach ($groups as $groupName => $group) { + $groupErrors = []; + $documentName = (string) ($group['entry_name'] ?? $groupName); + $artifact = $this->releases->getDocument($releaseId, $documentName); + + if (! is_string($artifact) || $artifact === '') { + $groupErrors[] = 'Missing artifact [' . $documentName . '].'; + } else { + $artifactXml = $this->loadXml($artifact); + $expected = $this->build->buildNamed($documentName, true, false); + $expectedXml = $expected !== null ? $this->loadXml((string) $expected['content']) : null; + + if ($artifactXml === null || $expectedXml === null) { + $groupErrors[] = 'Invalid XML in group artifact [' . $documentName . '].'; + } elseif ($this->extractLocs($artifactXml, 'sitemap') !== $this->extractLocs($expectedXml, 'sitemap')) { + $groupErrors[] = 'Group index artifact [' . $documentName . '] does not match expected sitemap references.'; + } + } + + $groupReports[] = [ + 'group' => $groupName, + 'documents' => count((array) ($group['documents'] ?? [])), + 'url_count' => (int) ($group['url_count'] ?? 0), + 'errors' => $groupErrors, + ]; + + foreach ($groupErrors as $groupError) { + $errors[] = $groupName . ': ' . $groupError; + } + } + return [ 'ok' => $errors === [], 'release_id' => $releaseId, 'errors' => $errors, 'families' => $reports, + 'groups' => $groupReports, 'totals' => [ 'families' => count($families), + 'groups' => count($groups), 'documents' => count($documents), 'urls' => array_sum(array_map(static fn (array $family): int => (int) ($family['url_count'] ?? 0), $families)), 'shards' => array_sum(array_map(static fn (array $family): int => (int) ($family['shard_count'] ?? 0), $families)), @@ -255,4 +290,4 @@ final class SitemapReleaseValidator return null; } -} \ No newline at end of file +} diff --git a/app/Services/Sitemaps/SitemapStaticPublisher.php b/app/Services/Sitemaps/SitemapStaticPublisher.php index e3e7d171..9f9353d7 100644 --- a/app/Services/Sitemaps/SitemapStaticPublisher.php +++ b/app/Services/Sitemaps/SitemapStaticPublisher.php @@ -8,8 +8,8 @@ use Illuminate\Support\Facades\Storage; /** * Writes every document from a published release to the public disk so nginx - * can serve sitemap.xml and sitemaps/{name}.xml as plain static files, - * bypassing PHP entirely on subsequent requests. + * can serve sitemaps/{name}.xml as plain static files, bypassing PHP on + * subsequent child-sitemap requests. The root /sitemap.xml stays dynamic. */ final class SitemapStaticPublisher { @@ -50,6 +50,7 @@ final class SitemapStaticPublisher } $disk->put((string) $relativePath, $content); + $written++; } diff --git a/app/Services/Sitemaps/SitemapValidationService.php b/app/Services/Sitemaps/SitemapValidationService.php index bbddcee6..3993570d 100644 --- a/app/Services/Sitemaps/SitemapValidationService.php +++ b/app/Services/Sitemaps/SitemapValidationService.php @@ -29,15 +29,7 @@ final class SitemapValidationService ? array_values(array_filter($onlyFamilies, fn (string $family): bool => $this->registry->get($family) !== null)) : $this->build->enabledFamilies(); - $expectedIndexLocs = array_map( - static fn (SitemapIndexItem $item): string => $item->loc, - array_values(array_filter( - $this->index->items(), - fn (SitemapIndexItem $item): bool => $this->isFamilySelected($families, $item->loc), - )), - ); - - $indexBuild = $this->build->buildIndex(true, false); + $indexBuild = $this->build->buildIndex(true, false, $families); $indexErrors = []; $indexXml = $this->loadXml($indexBuild['content']); @@ -45,6 +37,7 @@ final class SitemapValidationService $indexErrors[] = 'The main sitemap index XML could not be parsed.'; } + $expectedIndexLocs = $this->extractLocsFromContent((string) $indexBuild['content'], 'sitemap'); $actualIndexLocs = $indexXml ? $this->extractLocs($indexXml, 'sitemap') : []; if ($indexXml !== null && $actualIndexLocs !== $expectedIndexLocs) { $indexErrors[] = 'Main sitemap index child references do not match the expected shard-aware manifest.'; @@ -214,15 +207,14 @@ final class SitemapValidationService return $locs; } - private function isFamilySelected(array $families, string $loc): bool + /** + * @return list + */ + private function extractLocsFromContent(string $content, string $nodeName): array { - foreach ($families as $family) { - if (str_contains($loc, '/sitemaps/' . $family . '.xml') || str_contains($loc, '/sitemaps/' . $family . '-')) { - return true; - } - } + $document = $this->loadXml($content); - return false; + return $document === null ? [] : $this->extractLocs($document, $nodeName); } private function urlError(string $family, string $loc): ?string @@ -283,4 +275,4 @@ final class SitemapValidationService return $user === null ? 'Non-public user URL emitted' : null; } -} \ No newline at end of file +} diff --git a/app/Services/Vision/AiArtworkVectorSearchService.php b/app/Services/Vision/AiArtworkVectorSearchService.php index 8e379fb7..8567c635 100644 --- a/app/Services/Vision/AiArtworkVectorSearchService.php +++ b/app/Services/Vision/AiArtworkVectorSearchService.php @@ -58,10 +58,12 @@ final class AiArtworkVectorSearchService */ private function downloadArtworkImage(Artwork $artwork, string $url): ?array { + // Runs synchronously in the web request path — keep the budget tight + // so a slow origin can't pin an FPM worker for 20s+. $response = Http::accept('*/*') - ->connectTimeout(5) - ->timeout(20) - ->retry(1, 200, throw: false) + ->connectTimeout(2) + ->timeout(6) + ->retry(0) ->get($url); if (! $response->ok()) { @@ -91,6 +93,10 @@ final class AiArtworkVectorSearchService return []; } + if ($this->client->circuitOpen()) { + throw new RuntimeException('Vector gateway temporarily unavailable (circuit open after a recent failure).'); + } + $fileFailure = null; try { diff --git a/app/Services/Vision/VectorGatewayClient.php b/app/Services/Vision/VectorGatewayClient.php index 58cee257..47f6786d 100644 --- a/app/Services/Vision/VectorGatewayClient.php +++ b/app/Services/Vision/VectorGatewayClient.php @@ -7,11 +7,14 @@ namespace App\Services\Vision; use Illuminate\Http\UploadedFile; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use RuntimeException; final class VectorGatewayClient { + private const CIRCUIT_KEY = 'vision.vector_gateway.circuit_open'; + public function isConfigured(): bool { return (bool) config('vision.vector_gateway.enabled', true) @@ -19,9 +22,25 @@ final class VectorGatewayClient && $this->apiKey() !== ''; } + /** + * True while the gateway is presumed down after a recent failure — callers + * on the request path should skip the network round trip entirely. + */ + public function circuitOpen(): bool + { + return Cache::has(self::CIRCUIT_KEY); + } + + public function tripCircuit(): void + { + $seconds = max(1, (int) config('vision.vector_gateway.circuit_breaker_seconds', 30)); + Cache::put(self::CIRCUIT_KEY, true, $seconds); + } + public function upsertByUrl(string $imageUrl, int|string $id, array $metadata = []): array { $response = $this->postJson( + $this->request(), $this->url((string) config('vision.vector_gateway.upsert_endpoint', '/vectors/upsert')), [ 'url' => $imageUrl, @@ -65,15 +84,24 @@ final class VectorGatewayClient */ public function searchByUrl(string $imageUrl, int $limit = 5): array { - $response = $this->postJson( - $this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')), - [ - 'url' => $imageUrl, - 'limit' => max(1, $limit), - ] - ); + $this->guardCircuit(); + + try { + $response = $this->postJson( + $this->searchRequest(), + $this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')), + [ + 'url' => $imageUrl, + 'limit' => max(1, $limit), + ] + ); + } catch (\Throwable $e) { + $this->tripCircuit(); + throw $e; + } if ($response->failed()) { + $this->tripCircuit(); throw new RuntimeException($this->failureMessage('Vector search', $response)); } @@ -85,16 +113,24 @@ final class VectorGatewayClient */ public function searchByFileContents(string $contents, string $filename, int $limit = 5): array { - $response = $this->request() - ->attach('file', $contents, $filename) - ->post( - $this->url((string) config('vision.vector_gateway.search_file_endpoint', '/vectors/search/file')), - [ - 'limit' => max(1, $limit), - ] - ); + $this->guardCircuit(); + + try { + $response = $this->searchRequest() + ->attach('file', $contents, $filename) + ->post( + $this->url((string) config('vision.vector_gateway.search_file_endpoint', '/vectors/search/file')), + [ + 'limit' => max(1, $limit), + ] + ); + } catch (\Throwable $e) { + $this->tripCircuit(); + throw $e; + } if ($response->failed()) { + $this->tripCircuit(); throw new RuntimeException($this->failureMessage('Vector search', $response)); } @@ -122,6 +158,7 @@ final class VectorGatewayClient public function deleteByIds(array $ids): array { $response = $this->postJson( + $this->request(), $this->url((string) config('vision.vector_gateway.delete_endpoint', '/vectors/delete')), [ 'ids' => array_values(array_map(static fn (int|string $id): string => (string) $id, $ids)), @@ -137,6 +174,10 @@ final class VectorGatewayClient return is_array($json) ? $json : []; } + /** + * Used by upsert/delete — only ever called from queued/console indexing + * jobs, so a more generous budget is fine. + */ private function request(): PendingRequest { if (! $this->isConfigured()) { @@ -156,12 +197,42 @@ final class VectorGatewayClient ); } + /** + * Used by search — runs synchronously inside web requests, so it gets a + * tight timeout budget and no retries to avoid pinning FPM workers. + */ + private function searchRequest(): PendingRequest + { + if (! $this->isConfigured()) { + throw new RuntimeException('Vision vector gateway is not configured. Set VISION_VECTOR_GATEWAY_URL and VISION_VECTOR_GATEWAY_API_KEY.'); + } + + return Http::acceptJson() + ->withHeaders([ + 'X-API-Key' => $this->apiKey(), + ]) + ->connectTimeout(max(1, (int) config('vision.vector_gateway.search_connect_timeout_seconds', 2))) + ->timeout(max(1, (int) config('vision.vector_gateway.search_timeout_seconds', 6))) + ->retry( + max(0, (int) config('vision.vector_gateway.search_retries', 0)), + max(0, (int) config('vision.vector_gateway.retry_delay_ms', 250)), + throw: false, + ); + } + + private function guardCircuit(): void + { + if ($this->circuitOpen()) { + throw new RuntimeException('Vector gateway temporarily unavailable (circuit open after a recent failure).'); + } + } + /** * @param array $payload */ - private function postJson(string $url, array $payload): Response + private function postJson(PendingRequest $request, string $url, array $payload): Response { - $response = $this->request()->post($url, $payload); + $response = $request->post($url, $payload); if (! $response instanceof Response) { throw new RuntimeException('Vector gateway request did not return an HTTP response.'); diff --git a/bootstrap/ssr/ssr-manifest.json b/bootstrap/ssr/ssr-manifest.json index 9dee535b..bf340c57 100644 --- a/bootstrap/ssr/ssr-manifest.json +++ b/bootstrap/ssr/ssr-manifest.json @@ -2073,6 +2073,8 @@ "resources/js/Pages/Admin/HomepageAnnouncements/Index.jsx": [], "resources/js/Pages/Admin/Settings.jsx": [], "resources/js/Pages/Admin/Stories.jsx": [], + "resources/js/Pages/Admin/System/SecurityReportIndex.jsx": [], + "resources/js/Pages/Admin/System/SecurityReportShow.jsx": [], "resources/js/Pages/Admin/UploadQueue.jsx": [], "resources/js/Pages/Admin/UsernameQueue.jsx": [], "resources/js/Pages/Admin/Users/Index.jsx": [], diff --git a/bootstrap/ssr/ssr.js b/bootstrap/ssr/ssr.js index 9ef81e07..81b80594 100644 --- a/bootstrap/ssr/ssr.js +++ b/bootstrap/ssr/ssr.js @@ -12378,6 +12378,41 @@ function AccessBadge({ tier = "free", className = "" }) { const tone = CLASSES[normalizedTier] || CLASSES.free; return /* @__PURE__ */ React.createElement("span", { className: `inline-flex items-center rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] ${tone} ${className}`.trim() }, label); } +function normalizeJsonLd(input) { + if (!input) return []; + return (Array.isArray(input) ? input : [input]).filter((schema) => schema && typeof schema === "object"); +} +function SeoHead({ seo = {}, title = null, description = null, jsonLd = null }) { + const metaTitle = seo?.title || title || "Skinbase"; + const metaDescription = seo?.description || description || ""; + const canonical = seo?.canonical || null; + const robots = seo?.robots || null; + const prev = seo?.prev || null; + const next = seo?.next || null; + const ogTitle = seo?.og_title || metaTitle; + const ogDescription = seo?.og_description || metaDescription; + const ogUrl = seo?.og_url || canonical; + const ogType = seo?.og_type || "website"; + const ogImage = seo?.og_image || null; + const keywords = seo?.keywords || null; + const twitterCard = seo?.twitter_card || (ogImage ? "summary_large_image" : "summary"); + const twitterTitle = seo?.twitter_title || ogTitle; + const twitterDescription = seo?.twitter_description || ogDescription; + const twitterImage = seo?.twitter_image || ogImage || null; + const schemas = [...normalizeJsonLd(seo?.json_ld), ...normalizeJsonLd(jsonLd)]; + return /* @__PURE__ */ React.createElement(Se$1, null, /* @__PURE__ */ React.createElement("title", null, metaTitle), metaDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "description", name: "description", content: metaDescription }) : null, keywords ? /* @__PURE__ */ React.createElement("meta", { "head-key": "keywords", name: "keywords", content: keywords }) : null, robots ? /* @__PURE__ */ React.createElement("meta", { "head-key": "robots", name: "robots", content: robots }) : null, canonical ? /* @__PURE__ */ React.createElement("link", { "head-key": "canonical", rel: "canonical", href: canonical }) : null, prev ? /* @__PURE__ */ React.createElement("link", { "head-key": "prev", rel: "prev", href: prev }) : null, next ? /* @__PURE__ */ React.createElement("link", { "head-key": "next", rel: "next", href: next }) : null, /* @__PURE__ */ React.createElement("meta", { "head-key": "og:site_name", property: "og:site_name", content: seo?.og_site_name || "Skinbase" }), /* @__PURE__ */ React.createElement("meta", { "head-key": "og:type", property: "og:type", content: ogType }), /* @__PURE__ */ React.createElement("meta", { "head-key": "og:title", property: "og:title", content: ogTitle }), ogDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:description", property: "og:description", content: ogDescription }) : null, ogUrl ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:url", property: "og:url", content: ogUrl }) : null, ogImage ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image", property: "og:image", content: ogImage }) : null, seo?.og_image_alt ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:alt", property: "og:image:alt", content: seo.og_image_alt }) : null, seo?.og_image_width ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:width", property: "og:image:width", content: String(seo.og_image_width) }) : null, seo?.og_image_height ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:height", property: "og:image:height", content: String(seo.og_image_height) }) : null, /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:card", name: "twitter:card", content: twitterCard }), /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:title", name: "twitter:title", content: twitterTitle }), twitterDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:description", name: "twitter:description", content: twitterDescription }) : null, twitterImage ? /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:image", name: "twitter:image", content: twitterImage }) : null, schemas.map((schema, index2) => { + const schemaType = typeof schema?.["@type"] === "string" ? schema["@type"] : "schema"; + return /* @__PURE__ */ React.createElement( + "script", + { + key: `jsonld-${schemaType}-${index2}`, + "head-key": `jsonld-${schemaType}-${index2}`, + type: "application/ld+json", + dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } + } + ); + })); +} function formatDate$j(iso) { if (!iso) return null; try { @@ -12386,7 +12421,7 @@ function formatDate$j(iso) { return null; } } -function AcademyBillingAccount({ currentTier, isSubscribed, subscription, activePlan = null, links = {} }) { +function AcademyBillingAccount({ seo, currentTier, isSubscribed, subscription, activePlan = null, links = {} }) { const { flash, auth } = X$1().props; const { data, setData, post: post2, processing } = G$1({ issue_type: "billing", @@ -12443,7 +12478,7 @@ function AcademyBillingAccount({ currentTier, isSubscribed, subscription, active const endsAt = formatDate$j(subscription?.endsAt); const onGracePeriod = subscription?.onGracePeriod === true; const subscriptionActive = subscription?.active === true; - return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Academy Subscription" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1280px] space-y-8" }, flash?.error ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[20px] border border-rose-300/20 bg-rose-300/8 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-rose-100" }, flash.error)) : null, flash?.success ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[20px] border border-emerald-300/20 bg-emerald-300/8 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-emerald-100" }, flash.success)) : null, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(15,23,42,0.96))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/85" }, "Skinbase Academy"), /* @__PURE__ */ React.createElement(AccessBadge, { tier: currentTier })), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, isSubscribed ? "Your subscription" : "Academy subscription"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-base leading-8 text-slate-300" }, isSubscribed ? "Your Academy access is active. Manage, upgrade, or cancel your subscription here at any time." : "You are on the free Academy tier. Upgrade to Creator or Pro to unlock premium content.")), onGracePeriod && endsAt ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-amber-300/25 bg-amber-300/[0.06] px-6 py-5" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-amber-100" }, "Your subscription was cancelled and will end on ", endsAt, "."), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-100/75" }, "You still have full access until that date. Open the subscription portal to resume your plan if you change your mind."), /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: "Academy Subscription" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1280px] space-y-8" }, flash?.error ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[20px] border border-rose-300/20 bg-rose-300/8 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-rose-100" }, flash.error)) : null, flash?.success ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[20px] border border-emerald-300/20 bg-emerald-300/8 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-emerald-100" }, flash.success)) : null, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(15,23,42,0.96))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/85" }, "Skinbase Academy"), /* @__PURE__ */ React.createElement(AccessBadge, { tier: currentTier })), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, isSubscribed ? "Your subscription" : "Academy subscription"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-base leading-8 text-slate-300" }, isSubscribed ? "Your Academy access is active. Manage, upgrade, or cancel your subscription here at any time." : "You are on the free Academy tier. Upgrade to Creator or Pro to unlock premium content.")), onGracePeriod && endsAt ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-amber-300/25 bg-amber-300/[0.06] px-6 py-5" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-amber-100" }, "Your subscription was cancelled and will end on ", endsAt, "."), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-100/75" }, "You still have full access until that date. Open the subscription portal to resume your plan if you change your mind."), /* @__PURE__ */ React.createElement( "a", { href: links.portal, @@ -12528,48 +12563,13 @@ const __vite_glob_0_0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.def __proto__: null, default: AcademyBillingAccount }, Symbol.toStringTag, { value: "Module" })); -function AcademyBillingCancel({ message, links = {} }) { - return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(148,163,184,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Academy Billing Canceled" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[920px] space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(67,20,7,0.78))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-100/85" }, "Checkout canceled"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, "No payment was made."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-base leading-8 text-slate-300" }, message)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: links.pricing || "/academy/pricing", className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18" }, "Return to pricing"), /* @__PURE__ */ React.createElement(xe, { href: links.academy || "/academy", className: "rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, "Back to Academy")))); +function AcademyBillingCancel({ seo, message, links = {} }) { + return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(148,163,184,0.14),_transparent_26%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: "Academy Billing Canceled" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[920px] space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.9),rgba(67,20,7,0.78))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-100/85" }, "Checkout canceled"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, "No payment was made."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-base leading-8 text-slate-300" }, message)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: links.pricing || "/academy/pricing", className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18" }, "Return to pricing"), /* @__PURE__ */ React.createElement(xe, { href: links.academy || "/academy", className: "rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, "Back to Academy")))); } const __vite_glob_0_1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AcademyBillingCancel }, Symbol.toStringTag, { value: "Module" })); -function normalizeJsonLd(input) { - if (!input) return []; - return (Array.isArray(input) ? input : [input]).filter((schema) => schema && typeof schema === "object"); -} -function SeoHead({ seo = {}, title = null, description = null, jsonLd = null }) { - const metaTitle = seo?.title || title || "Skinbase"; - const metaDescription = seo?.description || description || ""; - const canonical = seo?.canonical || null; - const robots = seo?.robots || null; - const prev = seo?.prev || null; - const next = seo?.next || null; - const ogTitle = seo?.og_title || metaTitle; - const ogDescription = seo?.og_description || metaDescription; - const ogUrl = seo?.og_url || canonical; - const ogType = seo?.og_type || "website"; - const ogImage = seo?.og_image || null; - const keywords = seo?.keywords || null; - const twitterCard = seo?.twitter_card || (ogImage ? "summary_large_image" : "summary"); - const twitterTitle = seo?.twitter_title || ogTitle; - const twitterDescription = seo?.twitter_description || ogDescription; - const twitterImage = seo?.twitter_image || ogImage || null; - const schemas = [...normalizeJsonLd(seo?.json_ld), ...normalizeJsonLd(jsonLd)]; - return /* @__PURE__ */ React.createElement(Se$1, null, /* @__PURE__ */ React.createElement("title", null, metaTitle), metaDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "description", name: "description", content: metaDescription }) : null, keywords ? /* @__PURE__ */ React.createElement("meta", { "head-key": "keywords", name: "keywords", content: keywords }) : null, robots ? /* @__PURE__ */ React.createElement("meta", { "head-key": "robots", name: "robots", content: robots }) : null, canonical ? /* @__PURE__ */ React.createElement("link", { "head-key": "canonical", rel: "canonical", href: canonical }) : null, prev ? /* @__PURE__ */ React.createElement("link", { "head-key": "prev", rel: "prev", href: prev }) : null, next ? /* @__PURE__ */ React.createElement("link", { "head-key": "next", rel: "next", href: next }) : null, /* @__PURE__ */ React.createElement("meta", { "head-key": "og:site_name", property: "og:site_name", content: seo?.og_site_name || "Skinbase" }), /* @__PURE__ */ React.createElement("meta", { "head-key": "og:type", property: "og:type", content: ogType }), /* @__PURE__ */ React.createElement("meta", { "head-key": "og:title", property: "og:title", content: ogTitle }), ogDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:description", property: "og:description", content: ogDescription }) : null, ogUrl ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:url", property: "og:url", content: ogUrl }) : null, ogImage ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image", property: "og:image", content: ogImage }) : null, seo?.og_image_alt ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:alt", property: "og:image:alt", content: seo.og_image_alt }) : null, seo?.og_image_width ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:width", property: "og:image:width", content: String(seo.og_image_width) }) : null, seo?.og_image_height ? /* @__PURE__ */ React.createElement("meta", { "head-key": "og:image:height", property: "og:image:height", content: String(seo.og_image_height) }) : null, /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:card", name: "twitter:card", content: twitterCard }), /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:title", name: "twitter:title", content: twitterTitle }), twitterDescription ? /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:description", name: "twitter:description", content: twitterDescription }) : null, twitterImage ? /* @__PURE__ */ React.createElement("meta", { "head-key": "twitter:image", name: "twitter:image", content: twitterImage }) : null, schemas.map((schema, index2) => { - const schemaType = typeof schema?.["@type"] === "string" ? schema["@type"] : "schema"; - return /* @__PURE__ */ React.createElement( - "script", - { - key: `jsonld-${schemaType}-${index2}`, - "head-key": `jsonld-${schemaType}-${index2}`, - type: "application/ld+json", - dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } - } - ); - })); -} function ActionButton$1({ disabled, children, onClick, href, tone = "primary" }) { const toneClass = { primary: "border-sky-300/25 bg-sky-300/12 text-sky-100 hover:border-sky-300/40 hover:bg-sky-300/18", @@ -12960,12 +12960,12 @@ const __vite_glob_0_2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.def __proto__: null, default: AcademyBillingPricing }, Symbol.toStringTag, { value: "Module" })); -function AcademyBillingSuccess({ currentTier, isSubscribed, links = {} }) { +function AcademyBillingSuccess({ seo, currentTier, isSubscribed, links = {} }) { const { auth } = X$1().props; const sessionId = X$1().props.sessionId || null; const userEmail = auth?.user?.email ?? null; const { data, setData, post: post2, processing } = G$1({ message: "", session_id: sessionId }); - return /* @__PURE__ */ React.createElement("main", { className: "flex min-h-screen items-center bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(16,185,129,0.14),_transparent_24%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Subscription Confirmed" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto w-full max-w-[640px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-emerald-300/20 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.92),rgba(6,78,59,0.82))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-3xl leading-none" }, "🎉"), isSubscribed ? /* @__PURE__ */ React.createElement(AccessBadge, { tier: currentTier }) : null), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, isSubscribed ? "Welcome to Academy." : "You're all set."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-lg text-base leading-8 text-slate-300" }, isSubscribed ? "Your subscription is active and all premium content for your plan is now unlocked. Head to Academy and start exploring." : "Your payment was confirmed and your subscription is activating now. This usually takes just a moment. If you don't see your access right away, refresh the Academy page in a few seconds."), !isSubscribed ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-amber-300/20 bg-amber-300/8 px-4 py-3 text-sm text-amber-50" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold" }, "If your access isn't updated automatically"), /* @__PURE__ */ React.createElement("p", { className: "mt-1" }, "If your Academy access doesn't appear within a few minutes, email ", /* @__PURE__ */ React.createElement("strong", null, "academy@skinbase.org"), " or click the button below to open a prefilled message. Include your account email", userEmail ? ` (${userEmail})` : "", " and the checkout session id", sessionId ? `: ${sessionId}` : "."), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap items-start gap-4" }, /* @__PURE__ */ React.createElement("form", { onSubmit: (e) => { + return /* @__PURE__ */ React.createElement("main", { className: "flex min-h-screen items-center bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(16,185,129,0.14),_transparent_24%),linear-gradient(180deg,_#07111f_0%,_#0f172a_45%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: "Academy Subscription Confirmed" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto w-full max-w-[640px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-emerald-300/20 bg-[linear-gradient(135deg,rgba(7,17,31,0.95),rgba(12,24,45,0.92),rgba(6,78,59,0.82))] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.42)] md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-3xl leading-none" }, "🎉"), isSubscribed ? /* @__PURE__ */ React.createElement(AccessBadge, { tier: currentTier }) : null), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, isSubscribed ? "Welcome to Academy." : "You're all set."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-lg text-base leading-8 text-slate-300" }, isSubscribed ? "Your subscription is active and all premium content for your plan is now unlocked. Head to Academy and start exploring." : "Your payment was confirmed and your subscription is activating now. This usually takes just a moment. If you don't see your access right away, refresh the Academy page in a few seconds."), !isSubscribed ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-amber-300/20 bg-amber-300/8 px-4 py-3 text-sm text-amber-50" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold" }, "If your access isn't updated automatically"), /* @__PURE__ */ React.createElement("p", { className: "mt-1" }, "If your Academy access doesn't appear within a few minutes, email ", /* @__PURE__ */ React.createElement("strong", null, "academy@skinbase.org"), " or click the button below to open a prefilled message. Include your account email", userEmail ? ` (${userEmail})` : "", " and the checkout session id", sessionId ? `: ${sessionId}` : "."), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap items-start gap-4" }, /* @__PURE__ */ React.createElement("form", { onSubmit: (e) => { e.preventDefault(); post2(links.reportIssue, { preserveScroll: true }); }, className: "flex w-full max-w-lg items-start gap-2" }, /* @__PURE__ */ React.createElement("textarea", { value: data.message, onChange: (e) => setData("message", e.target.value), placeholder: "Optional: Tell us what you expected to see or any useful details", className: "flex-1 rounded-md bg-black/20 border border-amber-300/20 p-2 text-sm text-amber-50", rows: 3 }), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: processing, className: "rounded-full border border-amber-300/30 bg-amber-300/12 px-4 py-2 text-sm font-semibold text-amber-900 hover:bg-amber-300/16" }, "Send report")), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-amber-100" }, /* @__PURE__ */ React.createElement("div", null, "- Wait 2–3 minutes and refresh the Academy page."), /* @__PURE__ */ React.createElement("div", null, "- If you still lack access, use the form above or email academy@skinbase.org.")))) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement( @@ -13289,7 +13289,7 @@ function AcademyChallengeSubmit({ seo, challenge, artworks, submitUrl }) { is_ai_assisted: true }); const artworkOptions = artworks.map((artwork) => ({ value: artwork.id, label: artwork.title })); - return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.15),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: `Submit to ${challenge.title}` }), /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: `Submit to ${challenge.title}`, description: challenge.excerpt || challenge.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[960px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Academy challenge"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, "Submit to ", challenge.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-base leading-8 text-slate-300" }, "Attach one of your public artworks and optionally include the prompt or workflow notes used to create it.")), /* @__PURE__ */ React.createElement("form", { onSubmit: (event) => { + return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.15),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: `Submit to ${challenge.title}`, description: challenge.excerpt || challenge.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[960px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Academy challenge"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, "Submit to ", challenge.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-base leading-8 text-slate-300" }, "Attach one of your public artworks and optionally include the prompt or workflow notes used to create it.")), /* @__PURE__ */ React.createElement("form", { onSubmit: (event) => { event.preventDefault(); form.post(submitUrl); }, className: "space-y-5 rounded-[30px] border border-white/10 bg-white/[0.04] p-6" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "text-sm font-semibold text-white" }, "Artwork"), /* @__PURE__ */ React.createElement( @@ -13647,8 +13647,22 @@ function MetricCard$2({ label, value, accent }) { return /* @__PURE__ */ React.createElement("div", { className: `rounded-[24px] border px-5 py-5 backdrop-blur-sm ${accent.shell}` }, /* @__PURE__ */ React.createElement("p", { className: `text-[10px] font-semibold uppercase tracking-[0.2em] ${accent.label}` }, label), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.05em] text-white md:text-[2.4rem]" }, value)); } function FeaturedCourseCard({ course }) { - const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ""; - return /* @__PURE__ */ React.createElement(xe, { href: course.public_url, className: "group relative overflow-hidden rounded-[28px] border border-sky-200/12 bg-[linear-gradient(180deg,rgba(15,23,42,0.9),rgba(15,23,42,0.72))] transition hover:-translate-y-1 hover:border-sky-300/24 hover:shadow-[0_24px_72px_rgba(2,6,23,0.3)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.14),transparent_24%),linear-gradient(135deg,transparent_0%,transparent_48%,rgba(125,211,252,0.05)_48%,rgba(125,211,252,0.05)_52%,transparent_52%,transparent_100%)] opacity-80" }), /* @__PURE__ */ React.createElement("div", { className: "relative h-44 overflow-hidden bg-[linear-gradient(135deg,rgba(14,165,233,0.24),rgba(15,23,42,0.92))]" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: "", "aria-hidden": "true", className: "h-full w-full object-cover" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.82))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, course.difficulty), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200" }, course.access_level))), /* @__PURE__ */ React.createElement("div", { className: "relative p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.2em] text-sky-100/75" }, "Guided course"), /* @__PURE__ */ React.createElement("h3", { className: "text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100" }, course.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, course.excerpt || course.description || "Guided Academy course."), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, course.lessons_count || 0, " lessons · ", course.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible duration"), /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100 transition group-hover:translate-x-1" }, "Open path")))); + const cover = course?.cover_image_thumb_url || course?.cover_image_url || course?.teaser_image_thumb_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ""; + const coverSrcSet = course?.cover_image_srcset || course?.teaser_image_srcset || ""; + return /* @__PURE__ */ React.createElement(xe, { href: course.public_url, className: "group relative overflow-hidden rounded-[28px] border border-sky-200/12 bg-[linear-gradient(180deg,rgba(15,23,42,0.9),rgba(15,23,42,0.72))] transition hover:-translate-y-1 hover:border-sky-300/24 hover:shadow-[0_24px_72px_rgba(2,6,23,0.3)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.14),transparent_24%),linear-gradient(135deg,transparent_0%,transparent_48%,rgba(125,211,252,0.05)_48%,rgba(125,211,252,0.05)_52%,transparent_52%,transparent_100%)] opacity-80" }), /* @__PURE__ */ React.createElement("div", { className: "relative h-44 overflow-hidden bg-[linear-gradient(135deg,rgba(14,165,233,0.24),rgba(15,23,42,0.92))]" }, cover ? /* @__PURE__ */ React.createElement( + "img", + { + src: cover, + srcSet: coverSrcSet || void 0, + sizes: "(min-width: 1280px) 31vw, (min-width: 768px) 50vw, 100vw", + alt: "", + "aria-hidden": "true", + loading: "lazy", + decoding: "async", + fetchPriority: "low", + className: "h-full w-full object-cover" + } + ) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.82))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, course.difficulty), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200" }, course.access_level))), /* @__PURE__ */ React.createElement("div", { className: "relative p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.2em] text-sky-100/75" }, "Guided course"), /* @__PURE__ */ React.createElement("h3", { className: "text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100" }, course.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, course.excerpt || course.description || "Guided Academy course."), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, course.lessons_count || 0, " lessons · ", course.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible duration"), /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100 transition group-hover:translate-x-1" }, "Open path")))); } function formatAccessDate$1(value) { if (!value) { @@ -14557,7 +14571,7 @@ function PromptFilledExampleCard({ example, analytics, contentId, index: index2 eventType: "academy_prompt_filled_example_copy", metadata: { copy_type: "filled_example", filled_example_index: index2, source: "prompt_filled_examples" } } - ) : null), placeholderEntries.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, placeholderEntries.map(([key, value]) => /* @__PURE__ */ React.createElement("span", { key, className: "rounded-full border border-violet-300/20 bg-violet-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-violet-100" }, key, ": ", /* @__PURE__ */ React.createElement("span", { className: "normal-case tracking-normal text-white" }, String(value))))) : null, example?.prompt ? /* @__PURE__ */ React.createElement("pre", { className: "mt-5 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100" }, example.prompt) : null, example?.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-slate-950/60 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement( + ) : null), placeholderEntries.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, placeholderEntries.map(([key, value]) => /* @__PURE__ */ React.createElement("span", { key, className: "rounded-full border border-violet-300/20 bg-violet-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-violet-100" }, key, ": ", /* @__PURE__ */ React.createElement("span", { className: "normal-case tracking-normal text-white" }, String(value))))) : null, example?.prompt ? /* @__PURE__ */ React.createElement("pre", { className: "mt-5 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100 [overflow-wrap:anywhere]" }, example.prompt) : null, example?.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-slate-950/60 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement( PromptCopyButton, { prompt: example.negative_prompt, @@ -14668,11 +14682,11 @@ function PromptFilledExamplesSection({ examples, analytics, contentId }) { } function PromptHelperPromptCard({ helperPrompt, analytics, contentId }) { if (!helperPrompt || typeof helperPrompt !== "object") return null; - return /* @__PURE__ */ React.createElement("details", { className: "group rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(15,23,42,0.2))] p-5 shadow-[0_16px_40px_rgba(2,6,23,0.16)]" }, /* @__PURE__ */ React.createElement("summary", { className: "flex cursor-pointer list-none flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-[#ffd8cd]" }, "Helper prompt"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold tracking-[-0.03em] text-white" }, helperPrompt.title || "Helper prompt"), helperPrompt.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-7 text-slate-300" }, helperPrompt.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, helperPrompt.type ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, formatMetaDisplay(helperPrompt.type)) : null, helperPrompt.expected_output ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/18 bg-sky-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, helperPrompt.expected_output) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[24px] border border-white/10 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Prompt text"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: helperPrompt.prompt, label: "Copy helper prompt", analytics, contentId, eventType: "academy_prompt_helper_copy", metadata: { copy_type: "helper_prompt", helper_prompt_title: helperPrompt.title || "", source: "prompt_helper" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100" }, helperPrompt.prompt))); + return /* @__PURE__ */ React.createElement("details", { className: "group rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(15,23,42,0.2))] p-5 shadow-[0_16px_40px_rgba(2,6,23,0.16)]" }, /* @__PURE__ */ React.createElement("summary", { className: "flex cursor-pointer list-none flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-[#ffd8cd]" }, "Helper prompt"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold tracking-[-0.03em] text-white" }, helperPrompt.title || "Helper prompt"), helperPrompt.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-7 text-slate-300" }, helperPrompt.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, helperPrompt.type ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, formatMetaDisplay(helperPrompt.type)) : null, helperPrompt.expected_output ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/18 bg-sky-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, helperPrompt.expected_output) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[24px] border border-white/10 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Prompt text"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: helperPrompt.prompt, label: "Copy helper prompt", analytics, contentId, eventType: "academy_prompt_helper_copy", metadata: { copy_type: "helper_prompt", helper_prompt_title: helperPrompt.title || "", source: "prompt_helper" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100 [overflow-wrap:anywhere]" }, helperPrompt.prompt))); } function PromptVariantCard({ variant, analytics, contentId }) { if (!variant || typeof variant !== "object") return null; - return /* @__PURE__ */ React.createElement("article", { className: `rounded-[28px] border p-5 shadow-[0_16px_40px_rgba(2,6,23,0.16)] ${variant.recommended ? "border-[#ffcfbf]/22 bg-[linear-gradient(180deg,rgba(255,207,191,0.08),rgba(15,23,42,0.24))]" : "border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(15,23,42,0.2))]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Prompt variant"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold tracking-[-0.03em] text-white" }, variant.title || "Variant"), variant.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-7 text-slate-300" }, variant.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, variant.recommended ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-[#ffcfbf]/22 bg-[#ffcfbf]/12 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, "Recommended") : null, variant.slug ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 font-mono text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, variant.slug) : null)), variant.recommended_for?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, variant.recommended_for.map((item) => /* @__PURE__ */ React.createElement("span", { key: item, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, item))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[24px] border border-white/10 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Variant prompt"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: variant.prompt, label: "Copy variant", analytics, contentId, eventType: "academy_prompt_variant_copy", metadata: { copy_type: "prompt_variant", variant_title: variant.title || "", source: "prompt_variant" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100" }, variant.prompt)), variant.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: variant.negative_prompt, label: "Copy negative", analytics, contentId, eventType: "academy_prompt_variant_negative_copy", metadata: { copy_type: "prompt_variant_negative", variant_title: variant.title || "", source: "prompt_variant" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-200" }, variant.negative_prompt)) : null, variant.risk_notes?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[22px] border border-amber-300/15 bg-amber-300/10 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, "Risk notes"), /* @__PURE__ */ React.createElement("ul", { className: "mt-3 space-y-2 text-sm leading-7 text-slate-100" }, variant.risk_notes.map((item) => /* @__PURE__ */ React.createElement("li", { key: item }, item)))) : null); + return /* @__PURE__ */ React.createElement("article", { className: `rounded-[28px] border p-5 shadow-[0_16px_40px_rgba(2,6,23,0.16)] ${variant.recommended ? "border-[#ffcfbf]/22 bg-[linear-gradient(180deg,rgba(255,207,191,0.08),rgba(15,23,42,0.24))]" : "border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(15,23,42,0.2))]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Prompt variant"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold tracking-[-0.03em] text-white" }, variant.title || "Variant"), variant.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-7 text-slate-300" }, variant.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, variant.recommended ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-[#ffcfbf]/22 bg-[#ffcfbf]/12 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, "Recommended") : null, variant.slug ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 font-mono text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, variant.slug) : null)), variant.recommended_for?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, variant.recommended_for.map((item) => /* @__PURE__ */ React.createElement("span", { key: item, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, item))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[24px] border border-white/10 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Variant prompt"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: variant.prompt, label: "Copy variant", analytics, contentId, eventType: "academy_prompt_variant_copy", metadata: { copy_type: "prompt_variant", variant_title: variant.title || "", source: "prompt_variant" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100 [overflow-wrap:anywhere]" }, variant.prompt)), variant.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: variant.negative_prompt, label: "Copy negative", analytics, contentId, eventType: "academy_prompt_variant_negative_copy", metadata: { copy_type: "prompt_variant_negative", variant_title: variant.title || "", source: "prompt_variant" } })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-200 [overflow-wrap:anywhere]" }, variant.negative_prompt)) : null, variant.risk_notes?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[22px] border border-amber-300/15 bg-amber-300/10 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, "Risk notes"), /* @__PURE__ */ React.createElement("ul", { className: "mt-3 space-y-2 text-sm leading-7 text-slate-100" }, variant.risk_notes.map((item) => /* @__PURE__ */ React.createElement("li", { key: item }, item)))) : null); } function PromptVariantsSection({ variants, analytics, contentId }) { const visibleVariants = Array.isArray(variants) ? variants.filter((variant) => variant && typeof variant === "object") : []; @@ -14806,7 +14820,7 @@ function AiComparisonSection({ block }) { const hasNegativePrompt = Boolean(payload.negative_prompt); const hasUsefulData = Boolean(block?.title || payload.title || payload.intro || hasPrompt || hasNegativePrompt || payload.aspect_ratio || criteria.length || results.length); if (!hasUsefulData) return null; - return /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,151,132,0.14),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffb8aa]" }, "AI Model Comparison"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, payload.title || block.title || "Same Prompt, Different AI Models"), payload.intro ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, payload.intro) : null), payload.aspect_ratio ? /* @__PURE__ */ React.createElement("div", { className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-200" }, "Aspect ratio ", payload.aspect_ratio) : null), hasPrompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[26px] border border-[#ffb8aa]/15 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-[#ffd0c6]" }, "Prompt used"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.16em] text-slate-500" }, "Shared source prompt across all compared models")), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: payload.prompt })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100" }, payload.prompt), hasNegativePrompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[22px] border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement("pre", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-300" }, payload.negative_prompt)) : null) : null, criteria.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "What we compare"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, criteria.map((criterion) => /* @__PURE__ */ React.createElement("span", { key: criterion, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-medium text-slate-100" }, criterion)))) : null, results.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 md:grid-cols-2 2xl:grid-cols-4" }, results.map((result) => { + return /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,151,132,0.14),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffb8aa]" }, "AI Model Comparison"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, payload.title || block.title || "Same Prompt, Different AI Models"), payload.intro ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, payload.intro) : null), payload.aspect_ratio ? /* @__PURE__ */ React.createElement("div", { className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-200" }, "Aspect ratio ", payload.aspect_ratio) : null), hasPrompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[26px] border border-[#ffb8aa]/15 bg-black/25 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-[#ffd0c6]" }, "Prompt used"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.16em] text-slate-500" }, "Shared source prompt across all compared models")), /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: payload.prompt })), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[22px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-100 [overflow-wrap:anywhere]" }, payload.prompt), hasNegativePrompt ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[22px] border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement("pre", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-300" }, payload.negative_prompt)) : null) : null, criteria.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "What we compare"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, criteria.map((criterion) => /* @__PURE__ */ React.createElement("span", { key: criterion, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-medium text-slate-100" }, criterion)))) : null, results.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 md:grid-cols-2 2xl:grid-cols-4" }, results.map((result) => { const imageUrl = result.thumb_url || result.image_url || result.thumb_path || result.image_path || ""; const score = Number(result.score || 0); const hasScore = Number.isFinite(score) && score > 0; @@ -15254,7 +15268,7 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], pre.dataset.academyCopyButtonMounted = "true"; }); }, [item?.content, lessonFontScale, pageType]); - return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(59,130,246,0.14),_transparent_26%),linear-gradient(180deg,_#0b1220_0%,_#111827_46%,_#0f172a_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: item?.title, description: item?.excerpt || item?.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1320px] space-y-6" }, flash.success ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm text-emerald-100" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-rose-300/20 bg-rose-300/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, item.locked ? /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: pageType, accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: `${pageType}_locked_panel` }) }) : null, pageType === "lesson" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[40px] border border-sky-200/12 bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_38%,rgba(251,191,36,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(125,211,252,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-70" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-10 h-36 w-36 rounded-full bg-sky-300/18 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-amber-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_360px] lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))] p-5 shadow-[0_20px_46px_rgba(2,6,23,0.18)] md:p-6 lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))]" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-4xl" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-200/18 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-50/90" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-200" }, "Lesson"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300" }, lessonCategory), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300" }, lessonDifficulty)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, item.lesson_label ? /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold uppercase tracking-[0.24em] text-amber-100" }, item.lesson_label) : null, /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-[3.8rem]" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95 md:text-lg" }, lessonSummary)), /* @__PURE__ */ React.createElement("span", { className: "hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-sky-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader" }))), lessonTags.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, lessonTags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: tag, className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-sky-50/90" }, tag))) : null, courseContext?.title ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-slate-950/35 p-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Part of course"), /* @__PURE__ */ React.createElement(xe, { href: courseContext.showUrl, className: "mt-2 inline-flex text-lg font-semibold text-sky-100 transition hover:text-white" }, courseContext.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, courseContext.subtitle || "This lesson is being viewed inside a structured Academy course path.")) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-7 flex flex-wrap gap-3" }, completeUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: markComplete, className: "rounded-full border border-emerald-300/25 bg-emerald-300/12 px-5 py-3 text-sm font-semibold text-emerald-100" }, completed ? "Completed" : "Mark complete") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleLike, className: "rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleSave, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), submitUrl ? /* @__PURE__ */ React.createElement(xe, { href: submitUrl, className: "rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-sky-300/25 hover:bg-sky-300/12 hover:text-sky-100" }, "Submit artwork") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-3 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatPill$2, { label: "Category", value: lessonCategory }), /* @__PURE__ */ React.createElement(StatPill$2, { label: "Reading", value: lessonMinutes }), /* @__PURE__ */ React.createElement(StatPill$2, { label: "Updated", value: lessonUpdated }), /* @__PURE__ */ React.createElement(StatPill$2, { label: courseContext?.title ? "Course progress" : "Access", value: courseContext?.progress ? `${courseContext.progress.percent}%` : item.access_level || "free" })))), /* @__PURE__ */ React.createElement("aside", { className: "grid gap-4 self-start" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden rounded-[32px] border border-white/10 bg-slate-950 shadow-[0_24px_56px_rgba(2,6,23,0.24)]" }, lessonCover ? /* @__PURE__ */ React.createElement("img", { src: lessonCover, alt: item.title, className: "h-[260px] w-full object-cover sm:h-[300px] lg:h-[320px]" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-[260px] items-center justify-center bg-[linear-gradient(135deg,_rgba(14,165,233,0.22),_rgba(17,24,39,0.96))] text-sm font-semibold uppercase tracking-[0.24em] text-slate-300 sm:h-[300px] lg:h-[320px]" }, "Lesson cover"), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.02),rgba(2,6,23,0.2)_48%,rgba(2,6,23,0.88))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 bottom-0 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 backdrop-blur-md" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100/80" }, "Lesson cover"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm font-semibold text-white" }, item.lesson_label || item.title)))), /* @__PURE__ */ React.createElement("div", { className: "space-y-3 rounded-[30px] border border-white/10 bg-black/20 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm" }, /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Series", value: lessonSeries }), item.formatted_lesson_number ? /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Lesson", value: item.formatted_lesson_number }) : null, /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Difficulty", value: lessonDifficulty }), /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Reading time", value: lessonMinutes }), /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Published", value: lessonUpdated })), /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-slate-950/35 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Lesson status"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.locked ? "This lesson is partially locked for your account level." : courseContext?.title ? "This lesson is being tracked inside a course. Completion updates your course progress." : "Full lesson content is available below."))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("article", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-4 border-b border-white/10 pb-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Article"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Lesson content")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-slate-300" }, lessonMinutes), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 rounded-full border border-white/10 bg-black/20 p-1" }, /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(59,130,246,0.14),_transparent_26%),linear-gradient(180deg,_#0b1220_0%,_#111827_46%,_#0f172a_100%)] px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: item?.title, description: item?.excerpt || item?.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto min-w-0 max-w-[1320px] space-y-6" }, flash.success ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm text-emerald-100" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-rose-300/20 bg-rose-300/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, item.locked ? /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: pageType, accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: `${pageType}_locked_panel` }) }) : null, pageType === "lesson" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[40px] border border-sky-200/12 bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_38%,rgba(251,191,36,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(125,211,252,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-70" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-10 h-36 w-36 rounded-full bg-sky-300/18 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-amber-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_360px] lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))] p-5 shadow-[0_20px_46px_rgba(2,6,23,0.18)] md:p-6 lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))]" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-4xl" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-200/18 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-50/90" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-200" }, "Lesson"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300" }, lessonCategory), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300" }, lessonDifficulty)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, item.lesson_label ? /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold uppercase tracking-[0.24em] text-amber-100" }, item.lesson_label) : null, /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-[3.8rem]" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95 md:text-lg" }, lessonSummary)), /* @__PURE__ */ React.createElement("span", { className: "hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-sky-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader" }))), lessonTags.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, lessonTags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: tag, className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-sky-50/90" }, tag))) : null, courseContext?.title ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-slate-950/35 p-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Part of course"), /* @__PURE__ */ React.createElement(xe, { href: courseContext.showUrl, className: "mt-2 inline-flex text-lg font-semibold text-sky-100 transition hover:text-white" }, courseContext.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, courseContext.subtitle || "This lesson is being viewed inside a structured Academy course path.")) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-7 flex flex-wrap gap-3" }, completeUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: markComplete, className: "rounded-full border border-emerald-300/25 bg-emerald-300/12 px-5 py-3 text-sm font-semibold text-emerald-100" }, completed ? "Completed" : "Mark complete") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleLike, className: "rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleSave, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), submitUrl ? /* @__PURE__ */ React.createElement(xe, { href: submitUrl, className: "rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-sky-300/25 hover:bg-sky-300/12 hover:text-sky-100" }, "Submit artwork") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-3 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatPill$2, { label: "Category", value: lessonCategory }), /* @__PURE__ */ React.createElement(StatPill$2, { label: "Reading", value: lessonMinutes }), /* @__PURE__ */ React.createElement(StatPill$2, { label: "Updated", value: lessonUpdated }), /* @__PURE__ */ React.createElement(StatPill$2, { label: courseContext?.title ? "Course progress" : "Access", value: courseContext?.progress ? `${courseContext.progress.percent}%` : item.access_level || "free" })))), /* @__PURE__ */ React.createElement("aside", { className: "grid gap-4 self-start" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden rounded-[32px] border border-white/10 bg-slate-950 shadow-[0_24px_56px_rgba(2,6,23,0.24)]" }, lessonCover ? /* @__PURE__ */ React.createElement("img", { src: lessonCover, alt: item.title, className: "h-[260px] w-full object-cover sm:h-[300px] lg:h-[320px]" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-[260px] items-center justify-center bg-[linear-gradient(135deg,_rgba(14,165,233,0.22),_rgba(17,24,39,0.96))] text-sm font-semibold uppercase tracking-[0.24em] text-slate-300 sm:h-[300px] lg:h-[320px]" }, "Lesson cover"), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.02),rgba(2,6,23,0.2)_48%,rgba(2,6,23,0.88))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 bottom-0 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 backdrop-blur-md" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100/80" }, "Lesson cover"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm font-semibold text-white" }, item.lesson_label || item.title)))), /* @__PURE__ */ React.createElement("div", { className: "space-y-3 rounded-[30px] border border-white/10 bg-black/20 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm" }, /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Series", value: lessonSeries }), item.formatted_lesson_number ? /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Lesson", value: item.formatted_lesson_number }) : null, /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Difficulty", value: lessonDifficulty }), /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Reading time", value: lessonMinutes }), /* @__PURE__ */ React.createElement(LessonInfoRow, { label: "Published", value: lessonUpdated })), /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-slate-950/35 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Lesson status"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.locked ? "This lesson is partially locked for your account level." : courseContext?.title ? "This lesson is being tracked inside a course. Completion updates your course progress." : "Full lesson content is available below."))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("article", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-4 border-b border-white/10 pb-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Article"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Lesson content")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-slate-300" }, lessonMinutes), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1 rounded-full border border-white/10 bg-black/20 p-1" }, /* @__PURE__ */ React.createElement( "button", { type: "button", @@ -15305,7 +15319,7 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], }, /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-sky-300/15 bg-sky-300/10 text-sm font-semibold text-sky-100" }, String(index2 + 1).padStart(2, "0")), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, relatedLesson.formatted_lesson_number ? /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, relatedLesson.formatted_lesson_number) : null, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white transition group-hover:text-sky-100" }, relatedLesson.title)), /* @__PURE__ */ React.createElement("span", { className: "shrink-0 rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, formatLessonMinutes(relatedLesson.reading_minutes))), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs leading-6 text-slate-400" }, relatedLesson.excerpt || relatedLesson.content_preview || "Continue the series with the next lesson.")) - )))) : null, relatedCourseList.length ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Related courses"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, relatedCourseList.map((course) => /* @__PURE__ */ React.createElement(xe, { key: course.id, href: course.public_url, className: "block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, course.difficulty, " · ", course.access_level), /* @__PURE__ */ React.createElement("h4", { className: "mt-2 text-sm font-semibold text-white" }, course.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs leading-6 text-slate-400" }, course.excerpt || course.description || "Open this course to continue with a guided path."))))) : null))) : pageType === "prompt" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[40px] border border-rose-200/12 bg-[linear-gradient(150deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_36%,rgba(45,212,191,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(251,113,133,0.15),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-10 h-36 w-36 rounded-full bg-rose-300/16 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-cyan-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_minmax(320px,0.72fr)] lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, academyBreadcrumbs.length ? /* @__PURE__ */ React.createElement("div", { className: "mb-5" }, /* @__PURE__ */ React.createElement(AcademyBreadcrumbs, { items: academyBreadcrumbs })) : null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-200/18 bg-rose-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-rose-50/90" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-200" }, "Prompt Library"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, lessonCategory), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, lessonDifficulty), item.aspect_ratio ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, item.aspect_ratio) : null, item.prompt_of_week ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/25 bg-amber-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-amber-100" }, "Prompt of the week") : null, item.featured ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, "Featured") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80" }, "Prompt template"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 max-w-[13ch] text-[clamp(2.6rem,5vw,4.8rem)] font-semibold leading-[0.95] tracking-[-0.05em] text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95" }, lessonSummary)), /* @__PURE__ */ React.createElement("span", { className: "hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-rose-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-wand-magic-sparkles" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleLike, className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleSave, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2.5 text-sm font-semibold text-sky-100" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), promptHasFullAccess ? /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: item.prompt, analytics, contentId: item.id, eventType: "academy_prompt_copy", metadata: { copy_type: "main_prompt", source: "prompt_detail" } }) : null, promptHasFullAccess && item.negative_prompt ? /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: item.negative_prompt, label: "Copy negative", analytics, contentId: item.id, eventType: "academy_prompt_negative_copy", metadata: { copy_type: "negative_prompt", source: "prompt_detail" } }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-7 grid gap-3 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement( + )))) : null, relatedCourseList.length ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Related courses"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, relatedCourseList.map((course) => /* @__PURE__ */ React.createElement(xe, { key: course.id, href: course.public_url, className: "block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, course.difficulty, " · ", course.access_level), /* @__PURE__ */ React.createElement("h4", { className: "mt-2 text-sm font-semibold text-white" }, course.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs leading-6 text-slate-400" }, course.excerpt || course.description || "Open this course to continue with a guided path."))))) : null))) : pageType === "prompt" ? /* @__PURE__ */ React.createElement("div", { className: "min-w-0 max-w-full space-y-8 overflow-x-clip" }, /* @__PURE__ */ React.createElement("section", { className: "relative w-full min-w-0 max-w-full overflow-hidden rounded-[40px] border border-rose-200/12 bg-[linear-gradient(150deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_36%,rgba(45,212,191,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(251,113,133,0.15),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-10 h-36 w-36 rounded-full bg-rose-300/16 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-cyan-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid min-w-0 grid-cols-1 gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_minmax(320px,0.72fr)] lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, academyBreadcrumbs.length ? /* @__PURE__ */ React.createElement("div", { className: "mb-5" }, /* @__PURE__ */ React.createElement(AcademyBreadcrumbs, { items: academyBreadcrumbs })) : null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-200/18 bg-rose-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-rose-50/90" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-200" }, "Prompt Library"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, lessonCategory), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, lessonDifficulty), item.aspect_ratio ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, item.aspect_ratio) : null, item.prompt_of_week ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/25 bg-amber-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-amber-100" }, "Prompt of the week") : null, item.featured ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, "Featured") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80" }, "Prompt template"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 max-w-[13ch] text-[clamp(2.6rem,5vw,4.8rem)] font-semibold leading-[0.95] tracking-[-0.05em] text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95" }, lessonSummary)), /* @__PURE__ */ React.createElement("span", { className: "hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-rose-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-wand-magic-sparkles" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleLike, className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: toggleSave, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2.5 text-sm font-semibold text-sky-100" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), promptHasFullAccess ? /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: item.prompt, analytics, contentId: item.id, eventType: "academy_prompt_copy", metadata: { copy_type: "main_prompt", source: "prompt_detail" } }) : null, promptHasFullAccess && item.negative_prompt ? /* @__PURE__ */ React.createElement(PromptCopyButton, { prompt: item.negative_prompt, label: "Copy negative", analytics, contentId: item.id, eventType: "academy_prompt_negative_copy", metadata: { copy_type: "negative_prompt", source: "prompt_detail" } }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-7 grid gap-3 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement( PromptHeaderStat, { label: "Category", @@ -15341,17 +15355,17 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], accentClassName: "border-white/10 bg-white/[0.05] text-slate-200", valueClassName: "text-white" } - ))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:pt-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col rounded-[30px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80" }, "Preview artwork"), promptPreviewImage ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, "Click to zoom") : null), /* @__PURE__ */ React.createElement( + ))), /* @__PURE__ */ React.createElement("div", { className: "grid min-w-0 w-full gap-4 lg:pt-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col rounded-[30px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80" }, "Preview artwork"), promptPreviewImage ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, "Click to zoom") : null), /* @__PURE__ */ React.createElement( "button", { type: "button", onClick: openPromptPreviewImage, - className: "group mt-4 flex min-h-[420px] flex-1 flex-col overflow-hidden rounded-[28px] border border-white/10 bg-black/30 text-left shadow-[0_24px_80px_rgba(2,6,23,0.26)] transition hover:border-sky-300/25 focus:outline-none focus:ring-2 focus:ring-sky-300/35 lg:min-h-[640px]", + className: "group mt-4 flex min-h-[420px] w-full min-w-0 flex-1 flex-col overflow-hidden rounded-[28px] border border-white/10 bg-black/30 text-left shadow-[0_24px_80px_rgba(2,6,23,0.26)] transition hover:border-sky-300/25 focus:outline-none focus:ring-2 focus:ring-sky-300/35 lg:min-h-[640px]", disabled: !promptPreviewImage, "aria-label": promptPreviewImage ? `Open preview image for ${item.title}` : "Preview image unavailable" }, promptPreviewImage ? /* @__PURE__ */ React.createElement("div", { className: "relative min-h-0 flex-1 overflow-hidden" }, /* @__PURE__ */ React.createElement("img", { src: promptPreviewImage || promptPreviewThumbImage, srcSet: promptPreviewSrcSet || void 0, sizes: "(max-width: 1023px) calc(100vw - 3rem), 34vw", alt: item.title, className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.02),rgba(2,6,23,0.36))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute bottom-4 left-4 right-4 flex items-end justify-between gap-4 rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 backdrop-blur-md" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100/80" }, "Prompt visual"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm font-semibold text-white" }, "Open full-size preview")), /* @__PURE__ */ React.createElement("span", { className: "inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/10 text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-expand" })))) : /* @__PURE__ */ React.createElement("div", { className: "flex min-h-0 flex-1 items-center justify-center bg-[linear-gradient(135deg,rgba(251,146,60,0.14),rgba(17,24,39,0.96))] px-8 text-center" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Visual placeholder"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-lg font-semibold text-white" }, "Preview image coming soon"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "This prompt page will feel much better once the generated cover image is attached."))) - ))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, !promptHasFullAccess && (promptPreviewImage || promptPublicExamples.length) ? /* @__PURE__ */ React.createElement("section", { className: "academy-public-examples rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]" }, "Public examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Example results from this prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Preview the visual direction before unlocking the full prompt.")), item.locked && promptAccessRequirement ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, promptAccessRequirement) : null), /* @__PURE__ */ React.createElement("div", { className: `mt-6 grid gap-4 ${promptPreviewImage ? "xl:grid-cols-[minmax(0,0.98fr)_minmax(0,1.02fr)] xl:items-start" : ""}` }, promptPreviewImage ? /* @__PURE__ */ React.createElement( + ))))), /* @__PURE__ */ React.createElement("div", { className: "grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0 space-y-8" }, !promptHasFullAccess && (promptPreviewImage || promptPublicExamples.length) ? /* @__PURE__ */ React.createElement("section", { className: "academy-public-examples rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]" }, "Public examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Example results from this prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Preview the visual direction before unlocking the full prompt.")), item.locked && promptAccessRequirement ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, promptAccessRequirement) : null), /* @__PURE__ */ React.createElement("div", { className: `mt-6 grid gap-4 ${promptPreviewImage ? "xl:grid-cols-[minmax(0,0.98fr)_minmax(0,1.02fr)] xl:items-start" : ""}` }, promptPreviewImage ? /* @__PURE__ */ React.createElement( "button", { type: "button", @@ -15392,7 +15406,7 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], eventType: "academy_prompt_copy", metadata: { copy_type: promptHasFullAccess ? "main_prompt" : "preview_prompt", source: "prompt_body" } } - ) : null), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100 md:p-5" }, promptBody || "Prompt text is not available yet."), !promptHasFullAccess ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-amber-300/20 bg-amber-300/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100/80" }, promptUnlockTitle || "Unlock the full prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-amber-50/90" }, promptUnlockDetails), promptAccessRequirement ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-xs font-semibold uppercase tracking-[0.18em] text-amber-100" }, promptAccessRequirement) : null) : null), item.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-5 md:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement( + ) : null), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100 [overflow-wrap:anywhere] md:p-5" }, promptBody || "Prompt text is not available yet."), !promptHasFullAccess ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-amber-300/20 bg-amber-300/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100/80" }, promptUnlockTitle || "Unlock the full prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-amber-50/90" }, promptUnlockDetails), promptAccessRequirement ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-xs font-semibold uppercase tracking-[0.18em] text-amber-100" }, promptAccessRequirement) : null) : null), item.negative_prompt ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-5 md:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Negative prompt"), /* @__PURE__ */ React.createElement( PromptCopyButton, { prompt: item.negative_prompt, @@ -15402,7 +15416,7 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], eventType: "academy_prompt_negative_copy", metadata: { copy_type: "negative_prompt", source: "prompt_body" } } - )), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-200 md:p-5" }, item.negative_prompt)) : null)), promptUsageNotes || promptWorkflowNotes ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.82))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Prompt guidance"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "How to use this prompt")), !promptHasFullAccess ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, "Full notes visible with access") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 md:grid-cols-2" }, promptUsageNotes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-200/75" }, "Usage notes"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200" }, promptUsageNotes)) : null, promptWorkflowNotes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-emerald-200/75" }, "Workflow notes"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200" }, promptWorkflowNotes)) : null)) : null, hasPromptDocumentation ? /* @__PURE__ */ React.createElement(PromptDocumentationPanel, { documentation: promptDocumentation }) : null, hasPromptPlaceholders ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Data"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Placeholders and required inputs"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Prepare these variables before using the final prompt so the output stays consistent and reusable.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 xl:grid-cols-2" }, promptPlaceholders.map((placeholder, index2) => /* @__PURE__ */ React.createElement(PromptPlaceholderCard, { key: `${placeholder.key || "placeholder"}-${index2}`, placeholder })))) : null, hasPromptFilledExamples ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80" }, "Filled examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, promptFilledExamplesTotal > 0 ? `${promptFilledExamplesTotal} ready-made prompt runs for real user inputs` : "Ready-made prompt runs for real user inputs"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, promptHasMoreFilledExamples ? `You can view ${promptFilledExamples.length} example${promptFilledExamples.length === 1 ? "" : "s"} right now. Upgrade to Pro to unlock all ${promptFilledExamplesTotal} filled prompt runs and copy a closer starting point instead of filling everything from scratch.` : "These examples show how the prompt looks after swapping real placeholder values, so you can copy a closer starting point instead of filling everything from scratch.")), /* @__PURE__ */ React.createElement(PromptFilledExamplesSection, { examples: promptFilledExamples, analytics, contentId: item.id })) : null, promptHasLockedFilledExamples ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80" }, "Filled examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, promptHasMoreFilledExamples && hasPromptFilledExamples ? `${Math.max(promptFilledExamplesTotal - promptFilledExamples.length, 0)} more filled prompt example${promptFilledExamplesTotal - promptFilledExamples.length === 1 ? "" : "s"} are available` : `${promptFilledExamplesTotal || 5} filled prompt examples are included`), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, promptHasMoreFilledExamples && hasPromptFilledExamples ? "Creator access includes a smaller set here. Upgrade to Academy Pro to unlock the remaining filled prompt runs." : "This prompt ships with ready-made filled examples for different user inputs, but they unlock only for Academy Pro members.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: "pro", onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_filled_examples_locked_panel" }) }))) : null, hasPromptHelperPrompts ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "Data helpers"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "Helper prompts for preparation and validation"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Use these supporting prompts before or after the main prompt when you need better source data, cleaner structure, or a validation pass.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-5" }, promptHelperPrompts.map((helperPrompt, index2) => /* @__PURE__ */ React.createElement(PromptHelperPromptCard, { key: `${helperPrompt.title || "helper"}-${index2}`, helperPrompt, analytics, contentId: item.id })))) : null, promptHasLockedHelperPrompts ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "Data helpers"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "Helper prompts are included with this template"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Data collection, validation, or refinement prompts are available once your Academy access matches this template.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_helper_locked_panel" }) }))) : null, hasPromptVariants ? /* @__PURE__ */ React.createElement(PromptVariantsSection, { variants: promptVariants, analytics, contentId: item.id }) : null, promptHasLockedVariants ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Variants"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Alternative prompt versions are included"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "This prompt includes recommended or model-specific variants, but they stay locked until your Academy access level matches the template.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_variant_locked_panel" }) }))) : null, promptComparisons.length ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "AI model comparisons"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "How different models respond to the same prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Use these notes to decide which provider fits the result you want before you start tuning or post-processing.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 xl:grid-cols-2" }, promptComparisons.map((note, index2) => /* @__PURE__ */ React.createElement(PromptToolNoteCard, { key: `${note.provider || "provider"}-${note.model_name || "model"}-${index2}`, note, index: index2, galleryIndex: index2, onOpenImage: openPromptComparisonGallery })))) : null), /* @__PURE__ */ React.createElement("aside", { className: "space-y-6 lg:sticky lg:top-6 lg:self-start" }, lessonTags.length ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Microtags"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, lessonTags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: tag, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, tag)))) : null, /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Best use case"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, promptBestUseCase))))) : /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] p-6 text-slate-200" }, pageType === "pack" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm leading-8 text-slate-200" }, item.description), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, (item.prompts || []).map((prompt) => /* @__PURE__ */ React.createElement("div", { key: prompt.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, prompt.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, prompt.excerpt || prompt.prompt_preview))))) : null, pageType === "challenge" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Brief"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap text-sm leading-8 text-slate-200" }, item.brief || item.description)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Rules"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap text-sm leading-8 text-slate-200" }, item.rules || "No special rules posted yet."))), (item.submissions || []).length ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Approved submissions"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, item.submissions.map((submission) => /* @__PURE__ */ React.createElement("div", { key: submission.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, submission.artwork?.title || "Submission"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, submission.user?.name || "Unknown creator"))))) : null) : null)), /* @__PURE__ */ React.createElement(ImageLightbox, { gallery: lightboxGallery, onClose: () => setLightboxGallery(null), onNavigate: navigateLightboxGallery })); + )), /* @__PURE__ */ React.createElement("pre", { className: "mt-4 max-w-full overflow-x-auto whitespace-pre-wrap break-words rounded-[24px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-200 [overflow-wrap:anywhere] md:p-5" }, item.negative_prompt)) : null)), promptUsageNotes || promptWorkflowNotes ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.82))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Prompt guidance"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "How to use this prompt")), !promptHasFullAccess ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, "Full notes visible with access") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 md:grid-cols-2" }, promptUsageNotes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-200/75" }, "Usage notes"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200" }, promptUsageNotes)) : null, promptWorkflowNotes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-emerald-200/75" }, "Workflow notes"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200" }, promptWorkflowNotes)) : null)) : null, hasPromptDocumentation ? /* @__PURE__ */ React.createElement(PromptDocumentationPanel, { documentation: promptDocumentation }) : null, hasPromptPlaceholders ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Data"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Placeholders and required inputs"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Prepare these variables before using the final prompt so the output stays consistent and reusable.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 xl:grid-cols-2" }, promptPlaceholders.map((placeholder, index2) => /* @__PURE__ */ React.createElement(PromptPlaceholderCard, { key: `${placeholder.key || "placeholder"}-${index2}`, placeholder })))) : null, hasPromptFilledExamples ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80" }, "Filled examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, promptFilledExamplesTotal > 0 ? `${promptFilledExamplesTotal} ready-made prompt runs for real user inputs` : "Ready-made prompt runs for real user inputs"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, promptHasMoreFilledExamples ? `You can view ${promptFilledExamples.length} example${promptFilledExamples.length === 1 ? "" : "s"} right now. Upgrade to Pro to unlock all ${promptFilledExamplesTotal} filled prompt runs and copy a closer starting point instead of filling everything from scratch.` : "These examples show how the prompt looks after swapping real placeholder values, so you can copy a closer starting point instead of filling everything from scratch.")), /* @__PURE__ */ React.createElement(PromptFilledExamplesSection, { examples: promptFilledExamples, analytics, contentId: item.id })) : null, promptHasLockedFilledExamples ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(167,139,250,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-violet-100/80" }, "Filled examples"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, promptHasMoreFilledExamples && hasPromptFilledExamples ? `${Math.max(promptFilledExamplesTotal - promptFilledExamples.length, 0)} more filled prompt example${promptFilledExamplesTotal - promptFilledExamples.length === 1 ? "" : "s"} are available` : `${promptFilledExamplesTotal || 5} filled prompt examples are included`), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, promptHasMoreFilledExamples && hasPromptFilledExamples ? "Creator access includes a smaller set here. Upgrade to Academy Pro to unlock the remaining filled prompt runs." : "This prompt ships with ready-made filled examples for different user inputs, but they unlock only for Academy Pro members.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: "pro", onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_filled_examples_locked_panel" }) }))) : null, hasPromptHelperPrompts ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "Data helpers"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "Helper prompts for preparation and validation"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Use these supporting prompts before or after the main prompt when you need better source data, cleaner structure, or a validation pass.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-5" }, promptHelperPrompts.map((helperPrompt, index2) => /* @__PURE__ */ React.createElement(PromptHelperPromptCard, { key: `${helperPrompt.title || "helper"}-${index2}`, helperPrompt, analytics, contentId: item.id })))) : null, promptHasLockedHelperPrompts ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "Data helpers"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "Helper prompts are included with this template"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Data collection, validation, or refinement prompts are available once your Academy access matches this template.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_helper_locked_panel" }) }))) : null, hasPromptVariants ? /* @__PURE__ */ React.createElement(PromptVariantsSection, { variants: promptVariants, analytics, contentId: item.id }) : null, promptHasLockedVariants ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Variants"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Alternative prompt versions are included"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "This prompt includes recommended or model-specific variants, but they stay locked until your Academy access level matches the template.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(LockedPanel, { pricingUrl, label: "prompt", accessLevel: item?.access_level, onUpgrade: () => trackUpgradeClick(analytics, { source: "prompt_variant_locked_panel" }) }))) : null, promptComparisons.length ? /* @__PURE__ */ React.createElement("section", { className: "academy-paywalled-content rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]" }, "AI model comparisons"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, "How different models respond to the same prompt"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300 md:text-base" }, "Use these notes to decide which provider fits the result you want before you start tuning or post-processing.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-5 xl:grid-cols-2" }, promptComparisons.map((note, index2) => /* @__PURE__ */ React.createElement(PromptToolNoteCard, { key: `${note.provider || "provider"}-${note.model_name || "model"}-${index2}`, note, index: index2, galleryIndex: index2, onOpenImage: openPromptComparisonGallery })))) : null), /* @__PURE__ */ React.createElement("aside", { className: "min-w-0 space-y-6 lg:sticky lg:top-6 lg:self-start" }, lessonTags.length ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Microtags"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, lessonTags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: tag, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, tag)))) : null, /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Best use case"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, promptBestUseCase))))) : /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] p-6 text-slate-200" }, pageType === "pack" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm leading-8 text-slate-200" }, item.description), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, (item.prompts || []).map((prompt) => /* @__PURE__ */ React.createElement("div", { key: prompt.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, prompt.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, prompt.excerpt || prompt.prompt_preview))))) : null, pageType === "challenge" ? /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Brief"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap text-sm leading-8 text-slate-200" }, item.brief || item.description)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Rules"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap text-sm leading-8 text-slate-200" }, item.rules || "No special rules posted yet."))), (item.submissions || []).length ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Approved submissions"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, item.submissions.map((submission) => /* @__PURE__ */ React.createElement("div", { key: submission.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, submission.artwork?.title || "Submission"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, submission.user?.name || "Unknown creator"))))) : null) : null)), /* @__PURE__ */ React.createElement(ImageLightbox, { gallery: lightboxGallery, onClose: () => setLightboxGallery(null), onNavigate: navigateLightboxGallery })); } const __vite_glob_0_9 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -15455,6 +15469,7 @@ const buildAdminNavGroups = (isAdmin) => [ label: "System", items: [ ...isAdmin ? [{ label: "Auth Audit", href: "/moderation/auth-audit", icon: "fa-solid fa-user-shield" }] : [], + ...isAdmin ? [{ label: "Security Report", href: "/moderation/system/security-report", icon: "fa-solid fa-shield-virus" }] : [], { label: "Settings", href: "/moderation/settings", icon: "fa-solid fa-gear" } ] } @@ -15540,7 +15555,7 @@ const __vite_glob_0_11 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de __proto__: null, default: AcademyAnalyticsFunnel }, Symbol.toStringTag, { value: "Module" })); -function SummaryCard$6({ label, value, description }) { +function SummaryCard$7({ label, value, description }) { return /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, label), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-bold text-white" }, Number(value || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, description)); } function RangeControls({ range: range2 }) { @@ -15606,7 +15621,7 @@ function AcademyAnalyticsIntelligence({ premiumInterest = {}, editorialRecommendations = {} }) { - return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Content Intelligence", subtitle: "Editorial and business signals from Academy rollups, search demand, engagement, and premium intent." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Content Intelligence" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement(RangeControls, { range: range2 }), /* @__PURE__ */ React.createElement(Section$2, { title: "Content Opportunities", description: "A fast view of where Academy demand is strongest, where content is underperforming, and which changes should be prioritized next." }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7" }, (contentOpportunities?.cards || []).map((card) => /* @__PURE__ */ React.createElement(SummaryCard$6, { key: card.label, label: card.label, value: card.value, description: card.description }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(OpportunityHighlights, { items: contentOpportunities?.highlights || [] }))), /* @__PURE__ */ React.createElement(Section$2, { title: "Search Gaps", description: "Queries that suggest missing content, weak relevance, or topics worth expanding because users are clearly engaging with them." }, searchGaps?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Query", "Searches", "Results", "Clicks", "CTR", "Suggested Action"] }, searchGaps.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.normalized_query }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.query), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), row.logged_in_searches > 1 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Logged-in x", row.logged_in_searches) : null, row.subscriber_searches > 0 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Subscribers x", row.subscriber_searches) : null)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.searches), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.results_count), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.clicks), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.ctr, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No Academy search gaps were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Prompt Insights", description: "Signals that show whether prompts need better quality, stronger discoverability, more examples, or a premium follow-up." }, promptInsights?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Prompt", "Views", "Copies", "Copy Rate", "Saves", "Likes", "Issue", "Suggested Action"] }, promptInsights.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-slate-500" }, row.content_type_label)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.prompt_copies), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.copy_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.saves), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.likes), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No prompt intelligence signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Lesson Drop-offs", description: "Lessons where users hesitate to start, fail to finish, or unexpectedly show strong premium interest." }, lessonDropoffs?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Lesson", "Views", "Starts", "Completions", "Completion Rate", "Issue", "Suggested Action"] }, lessonDropoffs.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-slate-500" }, "Start rate ", row.start_rate, "%")), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.starts), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completions), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completion_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No lesson drop-off signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Course Health", description: "Courses that need better positioning or restructuring, plus courses that have enough momentum to justify expansion." }, courseHealth?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Course", "Views", "Starts", "Completions", "Completion Rate", "Avg Progress", "Suggested Action"] }, courseHealth.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), row.learners > 0 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Learners ", row.learners) : null)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.starts), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completions), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completion_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.avg_progress, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No course health signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Premium Interest", description: "Free and premium Academy content that either converts well into upgrade intent or needs stronger teaser positioning." }, premiumInterest?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Content", "Type", "Premium Views", "Upgrade Clicks", "Upgrade Rate", "Suggested Action"] }, premiumInterest.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: `${row.content_type}-${row.content_id}` }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), /* @__PURE__ */ React.createElement(Badge$4, null, "Interest score ", row.premium_interest_score))), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.content_type_label), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.premium_preview_views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.upgrade_clicks), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.upgrade_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No premium interest signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Editorial Recommendations", description: "Prioritized recommendations that combine content demand, user behavior, and premium intent into concrete next actions." }, editorialRecommendations?.rows?.length ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 xl:grid-cols-2" }, editorialRecommendations.rows.map((row, index2) => /* @__PURE__ */ React.createElement("div", { key: `${row.title}-${index2}`, className: "rounded-[24px] border border-white/[0.08] bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-base font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.priority)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, row.description), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Reason"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, row.reason), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Suggested Action"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold leading-6 text-sky-100" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No editorial recommendations were generated for this range yet." })))); + return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Content Intelligence", subtitle: "Editorial and business signals from Academy rollups, search demand, engagement, and premium intent." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Content Intelligence" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement(RangeControls, { range: range2 }), /* @__PURE__ */ React.createElement(Section$2, { title: "Content Opportunities", description: "A fast view of where Academy demand is strongest, where content is underperforming, and which changes should be prioritized next." }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7" }, (contentOpportunities?.cards || []).map((card) => /* @__PURE__ */ React.createElement(SummaryCard$7, { key: card.label, label: card.label, value: card.value, description: card.description }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(OpportunityHighlights, { items: contentOpportunities?.highlights || [] }))), /* @__PURE__ */ React.createElement(Section$2, { title: "Search Gaps", description: "Queries that suggest missing content, weak relevance, or topics worth expanding because users are clearly engaging with them." }, searchGaps?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Query", "Searches", "Results", "Clicks", "CTR", "Suggested Action"] }, searchGaps.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.normalized_query }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.query), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), row.logged_in_searches > 1 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Logged-in x", row.logged_in_searches) : null, row.subscriber_searches > 0 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Subscribers x", row.subscriber_searches) : null)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.searches), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.results_count), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.clicks), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.ctr, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No Academy search gaps were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Prompt Insights", description: "Signals that show whether prompts need better quality, stronger discoverability, more examples, or a premium follow-up." }, promptInsights?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Prompt", "Views", "Copies", "Copy Rate", "Saves", "Likes", "Issue", "Suggested Action"] }, promptInsights.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-slate-500" }, row.content_type_label)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.prompt_copies), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.copy_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.saves), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.likes), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No prompt intelligence signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Lesson Drop-offs", description: "Lessons where users hesitate to start, fail to finish, or unexpectedly show strong premium interest." }, lessonDropoffs?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Lesson", "Views", "Starts", "Completions", "Completion Rate", "Issue", "Suggested Action"] }, lessonDropoffs.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-slate-500" }, "Start rate ", row.start_rate, "%")), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.starts), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completions), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completion_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No lesson drop-off signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Course Health", description: "Courses that need better positioning or restructuring, plus courses that have enough momentum to justify expansion." }, courseHealth?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Course", "Views", "Starts", "Completions", "Completion Rate", "Avg Progress", "Suggested Action"] }, courseHealth.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: row.content_id }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), row.learners > 0 ? /* @__PURE__ */ React.createElement(Badge$4, null, "Learners ", row.learners) : null)), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.starts), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completions), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.completion_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.avg_progress, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No course health signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Premium Interest", description: "Free and premium Academy content that either converts well into upgrade intent or needs stronger teaser positioning." }, premiumInterest?.rows?.length ? /* @__PURE__ */ React.createElement(Table, { columns: ["Content", "Type", "Premium Views", "Upgrade Clicks", "Upgrade Rate", "Suggested Action"] }, premiumInterest.rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: `${row.content_type}-${row.content_id}` }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 align-top" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.issue), /* @__PURE__ */ React.createElement(Badge$4, null, "Interest score ", row.premium_interest_score))), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.content_type_label), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.premium_preview_views), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.upgrade_clicks), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm text-slate-200" }, row.upgrade_rate, "%"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4 text-sm leading-6 text-slate-300" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No premium interest signals were detected in this range." })), /* @__PURE__ */ React.createElement(Section$2, { title: "Editorial Recommendations", description: "Prioritized recommendations that combine content demand, user behavior, and premium intent into concrete next actions." }, editorialRecommendations?.rows?.length ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 xl:grid-cols-2" }, editorialRecommendations.rows.map((row, index2) => /* @__PURE__ */ React.createElement("div", { key: `${row.title}-${index2}`, className: "rounded-[24px] border border-white/[0.08] bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-base font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement(Badge$4, { tone: row.priority }, row.priority)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, row.description), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Reason"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, row.reason), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Suggested Action"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold leading-6 text-sky-100" }, row.suggested_action)))) : /* @__PURE__ */ React.createElement(EmptyState$7, { text: "No editorial recommendations were generated for this range yet." })))); } const __vite_glob_0_12 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -15668,7 +15683,7 @@ const __vite_glob_0_15 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de function StatCard$g({ label, value, hint = null }) { return /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, label), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-bold text-white" }, value.toLocaleString()), hint ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, hint) : null); } -function formatTimestamp$1(value) { +function formatTimestamp$3(value) { if (!value) return "No webhook processed yet"; try { return new Intl.DateTimeFormat(void 0, { @@ -15698,7 +15713,7 @@ function formatEventSummary(summary) { function AcademyBilling({ summary, planBreakdown, recentEvents, links }) { const missingPlans = Array.isArray(summary.missing_plan_keys) ? summary.missing_plan_keys : []; const noData = summary.enabled && (summary.active_subscribers || 0) === 0 && (summary.ended_subscriptions || 0) === 0 && (summary.recent_webhook_count || 0) === 0; - return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Billing", subtitle: "Moderation overview of Academy subscriptions, Stripe webhook sync activity, and plan readiness." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Billing" }), noData ? /* @__PURE__ */ React.createElement("div", { className: "mb-6 rounded-2xl border border-sky-300/20 bg-sky-300/[0.06] px-5 py-4 text-sm text-sky-100" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold" }, "No subscriber data in the database yet."), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sky-100/70" }, "Subscription records are created when Stripe sends webhook events to this server after a completed checkout. In local development, use", " ", /* @__PURE__ */ React.createElement("code", { className: "rounded bg-black/30 px-1.5 py-0.5 font-mono text-xs" }, "stripe listen --forward-to ", window.location.origin, "/stripe/webhook"), " ", "to forward events. On production, confirm the Stripe webhook is configured and active.")) : null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$g, { label: "Active Subscribers", value: summary.active_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Creator Subscribers", value: summary.creator_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Pro Subscribers", value: summary.pro_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Grace Period", value: summary.grace_period_subscribers || 0, hint: "Canceled subscriptions that still keep access until the billing period ends." })), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[1.2fr_0.8fr]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Plan Health"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Configured Academy plans")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: links.dashboard, className: "rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-semibold text-slate-200 transition hover:border-white/15 hover:bg-white/[0.06] hover:text-white" }, "Dashboard"), /* @__PURE__ */ React.createElement(xe, { href: links.pricing, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/30 hover:bg-sky-300/15" }, "Public pricing"), /* @__PURE__ */ React.createElement(xe, { href: links.account, className: "rounded-full border border-emerald-300/20 bg-emerald-300/10 px-4 py-2 text-sm font-semibold text-emerald-100 transition hover:border-emerald-300/30 hover:bg-emerald-300/15" }, "My billing account"))), missingPlans.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-2xl border border-amber-300/25 bg-amber-300/10 px-4 py-3 text-sm text-amber-100" }, "Missing Stripe price IDs for: ", missingPlans.join(", ")) : /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm text-emerald-100" }, "All configured Academy plans have Stripe price IDs."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2" }, planBreakdown.map((plan) => /* @__PURE__ */ React.createElement("div", { key: plan.key, className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-base font-semibold text-white" }, plan.label), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, plan.tier, " · ", plan.interval)), /* @__PURE__ */ React.createElement("span", { className: `rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.18em] ${plan.configured ? "bg-emerald-300/12 text-emerald-100" : "bg-amber-300/12 text-amber-100"}` }, plan.configured ? "configured" : "missing")), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-3xl font-bold text-white" }, (plan.subscribers || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "active subscriptions on this plan"))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Webhook Sync"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Recent Stripe activity"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Billing enabled"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, summary.enabled ? "Yes" : "No")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Webhook audits stored"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, (summary.recent_webhook_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Last processed webhook"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, formatTimestamp$1(summary.last_webhook_at))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Ended subscriptions"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, (summary.ended_subscriptions || 0).toLocaleString()))))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Audit Trail"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Latest academy billing events")), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Only the safe local summary is stored, not the raw Stripe payload.")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "min-w-full divide-y divide-white/[0.08] text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "text-left text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Event"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Plan"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Tier"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "User"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Processed"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Summary"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.06]" }, recentEvents.length ? recentEvents.map((event) => /* @__PURE__ */ React.createElement("tr", { key: event.id }, /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3 font-medium text-white" }, event.event_type), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.academy_plan || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.academy_tier || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.user_id || "guest/unresolved"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, formatTimestamp$1(event.processed_at || event.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3 text-slate-400" }, formatEventSummary(event.payload_summary)))) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: "6", className: "px-3 py-6 text-center text-slate-400" }, "No Academy billing webhook audits have been stored yet."))))))); + return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Billing", subtitle: "Moderation overview of Academy subscriptions, Stripe webhook sync activity, and plan readiness." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Billing" }), noData ? /* @__PURE__ */ React.createElement("div", { className: "mb-6 rounded-2xl border border-sky-300/20 bg-sky-300/[0.06] px-5 py-4 text-sm text-sky-100" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold" }, "No subscriber data in the database yet."), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sky-100/70" }, "Subscription records are created when Stripe sends webhook events to this server after a completed checkout. In local development, use", " ", /* @__PURE__ */ React.createElement("code", { className: "rounded bg-black/30 px-1.5 py-0.5 font-mono text-xs" }, "stripe listen --forward-to ", window.location.origin, "/stripe/webhook"), " ", "to forward events. On production, confirm the Stripe webhook is configured and active.")) : null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$g, { label: "Active Subscribers", value: summary.active_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Creator Subscribers", value: summary.creator_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Pro Subscribers", value: summary.pro_subscribers || 0 }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Grace Period", value: summary.grace_period_subscribers || 0, hint: "Canceled subscriptions that still keep access until the billing period ends." })), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[1.2fr_0.8fr]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Plan Health"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Configured Academy plans")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: links.dashboard, className: "rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-semibold text-slate-200 transition hover:border-white/15 hover:bg-white/[0.06] hover:text-white" }, "Dashboard"), /* @__PURE__ */ React.createElement(xe, { href: links.pricing, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/30 hover:bg-sky-300/15" }, "Public pricing"), /* @__PURE__ */ React.createElement(xe, { href: links.account, className: "rounded-full border border-emerald-300/20 bg-emerald-300/10 px-4 py-2 text-sm font-semibold text-emerald-100 transition hover:border-emerald-300/30 hover:bg-emerald-300/15" }, "My billing account"))), missingPlans.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-2xl border border-amber-300/25 bg-amber-300/10 px-4 py-3 text-sm text-amber-100" }, "Missing Stripe price IDs for: ", missingPlans.join(", ")) : /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm text-emerald-100" }, "All configured Academy plans have Stripe price IDs."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2" }, planBreakdown.map((plan) => /* @__PURE__ */ React.createElement("div", { key: plan.key, className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-base font-semibold text-white" }, plan.label), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, plan.tier, " · ", plan.interval)), /* @__PURE__ */ React.createElement("span", { className: `rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.18em] ${plan.configured ? "bg-emerald-300/12 text-emerald-100" : "bg-amber-300/12 text-amber-100"}` }, plan.configured ? "configured" : "missing")), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-3xl font-bold text-white" }, (plan.subscribers || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "active subscriptions on this plan"))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Webhook Sync"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Recent Stripe activity"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Billing enabled"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, summary.enabled ? "Yes" : "No")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Webhook audits stored"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, (summary.recent_webhook_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Last processed webhook"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, formatTimestamp$3(summary.last_webhook_at))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.08] bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Ended subscriptions"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, (summary.ended_subscriptions || 0).toLocaleString()))))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[28px] border border-white/[0.08] bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Audit Trail"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Latest academy billing events")), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "Only the safe local summary is stored, not the raw Stripe payload.")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "min-w-full divide-y divide-white/[0.08] text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "text-left text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Event"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Plan"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Tier"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "User"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Processed"), /* @__PURE__ */ React.createElement("th", { className: "px-3 py-3" }, "Summary"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.06]" }, recentEvents.length ? recentEvents.map((event) => /* @__PURE__ */ React.createElement("tr", { key: event.id }, /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3 font-medium text-white" }, event.event_type), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.academy_plan || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.academy_tier || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, event.user_id || "guest/unresolved"), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3" }, formatTimestamp$3(event.processed_at || event.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-3 py-3 text-slate-400" }, formatEventSummary(event.payload_summary)))) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: "6", className: "px-3 py-6 text-center text-slate-400" }, "No Academy billing webhook audits have been stored yet."))))))); } const __vite_glob_0_16 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -25919,7 +25934,7 @@ function AiBiographyAdmin() { )), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2 text-xs leading-relaxed text-slate-300" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Approved:"), " ", formatDateTime$8(record.approved_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Created:"), " ", formatDateTime$8(record.created_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Updated:"), " ", formatDateTime$8(record.updated_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Source hash:"), " ", record.source_hash || "—"))))); })), records.prev_page_url || records.next_page_url ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, records.prev_page_url ? /* @__PURE__ */ React.createElement(xe, { href: records.prev_page_url, preserveScroll: true, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left text-[10px]" }), "Previous") : null), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, "Showing page ", records.current_page || 1, " of ", records.last_page || 1), /* @__PURE__ */ React.createElement("div", null, records.next_page_url ? /* @__PURE__ */ React.createElement(xe, { href: records.next_page_url, preserveScroll: true, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Next", /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right text-[10px]" })) : null)) : null); } -const __vite_glob_0_94 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_96 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AiBiographyAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -25964,7 +25979,7 @@ const STATUS_BADGES = { success: "border-emerald-400/20 bg-emerald-400/10 text-emerald-200", failed: "border-rose-400/20 bg-rose-400/10 text-rose-200" }; -function formatTimestamp(value) { +function formatTimestamp$2(value) { if (!value) { return "Unknown"; } @@ -25979,7 +25994,7 @@ function formatLabel(value) { } return value.replaceAll("_", " ").replace(/\b\w/g, (match) => match.toUpperCase()); } -function SummaryCard$5({ label, value, tone = "slate" }) { +function SummaryCard$6({ label, value, tone = "slate" }) { const tones2 = { slate: "border-white/[0.07] bg-white/[0.02] text-white", rose: "border-rose-400/15 bg-rose-500/10 text-rose-100", @@ -26013,7 +26028,7 @@ function AuthAudit({ logs, filters, eventOptions, statusOptions }) { preserveScroll: true }); }; - return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Auth Audit", subtitle: "Review login, registration, forgot-password, and reset-password activity with IPs, timestamps, status, and failure reasons." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Auth Audit" }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-3" }, /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Visible records", value: items.length.toLocaleString(), tone: "slate" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Failures on page", value: failedCount.toLocaleString(), tone: "rose" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Unique IPs on page", value: uniqueIpCount.toLocaleString(), tone: "sky" })), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between" }, /* @__PURE__ */ React.createElement("form", { onSubmit: handleSearch, className: "flex flex-1 flex-col gap-3 md:flex-row" }, /* @__PURE__ */ React.createElement("label", { className: "flex-1" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search"), /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Auth Audit", subtitle: "Review login, registration, forgot-password, and reset-password activity with IPs, timestamps, status, and failure reasons." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Auth Audit" }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-3" }, /* @__PURE__ */ React.createElement(SummaryCard$6, { label: "Visible records", value: items.length.toLocaleString(), tone: "slate" }), /* @__PURE__ */ React.createElement(SummaryCard$6, { label: "Failures on page", value: failedCount.toLocaleString(), tone: "rose" }), /* @__PURE__ */ React.createElement(SummaryCard$6, { label: "Unique IPs on page", value: uniqueIpCount.toLocaleString(), tone: "sky" })), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between" }, /* @__PURE__ */ React.createElement("form", { onSubmit: handleSearch, className: "flex flex-1 flex-col gap-3 md:flex-row" }, /* @__PURE__ */ React.createElement("label", { className: "flex-1" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search"), /* @__PURE__ */ React.createElement( "input", { name: "search", @@ -26037,7 +26052,7 @@ function AuthAudit({ logs, filters, eventOptions, statusOptions }) { className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10" }, statusOptions.map((option) => /* @__PURE__ */ React.createElement("option", { key: option.value, value: option.value, className: "bg-slate-950 text-white" }, option.label)) - ))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full min-w-[980px] text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "When"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Event"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Status"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Identifier"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "User"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "IP"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Reason"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Details"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.05]" }, items.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 8, className: "px-5 py-12 text-center text-slate-500" }, "No auth audit records matched the current filters.")) : items.map((entry) => /* @__PURE__ */ React.createElement("tr", { key: entry.id, className: "align-top transition hover:bg-white/[0.025]" }, /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, formatTimestamp(entry.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-white" }, EVENT_LABELS[entry.event_type] ?? formatLabel(entry.event_type)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${STATUS_BADGES[entry.status] ?? "border-white/10 bg-white/[0.04] text-white/70"}` }, entry.status)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, entry.identifier || "Not recorded"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, entry.user ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-medium text-white" }, entry.user.name), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, entry.user.username ? `@${entry.user.username}` : entry.user.email)) : /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Unknown user")), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, entry.ip || "Unknown"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, formatLabel(entry.reason)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("details", { className: "group w-72 max-w-full" }, /* @__PURE__ */ React.createElement("summary", { className: "cursor-pointer list-none text-sm font-medium text-sky-200 transition hover:text-sky-100" }, "View payload"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3 rounded-2xl border border-white/10 bg-black/20 p-4 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-semibold uppercase tracking-[0.16em] text-slate-500" }, "User agent"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 break-words leading-5 text-slate-300" }, entry.user_agent || "Not recorded")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Metadata"), /* @__PURE__ */ React.createElement("pre", { className: "mt-1 overflow-x-auto whitespace-pre-wrap break-words rounded-xl bg-slate-950/70 p-3 text-[11px] leading-5 text-slate-300" }, JSON.stringify(entry.metadata || {}, null, 2))))))))))), logs?.last_page > 1 ? /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between border-t border-white/[0.06] px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Showing ", logs.from, "–", logs.to, " of ", logs.total, " audit records"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-1" }, logs.links.map((link2, index2) => link2.url ? /* @__PURE__ */ React.createElement( + ))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full min-w-[980px] text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "When"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Event"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Status"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Identifier"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "User"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "IP"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Reason"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Details"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.05]" }, items.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 8, className: "px-5 py-12 text-center text-slate-500" }, "No auth audit records matched the current filters.")) : items.map((entry) => /* @__PURE__ */ React.createElement("tr", { key: entry.id, className: "align-top transition hover:bg-white/[0.025]" }, /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, formatTimestamp$2(entry.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-white" }, EVENT_LABELS[entry.event_type] ?? formatLabel(entry.event_type)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${STATUS_BADGES[entry.status] ?? "border-white/10 bg-white/[0.04] text-white/70"}` }, entry.status)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, entry.identifier || "Not recorded"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, entry.user ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-medium text-white" }, entry.user.name), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, entry.user.username ? `@${entry.user.username}` : entry.user.email)) : /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Unknown user")), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, entry.ip || "Unknown"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, formatLabel(entry.reason)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("details", { className: "group w-72 max-w-full" }, /* @__PURE__ */ React.createElement("summary", { className: "cursor-pointer list-none text-sm font-medium text-sky-200 transition hover:text-sky-100" }, "View payload"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3 rounded-2xl border border-white/10 bg-black/20 p-4 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-semibold uppercase tracking-[0.16em] text-slate-500" }, "User agent"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 break-words leading-5 text-slate-300" }, entry.user_agent || "Not recorded")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Metadata"), /* @__PURE__ */ React.createElement("pre", { className: "mt-1 overflow-x-auto whitespace-pre-wrap break-words rounded-xl bg-slate-950/70 p-3 text-[11px] leading-5 text-slate-300" }, JSON.stringify(entry.metadata || {}, null, 2))))))))))), logs?.last_page > 1 ? /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between border-t border-white/[0.06] px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Showing ", logs.from, "–", logs.to, " of ", logs.total, " audit records"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-1" }, logs.links.map((link2, index2) => link2.url ? /* @__PURE__ */ React.createElement( "button", { key: `${link2.label}-${index2}`, @@ -26538,7 +26553,6 @@ function FeaturedArtworksAdmin() { const { props } = X$1(); const composerRef = React.useRef(null); const rosterRef = React.useRef(null); - const loadMoreRef = React.useRef(null); const endpoints = props.endpoints || {}; const capabilities = props.capabilities || {}; const seo = props.seo || {}; @@ -26557,14 +26571,14 @@ function FeaturedArtworksAdmin() { const [selectedArtwork, setSelectedArtwork] = React.useState(null); const [editingId, setEditingId] = React.useState(null); const [form, setForm] = React.useState(emptyForm()); - const [visibleCount, setVisibleCount] = React.useState(PAGE_SIZE$1); + const [currentPage, setCurrentPage] = React.useState(1); React.useEffect(() => { setEntries(Array.isArray(props.entries) ? props.entries : []); setWinner(props.winner || null); setStats(props.stats || {}); }, [props.entries, props.stats, props.winner]); React.useEffect(() => { - setVisibleCount(PAGE_SIZE$1); + setCurrentPage(1); }, [deferredListQuery, filter2, sortDirection, sortKey]); function scrollToSection(ref2) { ref2.current?.scrollIntoView({ behavior: "smooth", block: "start" }); @@ -26787,22 +26801,12 @@ function FeaturedArtworksAdmin() { return haystack.includes(query); }).sort((left, right) => compareEntries(left, right, sortKey, sortDirection)); }, [deferredListQuery, entries, filter2, sortDirection, sortKey]); - const visibleEntries = React.useMemo(() => filteredEntries.slice(0, visibleCount), [filteredEntries, visibleCount]); - const hasMoreEntries = visibleEntries.length < filteredEntries.length; - React.useEffect(() => { - if (!hasMoreEntries || typeof IntersectionObserver === "undefined" || !loadMoreRef.current) { - return void 0; - } - const observer = new IntersectionObserver((observerEntries) => { - if (observerEntries.some((observerEntry) => observerEntry.isIntersecting)) { - setVisibleCount((current) => Math.min(current + PAGE_SIZE$1, filteredEntries.length)); - } - }, { rootMargin: "320px 0px" }); - observer.observe(loadMoreRef.current); - return () => observer.disconnect(); - }, [filteredEntries.length, hasMoreEntries, visibleCount]); - function loadMoreEntries() { - setVisibleCount((current) => Math.min(current + PAGE_SIZE$1, filteredEntries.length)); + const totalPages = Math.max(1, Math.ceil(filteredEntries.length / PAGE_SIZE$1)); + const visibleEntries = React.useMemo(() => filteredEntries.slice((currentPage - 1) * PAGE_SIZE$1, currentPage * PAGE_SIZE$1), [filteredEntries, currentPage]); + function goToPage(next) { + const page = Math.max(1, Math.min(totalPages, next)); + setCurrentPage(page); + if (typeof window !== "undefined") window.scrollTo({ top: 0, behavior: "smooth" }); } return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Se$1, null, /* @__PURE__ */ React.createElement("title", null, seo.title || "Featured Artworks"), seo.description ? /* @__PURE__ */ React.createElement("meta", { name: "description", content: seo.description }) : null, seo.robots ? /* @__PURE__ */ React.createElement("meta", { name: "robots", content: seo.robots }) : null, seo.canonical ? /* @__PURE__ */ React.createElement("link", { rel: "canonical", href: seo.canonical }) : null), /* @__PURE__ */ React.createElement("div", { className: "min-h-screen bg-[#07111c] pb-28 text-white xl:pb-0" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex w-full max-w-none flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8 2xl:px-10" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_35%),radial-gradient(circle_at_bottom_right,_rgba(245,158,11,0.14),_transparent_35%),linear-gradient(180deg,_rgba(6,14,25,0.92),_rgba(8,18,32,0.96))] p-8 shadow-[0_28px_90px_rgba(2,6,23,0.45)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-8 xl:flex-row xl:items-start xl:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("div", { className: "inline-flex rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Featured Artworks"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.06em] text-white sm:text-5xl" }, "Curate the homepage with live winner context, not guesswork."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-sm leading-7 text-slate-300 sm:text-base" }, "This workspace now behaves like an editorial command center: current winner visibility, natural fallback awareness, faster filters, and a cleaner composer for new or existing featured rows."), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => scrollToSection(composerRef), className: "rounded-full bg-white px-5 py-3 text-sm font-semibold text-slate-950 transition hover:bg-slate-100" }, "Add or edit featured row"), /* @__PURE__ */ React.createElement( "button", @@ -26857,15 +26861,25 @@ function FeaturedArtworksAdmin() { onClick: (nextValue) => React.startTransition(() => setFilter(nextValue)), count: filterCounts[option.value] ?? 0 } - )))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-4" }, filteredEntries.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-dashed border-white/10 bg-black/20 px-6 py-12 text-center text-sm leading-6 text-slate-400" }, "No featured entries match the current search and filter combination.") : visibleEntries.map((entry) => /* @__PURE__ */ React.createElement("article", { key: entry.id, className: "rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "flex min-w-0 gap-4" }, /* @__PURE__ */ React.createElement("a", { href: entry.artwork?.canonical_url || "#", target: "_blank", rel: "noreferrer", className: "h-28 w-28 shrink-0 overflow-hidden rounded-[22px] border border-white/10 bg-[#08111d]" }, /* @__PURE__ */ React.createElement("img", { src: entry.artwork?.thumbnail?.url, alt: entry.artwork?.title || "Artwork preview", className: "h-full w-full object-cover" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold tracking-[-0.03em] text-white" }, entry.artwork?.title || "Missing artwork"), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-400" }, "Artwork #", entry.artwork?.id || entry.artwork_id), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-500" }, "Row #", entry.id)), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-300" }, formatOwner(entry)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-6 text-slate-400" }, buildEntrySummary(entry)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, (entry.status_badges || []).map((badge, index2) => /* @__PURE__ */ React.createElement(Badge$2, { key: `${entry.id}-${badge.label}-${index2}`, label: badge.label, tone: badge.tone }))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-2 xl:w-[420px] xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(MetricTile, { label: "Priority", value: entry.priority, hint: "Higher wins first" }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Medal score", value: entry.medals?.score_30d || 0, hint: "Last 30d" }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Featured", value: formatShortDate$3(entry.featured_at), hint: formatDateTime$6(entry.featured_at) }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Expires", value: formatRelativeExpiry(entry.expires_at), hint: formatDateTime$6(entry.expires_at) }))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-col gap-4 border-t border-white/10 pt-4 xl:flex-row xl:items-center xl:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm leading-6 text-slate-400" }, "Visibility: ", /* @__PURE__ */ React.createElement("span", { className: "text-slate-200" }, entry.artwork?.visibility || "—"), /* @__PURE__ */ React.createElement("span", { className: "mx-2 text-slate-600" }, "•"), "Published: ", /* @__PURE__ */ React.createElement("span", { className: "text-slate-200" }, entry.artwork?.published_at ? "Yes" : "No")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2 xl:justify-end" }, /* @__PURE__ */ React.createElement("a", { href: entry.artwork?.canonical_url || "#", target: "_blank", rel: "noreferrer", className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Open artwork"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => editEntry(entry), className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Edit row"), capabilities.forceHeroEnabled ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleForceHero(entry), disabled: busy === `force-${entry.id}`, className: cn("rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] transition disabled:cursor-not-allowed disabled:opacity-60", entry.is_force_hero ? "border-amber-300/25 text-amber-100 hover:border-amber-300/40 hover:bg-amber-400/10" : "border-amber-300/15 text-amber-50 hover:border-amber-300/30 hover:bg-amber-400/5") }, busy === `force-${entry.id}` ? "Saving…" : entry.is_force_hero ? "Disable Force Hero" : "Force Hero") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleToggle(entry), disabled: busy === `toggle-${entry.id}`, className: "rounded-full border border-sky-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-400/10 disabled:cursor-not-allowed disabled:opacity-60" }, busy === `toggle-${entry.id}` ? "Saving…" : entry.is_active ? "Deactivate" : "Activate"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleDelete(entry), disabled: busy === `delete-${entry.id}`, className: "rounded-full border border-rose-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-rose-100 transition hover:border-rose-300/40 hover:bg-rose-400/10 disabled:cursor-not-allowed disabled:opacity-60" }, busy === `delete-${entry.id}` ? "Deleting…" : "Delete"))))), hasMoreEntries ? /* @__PURE__ */ React.createElement("div", { ref: loadMoreRef, className: "rounded-[24px] border border-dashed border-white/10 bg-black/20 px-5 py-6 text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, "Loading more rows as you reach the bottom."), /* @__PURE__ */ React.createElement( + )))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-4" }, filteredEntries.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-dashed border-white/10 bg-black/20 px-6 py-12 text-center text-sm leading-6 text-slate-400" }, "No featured entries match the current search and filter combination.") : visibleEntries.map((entry) => /* @__PURE__ */ React.createElement("article", { key: entry.id, className: "rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "flex min-w-0 gap-4" }, /* @__PURE__ */ React.createElement("a", { href: entry.artwork?.canonical_url || "#", target: "_blank", rel: "noreferrer", className: "h-28 w-28 shrink-0 overflow-hidden rounded-[22px] border border-white/10 bg-[#08111d]" }, /* @__PURE__ */ React.createElement("img", { src: entry.artwork?.thumbnail?.url, alt: entry.artwork?.title || "Artwork preview", className: "h-full w-full object-cover" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold tracking-[-0.03em] text-white" }, entry.artwork?.title || "Missing artwork"), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-400" }, "Artwork #", entry.artwork?.id || entry.artwork_id), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-500" }, "Row #", entry.id)), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-300" }, formatOwner(entry)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-6 text-slate-400" }, buildEntrySummary(entry)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, (entry.status_badges || []).map((badge, index2) => /* @__PURE__ */ React.createElement(Badge$2, { key: `${entry.id}-${badge.label}-${index2}`, label: badge.label, tone: badge.tone }))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-2 xl:w-[420px] xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(MetricTile, { label: "Priority", value: entry.priority, hint: "Higher wins first" }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Medal score", value: entry.medals?.score_30d || 0, hint: "Last 30d" }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Featured", value: formatShortDate$3(entry.featured_at), hint: formatDateTime$6(entry.featured_at) }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Expires", value: formatRelativeExpiry(entry.expires_at), hint: formatDateTime$6(entry.expires_at) }))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-col gap-4 border-t border-white/10 pt-4 xl:flex-row xl:items-center xl:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm leading-6 text-slate-400" }, "Visibility: ", /* @__PURE__ */ React.createElement("span", { className: "text-slate-200" }, entry.artwork?.visibility || "—"), /* @__PURE__ */ React.createElement("span", { className: "mx-2 text-slate-600" }, "•"), "Published: ", /* @__PURE__ */ React.createElement("span", { className: "text-slate-200" }, entry.artwork?.published_at ? "Yes" : "No")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2 xl:justify-end" }, /* @__PURE__ */ React.createElement("a", { href: entry.artwork?.canonical_url || "#", target: "_blank", rel: "noreferrer", className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Open artwork"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => editEntry(entry), className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Edit row"), capabilities.forceHeroEnabled ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleForceHero(entry), disabled: busy === `force-${entry.id}`, className: cn("rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] transition disabled:cursor-not-allowed disabled:opacity-60", entry.is_force_hero ? "border-amber-300/25 text-amber-100 hover:border-amber-300/40 hover:bg-amber-400/10" : "border-amber-300/15 text-amber-50 hover:border-amber-300/30 hover:bg-amber-400/5") }, busy === `force-${entry.id}` ? "Saving…" : entry.is_force_hero ? "Disable Force Hero" : "Force Hero") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleToggle(entry), disabled: busy === `toggle-${entry.id}`, className: "rounded-full border border-sky-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-400/10 disabled:cursor-not-allowed disabled:opacity-60" }, busy === `toggle-${entry.id}` ? "Saving…" : entry.is_active ? "Deactivate" : "Activate"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleDelete(entry), disabled: busy === `delete-${entry.id}`, className: "rounded-full border border-rose-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-rose-100 transition hover:border-rose-300/40 hover:bg-rose-400/10 disabled:cursor-not-allowed disabled:opacity-60" }, busy === `delete-${entry.id}` ? "Deleting…" : "Delete"))))), filteredEntries.length > PAGE_SIZE$1 ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex items-center justify-center gap-4" }, /* @__PURE__ */ React.createElement( "button", { type: "button", - onClick: loadMoreEntries, - className: "mt-4 rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition hover:border-white/20 hover:bg-white/5" + onClick: () => goToPage(currentPage - 1), + disabled: currentPage === 1, + className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed hover:border-white/20 hover:bg-white/5" }, - "Load 24 more" - )) : filteredEntries.length > PAGE_SIZE$1 ? /* @__PURE__ */ React.createElement("div", { className: "text-center text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "All matching rows loaded") : null)), /* @__PURE__ */ React.createElement("section", { ref: composerRef, className: "rounded-[30px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_22px_70px_rgba(2,6,23,0.28)] backdrop-blur-sm xl:sticky xl:top-6 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Editorial Composer"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, editingId ? `Edit featured row #${editingId}` : "Build a new featured row"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, "Search, inspect readiness, choose timing, and ship the change without leaving the page context.")), editingId ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetEditor, className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-200 transition hover:border-white/20 hover:bg-white/5" }, "Cancel edit") : null), !editingId ? /* @__PURE__ */ React.createElement("form", { onSubmit: handleArtworkSearch, className: "mt-6 space-y-4 rounded-[26px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement(Field$5, { label: "Artwork selector", help: "Search by artwork ID, title, slug, artist, or group, then lock one result into the composer." }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement( + "Previous" + ), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, "Page ", currentPage, " of ", totalPages), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => goToPage(currentPage + 1), + disabled: currentPage === totalPages, + className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed hover:border-white/20 hover:bg-white/5" + }, + "Next" + )) : null)), /* @__PURE__ */ React.createElement("section", { ref: composerRef, className: "rounded-[30px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_22px_70px_rgba(2,6,23,0.28)] backdrop-blur-sm xl:sticky xl:top-6 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Editorial Composer"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, editingId ? `Edit featured row #${editingId}` : "Build a new featured row"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, "Search, inspect readiness, choose timing, and ship the change without leaving the page context.")), editingId ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetEditor, className: "rounded-full border border-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-200 transition hover:border-white/20 hover:bg-white/5" }, "Cancel edit") : null), !editingId ? /* @__PURE__ */ React.createElement("form", { onSubmit: handleArtworkSearch, className: "mt-6 space-y-4 rounded-[26px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement(Field$5, { label: "Artwork selector", help: "Search by artwork ID, title, slug, artist, or group, then lock one result into the composer." }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement( "input", { type: "text", @@ -26999,7 +27013,7 @@ function FeaturedArtworksAdmin() { } ))))); } -const __vite_glob_0_50 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_52 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: FeaturedArtworksAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -27792,6 +27806,117 @@ const __vite_glob_0_34 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de __proto__: null, default: AdminStories }, Symbol.toStringTag, { value: "Module" })); +function formatTimestamp$1(value) { + if (!value) { + return "Not finished"; + } + return new Intl.DateTimeFormat("en-GB", { + dateStyle: "medium", + timeStyle: "short" + }).format(new Date(value)); +} +function SummaryCard$5({ label, value, tone }) { + const tones2 = { + red: "border-rose-400/15 bg-rose-500/10 text-rose-100", + orange: "border-orange-400/15 bg-orange-500/10 text-orange-100", + yellow: "border-amber-400/15 bg-amber-500/10 text-amber-100", + blue: "border-sky-400/15 bg-sky-500/10 text-sky-100", + slate: "border-white/[0.07] bg-white/[0.02] text-white" + }; + return /* @__PURE__ */ React.createElement("div", { className: `rounded-2xl border p-5 ${tones2[tone] ?? tones2.slate}` }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, label), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-tight" }, value)); +} +function RiskBadge({ label }) { + const tone = label === "Critical" ? "border-rose-400/20 bg-rose-500/10 text-rose-200" : label === "High" ? "border-orange-400/20 bg-orange-500/10 text-orange-200" : label === "Medium" ? "border-amber-400/20 bg-amber-500/10 text-amber-200" : label === "Low" ? "border-sky-400/20 bg-sky-500/10 text-sky-200" : "border-emerald-400/20 bg-emerald-500/10 text-emerald-200"; + return /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${tone}` }, label); +} +function SecurityReportIndex({ latest, reports, canRunScan }) { + const flash = X$1().props.flash ?? {}; + const items = reports?.data ?? []; + const runScan = () => { + At.post("/moderation/system/security-report/run"); + }; + return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Security Report", subtitle: "Private Composer and npm vulnerability reporting for administrators." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Security Report" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, flash.success ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100" }, flash.success) : null, latest ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-5" }, /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Critical", value: latest.total_critical, tone: "red" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "High", value: latest.total_high, tone: "orange" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Medium", value: latest.total_medium, tone: "yellow" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Low", value: latest.total_low, tone: "blue" }), /* @__PURE__ */ React.createElement(SummaryCard$5, { label: "Unknown", value: latest.total_unknown, tone: "slate" })), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Latest scan"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex items-center gap-3" }, /* @__PURE__ */ React.createElement(RiskBadge, { label: latest.risk_label }), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-400" }, latest.status))), canRunScan ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: runScan, + className: "rounded-2xl bg-rose-500/80 px-5 py-3 text-sm font-semibold text-white transition hover:bg-rose-500" + }, + "Run Scan" + ) : null), /* @__PURE__ */ React.createElement("dl", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Started"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, formatTimestamp$1(latest.started_at))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Finished"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, formatTimestamp$1(latest.finished_at))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Triggered by"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, latest.triggered_by || "Unknown")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Composer outdated"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, latest.composer_outdated_count)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "npm outdated"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, latest.npm_outdated_count)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("dt", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Requested by"), /* @__PURE__ */ React.createElement("dd", { className: "mt-2 text-sm text-white" }, latest.user ? latest.user.username ? `@${latest.user.username}` : latest.user.name : "System"))), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("a", { href: latest.show_url, className: "inline-flex rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-medium text-white transition hover:bg-white/[0.08]" }, "View Details")))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-amber-400/20 bg-amber-500/10 p-6 text-amber-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold" }, "No security report has been generated yet."), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-amber-50/80" }, "Run ", /* @__PURE__ */ React.createElement("code", { className: "rounded bg-black/20 px-1.5 py-0.5" }, "php artisan security:scan --notify"), " or use the manual scan button after the queue worker is running."), canRunScan ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: runScan, + className: "mt-4 rounded-2xl bg-rose-500/80 px-5 py-3 text-sm font-semibold text-white transition hover:bg-rose-500" + }, + "Run Scan" + ) : null), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("div", { className: "border-b border-white/[0.06] px-5 py-4" }, /* @__PURE__ */ React.createElement("h2", { className: "text-sm font-bold uppercase tracking-[0.18em] text-slate-400" }, "Scan History")), /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full min-w-[980px] text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "ID"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Finished"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Status"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Risk"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Critical"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "High"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Medium"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Low"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }, "Outdated"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4" }))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.05]" }, items.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 10, className: "px-5 py-12 text-center text-slate-500" }, "No reports recorded yet.")) : items.map((report) => /* @__PURE__ */ React.createElement("tr", { key: report.id, className: "transition hover:bg-white/[0.025]" }, /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-white" }, "#", report.id), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, formatTimestamp$1(report.finished_at)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, report.status), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement(RiskBadge, { label: report.risk_label })), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, report.total_critical), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, report.total_high), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, report.total_medium), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, report.total_low), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, "Composer: ", report.composer_outdated_count, ", npm: ", report.npm_outdated_count), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("a", { href: report.show_url, className: "text-sky-200 transition hover:text-sky-100" }, "Details"))))))), reports?.last_page > 1 ? /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between border-t border-white/[0.06] px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Showing ", reports.from, "–", reports.to, " of ", reports.total, " reports"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-1" }, reports.links.map((link2, index2) => link2.url ? /* @__PURE__ */ React.createElement( + "button", + { + key: `${link2.label}-${index2}`, + type: "button", + onClick: () => At.get(link2.url, {}, { preserveScroll: true, preserveState: true }), + className: `rounded-lg px-3 py-1.5 text-xs transition ${link2.active ? "bg-rose-500/20 font-semibold text-rose-300" : "text-slate-500 hover:bg-white/[0.06] hover:text-white"}`, + dangerouslySetInnerHTML: { __html: link2.label } + } + ) : /* @__PURE__ */ React.createElement("span", { key: `${link2.label}-${index2}`, className: "rounded-lg px-3 py-1.5 text-xs text-slate-700", dangerouslySetInnerHTML: { __html: link2.label } })))) : null))); +} +const __vite_glob_0_35 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: SecurityReportIndex +}, Symbol.toStringTag, { value: "Module" })); +function formatTimestamp(value) { + if (!value) { + return "Not recorded"; + } + return new Intl.DateTimeFormat("en-GB", { + dateStyle: "medium", + timeStyle: "medium" + }).format(new Date(value)); +} +function JsonPanel({ title, value }) { + return /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-sm font-bold uppercase tracking-[0.18em] text-slate-400" }, title), /* @__PURE__ */ React.createElement("pre", { className: "nova-scrollbar mt-4 max-h-[28rem] overflow-auto rounded-2xl bg-slate-950/80 p-4 text-[11px] leading-5 text-slate-200" }, JSON.stringify(value ?? {}, null, 2))); +} +function FindingTable({ title, rows, columns }) { + return /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("div", { className: "border-b border-white/[0.06] px-5 py-4" }, /* @__PURE__ */ React.createElement("h2", { className: "text-sm font-bold uppercase tracking-[0.18em] text-slate-400" }, title)), /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full min-w-[900px] text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, columns.map((column) => /* @__PURE__ */ React.createElement("th", { key: column.key, className: "px-5 py-4" }, column.label)))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.05]" }, rows.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: columns.length, className: "px-5 py-12 text-center text-slate-500" }, "No entries in this section.")) : rows.map((row, index2) => /* @__PURE__ */ React.createElement("tr", { key: `${row.package}-${index2}`, className: "align-top transition hover:bg-white/[0.025]" }, columns.map((column) => /* @__PURE__ */ React.createElement("td", { key: column.key, className: "px-5 py-4 text-slate-300 break-words" }, row[column.key] || "—")))))))); +} +function SecurityReportShow({ report }) { + return /* @__PURE__ */ React.createElement(AdminLayout, { title: `Security Report #${report.id}`, subtitle: "Private dependency audit details for administrators only." }, /* @__PURE__ */ React.createElement(Se$1, { title: `Admin · Security Report #${report.id}` }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Status"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.status)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Risk"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.risk_label)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Started"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, formatTimestamp(report.started_at))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Finished"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, formatTimestamp(report.finished_at))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Triggered by"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.triggered_by || "Unknown")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Requested by"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.user ? report.user.username ? `@${report.user.username}` : report.user.name : "System")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Composer outdated"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.composer_outdated_count)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "npm outdated"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, report.npm_outdated_count))), report.error_message ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-100" }, report.error_message) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("a", { href: report.index_url, className: "inline-flex rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-medium text-white transition hover:bg-white/[0.08]" }, "Back to Reports"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-5" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-rose-400/15 bg-rose-500/10 p-5 text-rose-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-rose-200/70" }, "Critical"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold" }, report.total_critical)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-orange-400/15 bg-orange-500/10 p-5 text-orange-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-orange-200/70" }, "High"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold" }, report.total_high)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-amber-400/15 bg-amber-500/10 p-5 text-amber-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-amber-200/70" }, "Medium"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold" }, report.total_medium)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-sky-400/15 bg-sky-500/10 p-5 text-sky-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-sky-200/70" }, "Low"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold" }, report.total_low)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.07] bg-white/[0.02] p-5 text-white" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Unknown"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold" }, report.total_unknown))), /* @__PURE__ */ React.createElement( + FindingTable, + { + title: "Composer Advisories", + rows: report.composer_advisories ?? [], + columns: [ + { key: "package", label: "Package" }, + { key: "severity", label: "Severity" }, + { key: "title", label: "Title" }, + { key: "cve", label: "CVE / Ref" }, + { key: "affected_versions", label: "Affected versions" }, + { key: "link", label: "Source URL" } + ] + } + ), /* @__PURE__ */ React.createElement( + FindingTable, + { + title: "npm Vulnerabilities", + rows: report.npm_vulnerabilities ?? [], + columns: [ + { key: "package", label: "Package" }, + { key: "severity", label: "Severity" }, + { key: "title", label: "Title" }, + { key: "cve", label: "CVE" }, + { key: "range", label: "Affected range" }, + { key: "fix_available", label: "Fix available" }, + { key: "url", label: "Source URL" } + ] + } + ), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement(JsonPanel, { title: "Composer Audit Raw JSON", value: report.composer_audit }), /* @__PURE__ */ React.createElement(JsonPanel, { title: "npm Audit Raw JSON", value: report.npm_audit }), /* @__PURE__ */ React.createElement(JsonPanel, { title: "Composer Outdated Raw JSON", value: report.composer_outdated }), /* @__PURE__ */ React.createElement(JsonPanel, { title: "npm Outdated Raw JSON", value: report.npm_outdated })))); +} +const __vite_glob_0_36 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: SecurityReportShow +}, Symbol.toStringTag, { value: "Module" })); function AdminUploadQueue() { const [items, setItems] = reactExports.useState([]); const [loading, setLoading] = reactExports.useState(false); @@ -27854,7 +27979,7 @@ function AdminUploadQueue() { function UploadQueuePage() { return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Upload Queue", subtitle: "Review and moderate pending artwork submissions" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Upload Queue" }), /* @__PURE__ */ React.createElement(AdminUploadQueue, null)); } -const __vite_glob_0_35 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_37 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UploadQueuePage }, Symbol.toStringTag, { value: "Module" })); @@ -27918,7 +28043,7 @@ function AdminUsernameQueue() { function UsernameQueuePage() { return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Username Queue", subtitle: "Review and approve pending username change requests" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Username Queue" }), /* @__PURE__ */ React.createElement(AdminUsernameQueue, null)); } -const __vite_glob_0_36 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_38 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UsernameQueuePage }, Symbol.toStringTag, { value: "Module" })); @@ -28014,7 +28139,7 @@ function UsersIndex({ users, filters, roles }) { } ) : /* @__PURE__ */ React.createElement("span", { key: i, className: "rounded-lg px-3 py-1.5 text-xs text-slate-700", dangerouslySetInnerHTML: { __html: link2.label } })))))); } -const __vite_glob_0_37 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_39 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UsersIndex }, Symbol.toStringTag, { value: "Module" })); @@ -28078,7 +28203,7 @@ function SimilarArtworksHeader({ artwork }) { artwork.content_type_name || "artworks" ))))); } -const __vite_glob_0_38 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_40 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SimilarArtworksHeader }, Symbol.toStringTag, { value: "Module" })); @@ -43330,7 +43455,7 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present } )); } -const __vite_glob_0_39 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_41 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ArtworkPage }, Symbol.toStringTag, { value: "Module" })); @@ -75680,7 +75805,7 @@ if (typeof document !== "undefined") { clientExports.createRoot(mountElement).render(/* @__PURE__ */ React.createElement(CategoriesPage, { ...props })); } } -const __vite_glob_0_40 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_42 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CategoriesPage }, Symbol.toStringTag, { value: "Module" })); @@ -75702,7 +75827,7 @@ function CollectionAnalytics() { const seo = props.seo || {}; return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Se$1, null, /* @__PURE__ */ React.createElement("title", null, seo.title || `${collection.title || "Collection"} Analytics — Skinbase`), /* @__PURE__ */ React.createElement("meta", { name: "description", content: seo.description || "Collection analytics overview." }), seo.canonical ? /* @__PURE__ */ React.createElement("link", { rel: "canonical", href: seo.canonical }) : null, /* @__PURE__ */ React.createElement("meta", { name: "robots", content: seo.robots || "noindex,follow" })), /* @__PURE__ */ React.createElement("div", { className: "relative min-h-screen overflow-hidden pb-16" }, /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[32rem] opacity-95", style: { background: "radial-gradient(circle at 14% 14%, rgba(56,189,248,0.18), transparent 26%), radial-gradient(circle at 86% 18%, rgba(16,185,129,0.16), transparent 24%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #08111f 100%)" } }), /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 -z-10 opacity-[0.05]", style: { backgroundImage: "url(/gfx/noise.png)", backgroundSize: "180px" } }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pt-8 md:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-sm text-slate-300" }, props.dashboardUrl ? /* @__PURE__ */ React.createElement("a", { href: props.dashboardUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left fa-fw text-[11px]" }), "Dashboard") : null, props.historyUrl ? /* @__PURE__ */ React.createElement("a", { href: props.historyUrl, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 font-semibold text-sky-100 transition hover:bg-sky-400/15" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-timeline fa-fw text-[11px]" }), "History") : null, collection.manage_url ? /* @__PURE__ */ React.createElement("a", { href: collection.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen-to-square fa-fw text-[11px]" }), "Manage") : null), /* @__PURE__ */ React.createElement("section", { className: "mt-6 rounded-[34px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm md:p-8" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Performance"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, collection.title || "Collection analytics"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-relaxed text-slate-300 md:text-[15px]" }, "Review activity velocity, audience response, and the artworks carrying the most discovery value over the last ", range2.days || 30, " days.")), /* @__PURE__ */ React.createElement("section", { className: "mt-8 grid gap-5 md:grid-cols-2 xl:grid-cols-3" }, /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Views", value: totals.views, delta: range2.views_delta, icon: "fa-eye" }), /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Likes", value: totals.likes, delta: range2.likes_delta, icon: "fa-heart" }), /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Follows", value: totals.follows, delta: range2.follows_delta, icon: "fa-bell" }), /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Saves", value: totals.saves, delta: range2.saves_delta, icon: "fa-bookmark" }), /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Comments", value: totals.comments, delta: range2.comments_delta, icon: "fa-comments" }), /* @__PURE__ */ React.createElement(MetricCard$1, { label: "Submissions", value: totals.submissions, delta: totals.submissions, icon: "fa-inbox" })), /* @__PURE__ */ React.createElement("div", { className: "mt-8 space-y-6" }, /* @__PURE__ */ React.createElement(TimelineChart, { timeline: analytics.timeline }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Artworks"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Top artwork drivers")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, topArtworks.length)), topArtworks.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, topArtworks.map((artwork) => /* @__PURE__ */ React.createElement("div", { key: artwork.id, className: "overflow-hidden rounded-[24px] border border-white/10 bg-slate-950/40" }, /* @__PURE__ */ React.createElement("div", { className: "aspect-square bg-slate-950/60" }, artwork.thumb ? /* @__PURE__ */ React.createElement("img", { src: artwork.thumb, alt: artwork.title, className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-full w-full items-center justify-center text-slate-500" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-3xl" }))), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "truncate text-sm font-semibold text-white" }, artwork.title), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Views: ", Number(artwork.views || 0).toLocaleString()), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Favs: ", Number(artwork.favourites || 0).toLocaleString()), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Shares: ", Number(artwork.shares || 0).toLocaleString()), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Rank: ", Number(artwork.ranking_score || 0).toFixed(1))))))) : /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12 text-sm text-slate-300" }, "Attach or publish more artworks before artwork-level ranking can be surfaced here.")))))); } -const __vite_glob_0_41 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_43 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -76169,7 +76294,7 @@ function CollectionDashboard() { } ), /* @__PURE__ */ React.createElement(SearchResults, { state: searchState, endpoints, selectedIds, onToggleSelected: toggleSelected })), /* @__PURE__ */ React.createElement(WarningList, { warnings: healthWarnings, endpoints }), /* @__PURE__ */ React.createElement(CollectionStrip, { title: "Top Performing", eyebrow: "Momentum", collections: topPerforming, emptyLabel: "No collections have enough activity yet to rank here.", endpoints }), /* @__PURE__ */ React.createElement(CollectionStrip, { title: "Needs Attention", eyebrow: "Quality", collections: needsAttention, emptyLabel: "No collections currently need manual intervention.", endpoints }), /* @__PURE__ */ React.createElement(CollectionStrip, { title: "Expiring Campaigns", eyebrow: "Timing", collections: expiringCampaigns, emptyLabel: "No campaigns are approaching their sunset window.", endpoints }))))); } -const __vite_glob_0_42 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_44 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionDashboard }, Symbol.toStringTag, { value: "Module" })); @@ -76365,7 +76490,7 @@ function CollectionFeaturedIndex() { } ), /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 -z-10 opacity-[0.05]", style: { backgroundImage: "url(/gfx/noise.png)", backgroundSize: "180px" } }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pt-8 md:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("a", { href: "/", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left fa-fw text-[11px]" }), "Back to home"), /* @__PURE__ */ React.createElement("a", { href: "/collections/featured", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Featured"), /* @__PURE__ */ React.createElement("a", { href: "/collections/trending", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Trending"), /* @__PURE__ */ React.createElement("a", { href: "/collections/community", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Community"), /* @__PURE__ */ React.createElement("a", { href: "/collections/editorial", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Editorial"), /* @__PURE__ */ React.createElement("a", { href: "/collections/seasonal", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Seasonal")), /* @__PURE__ */ React.createElement("section", { className: "mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.18fr)_400px] xl:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, eyebrow), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, title), campaign?.badge_label ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 inline-flex items-center rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, campaign.badge_label) : program?.promotion_tier ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 inline-flex items-center rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Promotion tier: ", program.promotion_tier) : null, /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-relaxed text-slate-300 md:text-[15px]" }, description), campaign ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Campaign key: ", campaign.key), campaign.event_label ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Event: ", campaign.event_label) : null, campaign.season_key ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Season: ", campaign.season_key) : null, Array.isArray(campaign.active_surface_keys) && campaign.active_surface_keys.length ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Surfaces: ", campaign.active_surface_keys.join(", ")) : null) : program ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Program key: ", program.key), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Collections: ", program.collections_count ?? collections.length), program.trust_tier ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Trust: ", program.trust_tier) : null, Array.isArray(program.partner_labels) && program.partner_labels.length ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Partners: ", program.partner_labels.join(", ")) : null, Array.isArray(program.sponsorship_labels) && program.sponsorship_labels.length ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-2" }, "Sponsors: ", program.sponsorship_labels.join(", ")) : null) : null), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-3 xl:grid-cols-1" }, /* @__PURE__ */ React.createElement(HeroStat$2, { icon: "fa-layer-group", label: "Collections", value: collections.length.toLocaleString() }), /* @__PURE__ */ React.createElement(HeroStat$2, { icon: "fa-wand-magic-sparkles", label: "Smart", value: smartCount.toLocaleString() }), /* @__PURE__ */ React.createElement(HeroStat$2, { icon: "fa-images", label: "Artworks", value: totalArtworks.toLocaleString() })))), /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, /* @__PURE__ */ React.createElement(SearchPanel, { search: search2 })), /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, collections.length ? /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, collections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: mainSave.context, saveContextMeta: mainSave.meta }))) : /* @__PURE__ */ React.createElement(EmptyState$4, null)), communityCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Community"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Collaborative picks"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, communityCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: "community_row", saveContextMeta: { surface_label: "community collections" } })))) : null, trendingCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Trending"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Momentum right now")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, trendingCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: "trending_row", saveContextMeta: { surface_label: "trending collections" } })))) : null, editorialCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Editorial"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Staff and campaign collections")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, editorialCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: "editorial_row", saveContextMeta: { surface_label: "editorial collections" } })))) : null, seasonalCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Seasonal"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Campaign and event spotlights")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, seasonalCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: "seasonal_row", saveContextMeta: { surface_label: "seasonal collections" } })))) : null, recentCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-10 pb-8" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Recent"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Freshly published collections")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, recentCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false, saveContext: "recent_row", saveContextMeta: { surface_label: "recent collections" } })))) : null))); } -const __vite_glob_0_43 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_45 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionFeaturedIndex }, Symbol.toStringTag, { value: "Module" })); @@ -76436,7 +76561,7 @@ function CollectionHistory() { busyId === entry.id ? "Restoring…" : "Restore" ) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 lg:grid-cols-2" }, /* @__PURE__ */ React.createElement(FieldChanges, { label: "Before", value: entry.before }), /* @__PURE__ */ React.createElement(FieldChanges, { label: "After", value: entry.after })))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-14 text-sm text-slate-300" }, "No audit entries have been recorded for this collection yet.")), Number(meta.last_page || 1) > 1 ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 flex flex-wrap items-center justify-between gap-3 rounded-[28px] border border-white/10 bg-white/[0.04] px-5 py-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", null, "Page ", meta.current_page || 1, " of ", meta.last_page || 1), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, (meta.current_page || 1) > 1 ? /* @__PURE__ */ React.createElement("a", { href: buildPageUrl((meta.current_page || 1) - 1), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left fa-fw text-[10px]" }), "Previous") : null, (meta.current_page || 1) < (meta.last_page || 1) ? /* @__PURE__ */ React.createElement("a", { href: buildPageUrl((meta.current_page || 1) + 1), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 font-semibold text-white transition hover:bg-white/[0.07]" }, "Next", /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right fa-fw text-[10px]" })) : null)) : null))); } -const __vite_glob_0_44 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_46 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionHistory }, Symbol.toStringTag, { value: "Module" })); @@ -78396,7 +78521,7 @@ function CollectionManage() { } )), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("span", null, "Allow comments"), /* @__PURE__ */ React.createElement(Checkbox, { checked: form.allow_comments, onChange: (event) => handleModerationToggle("allow_comments", event.target.checked) })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("span", null, "Allow submissions"), /* @__PURE__ */ React.createElement(Checkbox, { checked: form.allow_submissions, onChange: (event) => handleModerationToggle("allow_submissions", event.target.checked) })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("span", null, "Allow saves"), /* @__PURE__ */ React.createElement(Checkbox, { checked: form.allow_saves, onChange: (event) => handleModerationToggle("allow_saves", event.target.checked) })))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Rapid actions"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: handleAdminUnfeature, className: "rounded-2xl border border-amber-300/25 bg-amber-300/10 px-4 py-3 text-sm font-semibold text-amber-100 transition hover:bg-amber-300/15" }, "Remove featured placement"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleModerationStatusChange("under_review"), className: "rounded-2xl border border-white/12 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, "Send to review"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleModerationStatusChange("restricted"), className: "rounded-2xl border border-rose-400/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15" }, "Restrict public access")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-4 text-sm leading-relaxed text-slate-300" }, "Current state: ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, (collectionState?.moderation_status || "active").replace("_", " ")))))) : null))); } -const __vite_glob_0_45 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_47 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionManage }, Symbol.toStringTag, { value: "Module" })); @@ -78413,7 +78538,7 @@ function CollectionSeriesShow() { const stats = props.stats || {}; return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo, title: seo.title || `${title} — Skinbase`, description: seo.description || description }), /* @__PURE__ */ React.createElement("div", { className: "relative min-h-screen overflow-hidden pb-16" }, /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[36rem] opacity-95", style: { background: "radial-gradient(circle at 10% 15%, rgba(59,130,246,0.18), transparent 28%), radial-gradient(circle at 84% 18%, rgba(34,197,94,0.16), transparent 24%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #08111f 100%)" } }), /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 -z-10 opacity-[0.05]", style: { backgroundImage: "url(/gfx/noise.png)", backgroundSize: "180px" } }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pt-8 md:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("a", { href: "/collections/featured", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left fa-fw text-[11px]" }), "Back to collections"), leadCollection?.url ? /* @__PURE__ */ React.createElement("a", { href: leadCollection.url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, "Lead collection") : null), /* @__PURE__ */ React.createElement("section", { className: "mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm md:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_400px] xl:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Series"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-relaxed text-slate-300 md:text-[15px]" }, description), props.seriesKey ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 inline-flex rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-300" }, props.seriesKey) : null), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-3 xl:grid-cols-1" }, /* @__PURE__ */ React.createElement(StatCard$9, { icon: "fa-layer-group", label: "Collections", value: Number(stats.collections || collections.length).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$9, { icon: "fa-user-group", label: "Creators", value: Number(stats.owners || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$9, { icon: "fa-images", label: "Artworks", value: Number(stats.artworks || 0).toLocaleString() })))), leadCollection ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Lead Entry"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Start with the opening collection")), stats.latest_activity_at ? /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, "Latest activity ", new Date(stats.latest_activity_at).toLocaleDateString()) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 max-w-xl" }, /* @__PURE__ */ React.createElement(CollectionCard, { collection: leadCollection, isOwner: false }))) : null, /* @__PURE__ */ React.createElement("section", { className: "mt-8 pb-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Sequence"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Public collections in order"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3" }, collections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false }))))))); } -const __vite_glob_0_46 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_48 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionSeriesShow }, Symbol.toStringTag, { value: "Module" })); @@ -80123,7 +80248,7 @@ function CollectionShow() { const renderedSidebarModules = enabledModules.filter((module) => module.slot === "sidebar").map(renderModule).filter(Boolean); return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo, title: metaTitle, description: metaDescription, jsonLd: collectionSchema }), /* @__PURE__ */ React.createElement("div", { className: "relative min-h-screen overflow-hidden pb-16" }, /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[36rem] opacity-95", style: { background: "radial-gradient(circle at top left, rgba(56,189,248,0.18), transparent 32%), radial-gradient(circle at 82% 10%, rgba(249,115,22,0.18), transparent 26%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #08111f 100%)" } }), /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 -z-10 opacity-[0.05]", style: { backgroundImage: "url(/gfx/noise.png)", backgroundSize: "180px" } }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pt-8 md:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("a", { href: profileCollectionsUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left fa-fw text-[11px]" }), "Back to collections"), isOwner && manageUrl ? /* @__PURE__ */ React.createElement("a", { href: manageUrl, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 font-semibold text-sky-100 transition hover:bg-sky-400/15" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-grip fa-fw text-[11px]" }), "Manage artworks") : null, isOwner && editUrl ? /* @__PURE__ */ React.createElement("a", { href: editUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen-to-square fa-fw text-[11px]" }), "Edit details") : null, isOwner && analyticsUrl ? /* @__PURE__ */ React.createElement("a", { href: analyticsUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chart-column fa-fw text-[11px]" }), "Analytics") : null, isOwner && historyUrl ? /* @__PURE__ */ React.createElement("a", { href: historyUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-timeline fa-fw text-[11px]" }), "History") : null), /* @__PURE__ */ React.createElement("section", { className: "mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] shadow-[0_30px_90px_rgba(2,6,23,0.32)] backdrop-blur-xl" }, /* @__PURE__ */ React.createElement("div", { className: `h-[3px] bg-gradient-to-r ${collection?.type === "editorial" ? "from-amber-400/80 via-amber-400/30 to-transparent" : collection?.type === "community" ? "from-emerald-400/80 via-emerald-400/30 to-transparent" : collection?.mode === "smart" ? "from-sky-400/80 via-sky-400/30 to-transparent" : "from-violet-400/80 via-violet-400/30 to-transparent"}` }), /* @__PURE__ */ React.createElement("div", { className: "grid items-start gap-6 p-5 md:p-7 xl:grid-cols-[minmax(0,1.2fr)_420px]" }, /* @__PURE__ */ React.createElement("div", { className: "relative self-start overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60" }, /* @__PURE__ */ React.createElement(CollectionCover, { collection }), /* @__PURE__ */ React.createElement("div", { className: "pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(2,6,23,0.8),rgba(2,6,23,0.08))]" })), /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_28%),radial-gradient(circle_at_90%_8%,rgba(251,191,36,0.14),transparent_24%),linear-gradient(180deg,rgba(15,23,42,0.94),rgba(10,18,32,0.92))] px-5 py-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] md:px-6 md:py-7" }, /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute -left-14 top-10 h-36 w-36 rounded-full bg-sky-400/10 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { "aria-hidden": "true", className: "pointer-events-none absolute -right-10 bottom-8 h-32 w-32 rounded-full bg-amber-300/10 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 flex h-full flex-col justify-between" }, collection?.banner_text ? /* @__PURE__ */ React.createElement("div", { className: `mb-4 inline-flex max-w-full items-center gap-2 rounded-[22px] border px-4 py-3 text-sm font-medium shadow-[0_18px_40px_rgba(2,6,23,0.2)] ${spotlightClasses}` }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-sparkles text-[12px]" }), /* @__PURE__ */ React.createElement("span", { className: "truncate" }, collection.banner_text)) : null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, collection?.is_featured ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-amber-300/25 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, "Featured Collection") : null, collection?.mode === "smart" ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Smart Collection") : null, /* @__PURE__ */ React.createElement(TypeBadge, { collection }), collection?.event_label ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white" }, collection.event_label) : null, collection?.campaign_label ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, collection.campaign_label) : null, collection?.badge_label ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white" }, collection.badge_label) : null, collection?.series_key ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white" }, "Series ", collection.series_order ? `#${collection.series_order}` : "") : null, isOwner ? /* @__PURE__ */ React.createElement(CollectionVisibilityBadge, { visibility: collection?.visibility }) : null), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 max-w-3xl text-4xl font-black tracking-[-0.06em] text-white md:text-5xl xl:text-[4rem] xl:leading-[0.92]" }, collection?.title), showIntroBlock ? /* @__PURE__ */ React.createElement(React.Fragment, null, collection?.subtitle ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-lg text-slate-300 md:text-xl" }, collection.subtitle) : null, collection?.summary || collection?.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-sm leading-relaxed text-slate-300 md:text-[15px]" }, collection?.summary || collection?.description) : /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-sm leading-relaxed text-slate-400 md:text-[15px]" }, "A curated selection from @", owner?.username, ", assembled as a focused gallery rather than a simple archive."), collection?.smart_summary ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 max-w-2xl rounded-[22px] border border-sky-300/15 bg-sky-400/[0.07] px-4 py-3 text-sm leading-relaxed text-sky-100/90" }, collection.smart_summary) : null, featuringCreatorsCount > 1 ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, "Featuring artworks by ", featuringCreatorsCount, " creators.") : null) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-7 space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(CollectionHeroAction, { onClick: handleLike, disabled: state.busy || !engagement?.like_url, icon: "fa-heart", label: state.liked ? "Liked" : "Like", tone: "rose", active: state.liked }), /* @__PURE__ */ React.createElement(CollectionHeroAction, { onClick: handleFollow, disabled: state.busy || !engagement?.follow_url, icon: "fa-bell", label: state.following ? "Following" : "Follow", tone: "emerald", active: state.following }), /* @__PURE__ */ React.createElement(CollectionHeroAction, { onClick: handleSave, disabled: state.busy || !engagement?.save_url && !engagement?.unsave_url, icon: "fa-bookmark", label: state.saved ? "Saved" : "Save", tone: "violet", active: state.saved })), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(CollectionHeroAction, { onClick: handleShare, icon: "fa-share-nodes", label: "Share", tone: "neutral", compact: true }), featuredCollectionsUrl ? /* @__PURE__ */ React.createElement(CollectionHeroAction, { href: featuredCollectionsUrl, icon: "fa-compass", label: "Explore", tone: "sky", compact: true }) : null, reportEndpoint && !isOwner ? /* @__PURE__ */ React.createElement(CollectionHeroAction, { onClick: () => handleReport("collection", collection?.id), icon: "fa-flag", label: "Report", tone: "amber", compact: true }) : null)), state.notice ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-sky-100" }, state.notice) : null, /* @__PURE__ */ React.createElement(OwnerCard, { owner, collectionType: collection?.type }))))), heroMetrics.length || heroSignals.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-6 rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_20px_70px_rgba(2,6,23,0.22)] backdrop-blur-xl md:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Collection Snapshot"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Stats and placement signals")), /* @__PURE__ */ React.createElement("p", { className: "max-w-xl text-sm leading-relaxed text-slate-400" }, "The engagement counters and ranking signals now live outside the hero so the header can stay focused on the artwork, title, and actions.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 xl:grid-cols-[minmax(0,1.6fr)_minmax(320px,0.95fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-5" }, heroMetrics.map((item) => /* @__PURE__ */ React.createElement(HeroMetricCard, { key: item.label, icon: item.icon, label: item.label, value: item.value, helper: item.helper, tone: item.tone }))), heroSignals.length ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-1" }, heroSignals.map((item) => /* @__PURE__ */ React.createElement(HeroSignalCard, { key: item.label, icon: item.icon, label: item.label, value: item.value, description: item.description, tone: item.tone }))) : null)) : null, seriesContext?.url || seriesContext?.previous || seriesContext?.next || Array.isArray(seriesContext?.siblings) && seriesContext.siblings.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Series"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, seriesContext?.title || "Connected collection sequence"), seriesContext?.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-relaxed text-slate-300" }, seriesContext.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, collection?.series_key ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, collection.series_key) : null, seriesContext?.url ? /* @__PURE__ */ React.createElement("a", { href: seriesContext.url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-list fa-fw text-[10px]" }), "View full series") : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, seriesContext?.previous ? /* @__PURE__ */ React.createElement("a", { href: seriesContext.previous.url, className: "flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.04] px-5 py-4 transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Previous"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, seriesContext.previous.title)), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left text-slate-500" })) : null, seriesContext?.next ? /* @__PURE__ */ React.createElement("a", { href: seriesContext.next.url, className: "flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.04] px-5 py-4 transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Next"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, seriesContext.next.title)), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right text-slate-500" })) : null), Array.isArray(seriesContext?.siblings) && seriesContext.siblings.length ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-4" }, seriesContext.siblings.slice(0, 2).map((item) => /* @__PURE__ */ React.createElement(CollectionCard, { key: item.id, collection: item, isOwner: false }))) : null)) : null, contextSignals.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-amber-300/15 bg-amber-400/10 text-amber-300" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-diagram-project text-sm" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80" }, "Related Context"), /* @__PURE__ */ React.createElement("h2", { className: "mt-1 text-2xl font-semibold text-white" }, "Campaign, event, and quality context"))), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, contextSignals.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, contextSignals.map((item) => /* @__PURE__ */ React.createElement(ContextSignalCard, { key: `${item.meta}-${item.title}`, item })))) : null, storyLinks.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-lime-300/15 bg-lime-400/10 text-lime-300" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open text-sm" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-lime-200/80" }, "Stories"), /* @__PURE__ */ React.createElement("h2", { className: "mt-1 text-2xl font-semibold text-white" }, "Stories and editorial references linked to this collection"))), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, storyLinks.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, storyLinks.map((item) => /* @__PURE__ */ React.createElement(EntityLinkCard, { key: `${item.linked_type}-${item.linked_id}-${item.id}`, item })))) : null, taxonomyLinks.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-violet-300/15 bg-violet-400/10 text-violet-300" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-tags text-sm" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-violet-200/80" }, "Browse The Theme"), /* @__PURE__ */ React.createElement("h2", { className: "mt-1 text-2xl font-semibold text-white" }, "Categories and tags that anchor this collection"))), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, taxonomyLinks.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, taxonomyLinks.map((item) => /* @__PURE__ */ React.createElement(EntityLinkCard, { key: `${item.linked_type}-${item.linked_id}-${item.id}`, item })))) : null, contributorLinks.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/15 bg-sky-400/10 text-sky-300" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user-group text-sm" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Connected Creators"), /* @__PURE__ */ React.createElement("h2", { className: "mt-1 text-2xl font-semibold text-white" }, "Creators and artworks that give the set its shape"))), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, contributorLinks.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, contributorLinks.map((item) => /* @__PURE__ */ React.createElement(EntityLinkCard, { key: `${item.linked_type}-${item.linked_id}-${item.id}`, item })))) : null, renderedFullModules.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 space-y-6" }, renderedFullModules) : null, renderedMainModules.length || renderedSidebarModules.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, renderedMainModules), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, renderedSidebarModules)) : null))); } -const __vite_glob_0_47 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_49 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionShow }, Symbol.toStringTag, { value: "Module" })); @@ -80519,7 +80644,7 @@ function CollectionStaffProgramming() { return /* @__PURE__ */ React.createElement("div", { key: `merge-pending-${item.id}`, className: `rounded-[24px] border border-white/10 bg-white/[0.04] p-4 transition ${cardBusy ? "ring-1 ring-sky-300/25" : ""}` }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, (item.comparison?.match_reasons || []).map((reason) => /* @__PURE__ */ React.createElement("span", { key: reason, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-slate-300" }, titleize(reason)))), cardBusy ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-circle-notch fa-spin fa-fw text-[10px]" }), "Processing ", titleize(activeQueueAction)) : null), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Source"), item.source ? /* @__PURE__ */ React.createElement(CollectionCard, { collection: item.source, isOwner: true }) : null), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Candidate"), item.target ? /* @__PURE__ */ React.createElement(CollectionCard, { collection: item.target, isOwner: true }) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-2 md:grid-cols-3 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Shared artworks: ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, item.comparison?.shared_artworks_count ?? 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Source count: ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, item.comparison?.source_artworks_count ?? 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2" }, "Target count: ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, item.comparison?.target_artworks_count ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, item.source?.manage_url ? /* @__PURE__ */ React.createElement("a", { href: item.source.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-xs font-semibold text-rose-100 transition hover:bg-rose-400/15" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-code-compare fa-fw text-[10px]" }), "Review source") : null, item.target?.manage_url ? /* @__PURE__ */ React.createElement("a", { href: item.target.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square fa-fw text-[10px]" }), "Open target") : null, item.source?.id && historyPattern ? /* @__PURE__ */ React.createElement("a", { href: historyPattern.replace("__COLLECTION__", String(item.source.id)), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-timeline fa-fw text-[10px]" }), "History") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleQueueAction("canonicalize", item), disabled: cardBusy, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-xs font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${activeQueueAction === "canonicalize" ? "fa-circle-notch fa-spin" : "fa-badge-check"} fa-fw text-[10px]` }), activeQueueAction === "canonicalize" ? "Canonicalizing..." : "Canonicalize"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleQueueAction("merge", item), disabled: cardBusy, className: "inline-flex items-center gap-2 rounded-full border border-emerald-300/20 bg-emerald-400/10 px-4 py-2 text-xs font-semibold text-emerald-100 transition hover:bg-emerald-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${activeQueueAction === "merge" ? "fa-circle-notch fa-spin" : "fa-code-merge"} fa-fw text-[10px]` }), activeQueueAction === "merge" ? "Merging..." : "Merge now"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleQueueAction("reject", item), disabled: cardBusy, className: "inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-xs font-semibold text-amber-100 transition hover:bg-amber-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${activeQueueAction === "reject" ? "fa-circle-notch fa-spin" : "fa-ban"} fa-fw text-[10px]` }), activeQueueAction === "reject" ? "Rejecting..." : "Reject"))); }) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-5 py-10 text-sm text-slate-300" }, "No pending merge candidates right now."))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Recent Decisions"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold text-white" }, "Canonical, reject, and merge history")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, mergeQueue?.recent?.length || 0)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-4" }, (mergeQueue?.recent || []).length ? mergeQueue.recent.map((item) => /* @__PURE__ */ React.createElement("div", { key: `merge-recent-${item.id}`, className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, titleize(item.action_type)), item.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, item.summary) : null, /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-slate-500" }, item.updated_at ? new Date(item.updated_at).toLocaleString() : "Unknown time", item.actor?.username ? ` • @${item.actor.username}` : ""))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 lg:grid-cols-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Source"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, item.source?.title || "Collection")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Target"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, item.target?.title || "Collection"))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, item.source?.manage_url ? /* @__PURE__ */ React.createElement("a", { href: item.source.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen-to-square fa-fw text-[10px]" }), "Open source") : null, item.target?.manage_url ? /* @__PURE__ */ React.createElement("a", { href: item.target.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square fa-fw text-[10px]" }), "Open target") : null))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-5 py-10 text-sm text-slate-300" }, "No recent merge decisions yet."))))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]" }, /* @__PURE__ */ React.createElement("form", { onSubmit: handleAssignmentSubmit, className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Assignment"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Program key and scope")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$3, { label: "Collection" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: String(assignmentForm.collection_id || ""), onChange: (val) => setAssignmentForm((current) => ({ ...current, collection_id: val })), options: collectionOptions.map((o) => ({ value: String(o.id), label: o.title })) })), /* @__PURE__ */ React.createElement(Field$3, { label: "Program Key", help: "Use stable internal names like discover-spring or homepage-hero." }, /* @__PURE__ */ React.createElement("input", { list: "program-key-options", value: assignmentForm.program_key, onChange: (event) => setAssignmentForm((current) => ({ ...current, program_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Placement Scope", help: "Optional placement scope such as homepage.hero or discover.rail." }, /* @__PURE__ */ React.createElement("input", { value: assignmentForm.placement_scope, onChange: (event) => setAssignmentForm((current) => ({ ...current, placement_scope: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Campaign Key" }, /* @__PURE__ */ React.createElement("input", { value: assignmentForm.campaign_key, onChange: (event) => setAssignmentForm((current) => ({ ...current, campaign_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Priority" }, /* @__PURE__ */ React.createElement("input", { type: "number", min: "-100", max: "100", value: assignmentForm.priority, onChange: (event) => setAssignmentForm((current) => ({ ...current, priority: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field$3, { label: "Starts At" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: assignmentForm.starts_at, onChange: (nextValue) => setAssignmentForm((current) => ({ ...current, starts_at: nextValue })), placeholder: "Start time", clearable: true, className: "bg-white/[0.04]" })), /* @__PURE__ */ React.createElement(Field$3, { label: "Ends At" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: assignmentForm.ends_at, onChange: (nextValue) => setAssignmentForm((current) => ({ ...current, ends_at: nextValue })), placeholder: "End time", clearable: true, className: "bg-white/[0.04]" }))), /* @__PURE__ */ React.createElement(Field$3, { label: "Notes", help: "Operational note for launch timing, overrides, or review context." }, /* @__PURE__ */ React.createElement("textarea", { value: assignmentForm.notes, onChange: (event) => setAssignmentForm((current) => ({ ...current, notes: event.target.value })), className: "mt-4 min-h-[120px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 1e3 })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy === "assignment", className: "inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "assignment" ? "fa-circle-notch fa-spin" : "fa-sliders"} fa-fw` }), assignmentForm.id ? "Update Assignment" : "Save Assignment"), assignmentForm.id ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetAssignmentForm, className: "inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-rotate-left fa-fw" }), "Cancel Edit") : null), /* @__PURE__ */ React.createElement("datalist", { id: "program-key-options" }, programKeyOptions.map((option) => /* @__PURE__ */ React.createElement("option", { key: option, value: option })))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("form", { onSubmit: handlePreview, className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80" }, "Preview"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Inspect a live program pool")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-[minmax(0,1fr)_140px_auto]" }, /* @__PURE__ */ React.createElement(Field$3, { label: "Program Key" }, /* @__PURE__ */ React.createElement("input", { list: "program-key-options", value: previewForm.program_key, onChange: (event) => setPreviewForm((current) => ({ ...current, program_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Limit" }, /* @__PURE__ */ React.createElement("input", { type: "number", min: "1", max: "24", value: previewForm.limit, onChange: (event) => setPreviewForm((current) => ({ ...current, limit: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "flex items-end" }, /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy === "preview", className: "inline-flex h-[50px] w-full items-center justify-center gap-2 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-5 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "preview" ? "fa-circle-notch fa-spin" : "fa-binoculars"} fa-fw` }), "Preview"))), previewCollections.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 xl:grid-cols-2" }, previewCollections.map((collection) => /* @__PURE__ */ React.createElement("div", { key: collection.id, className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4" }, /* @__PURE__ */ React.createElement(CollectionCard, { collection, isOwner: true })))) : /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-5 py-8 text-sm text-slate-300" }, "Run a preview to inspect which collections currently qualify for a given program key.")), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-lime-200/80" }, "Diagnostics"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Eligibility, duplicate risk, and ranking refresh")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 xl:grid-cols-[320px_minmax(0,1fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Operations summary"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-1" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-400" }, "Stale health"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-lg font-semibold text-white" }, Number(observabilitySummary?.counts?.stale_health || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-400" }, "Stale recommendations"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-lg font-semibold text-white" }, Number(observabilitySummary?.counts?.stale_recommendations || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-400" }, "Placement blocked"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-lg font-semibold text-white" }, Number(observabilitySummary?.counts?.placement_blocked || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-400" }, "Duplicate risk"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-lg font-semibold text-white" }, Number(observabilitySummary?.counts?.duplicate_risk || 0)))), observabilitySummary?.generated_at ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs text-slate-400" }, "Generated ", new Date(observabilitySummary.generated_at).toLocaleString()) : null), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Watchlist"), Array.isArray(observabilitySummary?.watchlist) && observabilitySummary.watchlist.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 xl:grid-cols-2" }, observabilitySummary.watchlist.map((collection) => /* @__PURE__ */ React.createElement("div", { key: `watch-${collection.id}`, className: "rounded-[20px] border border-white/10 bg-white/[0.04] p-3" }, /* @__PURE__ */ React.createElement(CollectionCard, { collection, isOwner: true })))) : /* @__PURE__ */ React.createElement("p", { className: "mt-3" }, "No watchlist items are currently flagged."))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]" }, /* @__PURE__ */ React.createElement(Field$3, { label: "Target Collection", help: "Leave a selection in place to inspect one collection. Change it any time before running a diagnostic." }, /* @__PURE__ */ React.createElement(NovaSelect, { value: String(selectedCollectionId || ""), onChange: (val) => setSelectedCollectionId(val), options: collectionOptions.map((o) => ({ value: String(o.id), label: o.title })) })), /* @__PURE__ */ React.createElement("div", { className: "flex items-end gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => runDiagnostic("eligibility"), disabled: busy !== "", className: "inline-flex items-center gap-2 rounded-2xl border border-lime-300/20 bg-lime-400/10 px-4 py-3 text-sm font-semibold text-lime-100 transition hover:bg-lime-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "eligibility" ? "fa-circle-notch fa-spin" : "fa-shield-check"} fa-fw` }), "Eligibility"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => runDiagnostic("duplicates"), disabled: busy !== "", className: "inline-flex items-center gap-2 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "duplicates" ? "fa-circle-notch fa-spin" : "fa-id-card"} fa-fw` }), "Duplicates"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => runDiagnostic("recommendations"), disabled: busy !== "", className: "inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "recommendations" ? "fa-circle-notch fa-spin" : "fa-arrows-rotate"} fa-fw` }), "Refresh"))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Eligibility"), diagnostics.eligibility ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, /* @__PURE__ */ React.createElement("p", null, diagnostics.eligibility.status === "queued" ? `${diagnostics.eligibility.count} collection(s) queued.` : `${diagnostics.eligibility.count} collection(s) evaluated.`), diagnostics.eligibility.message ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, diagnostics.eligibility.message) : null, (diagnostics.eligibility.items || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.collection_id, className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, item.health_state || "unknown", " · ", item.readiness_state || "unknown", " · ", item.placement_eligibility ? "eligible" : "blocked"))) : /* @__PURE__ */ React.createElement("p", { className: "mt-3" }, "Run an eligibility refresh to verify readiness and public placement safety.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Duplicate candidates"), diagnostics.duplicates ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, /* @__PURE__ */ React.createElement("p", null, diagnostics.duplicates.status === "queued" ? `${diagnostics.duplicates.count} collection(s) queued.` : `${diagnostics.duplicates.count} collection(s) with candidates.`), diagnostics.duplicates.message ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, diagnostics.duplicates.message) : null, (diagnostics.duplicates.items || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.collection_id, className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, item.candidates?.length ? item.candidates.map((candidate) => candidate.title).join(", ") : "No candidates"))) : /* @__PURE__ */ React.createElement("p", { className: "mt-3" }, "Run duplicate scan to surface overlap before programming a collection widely.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Recommendation refresh"), diagnostics.recommendations ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, /* @__PURE__ */ React.createElement("p", null, diagnostics.recommendations.status === "queued" ? `${diagnostics.recommendations.count} collection(s) queued.` : `${diagnostics.recommendations.count} collection(s) refreshed.`), diagnostics.recommendations.message ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, diagnostics.recommendations.message) : null, (diagnostics.recommendations.items || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.collection_id, className: "rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2" }, titleize(item.recommendation_tier || "unknown"), " · ", titleize(item.ranking_bucket || "unknown"), " · ", titleize(item.search_boost_tier || "unknown")))) : /* @__PURE__ */ React.createElement("p", { className: "mt-3" }, "Run a recommendation refresh to update ranking and search tiers for this collection.")))), /* @__PURE__ */ React.createElement("form", { onSubmit: handleHooksSubmit, className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-fuchsia-200/80" }, "Hooks"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Experiment and program governance"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-relaxed text-slate-300" }, "Control experiment keys, promotion tiers, and staff-only program governance hooks for the selected collection without leaving the programming studio.")), selectedCollection?.program_key && endpoints.publicProgramPattern ? /* @__PURE__ */ React.createElement("a", { href: buildProgramUrl(endpoints.publicProgramPattern, selectedCollection.program_key), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square fa-fw text-[11px]" }), "Open public program landing") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, /* @__PURE__ */ React.createElement(Field$3, { label: "Experiment Key", help: "Internal test or treatment key for cross-surface collection experiments." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.experiment_key, onChange: (event) => setHooksForm((current) => ({ ...current, experiment_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Treatment", help: "Variant or treatment label tied to the experiment key." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.experiment_treatment, onChange: (event) => setHooksForm((current) => ({ ...current, experiment_treatment: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Placement Variant", help: "Surface-specific placement variant such as homepage_a or search_dense." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.placement_variant, onChange: (event) => setHooksForm((current) => ({ ...current, placement_variant: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Ranking Variant", help: "Override or annotate ranking mode experiments without changing the live pool logic." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.ranking_mode_variant, onChange: (event) => setHooksForm((current) => ({ ...current, ranking_mode_variant: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Pool Version", help: "Snapshot or rollout version for the collection pool definition." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.collection_pool_version, onChange: (event) => setHooksForm((current) => ({ ...current, collection_pool_version: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Test Label", help: "Human-readable campaign or experiment label for operations and diagnostics." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.test_label, onChange: (event) => setHooksForm((current) => ({ ...current, test_label: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 120 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Promotion Tier", help: "Optional internal tier for elevated or restrained programming treatment." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.promotion_tier, onChange: (event) => setHooksForm((current) => ({ ...current, promotion_tier: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 40 })), viewer.isAdmin ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Field$3, { label: "Partner Key", help: "Admin-only internal key for trusted partner or program ownership." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.partner_key, onChange: (event) => setHooksForm((current) => ({ ...current, partner_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Trust Tier", help: "Admin-only trust marker used for internal partner/program review logic." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.trust_tier, onChange: (event) => setHooksForm((current) => ({ ...current, trust_tier: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 40 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Sponsorship State", help: "Admin-only state for sponsored, pending, or cleared program treatment." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.sponsorship_state, onChange: (event) => setHooksForm((current) => ({ ...current, sponsorship_state: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 40 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Ownership Domain", help: "Admin-only internal ownership domain such as editorial, partner, creator_program, or events." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.ownership_domain, onChange: (event) => setHooksForm((current) => ({ ...current, ownership_domain: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Commercial Review", help: "Admin-only commercial review status for future partner and sponsor programs." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.commercial_review_state, onChange: (event) => setHooksForm((current) => ({ ...current, commercial_review_state: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 40 })), /* @__PURE__ */ React.createElement(Field$3, { label: "Legal Review", help: "Admin-only legal review status when collections need compliance approval before wider promotion." }, /* @__PURE__ */ React.createElement("input", { value: hooksForm.legal_review_state, onChange: (event) => setHooksForm((current) => ({ ...current, legal_review_state: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 40 }))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-4 text-sm text-slate-300 md:col-span-2 xl:col-span-3" }, "Partner, sponsorship, ownership, and review metadata remain admin-only. Moderators can still manage experiment and promotion hooks here.")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-center gap-3 rounded-[20px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: hooksForm.placement_eligibility, onChange: (event) => setHooksForm((current) => ({ ...current, placement_eligibility: event.target.checked })), label: "Placement eligible override" })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Experiment"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.experiment_key || selectedCollection?.experiment_key || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Treatment"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.experiment_treatment || selectedCollection?.experiment_treatment || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Placement Variant"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.placement_variant || selectedCollection?.placement_variant || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Workflow"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, titleize(hooksDiagnostics?.workflow_state || selectedCollection?.workflow_state || "unknown"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Health"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, titleize(hooksDiagnostics?.health_state || selectedCollection?.health_state || "unknown"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Recommendation Tier"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, titleize(hooksDiagnostics?.recommendation_tier || selectedCollection?.recommendation_tier || "unknown"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Ranking Bucket"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, titleize(hooksDiagnostics?.ranking_bucket || selectedCollection?.ranking_bucket || "unknown"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Ranking Variant"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.ranking_mode_variant || selectedCollection?.ranking_mode_variant || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Pool Version"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.collection_pool_version || selectedCollection?.collection_pool_version || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Test Label"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.test_label || selectedCollection?.test_label || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Promotion Tier"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.promotion_tier || selectedCollection?.promotion_tier || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Partner Key"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.partner_key || selectedCollection?.partner_key || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Trust Tier"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.trust_tier || selectedCollection?.trust_tier || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Sponsorship State"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.sponsorship_state || selectedCollection?.sponsorship_state || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Ownership Domain"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.ownership_domain || selectedCollection?.ownership_domain || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Commercial Review"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.commercial_review_state || selectedCollection?.commercial_review_state || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Legal Review"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.legal_review_state || selectedCollection?.legal_review_state || "Not set")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Last Health Check"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.last_health_check_at ? new Date(hooksDiagnostics.last_health_check_at).toLocaleString() : "Not yet")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, "Last Recommendation Refresh"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, hooksDiagnostics?.last_recommendation_refresh_at ? new Date(hooksDiagnostics.last_recommendation_refresh_at).toLocaleString() : "Not yet"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy === "hooks" || !selectedCollectionId, className: "inline-flex items-center gap-2 rounded-2xl border border-fuchsia-300/20 bg-fuchsia-400/10 px-5 py-3 text-sm font-semibold text-fuchsia-100 transition hover:bg-fuchsia-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "hooks" ? "fa-circle-notch fa-spin" : "fa-flask-vial"} fa-fw` }), "Save Hooks"), selectedCollection?.manage_url ? /* @__PURE__ */ React.createElement("a", { href: selectedCollection.manage_url, className: "inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square fa-fw" }), "Open collection") : null)))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Assignments"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Current programming inventory")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, assignments.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-5" }, assignments.length ? assignments.map((assignment) => /* @__PURE__ */ React.createElement("div", { key: assignment.id, className: "rounded-[28px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, assignment.program_key), assignment.placement_scope ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, assignment.placement_scope) : null, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, "priority ", assignment.priority)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => hydrateAssignment(assignment), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen fa-fw text-[10px]" }), "Edit"), endpoints.managePattern ? /* @__PURE__ */ React.createElement("a", { href: endpoints.managePattern.replace("__COLLECTION__", String(assignment.collection?.id || "")), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square fa-fw text-[10px]" }), "Manage") : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-5 xl:grid-cols-[minmax(0,1fr)_280px]" }, /* @__PURE__ */ React.createElement("div", null, assignment.collection ? /* @__PURE__ */ React.createElement(CollectionCard, { collection: assignment.collection, isOwner: true }) : null), /* @__PURE__ */ React.createElement("div", { className: "space-y-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Campaign: ", assignment.campaign_key || "None"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Starts: ", assignment.starts_at ? new Date(assignment.starts_at).toLocaleString() : "Immediate"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Ends: ", assignment.ends_at ? new Date(assignment.ends_at).toLocaleString() : "Open-ended"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Placement: ", assignment.collection?.placement_eligibility ? "Eligible" : "Blocked"), assignment.notes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, assignment.notes) : null)))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12 text-sm text-slate-300" }, "No programming assignments yet. Create the first one above.")))))); } -const __vite_glob_0_48 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_50 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionStaffProgramming }, Symbol.toStringTag, { value: "Module" })); @@ -80845,7 +80970,7 @@ function CollectionStaffSurfaces() { return /* @__PURE__ */ React.createElement("label", { key: option.id, className: `flex cursor-pointer items-start gap-3 rounded-[22px] border px-4 py-3 transition ${checked ? "border-lime-300/30 bg-lime-400/10" : "border-white/10 bg-white/[0.04] hover:bg-white/[0.07]"}` }, /* @__PURE__ */ React.createElement(Checkbox, { checked, onChange: () => toggleBatchCollection(option.id) }), /* @__PURE__ */ React.createElement("span", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("span", { className: "block truncate text-sm font-semibold text-white" }, option.title), /* @__PURE__ */ React.createElement("span", { className: "mt-1 block text-xs text-slate-400" }, option.type || "collection", " · ", option.visibility || "public"))); }))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-white" }, "Campaign metadata"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$2, { label: "Campaign Key" }, /* @__PURE__ */ React.createElement("input", { value: batchForm.campaign_key, onChange: (event) => setBatchForm((current) => ({ ...current, campaign_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 })), /* @__PURE__ */ React.createElement(Field$2, { label: "Campaign Label" }, /* @__PURE__ */ React.createElement("input", { value: batchForm.campaign_label, onChange: (event) => setBatchForm((current) => ({ ...current, campaign_label: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 120 })), /* @__PURE__ */ React.createElement(Field$2, { label: "Event Label" }, /* @__PURE__ */ React.createElement("input", { value: batchForm.event_label, onChange: (event) => setBatchForm((current) => ({ ...current, event_label: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 120 })), /* @__PURE__ */ React.createElement(Field$2, { label: "Season Key" }, /* @__PURE__ */ React.createElement("input", { value: batchForm.season_key, onChange: (event) => setBatchForm((current) => ({ ...current, season_key: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 80 }))), /* @__PURE__ */ React.createElement(Field$2, { label: "Editorial Notes", help: "Shared context recorded on each selected collection." }, /* @__PURE__ */ React.createElement("textarea", { value: batchForm.editorial_notes, onChange: (event) => setBatchForm((current) => ({ ...current, editorial_notes: event.target.value })), className: "mt-4 min-h-[120px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 4e3 })))), /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-white" }, "Optional placement plan"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, "If you set a surface, the preview shows which collections can safely be placed and which ones will be skipped."), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$2, { label: "Surface" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: batchForm.surface_key, onChange: (val) => setBatchForm((current) => ({ ...current, surface_key: val })), placeholder: "No placement", options: surfaceKeyOptions.map((o) => ({ value: o, label: o })) })), /* @__PURE__ */ React.createElement(Field$2, { label: "Placement Type" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: batchForm.placement_type, onChange: (val) => setBatchForm((current) => ({ ...current, placement_type: val })), searchable: false, options: [{ value: "campaign", label: "Campaign" }, { value: "manual", label: "Manual" }, { value: "scheduled_override", label: "Scheduled override" }] })), /* @__PURE__ */ React.createElement(Field$2, { label: "Priority" }, /* @__PURE__ */ React.createElement("input", { type: "number", min: "-100", max: "100", value: batchForm.priority, onChange: (event) => setBatchForm((current) => ({ ...current, priority: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: batchForm.is_active, onChange: (event) => setBatchForm((current) => ({ ...current, is_active: event.target.checked })), label: "Active placement" })), /* @__PURE__ */ React.createElement(Field$2, { label: "Starts At" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: batchForm.starts_at, onChange: (nextValue) => setBatchForm((current) => ({ ...current, starts_at: nextValue })), placeholder: "Start time", clearable: true, className: "bg-white/[0.04]" })), /* @__PURE__ */ React.createElement(Field$2, { label: "Ends At" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: batchForm.ends_at, onChange: (nextValue) => setBatchForm((current) => ({ ...current, ends_at: nextValue })), placeholder: "End time", clearable: true, className: "bg-white/[0.04]" }))), /* @__PURE__ */ React.createElement(Field$2, { label: "Placement Notes" }, /* @__PURE__ */ React.createElement("textarea", { value: batchForm.notes, onChange: (event) => setBatchForm((current) => ({ ...current, notes: event.target.value })), className: "mt-4 min-h-[110px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none", maxLength: 1e3 })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleBatchEditorial("preview"), disabled: busy === "batch-preview", className: "inline-flex items-center gap-2 rounded-2xl border border-lime-300/20 bg-lime-400/10 px-5 py-3 text-sm font-semibold text-lime-100 transition hover:bg-lime-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "batch-preview" ? "fa-circle-notch fa-spin" : "fa-flask"} fa-fw` }), "Preview Batch"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleBatchEditorial("apply"), disabled: busy === "batch-apply", className: "inline-flex items-center gap-2 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === "batch-apply" ? "fa-circle-notch fa-spin" : "fa-wand-magic-sparkles"} fa-fw` }), "Apply Batch"))), batchResult ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[26px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-white" }, "Preview results"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-300" }, batchResult.collections_count, " collections reviewed, ", batchResult.placement_eligible_count, " placement-ready."))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (batchResult.items || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.collection?.id, className: "rounded-[22px] border border-white/10 bg-white/[0.04] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-white" }, item.collection?.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-slate-400" }, item.collection?.visibility, " · ", item.collection?.lifecycle_state, " · ", item.collection?.moderation_status)), item.placement ? /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] ${item.placement.eligible ? "border-lime-300/20 bg-lime-400/10 text-lime-100" : "border-rose-300/20 bg-rose-400/10 text-rose-100"}` }, item.placement.eligible ? `ready for ${item.placement.surface_key}` : "placement skipped") : /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, "metadata only")), item.eligibility?.reasons?.length ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-xs text-amber-100/80" }, "Campaign readiness: ", item.eligibility.reasons.join(" ")) : null, item.placement?.reasons?.length ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-rose-100/80" }, "Placement: ", item.placement.reasons.join(" ")) : null)))) : null))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Definitions"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Registered surfaces")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, definitions.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, definitions.map((definition2) => /* @__PURE__ */ React.createElement("div", { key: definition2.id, className: "rounded-[24px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, definition2.surface_key), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, definition2.mode)), /* @__PURE__ */ React.createElement("h3", { className: "mt-4 text-lg font-semibold text-white" }, definition2.title), definition2.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, definition2.description) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, definition2.ranking_mode), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, "max ", definition2.max_items), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, definition2.is_active ? "active" : "inactive"), definition2.starts_at ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, "starts ", new Date(definition2.starts_at).toLocaleString()) : null, definition2.ends_at ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, "ends ", new Date(definition2.ends_at).toLocaleString()) : null, definition2.fallback_surface_key ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, "fallback ", definition2.fallback_surface_key) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => hydrateDefinition(definition2), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen fa-fw text-[10px]" }), "Edit Definition"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleDeleteDefinition(definition2), disabled: busy === `delete-definition-${definition2.id}`, className: "inline-flex items-center gap-2 rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-xs font-semibold text-rose-100 transition hover:bg-rose-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === `delete-definition-${definition2.id}` ? "fa-circle-notch fa-spin" : "fa-trash"} fa-fw text-[10px]` }), "Delete"))))))), conflicts.length ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-rose-300/20 bg-rose-500/10 p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-rose-100/80" }, "Conflicts"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Schedule overlaps need review")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-3 py-1 text-xs font-semibold text-rose-100" }, conflicts.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, conflicts.map((conflict, index2) => /* @__PURE__ */ React.createElement("div", { key: `${conflict.surface_key}-${index2}`, className: "rounded-[24px] border border-rose-300/20 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-rose-100" }, conflict.surface_key)), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm text-rose-50" }, conflict.summary), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-xs text-rose-100/70" }, "Window: ", conflict.window?.starts_at ? new Date(conflict.window.starts_at).toLocaleString() : "Immediate", " to ", conflict.window?.ends_at ? new Date(conflict.window.ends_at).toLocaleString() : "Open-ended"))))) : null, /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80" }, "Placements"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Active and scheduled slots")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, placements.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-5" }, placements.map((placement) => /* @__PURE__ */ React.createElement("div", { key: placement.id, className: "rounded-[28px] border border-white/10 bg-slate-950/40 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, placement.surface_key), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, placement.placement_type), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, "priority ", placement.priority), conflictPlacementIds.has(placement.id) || placement.has_conflict ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-rose-100" }, "conflict") : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => hydratePlacement(placement), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-pen fa-fw text-[10px]" }), "Edit"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleDeletePlacement(placement), disabled: busy === `delete-placement-${placement.id}`, className: "inline-flex items-center gap-2 rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-xs font-semibold text-rose-100 transition hover:bg-rose-400/15 disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${busy === `delete-placement-${placement.id}` ? "fa-circle-notch fa-spin" : "fa-trash"} fa-fw text-[10px]` }), "Delete"))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-5 xl:grid-cols-[minmax(0,1fr)_280px]" }, /* @__PURE__ */ React.createElement("div", null, placement.collection ? /* @__PURE__ */ React.createElement(CollectionCard, { collection: placement.collection, isOwner: true }) : null), /* @__PURE__ */ React.createElement("div", { className: "space-y-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Starts: ", placement.starts_at ? new Date(placement.starts_at).toLocaleString() : "Immediate"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Ends: ", placement.ends_at ? new Date(placement.ends_at).toLocaleString() : "Open-ended"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Campaign: ", placement.campaign_key || "None"), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3" }, "Status: ", placement.is_active ? "Active" : "Inactive"), placement.notes ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-3 text-slate-300" }, placement.notes) : null))))))))); } -const __vite_glob_0_49 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_51 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CollectionStaffSurfaces }, Symbol.toStringTag, { value: "Module" })); @@ -81414,7 +81539,7 @@ function NovaCardsAdminIndex() { } )), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Featured"), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(card.featured), onChange: (event) => updateCard(card.id, { featured: event.target.checked }) })), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Allow remix"), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(card.allow_remix), onChange: (event) => updateCard(card.id, { allow_remix: event.target.checked }) }))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-4 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, card.likes_count || 0, " likes"), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, card.saves_count || 0, " saves"), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, card.remixes_count || 0, " remixes"), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, card.challenge_entries_count || 0, " challenge entries")), card.moderation_reason_labels?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-amber-300/15 bg-amber-400/10 px-4 py-3 text-sm text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Heuristic moderation flags"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2" }, card.moderation_reason_labels.map((label) => /* @__PURE__ */ React.createElement("span", { key: `${card.id}-${label}`, className: "rounded-full border border-amber-200/20 bg-amber-50/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-amber-50" }, label))), card.moderation_source ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-[11px] uppercase tracking-[0.14em] text-amber-100/70" }, "Source ", String(card.moderation_source).replaceAll("_", " ")) : null) : null, card.moderation_override ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-sky-300/15 bg-sky-400/10 px-4 py-3 text-sm text-sky-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-100/80" }, "Latest staff override"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-2 text-xs uppercase tracking-[0.14em] text-sky-100/80" }, /* @__PURE__ */ React.createElement("span", null, "Status ", card.moderation_override.moderation_status), card.moderation_override.disposition_label ? /* @__PURE__ */ React.createElement("span", null, card.moderation_override.disposition_label) : null, card.moderation_override.actor_username ? /* @__PURE__ */ React.createElement("span", null, "@", card.moderation_override.actor_username) : null, card.moderation_override.source ? /* @__PURE__ */ React.createElement("span", null, String(card.moderation_override.source).replaceAll("_", " ")) : null), card.moderation_override.note ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm leading-6 text-sky-50" }, card.moderation_override.note) : null) : null, card.moderation_override_history?.length > 1 ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-sky-300/10 bg-sky-400/[0.08] px-4 py-3 text-sm text-sky-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-100/75" }, "Recent override history"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, renderOverrideHistoryItems(card.moderation_override_history, `card-${card.id}`))) : null))))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Creator curation"), !featuredCreators.length ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-4 text-sm text-slate-400" }, "No public Nova creators are available for curation yet.") : null, /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, featuredCreators.map((creator) => /* @__PURE__ */ React.createElement("div", { key: creator.id, className: "rounded-2xl border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, creator.display_name), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "@", creator.username)), creator.public_url ? /* @__PURE__ */ React.createElement("a", { href: creator.public_url, className: "text-xs font-semibold uppercase tracking-[0.16em] text-sky-300 transition hover:text-sky-200" }, "Open profile") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-3 grid grid-cols-3 gap-2 text-xs text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-[#08111f]/70 px-3 py-3" }, creator.public_cards_count || 0, " public cards"), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-[#08111f]/70 px-3 py-3" }, creator.featured_cards_count || 0, " featured"), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-[#08111f]/70 px-3 py-3" }, creator.total_views_count || 0, " views")), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Feature on editorial page"), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(creator.nova_featured_creator), onChange: (event) => updateCreator(creator.id, { nova_featured_creator: event.target.checked }) })))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Categories"), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, categories.map((category) => /* @__PURE__ */ React.createElement("div", { key: category.id, className: "rounded-2xl border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, category.name), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, category.slug, " • ", category.cards_count, " cards")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => saveCategory(category), className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.08]" }, "Save")))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Add category"), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React.createElement("input", { value: newCategory.name, onChange: (event) => setNewCategory((current) => ({ ...current, name: event.target.value })), placeholder: "Name", className: "w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("input", { value: newCategory.slug, onChange: (event) => setNewCategory((current) => ({ ...current, slug: event.target.value })), placeholder: "Slug", className: "w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("textarea", { value: newCategory.description, onChange: (event) => setNewCategory((current) => ({ ...current, description: event.target.value })), placeholder: "Description", rows: 3, className: "w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => saveCategory(newCategory), className: "w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, "Create category")))))); } -const __vite_glob_0_51 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_53 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NovaCardsAdminIndex }, Symbol.toStringTag, { value: "Module" })); @@ -81477,7 +81602,7 @@ function NovaCardsAssetPackAdmin() { } }, rows: 10, className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 font-mono text-sm text-white md:col-span-2" })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.active), onChange: (event) => setForm((current) => ({ ...current, active: event.target.checked })), label: "Active" }), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.official), onChange: (event) => setForm((current) => ({ ...current, official: event.target.checked })), label: "Official" })), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: savePack, className: "mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, selectedId ? "Update pack" : "Create pack")))); } -const __vite_glob_0_52 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_54 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NovaCardsAssetPackAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -81543,7 +81668,7 @@ function NovaCardsChallengeAdmin() { } }, rows: 10, className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 font-mono text-sm text-white md:col-span-2" })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.official), onChange: (event) => setForm((current) => ({ ...current, official: event.target.checked })), label: "Official" }), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.featured), onChange: (event) => setForm((current) => ({ ...current, featured: event.target.checked })), label: "Featured" })), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: saveChallenge, className: "mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, selectedId ? "Update challenge" : "Create challenge")))); } -const __vite_glob_0_53 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_55 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NovaCardsChallengeAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -81628,7 +81753,7 @@ function NovaCardsCollectionAdmin() { } return /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pb-20 pt-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Nova Cards Collections" }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75" }, "Editorial layer"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Official and public card collections"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-7 text-slate-300" }, "Create editorial collections, assign owners, and curate the public card sets that the v2 browse surface links to.")), /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSelectedId(null), className: "rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, "New collection"), /* @__PURE__ */ React.createElement(xe, { href: endpoints.cards || "/cp/cards", className: "rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, "Back to cards")))), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Collections"), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, collections.map((collection) => /* @__PURE__ */ React.createElement("button", { key: collection.id, type: "button", onClick: () => setSelectedId(collection.id), className: `w-full rounded-[22px] border p-4 text-left transition ${selectedId === collection.id ? "border-sky-300/35 bg-sky-400/10" : "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-base font-semibold tracking-[-0.03em] text-white" }, collection.name), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, collection.featured ? "Featured • " : "", collection.official ? "Official" : "@" + (collection.owner?.username || "creator"))), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200" }, collection.cards_count, " cards")), collection.description ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, collection.description) : null)))), /* @__PURE__ */ React.createElement("section", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Collection editor"), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Owner"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.user_id, onChange: (val) => setForm((current) => ({ ...current, user_id: Number(val) })), options: admins.map((a) => ({ value: a.id, label: a.name || a.username })) })), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Visibility"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.visibility, onChange: (val) => setForm((current) => ({ ...current, visibility: val })), searchable: false, options: [{ value: "public", label: "public" }, { value: "private", label: "private" }] })), /* @__PURE__ */ React.createElement("input", { value: form.name, onChange: (event) => setForm((current) => ({ ...current, name: event.target.value })), placeholder: "Collection name", className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("input", { value: form.slug, onChange: (event) => setForm((current) => ({ ...current, slug: event.target.value })), placeholder: "Slug", className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("textarea", { value: form.description, onChange: (event) => setForm((current) => ({ ...current, description: event.target.value })), placeholder: "Description", rows: 4, className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" })), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-4" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.official), onChange: (event) => setForm((current) => ({ ...current, official: event.target.checked })), label: "Official collection" }), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.featured), onChange: (event) => setForm((current) => ({ ...current, featured: event.target.checked })), label: "Featured collection" })), selected?.public_url ? /* @__PURE__ */ React.createElement("a", { href: selected.public_url, className: "text-sky-100 transition hover:text-white", target: "_blank", rel: "noreferrer" }, "Open public page") : null), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: saveCollection, className: "mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, selectedId ? "Update collection" : "Create collection")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Curate cards"), !selectedId ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-center text-sm text-slate-400" }, "Create or select a collection first.") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: String(cardId || ""), onChange: (val) => setCardId(val), placeholder: "Select a card", options: cards.map((c) => ({ value: String(c.id), label: c.title })) }), /* @__PURE__ */ React.createElement("input", { value: cardNote, onChange: (event) => setCardNote(event.target.value), placeholder: "Optional curator note", className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: attachCard, className: "rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, "Add")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-3" }, (selected?.items || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "flex items-start justify-between gap-4 rounded-[22px] border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-base font-semibold text-white" }, item.card?.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, "#", item.sort_order, " ", item.card?.creator?.username ? `• @${item.card.creator.username}` : ""), item.note ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, item.note) : null), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => detachCard(selectedId, item.card.id), className: "rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15" }, "Remove"))))))))); } -const __vite_glob_0_54 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_56 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NovaCardsCollectionAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -81733,7 +81858,7 @@ function NovaCardsTemplateAdmin() { } return /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-7xl px-4 pb-20 pt-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Nova Cards Templates" }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75" }, "Template system"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Official Nova Cards templates"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-7 text-slate-300" }, "Keep starter templates config-driven so the editor and render pipeline stay aligned as new card styles ship.")), /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetForm, className: "rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, "New template"), /* @__PURE__ */ React.createElement(xe, { href: endpoints.cards || "/cp/cards", className: "rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, "Back to cards")))), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,1.4fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Existing templates"), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, templates.map((template) => /* @__PURE__ */ React.createElement("button", { key: template.id, type: "button", onClick: () => loadTemplate(template), className: `w-full rounded-[22px] border p-4 text-left transition ${selectedId === template.id ? "border-sky-300/35 bg-sky-400/10" : "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-base font-semibold tracking-[-0.03em] text-white" }, template.name), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, template.slug)), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200" }, template.supported_formats?.join(", "))), template.description ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, template.description) : null)))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Template editor"), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("input", { value: form.name, onChange: (event) => setForm((current) => ({ ...current, name: event.target.value })), placeholder: "Template name", className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("input", { value: form.slug, onChange: (event) => setForm((current) => ({ ...current, slug: event.target.value })), placeholder: "Slug", className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" }), /* @__PURE__ */ React.createElement("textarea", { value: form.description, onChange: (event) => setForm((current) => ({ ...current, description: event.target.value })), placeholder: "Description", rows: 3, className: "rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" }), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Font preset"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.config_json?.font_preset || "modern-sans", onChange: (val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, font_preset: val } })), options: fonts.map((f2) => ({ value: f2.key, label: f2.label })), searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Gradient preset"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.config_json?.gradient_preset || "midnight-nova", onChange: (val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, gradient_preset: val } })), options: gradients.map((g2) => ({ value: g2.key, label: g2.label })), searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Layout preset"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.config_json?.layout || "quote_heavy", onChange: (val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, layout: val } })), options: ["quote_heavy", "author_emphasis", "centered", "minimal"].map((v2) => ({ value: v2, label: v2 })), searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Text alignment"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.config_json?.text_align || "center", onChange: (val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, text_align: val } })), options: ["left", "center", "right"].map((v2) => ({ value: v2, label: v2 })), searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Overlay style"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.config_json?.overlay_style || "dark-soft", onChange: (val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, overlay_style: val } })), options: ["none", "dark-soft", "dark-strong", "light-soft"].map((v2) => ({ value: v2, label: v2 })), searchable: false })), /* @__PURE__ */ React.createElement("label", { className: "text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "mb-2 block" }, "Text color"), /* @__PURE__ */ React.createElement("input", { type: "color", value: form.config_json?.text_color || "#ffffff", onChange: (event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, text_color: event.target.value } })), className: "h-12 w-full rounded-2xl border border-white/10 bg-[#0d1726] p-2" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-5" }, /* @__PURE__ */ React.createElement("div", { className: "mb-3 text-sm font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Supported formats"), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, formats2.map((format) => /* @__PURE__ */ React.createElement("div", { key: format.key, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: form.supported_formats.includes(format.key), onChange: () => toggleFormat(format.key), label: format.label }))))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.active), onChange: (event) => setForm((current) => ({ ...current, active: event.target.checked })), label: "Active" }), /* @__PURE__ */ React.createElement(Checkbox, { checked: Boolean(form.official), onChange: (event) => setForm((current) => ({ ...current, official: event.target.checked })), label: "Official" })), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: saveTemplate, className: "mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, selectedId ? "Update template" : "Create template")))); } -const __vite_glob_0_55 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_57 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NovaCardsTemplateAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -82019,7 +82144,7 @@ function SavedCollections() { } ))))) : activeFilters.q || activeFilters.filter !== "all" || activeFilters.sort !== "saved_desc" || activeFilters.list ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[32px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-16 text-center text-sm text-slate-300" }, "No saved collections match the current search or filters.") : /* @__PURE__ */ React.createElement(EmptyState$3, { browseUrl })), recommendedCollections.length ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80" }, "Recommended Next"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Because of what you save")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, recommendedCollections.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid grid-cols-1 gap-5 xl:grid-cols-3" }, recommendedCollections.map((collection) => /* @__PURE__ */ React.createElement(CollectionCard, { key: collection.id, collection, isOwner: false })))) : null))))); } -const __vite_glob_0_56 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_58 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SavedCollections }, Symbol.toStringTag, { value: "Module" })); @@ -82357,7 +82482,7 @@ if (typeof document !== "undefined") { clientExports.createRoot(mountEl).render(/* @__PURE__ */ React.createElement(CommunityActivityPage, { ...props })); } } -const __vite_glob_0_57 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_59 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CommunityActivityPage }, Symbol.toStringTag, { value: "Module" })); @@ -82679,7 +82804,7 @@ if (typeof document !== "undefined") { clientExports.createRoot(mountEl).render(/* @__PURE__ */ React.createElement(LatestCommentsPage, { ...props })); } } -const __vite_glob_0_58 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_60 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: LatestCommentsPage }, Symbol.toStringTag, { value: "Module" })); @@ -82753,7 +82878,7 @@ function EnhanceCreate() { /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-current/70" }, "Optimized preset") ))), form.errors.mode ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-rose-300" }, form.errors.mode) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-4 text-sm leading-6 text-amber-50" }, "The original file is preserved separately. Completed outputs can be reviewed and downloaded before you decide how to use them."), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: form.processing, className: "mt-6 inline-flex w-full items-center justify-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-3 text-sm font-semibold text-sky-50 transition hover:bg-sky-400/20 disabled:cursor-not-allowed disabled:opacity-60" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-wand-magic-sparkles text-xs" }), form.processing ? "Starting enhance…" : sourceType === "artwork" ? "Enhance artwork image" : "Start enhance")))); } -const __vite_glob_0_59 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_61 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: EnhanceCreate }, Symbol.toStringTag, { value: "Module" })); @@ -82821,7 +82946,7 @@ function EnhanceIndex() { const enhanceConfig = props.enhanceConfig || {}; return /* @__PURE__ */ React.createElement("div", { className: "w-full pb-16 pt-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Skinbase Enhance" }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_36%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80" }, "Skinbase Enhance"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Image Upscaler"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, "Improve older wallpapers, digital art, and photos with a clean upscaled version. Your original file is never replaced automatically.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: props.createUrl, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-sparkles text-[10px]" }), "Start enhance"))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Daily limit"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-3xl font-semibold tracking-[-0.04em] text-white" }, formatEnhanceInteger(props.dailyLimit || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Total jobs"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-3xl font-semibold tracking-[-0.04em] text-white" }, formatEnhanceInteger(props.jobs?.total || jobs.length))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Completed outputs"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-3xl font-semibold tracking-[-0.04em] text-white" }, formatEnhanceInteger(latestCompleted.length))))), /* @__PURE__ */ React.createElement(EnhanceStubWarning, { config: enhanceConfig, className: "mt-6" }), flash.success ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, latestCompleted.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Latest completed"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.03em] text-white" }, "Recent enhanced outputs"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, latestCompleted.map((job) => /* @__PURE__ */ React.createElement(xe, { key: job.id, href: job.show_url, className: "overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] transition hover:border-sky-300/30 hover:bg-[#0b1524]" }, /* @__PURE__ */ React.createElement("div", { className: "aspect-square bg-black/20" }, job.output_url ? /* @__PURE__ */ React.createElement("img", { src: job.output_url, alt: `Enhance job ${job.id}`, className: "h-full w-full object-cover" }) : null), /* @__PURE__ */ React.createElement("div", { className: "p-4" }, /* @__PURE__ */ React.createElement(EnhanceStatusBadge, { status: job.status }), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-sm font-semibold text-white" }, "Job #", job.id), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, job.scale, "x • ", job.mode)))))) : null, /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, /* @__PURE__ */ React.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "History"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.03em] text-white" }, "Your enhance jobs")), jobs.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-300" }, "No enhance jobs yet. Upload an image to start your first upscale.") : /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, jobs.map((job) => /* @__PURE__ */ React.createElement(JobCard, { key: job.id, job }))))); } -const __vite_glob_0_60 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_62 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: EnhanceIndex }, Symbol.toStringTag, { value: "Module" })); @@ -82880,7 +83005,7 @@ function EnhanceShow() { At.delete(job.delete_url); }, className: "inline-flex items-center gap-2 rounded-full border border-rose-300/20 bg-rose-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-rose-100 transition hover:bg-rose-400/20" }, "Delete") : null))), /* @__PURE__ */ React.createElement(EnhanceStubWarning, { config: props.enhanceConfig, className: "mt-6" }), flash.success ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, errors.job ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, errors.job) : null, job.error_message ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, job.error_message) : null, canCompare ? /* @__PURE__ */ React.createElement("div", { className: "mt-8" }, /* @__PURE__ */ React.createElement(BeforeAfterSlider, { beforeUrl: job.source_url, afterUrl: job.output_url })) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_380px]" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 lg:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Original source"), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20" }, job.source_url ? /* @__PURE__ */ React.createElement("img", { src: job.source_url, alt: "Original source", className: "w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex min-h-[280px] items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-4xl" })))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Enhanced result"), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20" }, job.output_url ? /* @__PURE__ */ React.createElement("img", { src: job.output_url, alt: "Enhanced output", className: "w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex min-h-[280px] items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-hourglass-half text-4xl" })))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Metadata"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("pre", { className: "overflow-x-auto whitespace-pre-wrap break-words" }, JSON.stringify(job.metadata || {}, null, 2))))), /* @__PURE__ */ React.createElement("aside", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Job details"), /* @__PURE__ */ React.createElement("dl", { className: "mt-4" }, /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Created", value: formatDate$g(job.created_at) }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Queued", value: formatDate$g(job.queued_at) }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Started", value: formatDate$g(job.started_at) }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Finished", value: formatDate$g(job.finished_at) }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Expires", value: formatDate$g(job.expires_at) }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Input size", value: job.input_filesize ? `${(job.input_filesize / 1024 / 1024).toFixed(2)} MB` : "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Input mime", value: job.input_mime || "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Input dimensions", value: job.input_width && job.input_height ? `${job.input_width} × ${job.input_height}` : "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Output size", value: job.output_filesize ? `${(job.output_filesize / 1024 / 1024).toFixed(2)} MB` : "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Output mime", value: job.output_mime || "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Output dimensions", value: job.output_width && job.output_height ? `${job.output_width} × ${job.output_height}` : "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Processing seconds", value: job.processing_seconds ?? "—" }), /* @__PURE__ */ React.createElement(DetailRow$1, { label: "Artwork", value: job.artwork?.title ? /* @__PURE__ */ React.createElement("a", { href: job.artwork.url, className: "text-sky-300 hover:text-sky-200" }, job.artwork.title) : "Standalone upload" }))))); } -const __vite_glob_0_61 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_63 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: EnhanceShow }, Symbol.toStringTag, { value: "Module" })); @@ -83551,7 +83676,7 @@ function FollowingFeed() { loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-spinner fa-spin mr-2" }), "Loading…") : "Load more" ))))); } -const __vite_glob_0_62 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_64 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: FollowingFeed }, Symbol.toStringTag, { value: "Module" })); @@ -83611,7 +83736,7 @@ function HashtagFeed() { loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-spinner fa-spin mr-2" }), "Loading…") : "Load more" )))))); } -const __vite_glob_0_63 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_65 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: HashtagFeed }, Symbol.toStringTag, { value: "Module" })); @@ -83669,7 +83794,7 @@ function SavedFeed() { loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-spinner fa-spin mr-2" }), "Loading…") : "Load more" )))))); } -const __vite_glob_0_64 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_66 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SavedFeed }, Symbol.toStringTag, { value: "Module" })); @@ -83794,7 +83919,7 @@ function SearchFeed() { "Load more" ))), /* @__PURE__ */ React.createElement("aside", { className: "hidden lg:block w-64 shrink-0 space-y-4 pt-14" }, /* @__PURE__ */ React.createElement(TrendingHashtagsSidebar$1, { hashtags: trendingHashtags }), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.07] bg-white/[0.03] px-4 py-4 text-center" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500 leading-relaxed" }, "Tip: search ", /* @__PURE__ */ React.createElement("span", { className: "text-sky-400/80" }, "#hashtag"), " to find posts by topic."))))))); } -const __vite_glob_0_65 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_67 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SearchFeed }, Symbol.toStringTag, { value: "Module" })); @@ -83856,7 +83981,7 @@ function TrendingFeed() { loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-spinner fa-spin mr-2" }), "Loading…") : "Load more" ))), /* @__PURE__ */ React.createElement("aside", { className: "hidden lg:block w-64 shrink-0 space-y-4 pt-14" }, /* @__PURE__ */ React.createElement(TrendingHashtagsSidebar, { hashtags: trendingHashtags }), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/[0.07] bg-white/[0.03] px-4 py-4 text-center" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500 leading-relaxed" }, "Posts are ranked by likes, comments & engagement over the last 7 days."))))))); } -const __vite_glob_0_66 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_68 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: TrendingFeed }, Symbol.toStringTag, { value: "Module" })); @@ -83941,7 +84066,7 @@ function ForumCategory({ category, parentCategory = null, threads = [], paginati "New topic" ))), /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-2xl border border-white/[0.06] bg-nova-800/50 backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-4 border-b border-white/[0.06] px-5 py-3" }, /* @__PURE__ */ React.createElement("span", { className: "flex-1 text-xs font-semibold uppercase tracking-widest text-white/30" }, "Topics"), /* @__PURE__ */ React.createElement("span", { className: "w-16 text-center text-xs font-semibold uppercase tracking-widest text-white/30" }, "Replies")), threads.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "px-5 py-12 text-center" }, /* @__PURE__ */ React.createElement("svg", { className: "mx-auto mb-4 text-zinc-600", width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" })), /* @__PURE__ */ React.createElement("p", { className: "text-sm text-zinc-500" }, "No topics in this board yet."), isAuthenticated && slug && /* @__PURE__ */ React.createElement("a", { href: `/forum/${slug}/new`, className: "mt-3 inline-block text-sm text-sky-300 hover:text-sky-200" }, "Be the first to start a discussion →")) : /* @__PURE__ */ React.createElement("div", null, threads.map((thread, i) => /* @__PURE__ */ React.createElement(ThreadRow, { key: thread.topic_id ?? thread.id ?? i, thread, isFirst: i === 0 })))), pagination?.last_page > 1 && /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(Pagination$1, { meta: pagination })))); } -const __vite_glob_0_67 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_69 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumCategory }, Symbol.toStringTag, { value: "Module" })); @@ -84205,7 +84330,7 @@ function ForumEditPost({ post: post2, thread, csrfToken: csrfToken2, errors = {} ), /* @__PURE__ */ React.createElement(Button$1, { type: "submit", variant: "primary", size: "md", loading: submitting }, "Save changes")) ))); } -const __vite_glob_0_68 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_70 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumEditPost }, Symbol.toStringTag, { value: "Module" })); @@ -84296,7 +84421,7 @@ function formatLastActivity(value) { } return `Updated ${date.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}`; } -const __vite_glob_0_69 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_71 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumIndex }, Symbol.toStringTag, { value: "Module" })); @@ -84417,7 +84542,7 @@ function ForumNewThread({ category, csrfToken: csrfToken2, errors = {}, oldValue /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between pt-2" }, /* @__PURE__ */ React.createElement("a", { href: `/forum/${slug}`, className: "text-sm text-zinc-500 hover:text-zinc-300 transition-colors" }, "← Cancel"), /* @__PURE__ */ React.createElement(Button$1, { type: "submit", variant: "primary", size: "md", loading: submitting }, "Publish topic")) ))); } -const __vite_glob_0_70 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_72 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumNewThread }, Symbol.toStringTag, { value: "Module" })); @@ -84432,7 +84557,7 @@ function ForumSection({ category, boards = [], seo = {} }) { ]; return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-6xl px-4 pb-20 pt-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(Breadcrumbs, { items: breadcrumbs }), /* @__PURE__ */ React.createElement("section", { className: "mt-5 overflow-hidden rounded-3xl border border-white/10 bg-nova-800/55 shadow-xl backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "relative h-56 overflow-hidden sm:h-64" }, /* @__PURE__ */ React.createElement("img", { src: preview, alt: `${name2} preview`, className: "h-full w-full object-cover object-center" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 bottom-0 p-6 sm:p-8" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-cyan-200/85" }, "Forum Section"), /* @__PURE__ */ React.createElement("h1", { className: "mt-2 text-3xl font-black text-white sm:text-4xl" }, name2), description && /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm text-white/70 sm:text-base" }, description)))), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-2xl border border-white/8 bg-nova-800/45 p-5 backdrop-blur sm:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.16em] text-white/40" }, "Subcategories"), /* @__PURE__ */ React.createElement("h2", { className: "mt-1 text-2xl font-bold text-white" }, "Browse boards")), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-white/45 sm:text-sm" }, "Select a board to open its thread list.")), boards.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "py-12 text-center text-sm text-white/45" }, "No boards are available in this section yet.") : /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 md:grid-cols-2" }, boards.map((board) => /* @__PURE__ */ React.createElement("a", { key: board.id ?? board.slug, href: `/forum/${board.slug}`, className: "rounded-2xl border border-white/8 bg-white/[0.02] p-5 transition hover:border-cyan-400/25 hover:bg-white/[0.04] block" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, board.title), board.description && /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white/55" }, board.description)), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-cyan-300/20 bg-cyan-300/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-cyan-200" }, "Open")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-4 text-xs text-white/50" }, /* @__PURE__ */ React.createElement("span", null, board.topics_count ?? 0, " topics"), /* @__PURE__ */ React.createElement("span", null, board.posts_count ?? 0, " posts"), board.latest_topic?.title && /* @__PURE__ */ React.createElement("span", null, "Latest: ", board.latest_topic.title)))))))); } -const __vite_glob_0_71 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_73 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumSection }, Symbol.toStringTag, { value: "Module" })); @@ -84770,7 +84895,7 @@ function formatDate$d(dateStr) { return ""; } } -const __vite_glob_0_72 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_74 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumThread }, Symbol.toStringTag, { value: "Module" })); @@ -85014,7 +85139,7 @@ function GroupChallengeShow() { const outcomeSections = challenge.outcome_sections || {}; return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(234,179,8,0.15),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: props.seo || {}, title: `${challenge.title || group.name} - Skinbase`, description: challenge.summary || challenge.description || "Group challenge" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-6xl space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]" }, challenge.cover_url ? /* @__PURE__ */ React.createElement("img", { src: challenge.cover_url, alt: challenge.title, className: "h-56 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "h-40 bg-white/[0.03]" }), /* @__PURE__ */ React.createElement("div", { className: "p-6" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public, className: "text-sm font-semibold text-amber-200" }, group.name), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold text-white" }, challenge.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-7 text-slate-300" }, challenge.summary || challenge.description || "Group challenge"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3 text-xs uppercase tracking-[0.16em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, challenge.status), /* @__PURE__ */ React.createElement("span", null, challenge.visibility), /* @__PURE__ */ React.createElement("span", null, String(challenge.participation_scope || "").replace("_", " ")), challenge.start_at ? /* @__PURE__ */ React.createElement("span", null, "Starts ", new Date(challenge.start_at).toLocaleDateString()) : null, challenge.end_at ? /* @__PURE__ */ React.createElement("span", null, "Ends ", new Date(challenge.end_at).toLocaleDateString()) : null), /* @__PURE__ */ React.createElement(ChallengeWorldLinkBadge, { world: linkedWorld, className: "mt-5" }))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Challenge brief"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-300" }, challenge.description || "No extended challenge brief yet."), challenge.rules_text ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Rules"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, challenge.rules_text)) : null, challenge.submission_instructions ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Submission instructions"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, challenge.submission_instructions)) : null), /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement(OutcomeSection, { section: outcomeSections.winner }), /* @__PURE__ */ React.createElement(OutcomeSection, { section: outcomeSections.finalist }), /* @__PURE__ */ React.createElement(OutcomeSection, { section: outcomeSections.runner_up }), /* @__PURE__ */ React.createElement(OutcomeSection, { section: outcomeSections.honorable_mention }), /* @__PURE__ */ React.createElement(OutcomeSection, { section: outcomeSections.featured }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Entries"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-1" }, Array.isArray(challenge.artworks) && challenge.artworks.length > 0 ? challenge.artworks.map((artwork) => /* @__PURE__ */ React.createElement("a", { key: artwork.id, href: artwork.url, className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20" }, artwork.thumb ? /* @__PURE__ */ React.createElement("img", { src: artwork.thumb, alt: artwork.title, className: "aspect-[4/3] w-full object-cover" }) : null, /* @__PURE__ */ React.createElement("div", { className: "p-4 text-white" }, artwork.title))) : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "No entries linked yet."))))))); } -const __vite_glob_0_73 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_75 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupChallengeShow }, Symbol.toStringTag, { value: "Module" })); @@ -85024,7 +85149,7 @@ function GroupEventShow() { const event = props.event || {}; return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.15),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: props.seo || {}, title: `${event.title || group.name} - Skinbase`, description: event.summary || event.description || "Group event" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-5xl space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]" }, event.cover_url ? /* @__PURE__ */ React.createElement("img", { src: event.cover_url, alt: event.title, className: "h-56 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "h-40 bg-white/[0.03]" }), /* @__PURE__ */ React.createElement("div", { className: "p-6" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public, className: "text-sm font-semibold text-emerald-200" }, group.name), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold text-white" }, event.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-7 text-slate-300" }, event.summary || event.description || "Group event"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Starts"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-white" }, event.start_at ? new Date(event.start_at).toLocaleString() : "Not scheduled")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Details"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-white" }, event.event_type, " • ", event.visibility), event.location ? /* @__PURE__ */ React.createElement("div", { className: "mt-2" }, event.location) : null)), event.external_url ? /* @__PURE__ */ React.createElement("a", { href: event.external_url, className: "mt-5 inline-flex rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Open external link") : null)), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "About this event"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-300" }, event.description || "No extended event details yet.")))); } -const __vite_glob_0_74 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_76 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupEventShow }, Symbol.toStringTag, { value: "Module" })); @@ -85714,7 +85839,7 @@ function GroupFaqPage() { /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("a", { href: links.contact_support, className: "rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Contact"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Contact support"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Use this if your question is not answered here or if you need help with an account or workflow issue.")), /* @__PURE__ */ React.createElement("a", { href: links.report_issue, className: "rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Report"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Report a problem"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Use this if a route, role, contributor record, or Group workflow appears broken rather than just unclear."))) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Support flow"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.quickstart, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Quickstart"), /* @__PURE__ */ React.createElement("a", { href: links.full_documentation, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read full documentation"), /* @__PURE__ */ React.createElement("a", { href: links.group_studio, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Group Studio"), /* @__PURE__ */ React.createElement("a", { href: links.create_group, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Create a Group"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Quick troubleshooting rule"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "If something feels wrong, check three things first: are you in the right Group context, do you have the right role, and is the content public or internal?"))))))); } -const __vite_glob_0_75 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_77 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupFaqPage }, Symbol.toStringTag, { value: "Module" })); @@ -86262,7 +86387,7 @@ function GroupHelpPage() { /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("a", { href: links.create_group, className: "rounded-[28px] border border-sky-300/20 bg-sky-300/10 p-5 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100/80" }, "Create"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Create your first Group"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-sky-50/80" }, "Start with branding, visibility, and your first member invites.")), /* @__PURE__ */ React.createElement("a", { href: links.group_studio, className: "rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Manage"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Open Group Studio"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Check members, workflow, releases, recruitment, and review status.")), /* @__PURE__ */ React.createElement("a", { href: links.contact_support, className: "rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Contact"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Contact support"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Use the general support flow if you need help untangling an account or workflow issue.")), /* @__PURE__ */ React.createElement("a", { href: links.report_issue, className: "rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Report"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, "Report a problem"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, "Use this if a route, permission, credit record, or workflow appears broken."))) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick actions"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.groups_directory, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Browse public Groups"), /* @__PURE__ */ React.createElement("a", { href: links.group_studio, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Group Studio"), links.faq ? /* @__PURE__ */ React.createElement("a", { href: links.faq, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Groups FAQ") : null, /* @__PURE__ */ React.createElement("a", { href: "#publishing-as-a-group", className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Review publishing guidance"), /* @__PURE__ */ React.createElement("a", { href: "#contributor-credit", className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Check contributor credit rules"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Read this before launch day"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "Before the first public release or artwork, confirm the Group context, contributor credit, and review expectations. Those three checks prevent most avoidable confusion."))))))); } -const __vite_glob_0_76 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_78 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -86337,7 +86462,7 @@ function GroupIndex() { } )), leaderboardItems.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", { className: "mb-5 flex items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold tracking-[-0.02em] text-white" }, "Monthly group leaderboard"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "A fast view of the collaborative teams moving the most attention and publishing energy right now.")), /* @__PURE__ */ React.createElement("a", { href: "/leaderboard?type=groups&period=monthly", className: "text-sm font-semibold text-sky-200 transition hover:text-white" }, "View leaderboard")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 xl:grid-cols-3" }, leaderboardItems.slice(0, 3).map((item) => /* @__PURE__ */ React.createElement(GroupLeaderboardCard, { key: item.entity?.id || item.rank, item })))) : null, /* @__PURE__ */ React.createElement("section", { className: "mt-10" }, /* @__PURE__ */ React.createElement("div", { className: "mb-5 flex items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold tracking-[-0.02em] text-white" }, "Browse groups"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "Filter the directory by discovery surface, then jump into each group’s public page for artworks, releases, projects, events, and activity.")), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-500" }, Number(props.groups?.meta?.total || 0).toLocaleString(), " public groups")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-3" }, groups.map((group) => /* @__PURE__ */ React.createElement(GroupDiscoveryCard, { key: group.slug || group.id, group })))))); } -const __vite_glob_0_77 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_79 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupIndex }, Symbol.toStringTag, { value: "Module" })); @@ -86367,7 +86492,7 @@ function GroupPostShow() { }; return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: props.seo || {}, title: `${post2.title || group.name} - Skinbase`, description: post2.excerpt || group.headline || group.bio || "Group post" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-5xl" }, /* @__PURE__ */ React.createElement("article", { className: "rounded-[32px] border border-white/10 bg-white/[0.03] p-6 sm:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public, className: "text-sm font-semibold text-sky-200" }, "← Back to ", group.name), props.reportEndpoint ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: submitReport, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-semibold text-white" }, "Report") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-[0.16em] text-slate-400" }, post2.type ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1" }, post2.type) : null, post2.is_pinned ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-amber-100" }, "Pinned") : null), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 text-4xl font-semibold text-white" }, post2.title), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-sm text-slate-400" }, post2.author?.name || post2.author?.username || group.name, " • ", post2.published_at ? new Date(post2.published_at).toLocaleString() : "Recently"), post2.excerpt ? /* @__PURE__ */ React.createElement("p", { className: "mt-6 text-lg leading-8 text-slate-200" }, post2.excerpt) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8 whitespace-pre-wrap text-sm leading-7 text-slate-300" }, post2.content || "")), recentPosts.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "More from ", group.name), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, recentPosts.filter((item) => item.id !== post2.id).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.url, className: "rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, item.type), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, item.excerpt || "Read the full post."))))) : null)); } -const __vite_glob_0_78 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_80 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupPostShow }, Symbol.toStringTag, { value: "Module" })); @@ -86383,7 +86508,7 @@ function GroupProjectShow() { const project = props.project || {}; return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: props.seo || {}, title: `${project.title || group.name} - Skinbase`, description: project.summary || project.description || group.headline || "Group project" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-6xl space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]" }, project.cover_url ? /* @__PURE__ */ React.createElement("img", { src: project.cover_url, alt: project.title, className: "h-56 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "h-40 bg-white/[0.03]" }), /* @__PURE__ */ React.createElement("div", { className: "p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public, className: "text-sm font-semibold text-sky-200" }, group.name), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, project.status), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, project.visibility)), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold text-white" }, project.title), project.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-7 text-slate-300" }, project.summary) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-4 text-xs text-slate-400" }, project.start_date ? /* @__PURE__ */ React.createElement("span", null, "Started ", new Date(project.start_date).toLocaleDateString()) : null, project.target_date ? /* @__PURE__ */ React.createElement("span", null, "Target ", new Date(project.target_date).toLocaleDateString()) : null, project.released_at ? /* @__PURE__ */ React.createElement("span", null, "Released ", new Date(project.released_at).toLocaleDateString()) : null, project.lead?.name || project.lead?.username ? /* @__PURE__ */ React.createElement("span", null, "Lead: ", project.lead?.name || project.lead?.username) : null))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Overview"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-300" }, project.description || "No long-form description yet."), Array.isArray(project.milestones) && project.milestones.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-3" }, project.milestones.map((milestone) => /* @__PURE__ */ React.createElement("div", { key: milestone.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, milestone.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, milestone.status)), milestone.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, milestone.summary) : null, milestone.owner?.name || milestone.owner?.username ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs text-slate-500" }, "Owner: ", milestone.owner?.name || milestone.owner?.username) : null))) : null, /* @__PURE__ */ React.createElement(ArtworkGrid$2, { artworks: project.artworks })), /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Pipeline"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-sm leading-7 text-slate-300" }, "This project currently has ", project.counts?.milestones || 0, " milestones and is linked to ", project.release_count || project.counts?.releases || 0, " releases.")), Array.isArray(project.assets) && project.assets.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Assets"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, project.assets.map((asset) => /* @__PURE__ */ React.createElement("a", { key: asset.id, href: asset.download_url, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, asset.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, asset.category, " • ", asset.visibility))))) : null, Array.isArray(project.team) && project.team.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Team"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, project.team.map((member) => /* @__PURE__ */ React.createElement("div", { key: member.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, member.name || member.username), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, member.role_label || (member.is_lead ? "Lead" : "Contributor")))))) : null, project.pinned_post ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Pinned update"), /* @__PURE__ */ React.createElement("a", { href: project.pinned_post.url, className: "mt-4 inline-block text-sm font-semibold text-sky-200" }, project.pinned_post.title)) : null)))); } -const __vite_glob_0_79 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_81 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupProjectShow }, Symbol.toStringTag, { value: "Module" })); @@ -86682,7 +86807,7 @@ function GroupQuickstartPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: nextSteps }) ))))); } -const __vite_glob_0_80 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_82 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupQuickstartPage }, Symbol.toStringTag, { value: "Module" })); @@ -86700,7 +86825,7 @@ function GroupReleaseShow() { const milestones = Array.isArray(release.milestones) ? release.milestones : []; return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: props.seo || {}, title: `${release.title || group.name} - Skinbase`, description: release.summary || release.description || group.headline || "Group release" }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-6xl space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[32px] border border-white/10 bg-white/[0.03]" }, release.cover_url ? /* @__PURE__ */ React.createElement("img", { src: release.cover_url, alt: release.title, className: "h-64 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "h-44 bg-white/[0.03]" }), /* @__PURE__ */ React.createElement("div", { className: "p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public, className: "text-sm font-semibold text-sky-200" }, group.name), release.status ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, release.status) : null, release.current_stage ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, release.current_stage) : null), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold text-white" }, release.title), release.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-sm leading-7 text-slate-300" }, release.summary) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-4 text-xs text-slate-400" }, release.released_at ? /* @__PURE__ */ React.createElement("span", null, "Released ", new Date(release.released_at).toLocaleDateString()) : null, release.planned_release_at ? /* @__PURE__ */ React.createElement("span", null, "Planned ", new Date(release.planned_release_at).toLocaleDateString()) : null, release.lead?.name || release.lead?.username ? /* @__PURE__ */ React.createElement("span", null, "Lead: ", release.lead?.name || release.lead?.username) : null, /* @__PURE__ */ React.createElement("span", null, release.counts?.artworks || 0, " artworks"), /* @__PURE__ */ React.createElement("span", null, release.counts?.contributors || 0, " contributors"), /* @__PURE__ */ React.createElement("span", null, release.counts?.milestones || 0, " milestones")))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Overview"), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-300" }, release.description || "No long-form release description yet."), release.release_notes ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Release notes"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-300" }, release.release_notes)) : null, /* @__PURE__ */ React.createElement(ArtworkGrid$1, { artworks: release.artworks })), /* @__PURE__ */ React.createElement("div", { className: "space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Links"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, release.linked_project?.url ? /* @__PURE__ */ React.createElement("a", { href: release.linked_project.url, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, release.linked_project.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, "Linked project")) : null, release.linked_collection?.url ? /* @__PURE__ */ React.createElement("a", { href: release.linked_collection.url, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, release.linked_collection.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, "Linked collection")) : null, release.featured_artwork ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, release.featured_artwork.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, "Featured artwork")) : null)), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Contributors"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, contributors.length > 0 ? contributors.map((contributor) => /* @__PURE__ */ React.createElement("div", { key: contributor.id, className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, contributor.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: contributor.avatar_url, alt: contributor.name || contributor.username, className: "h-11 w-11 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "truncate font-semibold text-white" }, contributor.name || contributor.username), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, contributor.role_label || "Contributor")))) : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "No contributor credits yet."))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "Milestones"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, milestones.length > 0 ? milestones.map((milestone) => /* @__PURE__ */ React.createElement("div", { key: milestone.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, milestone.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, milestone.status)), milestone.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, milestone.summary) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs text-slate-500" }, milestone.owner?.name || milestone.owner?.username || "No owner", milestone.due_date ? ` • due ${milestone.due_date}` : ""))) : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "No milestones defined yet."))))))); } -const __vite_glob_0_81 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_83 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupReleaseShow }, Symbol.toStringTag, { value: "Module" })); @@ -87128,7 +87253,7 @@ function GroupShow() { return /* @__PURE__ */ React.createElement("section", { key: label, className: "relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: `absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r ${roleKey === "owner" ? "from-amber-400/70 to-transparent" : roleKey === "admin" ? "from-sky-400/70 to-transparent" : roleKey === "editor" ? "from-violet-400/70 to-transparent" : "from-emerald-400/70 to-transparent"}` }), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: `inline-flex h-8 w-8 items-center justify-center rounded-xl border ${roleStyle.badge}` }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${roleStyle.icon} fa-fw text-sm ${roleStyle.iconColor}` })), /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, label), /* @__PURE__ */ React.createElement("span", { className: "ml-auto rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300" }, bucket.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, bucket.map((member) => /* @__PURE__ */ React.createElement("a", { key: member.id, href: member.user?.profile_url || "#", className: "group flex items-center gap-3 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 transition hover:border-white/20 hover:bg-white/[0.04]" }, member.user?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: member.user.avatar_url, alt: member.user.name || member.user.username, className: "h-12 w-12 shrink-0 rounded-2xl object-cover ring-1 ring-white/10 transition group-hover:ring-white/20" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "truncate font-semibold text-white" }, member.user?.name || member.user?.username), /* @__PURE__ */ React.createElement("div", { className: `mt-1 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] ${roleStyle.badge}` }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${roleStyle.icon} fa-fw text-[9px]` }), member.role_label || member.role)))))); }))) : null, section === "about" ? /* @__PURE__ */ React.createElement("section", { className: "mt-8 relative overflow-hidden rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 top-0 h-[3px] rounded-t-[30px] bg-gradient-to-r from-slate-400/50 to-transparent" }), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "inline-flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/10 bg-white/[0.05] text-slate-300" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-id-card fa-fw" })), /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold text-white" }, "About")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-4 text-sm leading-7 text-slate-300" }, /* @__PURE__ */ React.createElement("p", null, group.bio || "No long-form description yet."), group.website_url ? /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("a", { href: group.website_url, className: "inline-flex items-center gap-1.5 text-sky-200 underline underline-offset-4 transition hover:text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-link" }), group.website_url)) : null, Array.isArray(group.links) && group.links.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, group.links.map((link2) => /* @__PURE__ */ React.createElement("a", { key: `${link2.label}-${link2.url}`, href: link2.url, className: "inline-flex items-center gap-2 rounded-[14px] border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-up-right-from-square text-slate-400" }), link2.label))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-4 border-t border-white/8 pt-4 text-xs text-slate-400" }, group.founded_at ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-2" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-calendar-days text-slate-500" }), "Founded ", new Date(group.founded_at).toLocaleDateString()) : null, group.type ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-2" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-tag text-slate-500" }), group.type) : null))) : null)); } -const __vite_glob_0_82 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_84 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupShow }, Symbol.toStringTag, { value: "Module" })); @@ -87386,7 +87511,7 @@ function AccountHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: signedIn ? links.profile_settings : links.login, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, signedIn ? "Open account settings" : "Open login"), /* @__PURE__ */ React.createElement("a", { href: links.help_auth, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read auth help"), /* @__PURE__ */ React.createElement("a", { href: links.help_profile, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Profile help"), /* @__PURE__ */ React.createElement("a", { href: links.help_troubleshooting, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open troubleshooting"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-emerald-300/20 bg-emerald-400/10 p-4 text-emerald-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-emerald-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-emerald-50/85" }, "The healthiest account is the one with a current email, a manageable password, and settings reviewed before they become emergency work."))))))); } -const __vite_glob_0_83 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_85 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AccountHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -87742,7 +87867,7 @@ function AuthHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.login, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open login"), /* @__PURE__ */ React.createElement("a", { href: links.register, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Create account"), /* @__PURE__ */ React.createElement("a", { href: links.password_request, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Reset password"), /* @__PURE__ */ React.createElement("a", { href: links.help_account, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read account settings help"), /* @__PURE__ */ React.createElement("a", { href: links.help_troubleshooting, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open troubleshooting hub"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "If access breaks, check four things first: the email, the password, the inbox, and whether the problem is really permissions rather than login."))))))); } -const __vite_glob_0_84 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_86 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AuthHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -88168,7 +88293,7 @@ function CardsHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.create_card, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Create a Card"), /* @__PURE__ */ React.createElement("a", { href: links.studio_cards, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Cards workspace"), /* @__PURE__ */ React.createElement("a", { href: links.cards_index, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Browse public Cards"), /* @__PURE__ */ React.createElement("a", { href: links.studio_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Studio help"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "If the content feels unclear, ask one question first: is this a Card, an artwork, a post, or a collection? The answer usually fixes the workflow too."))))))); } -const __vite_glob_0_85 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_87 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CardsHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -89077,7 +89202,7 @@ function HelpCenterPage() { /* @__PURE__ */ React.createElement(HelpSupportCta, { items: supportItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Help architecture"), /* @__PURE__ */ React.createElement("ul", { className: "mt-4 space-y-3 text-sm leading-6 text-slate-300" }, /* @__PURE__ */ React.createElement("li", null, "Use ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "/help"), " as the main hub."), /* @__PURE__ */ React.createElement("li", null, "Use ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "/help/topic"), " for overview pages."), /* @__PURE__ */ React.createElement("li", null, "Use ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "/help/topic/subpage"), " for quickstarts, FAQs, and troubleshooting."))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Current coverage"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, "Groups is the first complete multi-page topic family, and Studio, Upload, Cards, Profile, Signup / Login, Account Settings, and Troubleshooting are now live topic guides. The rest of the Help Center still follows the same predictable expansion path."))))))); } -const __vite_glob_0_86 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_88 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: HelpCenterPage }, Symbol.toStringTag, { value: "Module" })); @@ -89466,7 +89591,7 @@ function ProfileHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.profile_settings, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open profile settings"), /* @__PURE__ */ React.createElement("a", { href: links.groups_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Groups help"), /* @__PURE__ */ React.createElement("a", { href: links.studio_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Studio help"), /* @__PURE__ */ React.createElement("a", { href: links.upload_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Upload help"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "A better profile usually starts with three things: a recognizable avatar, a clearer bio, and a stronger sense of what you want people to remember about you."))))))); } -const __vite_glob_0_87 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_89 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -89921,7 +90046,7 @@ function StudioHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.open_studio, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Studio"), /* @__PURE__ */ React.createElement("a", { href: links.studio_drafts, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open drafts"), /* @__PURE__ */ React.createElement("a", { href: links.group_studio, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Group Studio"), /* @__PURE__ */ React.createElement("a", { href: links.groups_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Groups help"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "If something feels missing, check context first. Personal Studio and Group Studio are connected, but they are not identical workspaces."))))))); } -const __vite_glob_0_88 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_90 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -90163,7 +90288,7 @@ function TroubleshootingHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-rose-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.help_auth, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read auth help"), /* @__PURE__ */ React.createElement("a", { href: links.help_account, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read account settings help"), /* @__PURE__ */ React.createElement("a", { href: links.upload_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Upload help"), /* @__PURE__ */ React.createElement("a", { href: links.groups_faq, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Groups FAQ"), /* @__PURE__ */ React.createElement("a", { href: links.report_issue, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Report a problem"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-rose-300/20 bg-rose-400/10 p-4 text-rose-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-rose-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-rose-50/85" }, "A clear problem statement beats frantic guessing. Name the route, the context, and what changed before you decide the product is broken."))))))); } -const __vite_glob_0_89 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_91 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: TroubleshootingHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -90581,7 +90706,7 @@ function UploadHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.upload, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Start upload"), /* @__PURE__ */ React.createElement("a", { href: links.studio_drafts, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open drafts"), /* @__PURE__ */ React.createElement("a", { href: links.studio_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Studio help"), /* @__PURE__ */ React.createElement("a", { href: links.groups_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Groups help"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-amber-300/20 bg-amber-400/10 p-4 text-amber-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-amber-50/85" }, "If an upload feels wrong, check three things first: context, draft state, and contributor credit."))))))); } -const __vite_glob_0_90 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_92 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UploadHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -91044,7 +91169,7 @@ function WorldsHelpPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: relatedHelpItems }) )), /* @__PURE__ */ React.createElement("aside", { className: "hidden xl:block xl:sticky xl:top-24 xl:self-start" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_18px_50px_rgba(2,6,23,0.22)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Quick route map"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, /* @__PURE__ */ React.createElement("a", { href: links.create_world, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Create a World"), /* @__PURE__ */ React.createElement("a", { href: links.studio_worlds, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Open Worlds workspace"), /* @__PURE__ */ React.createElement("a", { href: links.worlds_index, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Browse public Worlds"), /* @__PURE__ */ React.createElement("a", { href: links.studio_help, className: "block rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.05]" }, "Read Studio help"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-sky-300/20 bg-sky-400/10 p-4 text-sky-50" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-100/80" }, "Fast reminder"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-sky-50/85" }, "A World should feel like an editorial decision, not a container. If the page feels cluttered, the usual fix is stronger curation, fewer modules, and clearer promotion intent."))))))); } -const __vite_glob_0_91 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_93 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldsHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -91155,7 +91280,7 @@ function LeaderboardPage() { const items = Array.isArray(data?.items) ? data.items : []; return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo, title: seo?.title || "Leaderboard — Skinbase", description: seo?.description || "Top creators, groups, artworks, stories, and Worlds on Skinbase." }), /* @__PURE__ */ React.createElement("div", { className: "leaderboard-page min-h-screen bg-[radial-gradient(circle_at_top,rgba(14,165,233,0.14),transparent_34%),linear-gradient(180deg,#020617_0%,#0f172a_48%,#020617_100%)] pb-16 text-slate-100" }, /* @__PURE__ */ React.createElement("div", { className: "mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement("header", { className: "leaderboard-page__hero rounded-[2rem] border border-white/10 bg-slate-950/70 px-6 py-8 shadow-[0_35px_120px_rgba(2,6,23,0.75)] backdrop-blur" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.28em] text-sky-300" }, "Skinbase Competition Board"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 max-w-3xl text-4xl font-black tracking-tight text-white sm:text-5xl" }, "Top creators, groups, standout artworks, stories, and Worlds with momentum."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-sm leading-6 text-slate-300 sm:text-base" }, "Switch between creators, groups, artworks, stories, and Worlds, then filter by daily, weekly, monthly, or all-time performance.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-4" }, /* @__PURE__ */ React.createElement(LeaderboardTabs, { items: TYPE_TABS, active: type2, onChange: setType, sticky: true, label: "Leaderboard type" }), /* @__PURE__ */ React.createElement(LeaderboardTabs, { items: PERIOD_TABS, active: period, onChange: setPeriod, label: "Leaderboard period" })), loading ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-3xl border border-white/10 bg-white/[0.03] px-6 py-5 text-sm text-slate-400" }, "Refreshing leaderboard...") : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8" }, /* @__PURE__ */ React.createElement(LeaderboardList, { items, type: type2 }))))); } -const __vite_glob_0_92 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_94 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: LeaderboardPage }, Symbol.toStringTag, { value: "Module" })); @@ -93093,7 +93218,7 @@ if (typeof document !== "undefined") { ); } } -const __vite_glob_0_93 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_95 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: MessagesPage }, Symbol.toStringTag, { value: "Module" })); @@ -93260,7 +93385,7 @@ function ArtworkMaturityQueue() { } )); } -const __vite_glob_0_95 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_97 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ArtworkMaturityQueue }, Symbol.toStringTag, { value: "Module" })); @@ -93281,7 +93406,7 @@ function ModerationEnhanceIndex() { } return /* @__PURE__ */ React.createElement("div", { className: "w-full px-6 pb-16 pt-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: "Enhance Jobs" }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_36%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80" }, "Moderation surface"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Enhance Jobs"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, "Review queued, processing, failed, and completed image upscale jobs without changing original artwork assets.")), /* @__PURE__ */ React.createElement("form", { onSubmit: applyFilters, className: "mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-7" }, /* @__PURE__ */ React.createElement("select", { value: filters.status || "all", onChange: (event) => setFilters((current) => ({ ...current, status: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }, (props.options?.statuses || []).map((option) => /* @__PURE__ */ React.createElement("option", { key: option, value: option }, option === "all" ? "All statuses" : option))), /* @__PURE__ */ React.createElement("select", { value: filters.engine || "all", onChange: (event) => setFilters((current) => ({ ...current, engine: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }, (props.options?.engines || []).map((option) => /* @__PURE__ */ React.createElement("option", { key: option, value: option }, option === "all" ? "All engines" : option))), /* @__PURE__ */ React.createElement("select", { value: filters.mode || "all", onChange: (event) => setFilters((current) => ({ ...current, mode: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }, (props.options?.modes || []).map((option) => /* @__PURE__ */ React.createElement("option", { key: option, value: option }, option === "all" ? "All modes" : option))), /* @__PURE__ */ React.createElement("select", { value: filters.scale || "all", onChange: (event) => setFilters((current) => ({ ...current, scale: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }, (props.options?.scales || []).map((option) => /* @__PURE__ */ React.createElement("option", { key: String(option), value: option }, option === "all" ? "All scales" : `${option}x`))), /* @__PURE__ */ React.createElement("input", { value: filters.user || "", onChange: (event) => setFilters((current) => ({ ...current, user: event.target.value })), placeholder: "User name or username", className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { type: "date", value: filters.date_from || "", onChange: (event) => setFilters((current) => ({ ...current, date_from: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { type: "date", value: filters.date_to || "", onChange: (event) => setFilters((current) => ({ ...current, date_to: event.target.value })), className: "rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-3 text-sm text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.1] xl:col-span-7" }, "Apply filters"))), /* @__PURE__ */ React.createElement(EnhanceStubWarning, { config: props.enhanceConfig, moderation: true, className: "mt-6" }), flash.success ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8 overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-wider text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Preview"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Job"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "User"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Artwork"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Status"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Mode"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Scale"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Dimensions"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5" }, "Created"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-3.5 text-right" }, "Actions"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/[0.04]" }, jobs.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 10, className: "px-5 py-12 text-center text-slate-400" }, "No enhance jobs match the current filters.")) : null, jobs.map((job) => /* @__PURE__ */ React.createElement("tr", { key: job.id, className: "transition hover:bg-white/[0.025]" }, /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "h-16 w-16 overflow-hidden rounded-xl border border-white/10 bg-black/20" }, job.preview_url || job.source_url ? /* @__PURE__ */ React.createElement("img", { src: job.preview_url || job.source_url, alt: `Enhance job ${job.id}`, className: "h-full w-full object-cover" }) : null)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-white" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, "#", job.id), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, job.engine)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, job.user?.name || "—", job.user?.username ? /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-500" }, "@", job.user.username) : null), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, job.artwork?.title ? /* @__PURE__ */ React.createElement("a", { href: job.artwork.url, className: "text-sky-300 hover:text-sky-200" }, job.artwork.title) : "—"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement(EnhanceStatusBadge, { status: job.status })), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, job.mode), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, job.scale, "x"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, job.input_width, " × ", job.input_height, job.output_width && job.output_height ? /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-500" }, "→ ", job.output_width, " × ", job.output_height) : null), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-400" }, formatDate$c(job.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-right" }, /* @__PURE__ */ React.createElement(xe, { href: job.show_url, className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Open"))))))))); } -const __vite_glob_0_96 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_98 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ModerationEnhanceIndex }, Symbol.toStringTag, { value: "Module" })); @@ -93301,14 +93426,14 @@ function ModerationEnhanceShow() { At.delete(job.delete_url); }, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Delete")))), /* @__PURE__ */ React.createElement(EnhanceStubWarning, { config: props.enhanceConfig, moderation: true, className: "mt-6" }), flash.success ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, flash.success) : null, flash.error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, flash.error) : null, errors.job ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, errors.job) : null, job.source_url && job.output_url ? /* @__PURE__ */ React.createElement("div", { className: "mt-8" }, /* @__PURE__ */ React.createElement(BeforeAfterSlider, { beforeUrl: job.source_url, afterUrl: job.output_url })) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_380px]" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 lg:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Source image"), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20" }, job.source_url ? /* @__PURE__ */ React.createElement("img", { src: job.source_url, alt: "Enhance source", className: "w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex min-h-[280px] items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-4xl" })))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Output image"), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20" }, job.output_url ? /* @__PURE__ */ React.createElement("img", { src: job.output_url, alt: "Enhance output", className: "w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex min-h-[280px] items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-hourglass-half text-4xl" })))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Metadata"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("pre", { className: "overflow-x-auto whitespace-pre-wrap break-words" }, JSON.stringify(job.metadata || {}, null, 2)))), job.error_message ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-rose-300/20 bg-rose-400/10 p-6 text-sm text-rose-100 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, job.error_message) : null), /* @__PURE__ */ React.createElement("aside", { className: "rounded-[30px] border border-white/10 bg-[#08111d] p-6 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Processing details"), /* @__PURE__ */ React.createElement("dl", { className: "mt-4" }, /* @__PURE__ */ React.createElement(DetailRow, { label: "Created", value: formatDate$b(job.created_at) }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Queued", value: formatDate$b(job.queued_at) }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Started", value: formatDate$b(job.started_at) }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Finished", value: formatDate$b(job.finished_at) }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Expires", value: formatDate$b(job.expires_at) }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Input mime", value: job.input_mime || "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Input size", value: job.input_filesize ? `${(job.input_filesize / 1024 / 1024).toFixed(2)} MB` : "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Input dimensions", value: job.input_width && job.input_height ? `${job.input_width} × ${job.input_height}` : "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Output mime", value: job.output_mime || "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Output size", value: job.output_filesize ? `${(job.output_filesize / 1024 / 1024).toFixed(2)} MB` : "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Output dimensions", value: job.output_width && job.output_height ? `${job.output_width} × ${job.output_height}` : "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Processing seconds", value: job.processing_seconds ?? "—" }), /* @__PURE__ */ React.createElement(DetailRow, { label: "Artwork", value: job.artwork?.title ? /* @__PURE__ */ React.createElement("a", { href: job.artwork.url, className: "text-sky-300 hover:text-sky-200" }, job.artwork.title) : "Standalone upload" }))))); } -const __vite_glob_0_97 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_99 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ModerationEnhanceShow }, Symbol.toStringTag, { value: "Module" })); function ModerationFeaturedArtworks() { return /* @__PURE__ */ React.createElement(AdminLayout, null, /* @__PURE__ */ React.createElement(FeaturedArtworksAdmin, null)); } -const __vite_glob_0_98 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_100 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ModerationFeaturedArtworks }, Symbol.toStringTag, { value: "Module" })); @@ -93353,7 +93478,7 @@ function StaffApplicationsIndex({ title, items, stats, filters, topics, endpoint (topics || []).map((topic) => /* @__PURE__ */ React.createElement("option", { key: topic, value: topic }, String(topic).replaceAll("_", " "))) ), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.1]" }, "Apply"))), /* @__PURE__ */ React.createElement("div", { className: "mt-8 overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "min-w-full text-sm" }, /* @__PURE__ */ React.createElement("thead", { className: "bg-white/[0.03] text-left text-xs uppercase tracking-[0.18em] text-slate-400" }, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Received"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Topic"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Name"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Email"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Role"), /* @__PURE__ */ React.createElement("th", { className: "px-5 py-4 font-medium" }, "Action"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/5 text-slate-200" }, rows.length === 0 ? /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 6, className: "px-5 py-14 text-center text-slate-400" }, "No staff applications matched the current filters.")) : rows.map((item) => /* @__PURE__ */ React.createElement("tr", { key: item.id, className: "hover:bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-400" }, formatDateTime$3(item.created_at)), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-200" }, String(item.topic || "contact").replaceAll("_", " "))), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 font-medium text-white" }, item.name), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, item.email), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4 text-slate-300" }, item.role || "—"), /* @__PURE__ */ React.createElement("td", { className: "px-5 py-4" }, /* @__PURE__ */ React.createElement(xe, { href: item.show_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "View")))))))), items?.prev_page_url || items?.next_page_url ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 flex items-center justify-between gap-3 rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, "Showing page ", items?.current_page || 1, " of ", items?.last_page || 1), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, items?.prev_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(items.prev_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Previous") : null, items?.next_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(items.next_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Next") : null)) : null); } -const __vite_glob_0_99 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_101 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StaffApplicationsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -93370,7 +93495,7 @@ function StaffApplicationShow({ title, item, backUrl }) { const payload = item?.payload || {}; return /* @__PURE__ */ React.createElement(AdminLayout, { title: title || "Staff Application", subtitle: "Read the full submission in a moderation-friendly layout." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Moderation · Staff Application" }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(34,211,238,0.12),transparent_34%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80" }, "Moderation surface"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, item?.name || "Staff application"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, "Topic: ", String(item?.topic || "contact").replaceAll("_", " "), " • Received ", formatDateTime$2(item?.created_at))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: backUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-5 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Back"), item?.email ? /* @__PURE__ */ React.createElement("a", { href: `mailto:${item.email}`, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/18" }, "Email") : null))), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-6 xl:grid-cols-[1.35fr_0.85fr]" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$1, { label: "Name" }, item?.name || "—"), /* @__PURE__ */ React.createElement(Field$1, { label: "Email" }, item?.email || "—"), /* @__PURE__ */ React.createElement(Field$1, { label: "Role" }, item?.role || "—"), /* @__PURE__ */ React.createElement(Field$1, { label: "Portfolio" }, item?.portfolio ? /* @__PURE__ */ React.createElement("a", { href: item.portfolio, className: "text-sky-300 hover:text-sky-200", target: "_blank", rel: "noreferrer" }, item.portfolio) : "—")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-[#08111d] p-5 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Message"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 whitespace-pre-wrap rounded-2xl border border-white/10 bg-slate-950/60 px-4 py-4 text-sm leading-7 text-slate-100" }, item?.message || "No message included."))), /* @__PURE__ */ React.createElement("aside", { className: "space-y-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-[#08111d] p-5 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Metadata"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "Received:"), " ", formatDateTime$2(item?.created_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "IP:"), " ", item?.ip || "—"), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "User agent:"), " ", item?.user_agent || "—"), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, "ID:"), " ", item?.id || "—"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-[#08111d] p-5 shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Payload"), /* @__PURE__ */ React.createElement("pre", { className: "mt-3 max-h-[420px] overflow-auto rounded-2xl border border-white/10 bg-slate-950/70 px-4 py-4 text-xs leading-6 text-slate-200" }, JSON.stringify(payload, null, 2)))))); } -const __vite_glob_0_100 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_102 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StaffApplicationShow }, Symbol.toStringTag, { value: "Module" })); @@ -93421,7 +93546,7 @@ function Stories({ title, stories, filters, stats, endpoints }) { /* @__PURE__ */ React.createElement("option", { value: "rejected" }, "Rejected") ), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.1]" }, "Apply"))), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-4 xl:grid-cols-2" }, items.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-300 xl:col-span-2" }, "No stories matched the current filters.") : items.map((story) => /* @__PURE__ */ React.createElement("article", { key: story.id, className: "overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-[180px_1fr]" }, /* @__PURE__ */ React.createElement("div", { className: "aspect-[3/4] bg-black/30" }, story.cover_url ? /* @__PURE__ */ React.createElement("img", { src: story.cover_url, alt: story.title, className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-full items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-feather-pointed text-4xl" }))), /* @__PURE__ */ React.createElement("div", { className: "p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${badgeTone$1(story.status)}` }, String(story.status || "draft").replaceAll("_", " ")), story.creator ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-200" }, "@", story.creator.username) : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.03em] text-white" }, story.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, "/", story.slug, story.creator ? ` • ${story.creator.name}` : ""), story.excerpt ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, story.excerpt) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-[0.16em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, story.published_at ? new Date(story.published_at).toLocaleDateString() : "Unpublished"), /* @__PURE__ */ React.createElement("span", null, story.created_at ? new Date(story.created_at).toLocaleDateString() : "—")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, story.open_url ? /* @__PURE__ */ React.createElement("a", { href: story.open_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Open") : /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.03] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-400" }, "No public view"))))))), stories?.prev_page_url || stories?.next_page_url ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 flex items-center justify-between gap-3 rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, "Showing page ", stories?.current_page || 1, " of ", stories?.last_page || 1), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, stories?.prev_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(stories.prev_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Previous") : null, stories?.next_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(stories.next_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Next") : null)) : null); } -const __vite_glob_0_101 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_103 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: Stories }, Symbol.toStringTag, { value: "Module" })); @@ -93542,7 +93667,7 @@ function UsernameQueue({ title, requests, stats, filters, options, endpoints }) busy === `reject-${item.id}` ? "Saving…" : "Reject" ))))))), requests?.prev_page_url || requests?.next_page_url ? /* @__PURE__ */ React.createElement("div", { className: "mt-8 flex items-center justify-between gap-3 rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, "Showing page ", requests?.current_page || 1, " of ", requests?.last_page || 1), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, requests?.prev_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(requests.prev_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Previous") : null, requests?.next_page_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.get(requests.next_page_url, {}, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Next") : null)) : null); } -const __vite_glob_0_102 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_104 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UsernameQueue }, Symbol.toStringTag, { value: "Module" })); @@ -93677,7 +93802,7 @@ function WorldWebStoriesIndex() { } ), /* @__PURE__ */ React.createElement("label", { className: "flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("input", { type: "checkbox", checked: generator.force, onChange: (event) => setGenerator((current) => ({ ...current, force: event.target.checked })) }), "Force"), /* @__PURE__ */ React.createElement("label", { className: "flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("input", { type: "checkbox", checked: generator.publish, onChange: (event) => setGenerator((current) => ({ ...current, publish: event.target.checked })) }), "Publish"), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busyKey === "generate", className: "rounded-2xl border border-sky-300/20 bg-sky-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20 disabled:opacity-60" }, "Generate")))), notice ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, notice) : null, error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, error) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-4 xl:grid-cols-2" }, (stories.data || []).map((story) => /* @__PURE__ */ React.createElement("article", { key: story.id, className: "overflow-hidden rounded-[28px] border border-white/10 bg-[#08111d] shadow-[0_18px_48px_rgba(2,6,23,0.2)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-[180px_1fr]" }, /* @__PURE__ */ React.createElement("div", { className: "aspect-[3/4] bg-black/30" }, story.poster_portrait_url ? /* @__PURE__ */ React.createElement("img", { src: story.poster_portrait_url, alt: story.title, className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-full items-center justify-center text-white/20" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader text-4xl" }))), /* @__PURE__ */ React.createElement("div", { className: "p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement(StatusBadge, { story }), !story.active ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-amber-300/20 bg-amber-400/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100" }, "inactive") : null, story.noindex ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex rounded-full border border-rose-300/20 bg-rose-400/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-rose-100" }, "noindex") : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.03em] text-white" }, story.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, "/", story.slug, story.world ? ` • ${story.world.title}` : ""), story.excerpt ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, story.excerpt) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2 text-xs uppercase tracking-[0.16em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, story.page_count, " pages"), story.published_at ? /* @__PURE__ */ React.createElement("span", null, new Date(story.published_at).toLocaleDateString()) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: replacePattern$1(endpoints.editPattern, story.id), className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Edit"), /* @__PURE__ */ React.createElement("a", { href: story.public_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Open"), story.status === "published" ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => performAction(`unpublish-${story.id}`, replacePattern$1(endpoints.unpublishPattern, story.id)), className: "inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-amber-100 transition hover:bg-amber-400/18" }, "Unpublish") : /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => performAction(`publish-${story.id}`, replacePattern$1(endpoints.publishPattern, story.id)), className: "inline-flex items-center gap-2 rounded-full border border-emerald-300/20 bg-emerald-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-100 transition hover:bg-emerald-400/18" }, "Publish"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => performAction(`delete-${story.id}`, replacePattern$1(endpoints.destroyPattern, story.id), "DELETE"), className: "inline-flex items-center gap-2 rounded-full border border-rose-300/20 bg-rose-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-rose-100 transition hover:bg-rose-400/18" }, "Delete")))))))); } -const __vite_glob_0_103 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_105 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldWebStoriesIndex }, Symbol.toStringTag, { value: "Module" })); @@ -93899,7 +94024,7 @@ function WorldWebStoryEditor() { } return /* @__PURE__ */ React.createElement("div", { className: "w-full pb-16 pt-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: isNew ? "New World Web Story" : `Edit ${story.title}` }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80" }, "Moderation surface"), /* @__PURE__ */ React.createElement("h1", { className: "mt-2 text-3xl font-semibold tracking-[-0.04em] text-white" }, isNew ? "Create World Web Story" : story.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-relaxed text-slate-300" }, "Build a standalone AMP story companion for a Skinbase World without changing the canonical World route.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: endpoints.index, className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Back"), !isNew && story.public_url ? /* @__PURE__ */ React.createElement("a", { href: story.public_url, className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Open story") : null, !isNew ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => performStoryAction(endpoints.publish), className: "rounded-full border border-emerald-300/20 bg-emerald-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-100 transition hover:bg-emerald-400/18" }, "Publish") : null, !isNew ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => performStoryAction(endpoints.unpublish), className: "rounded-full border border-amber-300/20 bg-amber-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-amber-100 transition hover:bg-amber-400/18" }, "Unpublish") : null)), notice ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50" }, notice) : null, error ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, error) : null, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "mt-6 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field, { label: "Related World" }, /* @__PURE__ */ React.createElement("select", { value: form.data.world_id, onChange: (event) => form.setData("world_id", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" }, /* @__PURE__ */ React.createElement("option", { value: "" }, "No related World"), worldOptions.map((world) => /* @__PURE__ */ React.createElement("option", { key: world.value, value: world.value }, world.label)))), /* @__PURE__ */ React.createElement(Field, { label: "Slug" }, /* @__PURE__ */ React.createElement("input", { value: form.data.slug, onChange: (event) => form.setData("slug", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Title" }, /* @__PURE__ */ React.createElement("input", { value: form.data.title, onChange: (event) => form.setData("title", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Subtitle" }, /* @__PURE__ */ React.createElement("input", { value: form.data.subtitle, onChange: (event) => form.setData("subtitle", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Excerpt" }, /* @__PURE__ */ React.createElement("textarea", { value: form.data.excerpt, onChange: (event) => form.setData("excerpt", event.target.value), rows: 3, className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Description" }, /* @__PURE__ */ React.createElement("textarea", { value: form.data.description, onChange: (event) => form.setData("description", event.target.value), rows: 3, className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "SEO title" }, /* @__PURE__ */ React.createElement("input", { value: form.data.seo_title, onChange: (event) => form.setData("seo_title", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "SEO description" }, /* @__PURE__ */ React.createElement("textarea", { value: form.data.seo_description, onChange: (event) => form.setData("seo_description", event.target.value), rows: 3, className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Poster portrait path" }, /* @__PURE__ */ React.createElement("input", { value: form.data.poster_portrait_path, onChange: (event) => form.setData("poster_portrait_path", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Poster square path" }, /* @__PURE__ */ React.createElement("input", { value: form.data.poster_square_path, onChange: (event) => form.setData("poster_square_path", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Publisher logo path" }, /* @__PURE__ */ React.createElement("input", { value: form.data.publisher_logo_path, onChange: (event) => form.setData("publisher_logo_path", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Status" }, /* @__PURE__ */ React.createElement("select", { value: form.data.status, onChange: (event) => form.setData("status", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" }, ["draft", "published", "archived"].map((value) => /* @__PURE__ */ React.createElement("option", { key: value, value }, value)))), /* @__PURE__ */ React.createElement(Field, { label: "Starts at" }, /* @__PURE__ */ React.createElement("input", { type: "datetime-local", value: form.data.starts_at ? form.data.starts_at.slice(0, 16) : "", onChange: (event) => form.setData("starts_at", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Ends at" }, /* @__PURE__ */ React.createElement("input", { type: "datetime-local", value: form.data.ends_at ? form.data.ends_at.slice(0, 16) : "", onChange: (event) => form.setData("ends_at", event.target.value), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-4" }, /* @__PURE__ */ React.createElement("label", { className: "flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("input", { type: "checkbox", checked: Boolean(form.data.featured), onChange: (event) => form.setData("featured", event.target.checked) }), " Featured"), /* @__PURE__ */ React.createElement("label", { className: "flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("input", { type: "checkbox", checked: Boolean(form.data.active), onChange: (event) => form.setData("active", event.target.checked) }), " Active"), /* @__PURE__ */ React.createElement("label", { className: "flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("input", { type: "checkbox", checked: Boolean(form.data.noindex), onChange: (event) => form.setData("noindex", event.target.checked) }), " Noindex")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: form.processing, className: "rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20 disabled:opacity-60" }, "Save story"), !isNew ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: generateFromWorld, className: "rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Regenerate from World") : null)), !isNew ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold tracking-[-0.03em] text-white" }, "Validation"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, "Publish only when poster, logo, page count, alt text, and CTA rules are satisfied.")), /* @__PURE__ */ React.createElement("div", { className: `rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] ${story.validation?.valid ? "border-emerald-300/20 bg-emerald-400/12 text-emerald-100" : "border-amber-300/20 bg-amber-400/12 text-amber-100"}` }, story.validation?.valid ? "Ready to publish" : "Needs fixes")), (story.validation?.errors || []).length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-4 text-sm text-amber-50" }, /* @__PURE__ */ React.createElement("ul", { className: "space-y-2" }, (story.validation.errors || []).map((item) => /* @__PURE__ */ React.createElement("li", { key: item }, item)))) : null), /* @__PURE__ */ React.createElement("section", { className: "mt-8 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-2xl font-semibold tracking-[-0.03em] text-white" }, "Story pages"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, "Keep each page short, visual, and clearly tied back to the World narrative."))), /* @__PURE__ */ React.createElement("form", { onSubmit: createPage, className: "mt-6 grid gap-3 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field, { label: "New page headline" }, /* @__PURE__ */ React.createElement("input", { value: newPage.headline, onChange: (event) => setNewPage((current) => ({ ...current, headline: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "New page caption" }, /* @__PURE__ */ React.createElement("input", { value: newPage.caption, onChange: (event) => setNewPage((current) => ({ ...current, caption: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "New page body" }, /* @__PURE__ */ React.createElement("textarea", { value: newPage.body, onChange: (event) => setNewPage((current) => ({ ...current, body: event.target.value })), rows: 3, className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Alt text" }, /* @__PURE__ */ React.createElement("input", { value: newPage.alt_text, onChange: (event) => setNewPage((current) => ({ ...current, alt_text: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Layout" }, /* @__PURE__ */ React.createElement("select", { value: newPage.layout, onChange: (event) => setNewPage((current) => ({ ...current, layout: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" }, ["cover", "artwork", "creator", "mood", "collection", "cta"].map((value) => /* @__PURE__ */ React.createElement("option", { key: value, value }, value)))), /* @__PURE__ */ React.createElement(Field, { label: "Background type" }, /* @__PURE__ */ React.createElement("select", { value: newPage.background_type, onChange: (event) => setNewPage((current) => ({ ...current, background_type: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" }, ["image", "video", "gradient"].map((value) => /* @__PURE__ */ React.createElement("option", { key: value, value }, value)))), /* @__PURE__ */ React.createElement(Field, { label: "Background path" }, /* @__PURE__ */ React.createElement("input", { value: newPage.background_path, onChange: (event) => setNewPage((current) => ({ ...current, background_path: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement(Field, { label: "Mobile background path" }, /* @__PURE__ */ React.createElement("input", { value: newPage.background_mobile_path, onChange: (event) => setNewPage((current) => ({ ...current, background_mobile_path: event.target.value })), className: "w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "xl:col-span-2 flex justify-end" }, /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20" }, "Add page"))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-4" }, pages2.sort((left, right) => left.position - right.position).map((page) => /* @__PURE__ */ React.createElement("div", { key: page.id }, /* @__PURE__ */ React.createElement("div", { className: "mb-2 flex justify-end gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => reorder(page.id, -1), className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Move up"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => reorder(page.id, 1), className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]" }, "Move down")), /* @__PURE__ */ React.createElement(StoryPageCard, { page, endpoints, onChanged: reloadEditor })))))) : null); } -const __vite_glob_0_104 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_106 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldWebStoryEditor }, Symbol.toStringTag, { value: "Module" })); @@ -94320,7 +94445,7 @@ if (typeof document !== "undefined") { clientExports.createRoot(mountEl).render(/* @__PURE__ */ React.createElement(NewsComments, { ...props })); } } -const __vite_glob_0_105 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_107 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NewsComments }, Symbol.toStringTag, { value: "Module" })); @@ -94407,7 +94532,7 @@ if (typeof document !== "undefined") { document.addEventListener("keydown", handleKeyDown); } const NewsImagePreview = null; -const __vite_glob_0_106 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_108 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NewsImagePreview }, Symbol.toStringTag, { value: "Module" })); @@ -95371,7 +95496,7 @@ function ProfileGallery() { } )))); } -const __vite_glob_0_107 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_109 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileGallery }, Symbol.toStringTag, { value: "Module" })); @@ -98221,7 +98346,7 @@ function ProfileShow() { } )))); } -const __vite_glob_0_108 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileShow }, Symbol.toStringTag, { value: "Module" })); @@ -99649,7 +99774,7 @@ function ProfileEdit() { ) ); } -const __vite_glob_0_109 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_111 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileEdit }, Symbol.toStringTag, { value: "Module" })); @@ -100162,7 +100287,7 @@ function StudioActivity() { /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 md:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "New since last read"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.new_items || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Unread notifications"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.unread_notifications || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Last inbox reset"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-base font-semibold text-white" }, summary.last_read_at ? formatDate$7(summary.last_read_at) : "Not yet"))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search activity"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilters({ q: event.target.value }), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white", placeholder: "Message, actor, or module" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Type"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.type || "all", onChange: (val) => updateFilters({ type: val }), options: typeOptions, searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Content type"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.module || "all", onChange: (val) => updateFilters({ module: val }), options: moduleOptions, searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "flex items-end" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => updateFilters({ q: "", type: "all", module: "all" }), className: "w-full rounded-2xl border border-white/10 px-4 py-3 text-sm text-slate-200" }, "Reset")))), /* @__PURE__ */ React.createElement("section", { className: "space-y-4" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: `rounded-[28px] border p-5 ${item.is_new ? "border-sky-300/25 bg-sky-300/10" : "border-white/10 bg-white/[0.03]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-4" }, item.actor?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: item.actor.avatar_url, alt: item.actor.name || "Activity actor", className: "h-12 w-12 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 items-center justify-center rounded-2xl bg-black/20 text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-bell" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("span", null, item.module_label), /* @__PURE__ */ React.createElement("span", null, formatDate$7(item.created_at)), item.is_new && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-sky-300/20 px-2 py-1 text-sky-100" }, "New")), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-lg font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, item.body), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center gap-3 text-sm text-slate-400" }, item.actor?.name && /* @__PURE__ */ React.createElement("span", null, item.actor.name), /* @__PURE__ */ React.createElement("a", { href: item.url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-slate-200" }, "Open")))))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400" }, "No activity matches this filter.")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) <= 1, onClick: () => updateFilters({ page: Math.max(1, (meta.current_page || 1) - 1) }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Previous"), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Page ", meta.current_page || 1, " of ", meta.last_page || 1), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) >= (meta.last_page || 1), onClick: () => updateFilters({ page: (meta.current_page || 1) + 1 }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Next"))) ); } -const __vite_glob_0_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_112 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioActivity }, Symbol.toStringTag, { value: "Module" })); @@ -100253,7 +100378,7 @@ function StudioAnalytics() { return /* @__PURE__ */ React.createElement("div", { key: item.key, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-slate-200" }, /* @__PURE__ */ React.createElement("i", { className: item.icon }), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, item.label), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-400" }, Number(item.published_count || 0).toLocaleString(), " published"))), /* @__PURE__ */ React.createElement("a", { href: moduleBreakdown?.find((entry) => entry.key === item.key)?.index_url, className: "text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Open")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, "Views"), /* @__PURE__ */ React.createElement("span", null, Number(item.views || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "mt-2 h-2 overflow-hidden rounded-full bg-white/5" }, /* @__PURE__ */ React.createElement("div", { className: "h-full rounded-full bg-emerald-400/60", style: { width: `${Math.max(4, Math.round(Number(item.views || 0) / viewMax * 100))}%` } }))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, "Engagement"), /* @__PURE__ */ React.createElement("span", null, Number(item.engagement || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "mt-2 h-2 overflow-hidden rounded-full bg-white/5" }, /* @__PURE__ */ React.createElement("div", { className: "h-full rounded-full bg-pink-400/60", style: { width: `${Math.max(4, Math.round(Number(item.engagement || 0) / engagementMax * 100))}%` } }))))); }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Readable insights"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3 text-sm text-slate-400" }, (insightBlocks || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.key, href: item.href, className: "block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 items-center justify-center rounded-2xl bg-white/[0.04] text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: item.icon })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 leading-6 text-slate-400" }, item.body), /* @__PURE__ */ React.createElement("span", { className: "mt-3 inline-flex items-center gap-2 text-sm font-medium text-sky-100" }, item.cta, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }))))))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Top content"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "w-full text-sm" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "border-b border-white/5 text-left text-[11px] uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("th", { className: "pb-3 pr-4" }, "Module"), /* @__PURE__ */ React.createElement("th", { className: "pb-3 pr-4" }, "Title"), /* @__PURE__ */ React.createElement("th", { className: "pb-3 pr-4 text-right" }, "Views"), /* @__PURE__ */ React.createElement("th", { className: "pb-3 pr-4 text-right" }, "Reactions"), /* @__PURE__ */ React.createElement("th", { className: "pb-3 pr-4 text-right" }, "Comments"), /* @__PURE__ */ React.createElement("th", { className: "pb-3 text-right" }, "Open"))), /* @__PURE__ */ React.createElement("tbody", { className: "divide-y divide-white/5" }, (topContent || []).map((item) => /* @__PURE__ */ React.createElement("tr", { key: item.id }, /* @__PURE__ */ React.createElement("td", { className: "py-3 pr-4 text-slate-300" }, item.module_label), /* @__PURE__ */ React.createElement("td", { className: "py-3 pr-4 text-white" }, item.title), /* @__PURE__ */ React.createElement("td", { className: "py-3 pr-4 text-right text-slate-300" }, Number(item.metrics?.views || 0).toLocaleString()), /* @__PURE__ */ React.createElement("td", { className: "py-3 pr-4 text-right text-slate-300" }, Number(item.metrics?.appreciation || 0).toLocaleString()), /* @__PURE__ */ React.createElement("td", { className: "py-3 pr-4 text-right text-slate-300" }, Number(item.metrics?.comments || 0).toLocaleString()), /* @__PURE__ */ React.createElement("td", { className: "py-3 text-right" }, /* @__PURE__ */ React.createElement("a", { href: item.analytics_url || item.view_url, className: "text-sky-100" }, "Open")))))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Recent comments"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (recentComments || []).map((comment) => /* @__PURE__ */ React.createElement("article", { key: comment.id, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70" }, comment.module_label), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white" }, comment.author_name, " on ", comment.item_title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, comment.body))))))); } -const __vite_glob_0_111 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_113 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -101098,7 +101223,7 @@ function StudioArchived() { } )); } -const __vite_glob_0_112 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_114 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArchived }, Symbol.toStringTag, { value: "Module" })); @@ -101134,7 +101259,7 @@ function StudioArtworkAnalytics() { } ), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-bold text-white" }, artwork?.title), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500 mt-1" }, "/", artwork?.slug))), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8" }, kpiItems$1.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.key, className: "bg-nova-900/60 border border-white/10 rounded-2xl p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 mb-2" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${item.icon} ${item.color}` }), /* @__PURE__ */ React.createElement("span", { className: "text-xs font-medium text-slate-400 uppercase tracking-wider" }, item.label)), /* @__PURE__ */ React.createElement("p", { className: "text-2xl font-bold text-white tabular-nums" }, (analytics?.[item.key] ?? 0).toLocaleString())))), /* @__PURE__ */ React.createElement("h3", { className: "text-base font-bold text-white mb-4" }, "Performance Metrics"), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8" }, metricCards.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.key, className: "bg-nova-900/60 border border-white/10 rounded-2xl p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 mb-3" }, /* @__PURE__ */ React.createElement("div", { className: `w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center ${item.color}` }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${item.icon} text-lg` })), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium text-slate-300" }, item.label)), /* @__PURE__ */ React.createElement("p", { className: "text-3xl font-bold text-white tabular-nums" }, (analytics?.[item.key] ?? 0).toFixed(1))))), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "bg-nova-900/40 border border-white/10 rounded-2xl p-6" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white mb-3" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chart-line mr-2 text-slate-500" }), "Traffic Sources"), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center py-8" }, /* @__PURE__ */ React.createElement("div", { className: "text-center" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chart-pie text-3xl text-slate-700 mb-3" }), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Coming soon"), /* @__PURE__ */ React.createElement("p", { className: "text-[10px] text-slate-600 mt-1" }, "Traffic source tracking is on the roadmap")))), /* @__PURE__ */ React.createElement("div", { className: "bg-nova-900/40 border border-white/10 rounded-2xl p-6" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white mb-3" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-share-from-square mr-2 text-slate-500" }), "Shares by Platform"), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center py-8" }, /* @__PURE__ */ React.createElement("div", { className: "text-center" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-share-nodes text-3xl text-slate-700 mb-3" }), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Coming soon"), /* @__PURE__ */ React.createElement("p", { className: "text-[10px] text-slate-600 mt-1" }, "Per-platform breakdown coming in a future update")))), /* @__PURE__ */ React.createElement("div", { className: "bg-nova-900/40 border border-white/10 rounded-2xl p-6 lg:col-span-2" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white mb-3" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-trophy mr-2 text-slate-500" }), "Ranking History"), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center py-8" }, /* @__PURE__ */ React.createElement("div", { className: "text-center" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chart-area text-3xl text-slate-700 mb-3" }), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-slate-500" }, "Coming soon"), /* @__PURE__ */ React.createElement("p", { className: "text-[10px] text-slate-600 mt-1" }, "Historical ranking data will be tracked in a future update")))))); } -const __vite_glob_0_113 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_115 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworkAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -103909,7 +104034,7 @@ function StudioArtworkEdit() { )), historyData.versions.length === 0 && /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-500 text-center py-8" }, "No version history yet.")) )); } -const __vite_glob_0_114 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_116 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworkEdit }, Symbol.toStringTag, { value: "Module" })); @@ -103930,7 +104055,7 @@ function StudioArtworks() { } )); } -const __vite_glob_0_115 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_117 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworks }, Symbol.toStringTag, { value: "Module" })); @@ -104056,7 +104181,7 @@ function StudioAssets() { /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }) )))); } -const __vite_glob_0_116 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_118 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioAssets }, Symbol.toStringTag, { value: "Module" })); @@ -104228,7 +104353,7 @@ function StudioCalendar() { }, [selectedDay]); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Scheduled"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.scheduled_total || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Unscheduled"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.unscheduled_total || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Overloaded days"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.overloaded_days || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Next publish"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-base font-semibold text-white" }, formatReleaseCountdown(summary.next_publish_at, nowMs)), summary.next_publish_at && /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, formatScheduledDate(summary.next_publish_at)))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 xl:col-span-2" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search planning queue"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilters({ q: event.target.value }), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white", placeholder: "Title or module" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "View"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.view || "month", onChange: (val) => updateFilters({ view: val }), options: calendar.view_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Module"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.module || "all", onChange: (val) => updateFilters({ module: val }), options: calendar.module_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Queue"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.status || "scheduled", onChange: (val) => updateFilters({ status: val }), options: calendar.status_options || [], searchable: false })))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_340px]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, filters.view === "week" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, calendar.week?.label), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, "Week planning")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => shiftCalendar(-1), className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Prev week"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetCalendarFocus, className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Today"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => shiftCalendar(1), className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Next week"))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-7" }, (calendar.week?.days || []).map((day) => /* @__PURE__ */ React.createElement("div", { key: day.date, className: "rounded-[22px] border border-white/10 bg-black/20 p-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, day.label), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, day.items.length > 0 ? day.items.map((item) => /* @__PURE__ */ React.createElement(CalendarInlineItem, { key: item.id, item })) : /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-500" }, "No scheduled items")))))) : filters.view === "agenda" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Agenda"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-4" }, (calendar.agenda || []).map((group) => /* @__PURE__ */ React.createElement("div", { key: group.date, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-base font-semibold text-white" }, group.label), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, group.count, " items")), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2" }, group.items.map((item) => /* @__PURE__ */ React.createElement(CalendarInlineItem, { key: item.id, item }))))))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, calendar.month?.label), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, "Month planning")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => shiftCalendar(-1), className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Prev month"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetCalendarFocus, className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Today"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => shiftCalendar(1), className: "rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100" }, "Next month"))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid grid-cols-7 gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((label) => /* @__PURE__ */ React.createElement("div", { key: label, className: "px-2 py-1" }, label))), /* @__PURE__ */ React.createElement("div", { className: "mt-2 grid grid-cols-7 gap-2" }, (calendar.month?.days || []).map((day) => /* @__PURE__ */ React.createElement(CalendarMonthDay, { key: day.date, day, onOpenDetail: setSelectedDay }))))), /* @__PURE__ */ React.createElement("aside", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Coverage gaps"), /* @__PURE__ */ React.createElement("a", { href: "/studio/drafts", className: "text-sm font-medium text-sky-100" }, "Open drafts")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (calendar.gaps || []).length > 0 ? (calendar.gaps || []).map((gap) => /* @__PURE__ */ React.createElement("div", { key: gap.date, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-200" }, gap.label)) : /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-dashed border-white/15 px-4 py-8 text-sm text-slate-500" }, "No empty days in the next two weeks."))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Unscheduled queue"), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, (calendar.unscheduled_items || []).length)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (calendar.unscheduled_items || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.edit_url || item.manage_url, className: "block rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, item.module_label, " · ", item.workflow?.readiness?.label || "Needs review"))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Upcoming actions"), /* @__PURE__ */ React.createElement("a", { href: "/studio/scheduled", className: "text-sm font-medium text-sky-100" }, "Open list")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (calendar.scheduled_items || []).slice(0, 5).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs font-medium text-sky-200" }, formatReleaseCountdown(item.scheduled_at, nowMs)), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, formatScheduledDate(item.scheduled_at)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busyKey === `publish:${item.id}`, onClick: () => runAction(props.endpoints.publishNowPattern, item, "publish"), className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs text-sky-100 disabled:opacity-50" }, "Publish now"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busyKey === `unschedule:${item.id}`, onClick: () => runAction(props.endpoints.unschedulePattern, item, "unschedule"), className: "rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-200 disabled:opacity-50" }, "Unschedule")))))))), /* @__PURE__ */ React.createElement(CalendarDayModal, { day: selectedDay, busyKey, endpoints: props.endpoints, onAction: runAction, onClose: () => setSelectedDay(null), nowMs }))); } -const __vite_glob_0_117 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_119 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCalendar }, Symbol.toStringTag, { value: "Module" })); @@ -104250,7 +104375,7 @@ function StudioCardAnalytics() { const { card, analytics } = props; return /* @__PURE__ */ React.createElement(StudioLayout, { title: `Analytics: ${card?.title || "Nova Card"}` }, /* @__PURE__ */ React.createElement(xe, { href: "/studio/cards", className: "mb-6 inline-flex items-center gap-2 text-sm text-slate-400 transition-colors hover:text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left" }), "Back to Cards"), /* @__PURE__ */ React.createElement("div", { className: "mb-8 flex items-center gap-4 rounded-2xl border border-white/10 bg-nova-900/60 p-4" }, card?.preview_url ? /* @__PURE__ */ React.createElement("img", { src: card.preview_url, alt: card.title, className: "h-20 w-20 rounded-xl object-cover bg-nova-800" }) : null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-bold text-white" }, card?.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-slate-500" }, "/", card?.slug), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-slate-400" }, card?.status, " • ", card?.visibility))), /* @__PURE__ */ React.createElement("div", { className: "mb-8 grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-6" }, kpiItems.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.key, className: "rounded-2xl border border-white/10 bg-nova-900/60 p-5" }, /* @__PURE__ */ React.createElement("div", { className: "mb-2 flex items-center gap-2" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${item.icon} ${item.color}` }), /* @__PURE__ */ React.createElement("span", { className: "text-xs font-medium uppercase tracking-wider text-slate-400" }, item.label)), /* @__PURE__ */ React.createElement("p", { className: "text-2xl font-bold tabular-nums text-white" }, (analytics?.[item.key] ?? 0).toLocaleString())))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-nova-900/40 p-6" }, /* @__PURE__ */ React.createElement("h3", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.18em] text-slate-300" }, "Ranking signals"), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Trending score"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-bold tabular-nums text-white" }, Number(analytics?.trending_score ?? 0).toFixed(2))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/[0.03] p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Last engaged"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-white" }, analytics?.last_engaged_at || "No activity yet")))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-nova-900/40 p-6" }, /* @__PURE__ */ React.createElement("h3", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.18em] text-slate-300" }, "Secondary metrics"), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, secondaryItems.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.key, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${item.icon} text-slate-500` }), item.label), /* @__PURE__ */ React.createElement("div", { className: "text-base font-semibold tabular-nums text-white" }, (analytics?.[item.key] ?? 0).toLocaleString()))))))); } -const __vite_glob_0_118 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_120 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -105394,7 +105519,7 @@ function StudioCardEditor() { fmt2.label ))), exportStatus && /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex items-center gap-3 rounded-[18px] border border-white/10 bg-white/[0.03] p-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Export status"), /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold capitalize text-white" }, exportStatus.status)), exportStatus.status === "ready" && exportStatus.output_url && /* @__PURE__ */ React.createElement("a", { href: exportStatus.output_url, download: true, className: "ml-auto rounded-full border border-emerald-300/20 bg-emerald-400/10 px-3 py-1.5 text-xs font-semibold text-emerald-200 transition hover:bg-emerald-400/15" }, "Download"))))), /* @__PURE__ */ React.createElement("nav", { className: "sticky bottom-0 z-20 mt-6 border-t border-white/10 bg-[rgba(2,6,23,0.92)] px-4 py-3 backdrop-blur xl:hidden" }, /* @__PURE__ */ React.createElement("div", { className: "mx-auto flex max-w-7xl items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => goToNextTab(-1), disabled: tabIndex === 0, className: "rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08] disabled:opacity-50" }, "Back"), /* @__PURE__ */ React.createElement("div", { className: "text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Step ", tabIndex + 1, " / ", editorTabs.length), /* @__PURE__ */ React.createElement("div", { className: "mt-0.5 text-sm font-semibold text-white" }, editorTabs[tabIndex]?.label)), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => goToNextTab(1), disabled: tabIndex >= editorTabs.length - 1, className: "rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-2.5 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:opacity-50" }, "Next")))); } -const __vite_glob_0_119 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_121 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardEditor }, Symbol.toStringTag, { value: "Module" })); @@ -105416,7 +105541,7 @@ function StudioCardsIndex() { } ))); } -const __vite_glob_0_120 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_122 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -105489,7 +105614,7 @@ function StudioChallenges() { "Challenge" ), /* @__PURE__ */ React.createElement("a", { href: entry.card.edit_url, className: "text-slate-300" }, "Edit card"), /* @__PURE__ */ React.createElement("a", { href: entry.card.analytics_url, className: "text-slate-300" }, "Analytics")))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Cards with challenge traction"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (cardLeaders || []).map((card) => /* @__PURE__ */ React.createElement("div", { key: card.id, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, card.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, card.status, " • ", card.challenge_entries_count, " challenge entries")), /* @__PURE__ */ React.createElement("a", { href: card.edit_url, className: "text-xs font-semibold uppercase tracking-[0.16em] text-sky-100" }, "Open")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid grid-cols-2 gap-3 text-sm text-slate-400" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Views"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, Number(card.views_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Comments"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, Number(card.comments_count || 0).toLocaleString()))))))))); } -const __vite_glob_0_121 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_123 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioChallenges }, Symbol.toStringTag, { value: "Module" })); @@ -105509,7 +105634,7 @@ function StudioCollections() { } )); } -const __vite_glob_0_122 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_124 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCollections }, Symbol.toStringTag, { value: "Module" })); @@ -105796,7 +105921,7 @@ function StudioComments() { /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }) ))))); } -const __vite_glob_0_123 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_125 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioComments }, Symbol.toStringTag, { value: "Module" })); @@ -105812,7 +105937,7 @@ function StudioContentIndex() { } )); } -const __vite_glob_0_124 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_126 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioContentIndex }, Symbol.toStringTag, { value: "Module" })); @@ -105938,7 +106063,7 @@ function StudioDashboard() { ["Comments", analytics.totals?.comments] ].map(([label, value]) => /* @__PURE__ */ React.createElement("div", { key: label, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", null, label), /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, Number(value || 0).toLocaleString()))))))), showWidget("stale_drafts") && /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Stale drafts"), /* @__PURE__ */ React.createElement("a", { href: "/studio/content?bucket=drafts&stale=only&module=stories", className: "text-sm font-medium text-sky-100" }, "Filter stale work")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-4" }, (overview.stale_drafts || []).map((item) => /* @__PURE__ */ React.createElement(ContinueWorkingCard, { key: item.id, item }))))); } -const __vite_glob_0_125 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_127 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioDashboard }, Symbol.toStringTag, { value: "Module" })); @@ -105955,7 +106080,7 @@ function StudioDrafts() { } )); } -const __vite_glob_0_126 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_128 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioDrafts }, Symbol.toStringTag, { value: "Module" })); @@ -106038,7 +106163,7 @@ function StudioFeatured() { })) : /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[24px] border border-dashed border-white/15 px-6 py-10 text-center text-sm text-slate-400" }, "No published ", module.label.toLowerCase(), " candidates yet.")); }))); } -const __vite_glob_0_127 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_129 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioFeatured }, Symbol.toStringTag, { value: "Module" })); @@ -106064,7 +106189,7 @@ function StudioFollowers() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 grid gap-4 md:grid-cols-3" }, /* @__PURE__ */ React.createElement(SummaryCard$1, { label: "Total followers", value: summary.total_followers, icon: "fa-solid fa-user-group" }), /* @__PURE__ */ React.createElement(SummaryCard$1, { label: "Following back", value: summary.following_back, icon: "fa-solid fa-arrows-rotate" }), /* @__PURE__ */ React.createElement(SummaryCard$1, { label: "Not followed yet", value: summary.not_followed, icon: "fa-solid fa-user-plus" })), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 lg:grid-cols-[minmax(0,1fr)_220px_220px]" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Search"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateQuery({ q: event.target.value, page: 1 }), placeholder: "Search followers", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Sort"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.sort || "recent", onChange: (val) => updateQuery({ sort: val, page: 1 }), options: listing.sort_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, "Relationship"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.relationship || "all", onChange: (val) => updateQuery({ relationship: val, page: 1 }), options: listing.relationship_options || [], searchable: false }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-3" }, items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: "flex flex-col gap-4 rounded-[24px] border border-white/10 bg-black/20 p-4 md:flex-row md:items-center md:justify-between" }, /* @__PURE__ */ React.createElement("a", { href: item.profile_url, className: "flex min-w-0 items-center gap-4" }, item.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: item.avatar_url, alt: item.username, className: "h-14 w-14 rounded-[18px] object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-14 w-14 items-center justify-center rounded-[18px] bg-white/5 text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "truncate text-base font-semibold text-white" }, item.name), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "@", item.username))), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-2 gap-4 text-sm text-slate-400 md:grid-cols-4 md:text-right" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Uploads"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, Number(item.uploads_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Followers"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, Number(item.followers_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Followed"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, item.followed_at ? new Date(item.followed_at).toLocaleDateString() : "—")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Status"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 font-semibold text-white" }, item.is_following_back ? "Following back" : "Not followed")))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) <= 1, onClick: () => updateQuery({ page: Math.max(1, (meta.current_page || 1) - 1) }), className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left" }), "Previous"), /* @__PURE__ */ React.createElement("span", null, "Page ", meta.current_page || 1, " of ", meta.last_page || 1), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) >= (meta.last_page || 1), onClick: () => updateQuery({ page: (meta.current_page || 1) + 1 }), className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Next", /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }))))); } -const __vite_glob_0_128 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_130 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioFollowers }, Symbol.toStringTag, { value: "Module" })); @@ -106073,7 +106198,7 @@ function StudioGroupActivity() { const items = Array.isArray(props.activity) ? props.activity : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("h2", { className: "text-base font-semibold text-white" }, item.headline), item.is_pinned ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-amber-100" }, "Pinned") : null, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, item.visibility)), item.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, item.summary) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-xs text-slate-500" }, item.actor?.name || item.actor?.username || "System", " • ", item.occurred_at ? new Date(item.occurred_at).toLocaleString() : "Recently"), item.subject?.url ? /* @__PURE__ */ React.createElement("a", { href: item.subject.url, className: "mt-3 inline-flex text-sm font-semibold text-sky-200" }, "Open subject") : null), props.pinPattern ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.pinPattern.replace("__ITEM__", String(item.id)), { is_pinned: !item.is_pinned }), className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-semibold text-white" }, item.is_pinned ? "Unpin" : "Pin") : null))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No activity yet."))); } -const __vite_glob_0_129 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_131 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupActivity }, Symbol.toStringTag, { value: "Module" })); @@ -106081,7 +106206,7 @@ function StudioGroupArtworks() { const { props } = X$1(); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 rounded-[28px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em]" }, "Group publish flow"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold" }, "Upload into ", props.studioGroup?.name), /* @__PURE__ */ React.createElement("a", { href: props.uploadUrl, className: "mt-4 inline-flex rounded-full border border-sky-200/20 bg-sky-200/10 px-4 py-2 text-sm font-semibold text-sky-50" }, "New group artwork")), /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: [{ key: "artworks", label: "Artwork", icon: "fa-solid fa-cloud-arrow-up", url: props.uploadUrl }], hideModuleFilter: true })); } -const __vite_glob_0_130 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_132 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupArtworks }, Symbol.toStringTag, { value: "Module" })); @@ -106122,7 +106247,7 @@ function StudioGroupAssets() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, props.storeUrl ? /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-6" }, /* @__PURE__ */ React.createElement("input", { value: form.data.title, onChange: (event) => form.setData("title", event.target.value), placeholder: "Asset title", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none lg:col-span-2" }), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.category, onChange: (val) => form.setData("category", val), options: props.categoryOptions || [], searchable: false }), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.visibility, onChange: (val) => form.setData("visibility", val), options: props.visibilityOptions || [], searchable: false }), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.status, onChange: (val) => form.setData("status", val), options: props.statusOptions || [], searchable: false }), /* @__PURE__ */ React.createElement("input", { type: "file", onChange: (event) => form.setData("file", event.target.files?.[0] || null), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("textarea", { value: form.data.description, onChange: (event) => form.setData("description", event.target.value), placeholder: "What is this asset for?", rows: 3, className: "mt-4 w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: String(form.data.linked_project_id || ""), onChange: (val) => form.setData("linked_project_id", val), placeholder: "No linked project", options: (props.projectOptions || []).map((o) => ({ value: String(o.id), label: o.title })) }), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement(Checkbox, { checked: form.data.is_featured, onChange: (event) => form.setData("is_featured", event.target.checked), label: "Featured asset" }))), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "mt-4 rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-sm font-semibold text-white" }, "Upload asset")) : null, /* @__PURE__ */ React.createElement("form", { onSubmit: applyFilters, className: "mt-6 rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Browse library"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Search and filter shared assets by visibility and category.")), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white" }, "Apply filters")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-4 lg:grid-cols-3" }, /* @__PURE__ */ React.createElement("input", { value: filters.data.q, onChange: (event) => filters.setData("q", event.target.value), placeholder: "Search title, description, or filename", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.data.category, onChange: (val) => filters.setData("category", val), options: [{ value: "all", label: "All categories" }, ...props.categoryOptions || []], searchable: false }), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.data.bucket, onChange: (val) => filters.setData("bucket", val), options: [{ value: "all", label: "All visibility levels" }, ...(props.listing?.bucket_options || []).filter((option) => option.value !== "all")], searchable: false }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, items.length > 0 ? items.map((asset) => /* @__PURE__ */ React.createElement("div", { key: asset.id, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, asset.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, asset.category, " • ", asset.visibility, " • ", asset.status)), /* @__PURE__ */ React.createElement("a", { href: asset.download_url, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-semibold text-white" }, "Download")), asset.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, asset.description) : null, props.updatePattern ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.patch(props.updatePattern.replace("__ASSET__", String(asset.id)), { title: asset.title, description: asset.description || "", category: asset.category, visibility: asset.visibility, status: asset.status === "active" ? "archived" : "active", linked_project_id: asset.linked_project?.id || "", is_featured: asset.is_featured }), className: "mt-4 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, asset.status === "active" ? "Archive" : "Reactivate") : null)) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No assets yet."))); } -const __vite_glob_0_131 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_133 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupAssets }, Symbol.toStringTag, { value: "Module" })); @@ -106194,7 +106319,7 @@ function StudioGroupChallengeEditor() { attachForm.post(props.attachArtworkUrl, { preserveScroll: true }); }, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Attach artwork"), /* @__PURE__ */ React.createElement(NovaSelect, { value: String(attachForm.data.artwork_id || ""), onChange: (val) => attachForm.setData("artwork_id", val), placeholder: "Choose artwork", options: (props.artworkOptions || []).map((o) => ({ value: String(o.id), label: o.title })) }), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "mt-4 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white" }, "Attach")) : null))); } -const __vite_glob_0_132 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_134 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupChallengeEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106203,7 +106328,7 @@ function StudioGroupChallenges() { const items = Array.isArray(props.listing?.items) ? props.listing.items : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "Challenges keep the group active between releases and give members a focused creative prompt."), props.createUrl ? /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Create challenge") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, items.length > 0 ? items.map((challenge) => /* @__PURE__ */ React.createElement("a", { key: challenge.id, href: challenge.urls?.edit || challenge.url, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, challenge.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, challenge.status)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, challenge.summary || "Challenge page"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-xs text-slate-500" }, challenge.entry_count || 0, " linked entries"))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No challenges yet."))); } -const __vite_glob_0_133 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_135 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupChallenges }, Symbol.toStringTag, { value: "Module" })); @@ -106211,7 +106336,7 @@ function StudioGroupCollections() { const { props } = X$1(); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 rounded-[28px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em]" }, "Shared curation"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold" }, "Create collections for ", props.studioGroup?.name), /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "mt-4 inline-flex rounded-full border border-sky-200/20 bg-sky-200/10 px-4 py-2 text-sm font-semibold text-sky-50" }, "New group collection")), /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: [{ key: "collections", label: "Collection", icon: "fa-solid fa-layer-group", url: props.createUrl }], hideModuleFilter: true })); } -const __vite_glob_0_134 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_136 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupCollections }, Symbol.toStringTag, { value: "Module" })); @@ -106325,7 +106450,7 @@ function StudioGroupCreate() { } )), /* @__PURE__ */ React.createElement("section", { className: "mx-auto max-w-3xl rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5" }, /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Name"), /* @__PURE__ */ React.createElement("input", { value: form.name, onChange: handleNameChange, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Slug"), /* @__PURE__ */ React.createElement("input", { value: form.slug, onChange: handleSlugChange, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Short description"), /* @__PURE__ */ React.createElement("input", { value: form.headline, onChange: (event) => setForm((current) => ({ ...current, headline: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "About"), /* @__PURE__ */ React.createElement("textarea", { value: form.bio, onChange: (event) => setForm((current) => ({ ...current, bio: event.target.value })), rows: 6, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Type / category"), /* @__PURE__ */ React.createElement("input", { value: form.type, onChange: (event) => setForm((current) => ({ ...current, type: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Founded date"), /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.founded_at, onChange: (nextValue) => setForm((current) => ({ ...current, founded_at: nextValue })), mode: "date", placeholder: "Pick the founding date", clearable: true, className: "bg-black/20" }))), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Website"), /* @__PURE__ */ React.createElement("input", { value: form.website_url, onChange: (event) => setForm((current) => ({ ...current, website_url: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, "Avatar / logo"), /* @__PURE__ */ React.createElement("div", { className: "flex h-28 w-28 items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]" }, resolvedAvatarPreview ? /* @__PURE__ */ React.createElement("img", { src: resolvedAvatarPreview, alt: "Avatar preview", className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-slate-500" })), /* @__PURE__ */ React.createElement("input", { ref: avatarInputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleFileSelected("avatar_file", setAvatarPreview), className: "hidden" }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => avatarInputRef.current?.click(), className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Upload avatar"), form.avatar_file ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => clearSelectedFile("avatar_file", setAvatarPreview, avatarInputRef), className: "rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300" }, "Use URL instead") : null), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Or paste an image URL"), /* @__PURE__ */ React.createElement("input", { value: form.avatar_path, onChange: (event) => setForm((current) => ({ ...current, avatar_path: event.target.value })), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, "Cover image"), /* @__PURE__ */ React.createElement("div", { className: "flex h-28 w-full items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]" }, resolvedBannerPreview ? /* @__PURE__ */ React.createElement("img", { src: resolvedBannerPreview, alt: "Cover preview", className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-panorama text-slate-500" })), /* @__PURE__ */ React.createElement("input", { ref: bannerInputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleFileSelected("banner_file", setBannerPreview), className: "hidden" }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => bannerInputRef.current?.click(), className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Upload cover"), form.banner_file ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => clearSelectedFile("banner_file", setBannerPreview, bannerInputRef), className: "rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300" }, "Use URL instead") : null), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Or paste an image URL"), /* @__PURE__ */ React.createElement("input", { value: form.banner_path, onChange: (event) => setForm((current) => ({ ...current, banner_path: event.target.value })), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Visibility"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.visibility, onChange: (val) => setForm((current) => ({ ...current, visibility: val })), options: props.visibilityOptions || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Membership policy"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.membership_policy, onChange: (val) => setForm((current) => ({ ...current, membership_policy: val })), options: props.membershipPolicyOptions || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-200" }, "Links"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: addLink, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white" }, "Add link")), form.links_json.map((item, index2) => /* @__PURE__ */ React.createElement("div", { key: `link-${index2}`, className: "grid gap-3 md:grid-cols-[0.8fr_1.2fr_auto]" }, /* @__PURE__ */ React.createElement("input", { value: item.label, onChange: (event) => updateLink(index2, "label", event.target.value), placeholder: "Label", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { value: item.url, onChange: (event) => updateLink(index2, "url", event.target.value), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => removeLink(index2), className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100" }, "Remove")))), /* @__PURE__ */ React.createElement("div", { className: "flex justify-end gap-3" }, /* @__PURE__ */ React.createElement("a", { href: "/studio/groups", className: "rounded-full border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-semibold text-white" }, "Cancel"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: submit, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100" }, "Create group"))))); } -const __vite_glob_0_135 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_137 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupCreate }, Symbol.toStringTag, { value: "Module" })); @@ -106390,7 +106515,7 @@ function StudioGroupDashboard() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-3 xl:grid-cols-6" }, /* @__PURE__ */ React.createElement(StatCard, { label: "Artworks", value: group?.counts?.artworks, icon: "fa-solid fa-images" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Collections", value: group?.counts?.collections, icon: "fa-solid fa-layer-group" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Followers", value: group?.counts?.followers, icon: "fa-solid fa-user-group" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Active members", value: dashboard?.active_members_count || group?.counts?.members, icon: "fa-solid fa-people-group" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Projects", value: dashboard?.projects_count, icon: "fa-solid fa-diagram-project" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Releases", value: dashboard?.published_releases_count || dashboard?.releases_count, icon: "fa-solid fa-rocket" }), /* @__PURE__ */ React.createElement(StatCard, { label: "Assets", value: dashboard?.assets_count, icon: "fa-solid fa-box-archive" })), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Quick actions"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Run the most common group tasks without leaving the dashboard."))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, quickActions.map((action) => /* @__PURE__ */ React.createElement("a", { key: action.label, href: action.href, className: `rounded-[24px] border px-4 py-4 transition hover:translate-y-[-1px] hover:border-white/20 ${toneClasses2[action.tone] || toneClasses2.sky}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-current/20 bg-black/10" }, /* @__PURE__ */ React.createElement("i", { className: action.icon })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold" }, action.label), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs opacity-80" }, action.detail)))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, "Pending action"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Drafts and scheduled items that still need a publishing decision.")), /* @__PURE__ */ React.createElement("div", { className: "text-right text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", null, Number(dashboard?.draft_artworks_count || 0), " drafts"), /* @__PURE__ */ React.createElement("div", null, Number(dashboard?.scheduled_artworks_count || 0), " scheduled"))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, draftsPendingAction.length > 0 ? draftsPendingAction.map((artwork) => /* @__PURE__ */ React.createElement(ContentCard, { key: artwork.id, item: artwork, fallbackLabel: "Draft" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No drafts waiting", description: "This group has no draft artworks waiting for review or completion right now." }))), pendingJoinRequests.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, "Pending join requests"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Applicants waiting for a review decision.")), group?.urls?.studio_join_requests ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_join_requests, className: "text-sm font-semibold text-sky-200" }, "Open queue") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, pendingJoinRequests.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, item.user?.name || item.user?.username), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, item.desired_role_label || item.desired_role || "Contributor", " • ", item.created_at ? new Date(item.created_at).toLocaleDateString() : "New"))))) : null), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Members"), /* @__PURE__ */ React.createElement("a", { href: group?.urls?.studio_members, className: "text-sm font-semibold text-sky-200" }, "Manage")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-2 sm:grid-cols-2" }, Object.entries(roleSummary).map(([role, count]) => /* @__PURE__ */ React.createElement("div", { key: role, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, role), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xl font-semibold text-white" }, Number(count))))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, members.slice(0, 6).map((member) => /* @__PURE__ */ React.createElement("div", { key: member.id, className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, member.user?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: member.user.avatar_url, alt: member.user.name || member.user.username, className: "h-11 w-11 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "truncate font-semibold text-white" }, member.user?.name || member.user?.username), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, member.role))))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Recruitment"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, recruitment?.is_recruiting ? recruitment.headline || "Recruiting is active" : "Recruitment is off"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, recruitment?.description || "Set open roles, skills, and contact instructions from the recruitment page.")))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Releases"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Track featured drops and current release pipelines.")), group?.urls?.studio_releases ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_releases, className: "text-sm font-semibold text-sky-200" }, "Manage") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentReleases.length > 0 ? recentReleases.map((release) => /* @__PURE__ */ React.createElement(ContentCard, { key: release.id, item: release, fallbackLabel: "Release" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No releases yet", description: "Create a release to track milestones, contributors, and publication status." }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Projects"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Recent structured releases and collaboration hubs.")), group?.urls?.studio_projects ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_projects, className: "text-sm font-semibold text-sky-200" }, "Manage") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentProjects.length > 0 ? recentProjects.map((project) => /* @__PURE__ */ React.createElement(ContentCard, { key: project.id, item: project, fallbackLabel: "Project" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No projects yet", description: "Create a project to bundle shared assets, linked artworks, and a release state." }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Challenges"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Current creative prompts and challenge arcs.")), group?.urls?.studio_challenges ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_challenges, className: "text-sm font-semibold text-sky-200" }, "Manage") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentChallenges.length > 0 ? recentChallenges.map((challenge) => /* @__PURE__ */ React.createElement(ContentCard, { key: challenge.id, item: challenge, fallbackLabel: "Challenge" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No challenges yet", description: "Launch a challenge to keep the group active between major releases." })))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Trust summary"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Public-facing trust labels and internal contributor health snapshot.")), group?.urls?.studio_reputation ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_reputation, className: "text-sm font-semibold text-sky-200" }, "Open dashboard") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, trustSignals.map((signal) => /* @__PURE__ */ React.createElement("span", { key: signal.key, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-semibold text-white" }, signal.label))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Contributors"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-2xl font-semibold text-white" }, Number(reputationSummary?.counts?.contributors || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Group badges"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-2xl font-semibold text-white" }, Number(reputationSummary?.counts?.group_badges || 0))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Contributor highlights"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Recent high-trust contributors and badge unlocks."))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, Array.isArray(reputationSummary?.top_contributors) && reputationSummary.top_contributors.length > 0 ? reputationSummary.top_contributors.slice(0, 4).map((entry) => /* @__PURE__ */ React.createElement("div", { key: entry.user?.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, entry.user?.name || entry.user?.username), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, entry.summary || "Contributor"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs text-slate-500" }, entry.counts?.releases || 0, " releases • ", entry.counts?.credited_artworks || 0, " artworks"))) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No contributor signals yet", description: "Release and milestone activity will populate contributor reputation here." })))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent artworks"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Latest published work released under this group identity.")), /* @__PURE__ */ React.createElement("a", { href: group?.urls?.studio_artworks, className: "text-sm font-semibold text-sky-200" }, "View all")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentArtworks.length > 0 ? recentArtworks.map((artwork) => /* @__PURE__ */ React.createElement(ContentCard, { key: artwork.id, item: artwork, fallbackLabel: "Published" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No published artworks yet", description: "Publish the first group artwork to start building this feed." }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Events"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Upcoming or recently updated moments on the group timeline.")), group?.urls?.studio_events ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_events, className: "text-sm font-semibold text-sky-200" }, "Manage") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentEvents.length > 0 ? recentEvents.map((event) => /* @__PURE__ */ React.createElement(ContentCard, { key: event.id, item: event, fallbackLabel: "Event" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No events yet", description: "Schedule a launch, stream, or milestone to start the group timeline." })))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent collections"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Collections most recently updated in this group workspace.")), /* @__PURE__ */ React.createElement("a", { href: group?.urls?.studio_collections, className: "text-sm font-semibold text-sky-200" }, "View all")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentCollections.length > 0 ? recentCollections.map((collection) => /* @__PURE__ */ React.createElement(ContentCard, { key: collection.id, item: collection, fallbackLabel: "Collection" })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No collections yet", description: "Create a collection to organize group work into campaigns, series, or themed sets." }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Activity feed"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Pinned and recent internal or public timeline items.")), group?.urls?.studio_activity ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_activity, className: "text-sm font-semibold text-sky-200" }, "Open feed") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, recentActivity.length > 0 ? recentActivity.map((item) => /* @__PURE__ */ React.createElement(ActivityCard, { key: item.id, item })) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No activity items yet", description: "Publishing projects, events, posts, and member milestones will populate this feed." })))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Review queue"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Latest artwork submissions waiting for moderation.")), group?.urls?.studio_review ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_review, className: "text-sm font-semibold text-sky-200" }, "Open queue") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, reviewQueuePreview.length > 0 ? reviewQueuePreview.map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.urls?.edit, className: "rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, item.group_review_status))) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No pending reviews", description: "Contributor submissions will appear here when they are sent for review." }))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent posts"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Announcements and updates published from the group.")), group?.urls?.studio_posts ? /* @__PURE__ */ React.createElement("a", { href: group.urls.studio_posts, className: "text-sm font-semibold text-sky-200" }, "Manage posts") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, recentPosts.length > 0 ? recentPosts.map((post2) => /* @__PURE__ */ React.createElement("a", { key: post2.id, href: post2.url, className: "rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, post2.type), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-base font-semibold text-white" }, post2.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, post2.excerpt || "Open post"))) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No posts yet", description: "Create the first group announcement to add a public news feed." })))), /* @__PURE__ */ React.createElement("section", { className: "mt-6 rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent history"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, recentHistory.length > 0 ? recentHistory.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.summary || item.action_type), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs text-slate-400" }, item.actor?.name || item.actor?.username || "System", " • ", item.created_at ? new Date(item.created_at).toLocaleString() : "Recently"))) : /* @__PURE__ */ React.createElement(EmptyCard, { title: "No history yet", description: "Audit events will appear here as members review requests, posts, and submissions." })))); } -const __vite_glob_0_136 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_138 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupDashboard }, Symbol.toStringTag, { value: "Module" })); @@ -106429,7 +106554,7 @@ function StudioGroupEventEditor() { form.post(props.publishUrl, { preserveScroll: true }); }, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-sm font-semibold text-white" }, "Publish event")) : null)); } -const __vite_glob_0_137 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_139 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupEventEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106438,7 +106563,7 @@ function StudioGroupEvents() { const items = Array.isArray(props.listing?.items) ? props.listing.items : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "Events let the group announce launches, sessions, milestones, and time-based updates."), props.createUrl ? /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Create event") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, items.length > 0 ? items.map((event) => /* @__PURE__ */ React.createElement("a", { key: event.id, href: event.urls?.edit || event.url, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, event.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, event.status)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, event.summary || "Event page"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-xs text-slate-500" }, event.start_at ? new Date(event.start_at).toLocaleString() : "Unscheduled", " • ", event.event_type))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No events yet."))); } -const __vite_glob_0_138 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_140 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupEvents }, Symbol.toStringTag, { value: "Module" })); @@ -106465,7 +106590,7 @@ function StudioGroupInvitations() { ); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("section", { className: "mb-6 rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/75" }, "Group invitations"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, "Invite collaborators into ", props.studioGroup?.name), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-6 text-slate-300" }, "Pending invites stay separate from active members here, so owners and admins can review who was invited, when the invite expires, and revoke access before acceptance.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: props.studioGroup?.urls?.studio_members, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Members"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100" }, pendingInvites.length, " pending"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-[1.1fr_0.8fr_1fr_0.7fr_auto]" }, /* @__PURE__ */ React.createElement("input", { value: invite.username, onChange: (event) => setInvite((current) => ({ ...current, username: event.target.value })), placeholder: "Username", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement(NovaSelect, { value: invite.role, onChange: (val) => setInvite((current) => ({ ...current, role: val })), searchable: false, options: [{ value: "contributor", label: "Contributor" }, { value: "editor", label: "Editor" }, { value: "admin", label: "Admin" }] }), /* @__PURE__ */ React.createElement("input", { value: invite.note, onChange: (event) => setInvite((current) => ({ ...current, note: event.target.value })), placeholder: "Optional note", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { value: invite.expires_in_days, onChange: (event) => setInvite((current) => ({ ...current, expires_in_days: event.target.value })), type: "number", min: "1", max: "30", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.endpoints?.invite, { ...invite, expires_in_days: Number(invite.expires_in_days || 7) || 7 }), className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100" }, "Send invite"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Pending invitations"), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-400" }, pendingInvites.length, " outstanding")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, pendingInvites.length > 0 ? pendingInvites.map((inviteRow) => /* @__PURE__ */ React.createElement("article", { key: inviteRow.id, className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 md:flex-row md:items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, inviteRow.user?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: inviteRow.user.avatar_url, alt: inviteRow.user.name || inviteRow.user.username, className: "h-12 w-12 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, inviteRow.user?.name || inviteRow.user?.username), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, inviteRow.role_label || inviteRow.role))), /* @__PURE__ */ React.createElement("div", { className: "md:ml-auto flex flex-wrap items-center gap-3 text-xs text-slate-400" }, inviteRow.invited_by ? /* @__PURE__ */ React.createElement("span", null, "Invited by ", inviteRow.invited_by.name || inviteRow.invited_by.username) : null, inviteRow.invited_at ? /* @__PURE__ */ React.createElement("span", null, "Sent ", formatInviteTimestamp(inviteRow.invited_at)) : null, inviteRow.expires_at ? /* @__PURE__ */ React.createElement("span", null, "Expires ", formatInviteTimestamp(inviteRow.expires_at)) : null)), inviteRow.note ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, inviteRow.note) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, inviteRow.can_revoke && inviteRow.revoke_url ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.delete(inviteRow.revoke_url), className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-3 py-2 text-sm font-semibold text-rose-100" }, "Revoke invite") : null))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 px-6 py-12 text-center text-slate-400" }, "No pending invites for this group."))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Recent invite history"), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-400" }, revokedInvites.length, " revoked or expired")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, revokedInvites.length > 0 ? revokedInvites.map((inviteRow) => /* @__PURE__ */ React.createElement("article", { key: inviteRow.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, inviteRow.user?.name || inviteRow.user?.username), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-400" }, inviteRow.is_expired ? "Expired" : "Revoked", " • ", inviteRow.role_label || inviteRow.role), inviteRow.invited_at ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, "Originally sent ", formatInviteTimestamp(inviteRow.invited_at)) : null)) : /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-dashed border-white/10 px-4 py-8 text-center text-slate-400" }, "No recent invite history yet."))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Active members"), /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-400" }, activeMembers.length, " active")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, activeMembers.slice(0, 6).map((member) => /* @__PURE__ */ React.createElement("div", { key: member.id, className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, member.user?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: member.user.avatar_url, alt: member.user.name || member.user.username, className: "h-11 w-11 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "truncate font-semibold text-white" }, member.user?.name || member.user?.username), /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.16em] text-slate-400" }, member.role_label || member.role))))))))); } -const __vite_glob_0_139 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_141 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupInvitations }, Symbol.toStringTag, { value: "Module" })); @@ -106494,7 +106619,7 @@ function routeUrl(baseUrl, id, action) { if (!baseUrl) return ""; return `${String(baseUrl).replace(/\/$/, "")}/${id}/${action}`; } -const __vite_glob_0_140 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_142 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupJoinRequests }, Symbol.toStringTag, { value: "Module" })); @@ -106561,7 +106686,7 @@ function StudioGroupMembers() { return /* @__PURE__ */ React.createElement("div", { key: option.value, className: "rounded-2xl border border-white/10 bg-white/[0.03] p-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, option.label), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setPermissionState(member.id, option.value, "inherit"), className: `rounded-full border px-3 py-1.5 text-xs font-semibold ${current === "inherit" ? "border-white/20 bg-white/[0.08] text-white" : "border-white/10 bg-transparent text-slate-300"}` }, "Inherit"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setPermissionState(member.id, option.value, "allow"), className: `rounded-full border px-3 py-1.5 text-xs font-semibold ${current === "allow" ? "border-emerald-300/20 bg-emerald-400/10 text-emerald-100" : "border-white/10 bg-transparent text-slate-300"}` }, "Allow"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setPermissionState(member.id, option.value, "deny"), className: `rounded-full border px-3 py-1.5 text-xs font-semibold ${current === "deny" ? "border-rose-300/20 bg-rose-400/10 text-rose-100" : "border-white/10 bg-transparent text-slate-300"}` }, "Deny"))); }))) : null)), filteredMembers.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "px-4 py-8 text-sm text-slate-400" }, "No members match the current search.") : null)))); } -const __vite_glob_0_141 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_143 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupMembers }, Symbol.toStringTag, { value: "Module" })); @@ -106585,7 +106710,7 @@ function StudioGroupPostEditor() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "grid gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Type"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.type, onChange: (val) => form.setData("type", val), options: Array.isArray(props.typeOptions) ? props.typeOptions : [], searchable: false })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Title"), /* @__PURE__ */ React.createElement("input", { value: form.data.title, onChange: (event) => form.setData("title", event.target.value), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Excerpt"), /* @__PURE__ */ React.createElement("textarea", { value: form.data.excerpt, onChange: (event) => form.setData("excerpt", event.target.value), rows: 3, className: "rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Content"), /* @__PURE__ */ React.createElement("textarea", { value: form.data.content, onChange: (event) => form.setData("content", event.target.value), rows: 12, className: "rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Post controls"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-3" }, /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: form.processing, className: "w-full rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-3 text-sm font-semibold text-sky-100 disabled:opacity-60" }, "Save"), props.publishUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.publishUrl), className: "w-full rounded-full border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm font-semibold text-emerald-100" }, "Publish") : null, props.pinUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.pinUrl), className: "w-full rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm font-semibold text-amber-100" }, "Toggle pinned") : null, props.archiveUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.archiveUrl), className: "w-full rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100" }, "Archive") : null)))); } -const __vite_glob_0_142 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_144 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupPostEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106594,7 +106719,7 @@ function StudioGroupPosts() { const items = Array.isArray(props.listing?.items) ? props.listing.items : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Post library"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Draft, publish, pin, and archive public group posts.")), props.createUrl ? /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100" }, "New post") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 md:grid-cols-2" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, item.type), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-lg font-semibold text-white" }, item.title)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-col items-end gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300" }, item.status), item.is_pinned ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-amber-100" }, "Pinned") : null)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-300" }, item.excerpt || item.content || "No excerpt yet."), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: item.urls?.edit, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Edit"), item.urls?.public ? /* @__PURE__ */ React.createElement("a", { href: item.urls.public, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "View") : null))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-5 text-sm text-slate-400" }, "No posts yet.")))); } -const __vite_glob_0_143 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_145 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupPosts }, Symbol.toStringTag, { value: "Module" })); @@ -106643,7 +106768,7 @@ function StudioGroupProjectEditor() { milestoneForm.post(props.storeMilestoneUrl, { preserveScroll: true, onSuccess: () => milestoneForm.reset("title", "summary", "due_date", "owner_user_id", "notes") }); }, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Milestones"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, /* @__PURE__ */ React.createElement("input", { value: milestoneForm.data.title, onChange: (event) => milestoneForm.setData("title", event.target.value), placeholder: "Milestone title", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("textarea", { value: milestoneForm.data.summary, onChange: (event) => milestoneForm.setData("summary", event.target.value), placeholder: "Summary", rows: 3, className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: milestoneForm.data.status, onChange: (val) => milestoneForm.setData("status", val), searchable: false, options: ["pending", "active", "blocked", "completed", "cancelled"].map((s2) => ({ value: s2, label: s2 })) }), /* @__PURE__ */ React.createElement(DateTimePicker, { value: milestoneForm.data.due_date, onChange: (nextValue) => milestoneForm.setData("due_date", nextValue), mode: "date", placeholder: "Due date", clearable: true, className: "bg-black/20" })), /* @__PURE__ */ React.createElement(NovaSelect, { value: String(milestoneForm.data.owner_user_id || ""), onChange: (val) => milestoneForm.setData("owner_user_id", val), placeholder: "No owner", options: (props.memberOptions || []).map((o) => ({ value: String(o.id), label: o.name || o.username })) }), /* @__PURE__ */ React.createElement("textarea", { value: milestoneForm.data.notes, onChange: (event) => milestoneForm.setData("notes", event.target.value), placeholder: "Notes", rows: 3, className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white" }, "Add milestone")), Array.isArray(project?.milestones) && project.milestones.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-3" }, project.milestones.map((milestone) => /* @__PURE__ */ React.createElement("div", { key: milestone.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, milestone.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, milestone.owner?.name || milestone.owner?.username || "No owner", milestone.due_date ? ` • due ${milestone.due_date}` : "")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.patch(props.updateMilestonePattern.replace("__MILESTONE__", String(milestone.id)), { title: milestone.title, summary: milestone.summary || "", status: milestone.status === "completed" ? "active" : "completed", due_date: milestone.due_date || "", owner_user_id: milestone.owner?.id || "", notes: milestone.notes || "" }, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold text-white" }, "Mark ", milestone.status === "completed" ? "active" : "complete")), milestone.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, milestone.summary) : null))) : null) : null))); } -const __vite_glob_0_144 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_146 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupProjectEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106653,7 +106778,7 @@ function StudioGroupProjects() { const items = Array.isArray(listing.items) ? listing.items : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "Projects give the group a structured place for releases, teams, and linked outputs."), props.createUrl ? /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Create project") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, items.length > 0 ? items.map((project) => /* @__PURE__ */ React.createElement("a", { key: project.id, href: project.urls?.edit || project.url, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, project.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, project.status)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, project.summary || "Project page"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-xs text-slate-500" }, project.counts?.artworks || 0, " artworks • ", project.counts?.assets || 0, " assets • ", project.counts?.team || 0, " team • ", project.counts?.milestones || 0, " milestones • ", project.counts?.releases || 0, " releases"))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No projects yet."))); } -const __vite_glob_0_145 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_147 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupProjects }, Symbol.toStringTag, { value: "Module" })); @@ -106684,7 +106809,7 @@ function StudioGroupRecruitment() { return /* @__PURE__ */ React.createElement("button", { key: option.value, type: "button", onClick: () => form.setData("skills_json", toggleItem(form.data.skills_json, option.value)), className: `rounded-full border px-3 py-1.5 text-xs font-semibold ${selected ? "border-sky-300/20 bg-sky-300/10 text-sky-100" : "border-white/10 bg-white/[0.03] text-slate-300"}` }, option.label); }))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Application settings"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Contact mode"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.contact_mode, onChange: (val) => form.setData("contact_mode", val), options: Array.isArray(props.contactModes) ? props.contactModes : [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Visibility"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.data.visibility, onChange: (val) => form.setData("visibility", val), options: Array.isArray(props.visibilityOptions) ? props.visibilityOptions : [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, "Public preview"), /* @__PURE__ */ React.createElement("p", { className: "mt-2" }, form.data.headline || "No headline yet."), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-slate-400" }, form.data.description || "Recruitment copy will show here once you add it."), form.data.roles_json.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, form.data.roles_json.map((role) => /* @__PURE__ */ React.createElement("span", { key: role, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-white" }, role))) : null), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: form.processing, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-3 text-sm font-semibold text-sky-100 disabled:opacity-60" }, "Save recruitment profile"))))); } -const __vite_glob_0_146 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_148 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupRecruitment }, Symbol.toStringTag, { value: "Module" })); @@ -106737,7 +106862,7 @@ function StudioGroupReleaseEditor() { milestoneForm.post(props.storeMilestoneUrl, { preserveScroll: true, onSuccess: () => milestoneForm.reset("title", "summary", "due_date", "owner_user_id", "notes") }); }, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Milestones"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, /* @__PURE__ */ React.createElement("input", { value: milestoneForm.data.title, onChange: (event) => milestoneForm.setData("title", event.target.value), placeholder: "Milestone title", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("textarea", { value: milestoneForm.data.summary, onChange: (event) => milestoneForm.setData("summary", event.target.value), placeholder: "Summary", rows: 3, className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: milestoneForm.data.status, onChange: (val) => milestoneForm.setData("status", val), searchable: false, options: ["pending", "active", "blocked", "completed", "cancelled"].map((s2) => ({ value: s2, label: s2 })) }), /* @__PURE__ */ React.createElement(DateTimePicker, { value: milestoneForm.data.due_date, onChange: (nextValue) => milestoneForm.setData("due_date", nextValue), mode: "date", placeholder: "Due date", clearable: true, className: "bg-black/20" })), /* @__PURE__ */ React.createElement(NovaSelect, { value: String(milestoneForm.data.owner_user_id || ""), onChange: (val) => milestoneForm.setData("owner_user_id", val), placeholder: "No owner", options: (props.memberOptions || []).map((o) => ({ value: String(o.id), label: o.name || o.username })) }), /* @__PURE__ */ React.createElement("textarea", { value: milestoneForm.data.notes, onChange: (event) => milestoneForm.setData("notes", event.target.value), placeholder: "Notes", rows: 3, className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white" }, "Add milestone")), Array.isArray(release?.milestones) && release.milestones.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-3" }, release.milestones.map((milestone) => /* @__PURE__ */ React.createElement("div", { key: milestone.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, milestone.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, milestone.owner?.name || milestone.owner?.username || "No owner", milestone.due_date ? ` • due ${milestone.due_date}` : "")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.patch(props.updateMilestonePattern.replace("__MILESTONE__", String(milestone.id)), { title: milestone.title, summary: milestone.summary || "", status: milestone.status === "completed" ? "active" : "completed", due_date: milestone.due_date || "", owner_user_id: milestone.owner?.id || "", notes: milestone.notes || "" }, { preserveScroll: true }), className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold text-white" }, "Mark ", milestone.status === "completed" ? "active" : "complete")), milestone.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, milestone.summary) : null))) : null) : null))); } -const __vite_glob_0_147 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_149 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReleaseEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106749,7 +106874,7 @@ function StudioGroupReleases() { const currentBucket = listing.filters?.bucket || "all"; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "Track the release pipeline from draft through public launch, with milestones and contributor credits."), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: currentBucket, onChange: (val) => At.get(window.location.pathname, { bucket: val }, { preserveScroll: true, preserveState: true }), options: bucketOptions, searchable: false }), props.createUrl ? /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Create release") : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 lg:grid-cols-2" }, items.length > 0 ? items.map((release) => /* @__PURE__ */ React.createElement("div", { key: release.id, className: "overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.03]" }, release.cover_url ? /* @__PURE__ */ React.createElement("img", { src: release.cover_url, alt: release.title, className: "aspect-[4/3] w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex aspect-[4/3] items-center justify-center bg-white/[0.03] text-slate-500" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-rocket text-2xl" })), /* @__PURE__ */ React.createElement("div", { className: "p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, release.status), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, release.current_stage), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, release.visibility)), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-xl font-semibold text-white" }, release.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, release.summary || "Release page"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-xs text-slate-500" }, release.counts?.artworks || 0, " artworks • ", release.counts?.contributors || 0, " contributors • ", release.counts?.milestones || 0, " milestones"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: release.urls?.edit || release.url, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Manage"), release.urls?.public ? /* @__PURE__ */ React.createElement("a", { href: release.urls.public, className: "rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm font-semibold text-white" }, "View public") : null)))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No releases yet."))); } -const __vite_glob_0_148 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_150 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReleases }, Symbol.toStringTag, { value: "Module" })); @@ -106766,7 +106891,7 @@ function StudioGroupReputation() { const memberBadgeUnlocks = Array.isArray(reputation.member_badge_unlocks) ? reputation.member_badge_unlocks : []; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5" }, /* @__PURE__ */ React.createElement(MetricCard, { label: "Freshness", value: metrics.freshness_score }), /* @__PURE__ */ React.createElement(MetricCard, { label: "Activity", value: metrics.activity_score }), /* @__PURE__ */ React.createElement(MetricCard, { label: "Release", value: metrics.release_score }), /* @__PURE__ */ React.createElement(MetricCard, { label: "Trust", value: metrics.trust_score }), /* @__PURE__ */ React.createElement(MetricCard, { label: "Collaboration", value: metrics.collaboration_score })), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Trust signals"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Public-safe labels that shape discovery and confidence."))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, trustSignals.map((signal) => /* @__PURE__ */ React.createElement("span", { key: signal.key, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-semibold text-white" }, signal.label))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Contributors"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-2xl font-semibold text-white" }, Number(reputation.counts?.contributors || 0))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Member badges"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-2xl font-semibold text-white" }, Number(reputation.counts?.member_badges || 0)))), metrics.last_calculated_at ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-xs text-slate-500" }, "Last calculated ", new Date(metrics.last_calculated_at).toLocaleString()) : null), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Top contributors"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Reputation summaries derived from visible collaboration history."))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, topContributors.length > 0 ? topContributors.map((entry) => /* @__PURE__ */ React.createElement("div", { key: entry.user?.id, className: "rounded-[24px] border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, entry.user?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: entry.user.avatar_url, alt: entry.user?.name || entry.user?.username, className: "h-11 w-11 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("div", { className: "truncate font-semibold text-white" }, entry.user?.name || entry.user?.username), entry.trusted_indicator ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-emerald-300/20 bg-emerald-300/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100" }, "Trusted") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, entry.summary || "Contributor"))), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-xs text-slate-500" }, entry.counts?.releases || 0, " releases • ", entry.counts?.projects || 0, " projects • ", entry.counts?.credited_artworks || 0, " artworks • ", entry.counts?.review_actions || 0, " reviews"), Array.isArray(entry.badges) && entry.badges.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, entry.badges.map((badge) => /* @__PURE__ */ React.createElement("span", { key: `${entry.user?.id}-${badge.key}`, className: "rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, badge.label))) : null)) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No contributor reputation signals yet.")))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Group badges"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, recentBadges.length > 0 ? recentBadges.map((badge) => /* @__PURE__ */ React.createElement("div", { key: badge.key, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, badge.label), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, badge.reason))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No group badges awarded yet."))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent member badge unlocks"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, memberBadgeUnlocks.length > 0 ? memberBadgeUnlocks.map((entry) => /* @__PURE__ */ React.createElement("div", { key: `${entry.user?.id}-${entry.badge?.key}`, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, entry.user?.name || entry.user?.username), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-sky-200" }, entry.badge?.label), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, entry.badge?.reason))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No member badge unlocks yet."))))); } -const __vite_glob_0_149 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_151 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReputation }, Symbol.toStringTag, { value: "Module" })); @@ -106783,7 +106908,7 @@ function StudioGroupReviewQueue() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Submission queue"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Review artwork drafts before they publish under the group identity.")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300" }, listing.filters?.bucket || "submitted")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-4" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start gap-4" }, item.thumb ? /* @__PURE__ */ React.createElement("img", { src: item.thumb, alt: item.title, className: "h-24 w-24 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-24 w-24 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.03] text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300" }, item.group_review_status)), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-3 text-xs text-slate-400" }, item.primary_author ? /* @__PURE__ */ React.createElement("span", null, "Author: ", item.primary_author.name || item.primary_author.username) : null, item.uploader ? /* @__PURE__ */ React.createElement("span", null, "Uploader: ", item.uploader.name || item.uploader.username) : null, item.submitted_at ? /* @__PURE__ */ React.createElement("span", null, "Submitted ", new Date(item.submitted_at).toLocaleString()) : null), item.group_review_notes ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 rounded-2xl border border-white/10 bg-white/[0.03] px-3 py-2 text-sm text-slate-300" }, item.group_review_notes) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: item.urls?.edit, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Open draft"), item.can_review ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => sendAction(item, "approve"), className: "rounded-full border border-emerald-300/20 bg-emerald-400/10 px-4 py-2 text-sm font-semibold text-emerald-100" }, "Approve") : null, item.can_review ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => sendAction(item, "needs_changes"), className: "rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-sm font-semibold text-amber-100" }, "Needs changes") : null, item.can_review ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => sendAction(item, "reject"), className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100" }, "Reject") : null))))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-dashed border-white/10 bg-white/[0.02] p-5 text-sm text-slate-400" }, "No submissions in this bucket."))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-xl font-semibold text-white" }, "Recent history"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (Array.isArray(props.recentHistory) ? props.recentHistory : []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.id, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.summary || item.action_type), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-400" }, item.actor?.name || item.actor?.username || "System", " • ", item.created_at ? new Date(item.created_at).toLocaleString() : "Recently"))))))); } -const __vite_glob_0_150 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_152 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReviewQueue }, Symbol.toStringTag, { value: "Module" })); @@ -106876,7 +107001,7 @@ function StudioGroupSettings() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("section", { className: "mx-auto max-w-3xl rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5" }, /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Name"), /* @__PURE__ */ React.createElement("input", { value: form.name, onChange: (event) => setForm((current) => ({ ...current, name: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Slug"), /* @__PURE__ */ React.createElement("input", { value: form.slug, onChange: (event) => setForm((current) => ({ ...current, slug: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Short description"), /* @__PURE__ */ React.createElement("input", { value: form.headline, onChange: (event) => setForm((current) => ({ ...current, headline: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "About"), /* @__PURE__ */ React.createElement("textarea", { value: form.bio, onChange: (event) => setForm((current) => ({ ...current, bio: event.target.value })), rows: 6, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Type / category"), /* @__PURE__ */ React.createElement("input", { value: form.type, onChange: (event) => setForm((current) => ({ ...current, type: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Founded date"), /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.founded_at, onChange: (nextValue) => setForm((current) => ({ ...current, founded_at: nextValue })), mode: "date", placeholder: "Pick the founding date", clearable: true, className: "bg-black/20" }))), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Website"), /* @__PURE__ */ React.createElement("input", { value: form.website_url, onChange: (event) => setForm((current) => ({ ...current, website_url: event.target.value })), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, "Avatar / logo"), /* @__PURE__ */ React.createElement("div", { className: "flex h-28 w-28 items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]" }, resolvedAvatarPreview ? /* @__PURE__ */ React.createElement("img", { src: resolvedAvatarPreview, alt: "Avatar preview", className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-slate-500" })), /* @__PURE__ */ React.createElement("input", { ref: avatarInputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleFileSelected("avatar_file", setAvatarPreview), className: "hidden" }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => avatarInputRef.current?.click(), className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Upload avatar"), form.avatar_file ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => clearSelectedFile("avatar_file", setAvatarPreview, avatarInputRef), className: "rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300" }, "Use current path") : null), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Or paste an image URL"), /* @__PURE__ */ React.createElement("input", { value: form.avatar_path, onChange: (event) => setForm((current) => ({ ...current, avatar_path: event.target.value })), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, "Cover image"), /* @__PURE__ */ React.createElement("div", { className: "flex h-28 w-full items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]" }, resolvedBannerPreview ? /* @__PURE__ */ React.createElement("img", { src: resolvedBannerPreview, alt: "Cover preview", className: "h-full w-full object-cover" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-panorama text-slate-500" })), /* @__PURE__ */ React.createElement("input", { ref: bannerInputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleFileSelected("banner_file", setBannerPreview), className: "hidden" }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => bannerInputRef.current?.click(), className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white" }, "Upload cover"), form.banner_file ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => clearSelectedFile("banner_file", setBannerPreview, bannerInputRef), className: "rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300" }, "Use current path") : null), /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Or paste an image URL"), /* @__PURE__ */ React.createElement("input", { value: form.banner_path, onChange: (event) => setForm((current) => ({ ...current, banner_path: event.target.value })), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Featured artwork"), /* @__PURE__ */ React.createElement(NovaSelect, { value: String(form.featured_artwork_id || ""), onChange: (val) => setForm((current) => ({ ...current, featured_artwork_id: val })), placeholder: "Use latest published artwork", options: featuredArtworkOptions.map((item) => ({ value: String(item.id), label: item.title })) })), selectedFeaturedArtwork ? /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 rounded-[20px] border border-white/10 bg-white/[0.04] p-3" }, selectedFeaturedArtwork.thumb ? /* @__PURE__ */ React.createElement("img", { src: selectedFeaturedArtwork.thumb, alt: selectedFeaturedArtwork.title, className: "h-16 w-16 rounded-2xl object-cover" }) : null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "font-semibold text-white" }, selectedFeaturedArtwork.title), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, selectedFeaturedArtwork.author || "Group member"))) : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "When this is empty, the public overview falls back to the latest published works automatically.")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Visibility"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.visibility, onChange: (val) => setForm((current) => ({ ...current, visibility: val })), options: props.visibilityOptions || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", null, "Membership policy"), /* @__PURE__ */ React.createElement(NovaSelect, { value: form.membership_policy, onChange: (val) => setForm((current) => ({ ...current, membership_policy: val })), options: props.membershipPolicyOptions || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm text-slate-200" }, "Links"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: addLink, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white" }, "Add link")), form.links_json.map((item, index2) => /* @__PURE__ */ React.createElement("div", { key: `link-${index2}`, className: "grid gap-3 md:grid-cols-[0.8fr_1.2fr_auto]" }, /* @__PURE__ */ React.createElement("input", { value: item.label, onChange: (event) => updateLink(index2, "label", event.target.value), placeholder: "Label", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { value: item.url, onChange: (event) => updateLink(index2, "url", event.target.value), placeholder: "https://", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => removeLink(index2), className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100" }, "Remove")))), /* @__PURE__ */ React.createElement("div", { className: "flex justify-between gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: archiveGroup, className: "rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100" }, "Archive group"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: submit, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100" }, "Save settings"))))); } -const __vite_glob_0_151 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_153 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupSettings }, Symbol.toStringTag, { value: "Module" })); @@ -106904,7 +107029,7 @@ function StudioGroupsIndex() { } ), /* @__PURE__ */ React.createElement("div", { className: "mb-6 flex items-center justify-between gap-3 rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Collective publishing"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Launch and manage shared identities")), /* @__PURE__ */ React.createElement(xe, { href: props.endpoints?.create, className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, "Create group")), pendingInvites.length > 0 ? /* @__PURE__ */ React.createElement("section", { className: "mb-6 rounded-[28px] border border-amber-300/20 bg-amber-400/10 p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-amber-50" }, "Pending invites"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, pendingInvites.map((invite) => /* @__PURE__ */ React.createElement("article", { key: invite.id, className: "rounded-2xl border border-white/10 bg-black/20 p-4 text-white" }, /* @__PURE__ */ React.createElement("h3", { className: "text-base font-semibold" }, invite.group?.name), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-amber-50/80" }, "Role: ", invite.role), invite.invited_by ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-amber-50/70" }, "Invited by ", invite.invited_by.name || invite.invited_by.username) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(invite.accept_url), className: "rounded-full border border-emerald-300/20 bg-emerald-400/10 px-3 py-2 text-sm font-semibold text-emerald-100" }, "Accept"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(invite.decline_url), className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-semibold text-white" }, "Decline")))))) : null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 xl:grid-cols-2" }, groups.length > 0 ? groups.map((group) => /* @__PURE__ */ React.createElement(GroupCard, { key: group.slug, group })) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/10 px-6 py-16 text-center text-slate-400" }, "No groups yet. Create one to start publishing collaboratively."))); } -const __vite_glob_0_152 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_154 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -107005,7 +107130,7 @@ function StudioGrowth() { /* @__PURE__ */ React.createElement("div", { className: "mt-3 grid grid-cols-3 gap-3 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Views"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm font-semibold text-white" }, Number(item.metrics?.views || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Reactions"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm font-semibold text-white" }, Number(item.metrics?.appreciation || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", null, "Comments"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm font-semibold text-white" }, Number(item.metrics?.comments || 0).toLocaleString()))) )))))); } -const __vite_glob_0_153 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_155 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGrowth }, Symbol.toStringTag, { value: "Module" })); @@ -107065,7 +107190,7 @@ function StudioInbox() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description, actions: /* @__PURE__ */ React.createElement("button", { type: "button", onClick: markAllRead, disabled: marking, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-100 disabled:opacity-50" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-check-double" }), marking ? "Updating..." : "Mark all read") }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Unread"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.unread_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "High priority"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.high_priority_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Comments"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.comment_count || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Followers"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.follower_count || 0).toLocaleString()))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]" }, /* @__PURE__ */ React.createElement("aside", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Filters"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilters({ q: event.target.value }), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white", placeholder: "Actor, title, or module" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Type"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.type || "all", onChange: (val) => updateFilters({ type: val }), options: inbox.type_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Module"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.module || "all", onChange: (val) => updateFilters({ module: val }), options: inbox.module_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Read state"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.read_state || "all", onChange: (val) => updateFilters({ read_state: val }), options: inbox.read_state_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Priority"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.priority || "all", onChange: (val) => updateFilters({ priority: val }), options: inbox.priority_options || [], searchable: false })))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Attention now"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (inbox.panels?.attention_now || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.url, className: "block rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, item.module_label)))))), /* @__PURE__ */ React.createElement("section", { className: "space-y-4" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: `rounded-[28px] border p-5 ${item.is_new ? "border-sky-300/20 bg-sky-300/10" : "border-white/10 bg-white/[0.03]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-4" }, item.actor?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: item.actor.avatar_url, alt: item.actor.name || "Actor", className: "h-12 w-12 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 items-center justify-center rounded-2xl bg-black/20 text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-bell" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("span", null, item.module_label), /* @__PURE__ */ React.createElement("span", { className: `inline-flex items-center rounded-full border px-2 py-1 ${priorityClasses[item.priority] || priorityClasses.low}` }, item.priority), item.is_new && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-sky-300/20 px-2 py-1 text-sky-100" }, "Unread")), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-lg font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, item.body), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center gap-3 text-sm text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, formatDate$2(item.created_at)), item.actor?.name && /* @__PURE__ */ React.createElement("span", null, item.actor.name), /* @__PURE__ */ React.createElement("a", { href: item.url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-slate-200" }, "Open")))))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400" }, "No inbox items match this filter."), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) <= 1, onClick: () => updateFilters({ page: Math.max(1, (meta.current_page || 1) - 1) }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Previous"), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Page ", meta.current_page || 1, " of ", meta.last_page || 1), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) >= (meta.last_page || 1), onClick: () => updateFilters({ page: (meta.current_page || 1) + 1 }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Next")))))); } -const __vite_glob_0_154 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_156 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioInbox }, Symbol.toStringTag, { value: "Module" })); @@ -108822,7 +108947,7 @@ function StudioNewsEditor() { } )); } -const __vite_glob_0_155 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_157 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsEditor }, Symbol.toStringTag, { value: "Module" })); @@ -109008,7 +109133,7 @@ function StudioNewsIndex() { "Next" ))) : null); } -const __vite_glob_0_156 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_158 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -109041,7 +109166,7 @@ function StudioNewsTaxonomies() { tagForm.post(props.storeTagUrl); }, className: "mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-center" }, /* @__PURE__ */ React.createElement("input", { value: tagForm.data.name, onChange: (event) => tagForm.setData("name", event.target.value), placeholder: "Tag name", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { value: tagForm.data.slug, onChange: (event) => tagForm.setData("slug", event.target.value), placeholder: "optional slug", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100" }, "Create tag")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-3" }, tags.map((tag, index2) => /* @__PURE__ */ React.createElement("div", { key: tag.id, className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] md:items-center" }, /* @__PURE__ */ React.createElement("input", { value: tag.name, onChange: (event) => updateTag(index2, "name", event.target.value), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("input", { value: tag.slug, onChange: (event) => updateTag(index2, "slug", event.target.value), className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.14em] text-slate-500" }, Number(tag.published_count || 0).toLocaleString(), " published"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => saveTag(tag), className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm font-semibold text-white" }, "Save")))))))); } -const __vite_glob_0_157 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_159 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsTaxonomies }, Symbol.toStringTag, { value: "Module" })); @@ -109191,7 +109316,7 @@ function StudioPreferences() { return /* @__PURE__ */ React.createElement("div", { key: widgetKey, className: "flex flex-col gap-3 rounded-[22px] border border-white/10 bg-black/20 p-4 md:flex-row md:items-center md:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, option.label), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, "Position ", index2 + 1)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => toggleWidget(widgetKey), className: `rounded-full border px-3 py-1.5 text-xs ${enabled ? "border-sky-300/25 bg-sky-300/10 text-sky-100" : "border-white/10 text-slate-300"}` }, enabled ? "Visible" : "Hidden"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => moveWidget(widgetKey, "up"), className: "rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-300" }, "Up"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => moveWidget(widgetKey, "down"), className: "rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-300" }, "Down"))); })))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Related surfaces"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (props.links || []).map((link2) => /* @__PURE__ */ React.createElement("a", { key: link2.url, href: link2.url, className: "block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: link2.icon }), /* @__PURE__ */ React.createElement("span", { className: "text-base font-semibold text-white" }, link2.label)))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Preference notes"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3 text-sm text-slate-400" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, "Landing page and widget order are stored in the shared Studio preference record, so new Creator Studio surfaces can plug into the same contract without another migration."), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, "Analytics range and card density stay here so Analytics, Growth, and the main dashboard can stay visually consistent.")))))); } -const __vite_glob_0_158 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_160 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioPreferences }, Symbol.toStringTag, { value: "Module" })); @@ -109371,7 +109496,7 @@ function StudioProfile() { /* @__PURE__ */ React.createElement("div", { className: "p-6 pt-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-end gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "relative" }, profile.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: profile.avatar_url, alt: profile.username, className: "h-24 w-24 rounded-[28px] border border-white/10 object-cover shadow-lg" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-24 w-24 items-center justify-center rounded-[28px] border border-white/10 bg-black/30 text-slate-400 shadow-lg" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user text-2xl" })), /* @__PURE__ */ React.createElement("input", { ref: avatarInputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleAvatarSelected, className: "hidden" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => avatarInputRef.current?.click(), disabled: uploadingAvatar, className: "absolute -bottom-2 -right-2 inline-flex h-10 w-10 items-center justify-center rounded-full border border-sky-300/25 bg-sky-300/15 text-sky-100 disabled:opacity-50" }, /* @__PURE__ */ React.createElement("i", { className: `fa-solid ${uploadingAvatar ? "fa-spinner fa-spin" : "fa-camera"}` }))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-3xl font-semibold text-white" }, profile.name), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-300" }, "@", profile.username), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap gap-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", null, Number(profile.followers || 0).toLocaleString(), " followers"), profile.location && /* @__PURE__ */ React.createElement("span", null, profile.location)))), profile.cover_url && /* @__PURE__ */ React.createElement("div", { className: "w-full max-w-sm rounded-[24px] border border-white/10 bg-black/30 p-4" }, /* @__PURE__ */ React.createElement("label", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Banner position"), /* @__PURE__ */ React.createElement("input", { type: "range", min: "0", max: "100", value: coverPosition, onChange: (event) => setCoverPosition(Number(event.target.value)), className: "mt-3 w-full" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: saveCoverPosition, disabled: savingCoverPosition, className: "mt-3 inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-white disabled:opacity-50" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrows-up-down" }), savingCoverPosition ? "Saving..." : "Save banner position")))) )), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Public profile details"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Update the creator information that supports your public presence across Nova.")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: saveProfile, disabled: savingProfile, className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 disabled:opacity-50" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-floppy-disk" }), savingProfile ? "Saving..." : "Save profile")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 md:col-span-2" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Display name"), /* @__PURE__ */ React.createElement("input", { value: form.display_name, onChange: (event) => setForm((current) => ({ ...current, display_name: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white outline-none" })), /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 md:col-span-2" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Tagline"), /* @__PURE__ */ React.createElement("input", { value: form.tagline, onChange: (event) => setForm((current) => ({ ...current, tagline: event.target.value })), placeholder: "One-line creator summary", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500" })), /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 md:col-span-2" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Bio"), /* @__PURE__ */ React.createElement("textarea", { value: form.bio, onChange: (event) => setForm((current) => ({ ...current, bio: event.target.value })), rows: 5, placeholder: "Tell visitors what you create and what makes your work distinct.", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500" })), /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 md:col-span-2" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Website"), /* @__PURE__ */ React.createElement("input", { value: form.website, onChange: (event) => setForm((current) => ({ ...current, website: event.target.value })), placeholder: "https://example.com", className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "text-base font-semibold text-white" }, "Social links"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-slate-400" }, "Add the channels that matter for your creator identity.")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: addSocialLink, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-white" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-plus" }), "Add link")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, form.social_links.map((link2, index2) => /* @__PURE__ */ React.createElement("div", { key: `${index2}-${link2.platform}`, className: "grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 md:grid-cols-[180px_minmax(0,1fr)_auto]" }, /* @__PURE__ */ React.createElement("input", { value: link2.platform, onChange: (event) => updateSocialLink(index2, "platform", event.target.value), placeholder: "instagram", className: "rounded-2xl border border-white/10 bg-black/30 px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500" }), /* @__PURE__ */ React.createElement("input", { value: link2.url, onChange: (event) => updateSocialLink(index2, "url", event.target.value), placeholder: "https://...", className: "rounded-2xl border border-white/10 bg-black/30 px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => removeSocialLink(index2), className: "inline-flex items-center justify-center rounded-2xl border border-rose-300/20 bg-rose-300/10 px-4 py-3 text-sm text-rose-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-trash" }))))))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Publishing footprint"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4" }, (props.moduleSummaries || []).map((item) => /* @__PURE__ */ React.createElement("div", { key: item.key, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-slate-200" }, /* @__PURE__ */ React.createElement("i", { className: item.icon }), /* @__PURE__ */ React.createElement("span", null, item.label)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-3xl font-semibold text-white" }, Number(item.count || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, Number(item.published_count || 0).toLocaleString(), " published, ", Number(item.draft_count || 0).toLocaleString(), " drafts"))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Featured identity"), /* @__PURE__ */ React.createElement("a", { href: "/studio/featured", className: "text-sm font-medium text-sky-100" }, "Manage featured")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, featuredModules.length > 0 ? featuredModules.map((module) => /* @__PURE__ */ React.createElement("span", { key: module, className: "inline-flex items-center rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-sky-100" }, socialPlatformLabel(module))) : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-slate-400" }, "No featured modules selected yet.")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, Object.entries(featuredContent).map(([module, item]) => item ? /* @__PURE__ */ React.createElement("a", { key: module, href: item.view_url || item.preview_url || "/studio/featured", className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 p-3" }, item.image_url ? /* @__PURE__ */ React.createElement("img", { src: item.image_url, alt: item.title, className: "h-14 w-14 rounded-2xl object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-14 w-14 items-center justify-center rounded-2xl bg-white/5 text-slate-400" }, /* @__PURE__ */ React.createElement("i", { className: item.module_icon || "fa-solid fa-star" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, socialPlatformLabel(module)), /* @__PURE__ */ React.createElement("div", { className: "truncate text-sm font-semibold text-white" }, item.title))) : null))))))); } -const __vite_glob_0_159 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_161 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioProfile }, Symbol.toStringTag, { value: "Module" })); @@ -109448,7 +109573,7 @@ function StudioScheduled() { }, [items, summary.next_publish_at]); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Scheduled total"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-3xl font-semibold text-white" }, Number(summary.total || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4 md:col-span-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Next publish slot"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xl font-semibold text-white" }, formatReleaseCountdown(summary.next_publish_at, nowMs)), summary.next_publish_at && /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, formatScheduledDate(summary.next_publish_at)))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4" }, (summary.by_module || []).map((entry) => /* @__PURE__ */ React.createElement("div", { key: entry.key, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-slate-300" }, /* @__PURE__ */ React.createElement("i", { className: entry.icon }), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium text-white" }, entry.label)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-2xl font-semibold text-white" }, Number(entry.count || 0).toLocaleString()))))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Agenda"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, agenda.length > 0 ? agenda.slice(0, 6).map((day) => /* @__PURE__ */ React.createElement("div", { key: day.date, className: "rounded-[22px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, day.label), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, day.count, " items")), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, day.items.slice(0, 2).map((item) => item.title).join(" • ")))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-dashed border-white/15 px-4 py-8 text-sm text-slate-400" }, "No scheduled items yet.")))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search scheduled work"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilters({ q: event.target.value }), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white", placeholder: "Title or module" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Module"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.module || "all", onChange: (val) => updateFilters({ module: val }), options: listing.module_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Date range"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.range || "upcoming", onChange: (val) => updateFilters({ range: val }), options: rangeOptions2, searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Start date"), /* @__PURE__ */ React.createElement(DateTimePicker, { value: filters.start_date || "", onChange: (nextValue) => updateFilters({ range: "custom", start_date: nextValue }), mode: "date", placeholder: "Start date", clearable: true, className: "bg-black/20" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "End date"), /* @__PURE__ */ React.createElement(DateTimePicker, { value: filters.end_date || "", onChange: (nextValue) => updateFilters({ range: "custom", end_date: nextValue }), mode: "date", placeholder: "End date", clearable: true, className: "bg-black/20" })), /* @__PURE__ */ React.createElement("div", { className: "flex items-end" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => updateFilters({ q: "", module: "all", range: "upcoming", start_date: "", end_date: "" }), className: "w-full rounded-2xl border border-white/10 px-4 py-3 text-sm text-slate-200" }, "Reset")))), /* @__PURE__ */ React.createElement("section", { className: "space-y-4" }, items.length > 0 ? items.map((item) => /* @__PURE__ */ React.createElement("article", { key: item.id, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70" }, /* @__PURE__ */ React.createElement("span", null, item.module_label), /* @__PURE__ */ React.createElement("span", null, item.status)), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-xl font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap items-center gap-4 text-sm text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, formatReleaseCountdown(item.scheduled_at || item.published_at, nowMs)), /* @__PURE__ */ React.createElement("span", null, formatScheduledDate(item.scheduled_at || item.published_at)), item.visibility && /* @__PURE__ */ React.createElement("span", null, "Visibility: ", item.visibility), item.updated_at && /* @__PURE__ */ React.createElement("span", null, "Last edited ", formatScheduledDate(item.updated_at)), item.schedule_timezone && /* @__PURE__ */ React.createElement("span", null, item.schedule_timezone))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: item.edit_url || item.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200" }, "Edit"), /* @__PURE__ */ React.createElement("a", { href: item.edit_url || item.manage_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200" }, "Reschedule"), item.preview_url && /* @__PURE__ */ React.createElement("a", { href: item.preview_url, className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200" }, "Preview"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busyId === `publish:${item.id}`, onClick: () => runAction(item, "publish"), className: "inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm text-sky-100 disabled:opacity-50" }, "Publish now"), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busyId === `unschedule:${item.id}`, onClick: () => runAction(item, "unschedule"), className: "inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200 disabled:opacity-50" }, "Unschedule"))))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400" }, "No scheduled content matches this view.")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) <= 1, onClick: () => updateFilters({ page: Math.max(1, (meta.current_page || 1) - 1) }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Previous"), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Page ", meta.current_page || 1, " of ", meta.last_page || 1), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: (meta.current_page || 1) >= (meta.last_page || 1), onClick: () => updateFilters({ page: (meta.current_page || 1) + 1 }), className: "rounded-full border border-white/10 px-4 py-2 disabled:opacity-40" }, "Next")))); } -const __vite_glob_0_160 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_162 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioScheduled }, Symbol.toStringTag, { value: "Module" })); @@ -109466,7 +109591,7 @@ function StudioSearch() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 md:grid-cols-2 xl:grid-cols-5" }, /* @__PURE__ */ React.createElement("label", { className: "space-y-2 text-sm text-slate-300 xl:col-span-3" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Search Studio"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilters({ q: event.target.value }), className: "w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white", placeholder: "Search content, comments, inbox, or assets" })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Surface"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.type || "all", onChange: (val) => updateFilters({ type: val }), options: search2.type_options || [], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "space-y-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Module"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.module || "all", onChange: (val) => updateFilters({ module: val }), options: search2.module_options || [], searchable: false })))), filters.q ? /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-400" }, "Found ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, Number(search2.summary?.total || 0).toLocaleString()), " matches for ", /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, search2.summary?.query)), sections.length > 0 ? sections.map((section) => /* @__PURE__ */ React.createElement("section", { key: section.key, className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, section.label), /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, section.count, " matches")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, section.items.map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.href, className: "block rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 items-center justify-center rounded-2xl bg-white/[0.04] text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: item.icon })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "truncate text-base font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, item.subtitle), /* @__PURE__ */ React.createElement("p", { className: "mt-3 line-clamp-3 text-sm leading-6 text-slate-400" }, item.description)))))))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400" }, "No results matched this search yet.")) : /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Continue working"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2" }, (search2.empty_state?.continue_working || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.edit_url || item.manage_url, className: "block rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, item.module_label, " · ", item.workflow?.readiness?.label))))), /* @__PURE__ */ React.createElement("aside", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Stale drafts"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (search2.empty_state?.stale_drafts || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.id, href: item.edit_url || item.manage_url, className: "block rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, item.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-500" }, item.module_label))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "Quick create"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3" }, (props.quickCreate || []).map((item) => /* @__PURE__ */ React.createElement("a", { key: item.key, href: item.url, className: "inline-flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-100" }, /* @__PURE__ */ React.createElement("i", { className: item.icon }), /* @__PURE__ */ React.createElement("span", null, "New ", item.label))))))))); } -const __vite_glob_0_161 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_163 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioSearch }, Symbol.toStringTag, { value: "Module" })); @@ -109474,7 +109599,7 @@ function StudioSettings() { const { props } = X$1(); return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, "System handoff"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-6 text-slate-400" }, "Studio now keeps creator workflow preferences in their own surface. This page stays focused on links out to adjacent dashboards and the control points that do not belong in the day-to-day workflow UI."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 md:grid-cols-2" }, (props.links || []).map((link2) => /* @__PURE__ */ React.createElement("a", { key: link2.url, href: link2.url, className: "rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20 hover:bg-black/30" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: link2.icon }), /* @__PURE__ */ React.createElement("span", { className: "text-base font-semibold text-white" }, link2.label)), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, "Open the linked dashboard or settings surface without losing the Studio navigation shell as the default control plane."))))), /* @__PURE__ */ React.createElement("section", { className: "space-y-6" }, (props.sections || []).map((section) => /* @__PURE__ */ React.createElement("div", { key: section.title, className: "rounded-[30px] border border-white/10 bg-white/[0.03] p-6" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold text-white" }, section.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6 text-slate-400" }, section.body), /* @__PURE__ */ React.createElement("a", { href: section.href, className: "mt-4 inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, section.cta, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }))))))); } -const __vite_glob_0_162 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_164 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioSettings }, Symbol.toStringTag, { value: "Module" })); @@ -109495,7 +109620,7 @@ function StudioStories() { } )); } -const __vite_glob_0_163 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_165 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioStories }, Symbol.toStringTag, { value: "Module" })); @@ -110158,7 +110283,7 @@ function StudioUploadQueue() { ))))); }))))); } -const __vite_glob_0_164 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_166 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioUploadQueue }, Symbol.toStringTag, { value: "Module" })); @@ -111890,7 +112015,7 @@ function StudioWorldEditor() { )) : null )); } -const __vite_glob_0_165 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_167 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioWorldEditor }, Symbol.toStringTag, { value: "Module" })); @@ -111976,7 +112101,7 @@ function StudioWorldsIndex() { }; return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6" }, /* @__PURE__ */ React.createElement(WorldAnalyticsPortfolioPanel, { analytics: props.analytics }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_12rem_12rem_auto] lg:items-end" }, /* @__PURE__ */ React.createElement("label", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Search"), /* @__PURE__ */ React.createElement("input", { value: filters.q || "", onChange: (event) => updateFilter("q", event.target.value), placeholder: "Search title, slug, or summary", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Status"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.status || "", onChange: (val) => updateFilter("status", val), options: [{ value: "", label: "All statuses" }, ...props.statusOptions || []], searchable: false })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Type"), /* @__PURE__ */ React.createElement(NovaSelect, { value: filters.type || "", onChange: (val) => updateFilter("type", val), options: [{ value: "", label: "All types" }, ...props.typeOptions || []], searchable: false })), /* @__PURE__ */ React.createElement("a", { href: props.createUrl, className: "inline-flex items-center justify-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-plus" }), "New world"))), /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 xl:grid-cols-2" }, items.length > 0 ? items.map((world) => /* @__PURE__ */ React.createElement("a", { key: world.id, href: world.edit_url, className: "rounded-[28px] border border-white/10 bg-white/[0.03] p-5 transition hover:-translate-y-1 hover:border-white/20 hover:bg-white/[0.05]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement(WorldStatusBadge, { badge: { label: world.status, tone: "slate" } }), /* @__PURE__ */ React.createElement(WorldStatusBadge, { badge: { label: world.type, tone: "slate" } }), (Array.isArray(world.status_badges) ? world.status_badges : []).map((badge) => /* @__PURE__ */ React.createElement(WorldStatusBadge, { key: `${world.id}-${badge.label}`, badge }))), /* @__PURE__ */ React.createElement("h2", { className: "mt-4 text-2xl font-semibold tracking-[-0.03em] text-white" }, world.title), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-500" }, "/", world.slug), world.summary ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-6 text-slate-300" }, world.summary) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-4 text-sm text-slate-400" }, world.timeframe_label ? /* @__PURE__ */ React.createElement("span", null, world.timeframe_label) : null, world.promotion_window_label ? /* @__PURE__ */ React.createElement("span", null, world.promotion_window_label) : null, /* @__PURE__ */ React.createElement("span", null, world.relation_count, " relations"), world.live_submission_count > 0 ? /* @__PURE__ */ React.createElement("span", null, world.live_submission_count, " live submissions") : null, world.theme_key ? /* @__PURE__ */ React.createElement("span", null, world.theme_key) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3 text-sm font-semibold" }, /* @__PURE__ */ React.createElement("span", { className: "text-sky-100" }, "Edit"), /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Preview"), world.public_url ? /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Public") : null))) : /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400" }, "No worlds match this filter yet.")))); } -const __vite_glob_0_166 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_168 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioWorldsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -116610,7 +116735,7 @@ function UploadPage({ draftId = null, filesCdnUrl = "", chunkSize, chunkRequestT "Reset" )))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/5 p-6" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold" }, "Pipeline status"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white/60" }, "Stage: ", /* @__PURE__ */ React.createElement("span", { className: "text-white" }, statusLabel2)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 h-2 w-full overflow-hidden rounded-full bg-white/10" }, /* @__PURE__ */ React.createElement("div", { className: "h-full bg-sky-400 transition-all", style: { width: `${state.progress}%` } })), state.failureReason && /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-sm text-red-200" }, "Failure: ", state.failureReason), state.previewUrl && state.phase === phases.success && /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white/80" }, "CDN preview"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 overflow-hidden rounded-xl border border-white/10" }, /* @__PURE__ */ React.createElement("img", { src: state.previewUrl, alt: "CDN preview", className: "h-56 w-full object-cover" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-xs text-white/60" }, "Session: ", state.sessionId ?? "—")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-white/5 p-6" }, /* @__PURE__ */ React.createElement("h3", { className: "text-lg font-semibold" }, "Draft resume"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-white/60" }, "Use the draft link to resume an interrupted upload."), draftId ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-sm text-white/80" }, "Draft ID: ", draftId) : /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-sm text-white/50" }, "No draft loaded.")))))); } -const __vite_glob_0_167 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_169 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UploadPage }, Symbol.toStringTag, { value: "Module" })); @@ -116854,7 +116979,7 @@ function WorldIndex() { } ))); } -const __vite_glob_0_168 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_170 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldIndex }, Symbol.toStringTag, { value: "Module" })); @@ -117155,7 +117280,7 @@ function WorldShow() { } ))); } -const __vite_glob_0_169 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_171 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldShow }, Symbol.toStringTag, { value: "Module" })); @@ -142781,141 +142906,143 @@ const pages = /* @__PURE__ */ Object.assign({ "./Pages/Admin/HomepageAnnouncements/Index.jsx": __vite_glob_0_32, "./Pages/Admin/Settings.jsx": __vite_glob_0_33, "./Pages/Admin/Stories.jsx": __vite_glob_0_34, - "./Pages/Admin/UploadQueue.jsx": __vite_glob_0_35, - "./Pages/Admin/UsernameQueue.jsx": __vite_glob_0_36, - "./Pages/Admin/Users/Index.jsx": __vite_glob_0_37, - "./Pages/Artwork/SimilarArtworksHeader.jsx": __vite_glob_0_38, - "./Pages/ArtworkPage.jsx": __vite_glob_0_39, - "./Pages/CategoriesPage.jsx": __vite_glob_0_40, - "./Pages/Collection/CollectionAnalytics.jsx": __vite_glob_0_41, - "./Pages/Collection/CollectionDashboard.jsx": __vite_glob_0_42, - "./Pages/Collection/CollectionFeaturedIndex.jsx": __vite_glob_0_43, - "./Pages/Collection/CollectionHistory.jsx": __vite_glob_0_44, - "./Pages/Collection/CollectionManage.jsx": __vite_glob_0_45, - "./Pages/Collection/CollectionSeriesShow.jsx": __vite_glob_0_46, - "./Pages/Collection/CollectionShow.jsx": __vite_glob_0_47, - "./Pages/Collection/CollectionStaffProgramming.jsx": __vite_glob_0_48, - "./Pages/Collection/CollectionStaffSurfaces.jsx": __vite_glob_0_49, - "./Pages/Collection/FeaturedArtworksAdmin.jsx": __vite_glob_0_50, - "./Pages/Collection/NovaCardsAdminIndex.jsx": __vite_glob_0_51, - "./Pages/Collection/NovaCardsAssetPackAdmin.jsx": __vite_glob_0_52, - "./Pages/Collection/NovaCardsChallengeAdmin.jsx": __vite_glob_0_53, - "./Pages/Collection/NovaCardsCollectionAdmin.jsx": __vite_glob_0_54, - "./Pages/Collection/NovaCardsTemplateAdmin.jsx": __vite_glob_0_55, - "./Pages/Collection/SavedCollections.jsx": __vite_glob_0_56, - "./Pages/Community/CommunityActivityPage.jsx": __vite_glob_0_57, - "./Pages/Community/LatestCommentsPage.jsx": __vite_glob_0_58, - "./Pages/Enhance/Create.jsx": __vite_glob_0_59, - "./Pages/Enhance/Index.jsx": __vite_glob_0_60, - "./Pages/Enhance/Show.jsx": __vite_glob_0_61, - "./Pages/Feed/FollowingFeed.jsx": __vite_glob_0_62, - "./Pages/Feed/HashtagFeed.jsx": __vite_glob_0_63, - "./Pages/Feed/SavedFeed.jsx": __vite_glob_0_64, - "./Pages/Feed/SearchFeed.jsx": __vite_glob_0_65, - "./Pages/Feed/TrendingFeed.jsx": __vite_glob_0_66, - "./Pages/Forum/ForumCategory.jsx": __vite_glob_0_67, - "./Pages/Forum/ForumEditPost.jsx": __vite_glob_0_68, - "./Pages/Forum/ForumIndex.jsx": __vite_glob_0_69, - "./Pages/Forum/ForumNewThread.jsx": __vite_glob_0_70, - "./Pages/Forum/ForumSection.jsx": __vite_glob_0_71, - "./Pages/Forum/ForumThread.jsx": __vite_glob_0_72, - "./Pages/Group/GroupChallengeShow.jsx": __vite_glob_0_73, - "./Pages/Group/GroupEventShow.jsx": __vite_glob_0_74, - "./Pages/Group/GroupFaqPage.jsx": __vite_glob_0_75, - "./Pages/Group/GroupHelpPage.jsx": __vite_glob_0_76, - "./Pages/Group/GroupIndex.jsx": __vite_glob_0_77, - "./Pages/Group/GroupPostShow.jsx": __vite_glob_0_78, - "./Pages/Group/GroupProjectShow.jsx": __vite_glob_0_79, - "./Pages/Group/GroupQuickstartPage.jsx": __vite_glob_0_80, - "./Pages/Group/GroupReleaseShow.jsx": __vite_glob_0_81, - "./Pages/Group/GroupShow.jsx": __vite_glob_0_82, - "./Pages/Help/AccountHelpPage.jsx": __vite_glob_0_83, - "./Pages/Help/AuthHelpPage.jsx": __vite_glob_0_84, - "./Pages/Help/CardsHelpPage.jsx": __vite_glob_0_85, - "./Pages/Help/HelpCenterPage.jsx": __vite_glob_0_86, - "./Pages/Help/ProfileHelpPage.jsx": __vite_glob_0_87, - "./Pages/Help/StudioHelpPage.jsx": __vite_glob_0_88, - "./Pages/Help/TroubleshootingHelpPage.jsx": __vite_glob_0_89, - "./Pages/Help/UploadHelpPage.jsx": __vite_glob_0_90, - "./Pages/Help/WorldsHelpPage.jsx": __vite_glob_0_91, - "./Pages/Leaderboard/LeaderboardPage.jsx": __vite_glob_0_92, - "./Pages/Messages/Index.jsx": __vite_glob_0_93, - "./Pages/Moderation/AiBiographyAdmin.jsx": __vite_glob_0_94, - "./Pages/Moderation/ArtworkMaturityQueue.jsx": __vite_glob_0_95, - "./Pages/Moderation/Enhance/Index.jsx": __vite_glob_0_96, - "./Pages/Moderation/Enhance/Show.jsx": __vite_glob_0_97, - "./Pages/Moderation/FeaturedArtworks.jsx": __vite_glob_0_98, - "./Pages/Moderation/StaffApplications/Index.jsx": __vite_glob_0_99, - "./Pages/Moderation/StaffApplications/Show.jsx": __vite_glob_0_100, - "./Pages/Moderation/Stories.jsx": __vite_glob_0_101, - "./Pages/Moderation/UsernameQueue.jsx": __vite_glob_0_102, - "./Pages/Moderation/WorldWebStoriesIndex.jsx": __vite_glob_0_103, - "./Pages/Moderation/WorldWebStoryEditor.jsx": __vite_glob_0_104, - "./Pages/News/NewsComments.jsx": __vite_glob_0_105, - "./Pages/News/NewsImagePreview.jsx": __vite_glob_0_106, - "./Pages/Profile/ProfileGallery.jsx": __vite_glob_0_107, - "./Pages/Profile/ProfileShow.jsx": __vite_glob_0_108, - "./Pages/Settings/ProfileEdit.jsx": __vite_glob_0_109, - "./Pages/Studio/StudioActivity.jsx": __vite_glob_0_110, - "./Pages/Studio/StudioAnalytics.jsx": __vite_glob_0_111, - "./Pages/Studio/StudioArchived.jsx": __vite_glob_0_112, - "./Pages/Studio/StudioArtworkAnalytics.jsx": __vite_glob_0_113, - "./Pages/Studio/StudioArtworkEdit.jsx": __vite_glob_0_114, - "./Pages/Studio/StudioArtworks.jsx": __vite_glob_0_115, - "./Pages/Studio/StudioAssets.jsx": __vite_glob_0_116, - "./Pages/Studio/StudioCalendar.jsx": __vite_glob_0_117, - "./Pages/Studio/StudioCardAnalytics.jsx": __vite_glob_0_118, - "./Pages/Studio/StudioCardEditor.jsx": __vite_glob_0_119, - "./Pages/Studio/StudioCardsIndex.jsx": __vite_glob_0_120, - "./Pages/Studio/StudioChallenges.jsx": __vite_glob_0_121, - "./Pages/Studio/StudioCollections.jsx": __vite_glob_0_122, - "./Pages/Studio/StudioComments.jsx": __vite_glob_0_123, - "./Pages/Studio/StudioContentIndex.jsx": __vite_glob_0_124, - "./Pages/Studio/StudioDashboard.jsx": __vite_glob_0_125, - "./Pages/Studio/StudioDrafts.jsx": __vite_glob_0_126, - "./Pages/Studio/StudioFeatured.jsx": __vite_glob_0_127, - "./Pages/Studio/StudioFollowers.jsx": __vite_glob_0_128, - "./Pages/Studio/StudioGroupActivity.jsx": __vite_glob_0_129, - "./Pages/Studio/StudioGroupArtworks.jsx": __vite_glob_0_130, - "./Pages/Studio/StudioGroupAssets.jsx": __vite_glob_0_131, - "./Pages/Studio/StudioGroupChallengeEditor.jsx": __vite_glob_0_132, - "./Pages/Studio/StudioGroupChallenges.jsx": __vite_glob_0_133, - "./Pages/Studio/StudioGroupCollections.jsx": __vite_glob_0_134, - "./Pages/Studio/StudioGroupCreate.jsx": __vite_glob_0_135, - "./Pages/Studio/StudioGroupDashboard.jsx": __vite_glob_0_136, - "./Pages/Studio/StudioGroupEventEditor.jsx": __vite_glob_0_137, - "./Pages/Studio/StudioGroupEvents.jsx": __vite_glob_0_138, - "./Pages/Studio/StudioGroupInvitations.jsx": __vite_glob_0_139, - "./Pages/Studio/StudioGroupJoinRequests.jsx": __vite_glob_0_140, - "./Pages/Studio/StudioGroupMembers.jsx": __vite_glob_0_141, - "./Pages/Studio/StudioGroupPostEditor.jsx": __vite_glob_0_142, - "./Pages/Studio/StudioGroupPosts.jsx": __vite_glob_0_143, - "./Pages/Studio/StudioGroupProjectEditor.jsx": __vite_glob_0_144, - "./Pages/Studio/StudioGroupProjects.jsx": __vite_glob_0_145, - "./Pages/Studio/StudioGroupRecruitment.jsx": __vite_glob_0_146, - "./Pages/Studio/StudioGroupReleaseEditor.jsx": __vite_glob_0_147, - "./Pages/Studio/StudioGroupReleases.jsx": __vite_glob_0_148, - "./Pages/Studio/StudioGroupReputation.jsx": __vite_glob_0_149, - "./Pages/Studio/StudioGroupReviewQueue.jsx": __vite_glob_0_150, - "./Pages/Studio/StudioGroupSettings.jsx": __vite_glob_0_151, - "./Pages/Studio/StudioGroupsIndex.jsx": __vite_glob_0_152, - "./Pages/Studio/StudioGrowth.jsx": __vite_glob_0_153, - "./Pages/Studio/StudioInbox.jsx": __vite_glob_0_154, - "./Pages/Studio/StudioNewsEditor.jsx": __vite_glob_0_155, - "./Pages/Studio/StudioNewsIndex.jsx": __vite_glob_0_156, - "./Pages/Studio/StudioNewsTaxonomies.jsx": __vite_glob_0_157, - "./Pages/Studio/StudioPreferences.jsx": __vite_glob_0_158, - "./Pages/Studio/StudioProfile.jsx": __vite_glob_0_159, - "./Pages/Studio/StudioScheduled.jsx": __vite_glob_0_160, - "./Pages/Studio/StudioSearch.jsx": __vite_glob_0_161, - "./Pages/Studio/StudioSettings.jsx": __vite_glob_0_162, - "./Pages/Studio/StudioStories.jsx": __vite_glob_0_163, - "./Pages/Studio/StudioUploadQueue.jsx": __vite_glob_0_164, - "./Pages/Studio/StudioWorldEditor.jsx": __vite_glob_0_165, - "./Pages/Studio/StudioWorldsIndex.jsx": __vite_glob_0_166, - "./Pages/Upload/Index.jsx": __vite_glob_0_167, - "./Pages/World/WorldIndex.jsx": __vite_glob_0_168, - "./Pages/World/WorldShow.jsx": __vite_glob_0_169 + "./Pages/Admin/System/SecurityReportIndex.jsx": __vite_glob_0_35, + "./Pages/Admin/System/SecurityReportShow.jsx": __vite_glob_0_36, + "./Pages/Admin/UploadQueue.jsx": __vite_glob_0_37, + "./Pages/Admin/UsernameQueue.jsx": __vite_glob_0_38, + "./Pages/Admin/Users/Index.jsx": __vite_glob_0_39, + "./Pages/Artwork/SimilarArtworksHeader.jsx": __vite_glob_0_40, + "./Pages/ArtworkPage.jsx": __vite_glob_0_41, + "./Pages/CategoriesPage.jsx": __vite_glob_0_42, + "./Pages/Collection/CollectionAnalytics.jsx": __vite_glob_0_43, + "./Pages/Collection/CollectionDashboard.jsx": __vite_glob_0_44, + "./Pages/Collection/CollectionFeaturedIndex.jsx": __vite_glob_0_45, + "./Pages/Collection/CollectionHistory.jsx": __vite_glob_0_46, + "./Pages/Collection/CollectionManage.jsx": __vite_glob_0_47, + "./Pages/Collection/CollectionSeriesShow.jsx": __vite_glob_0_48, + "./Pages/Collection/CollectionShow.jsx": __vite_glob_0_49, + "./Pages/Collection/CollectionStaffProgramming.jsx": __vite_glob_0_50, + "./Pages/Collection/CollectionStaffSurfaces.jsx": __vite_glob_0_51, + "./Pages/Collection/FeaturedArtworksAdmin.jsx": __vite_glob_0_52, + "./Pages/Collection/NovaCardsAdminIndex.jsx": __vite_glob_0_53, + "./Pages/Collection/NovaCardsAssetPackAdmin.jsx": __vite_glob_0_54, + "./Pages/Collection/NovaCardsChallengeAdmin.jsx": __vite_glob_0_55, + "./Pages/Collection/NovaCardsCollectionAdmin.jsx": __vite_glob_0_56, + "./Pages/Collection/NovaCardsTemplateAdmin.jsx": __vite_glob_0_57, + "./Pages/Collection/SavedCollections.jsx": __vite_glob_0_58, + "./Pages/Community/CommunityActivityPage.jsx": __vite_glob_0_59, + "./Pages/Community/LatestCommentsPage.jsx": __vite_glob_0_60, + "./Pages/Enhance/Create.jsx": __vite_glob_0_61, + "./Pages/Enhance/Index.jsx": __vite_glob_0_62, + "./Pages/Enhance/Show.jsx": __vite_glob_0_63, + "./Pages/Feed/FollowingFeed.jsx": __vite_glob_0_64, + "./Pages/Feed/HashtagFeed.jsx": __vite_glob_0_65, + "./Pages/Feed/SavedFeed.jsx": __vite_glob_0_66, + "./Pages/Feed/SearchFeed.jsx": __vite_glob_0_67, + "./Pages/Feed/TrendingFeed.jsx": __vite_glob_0_68, + "./Pages/Forum/ForumCategory.jsx": __vite_glob_0_69, + "./Pages/Forum/ForumEditPost.jsx": __vite_glob_0_70, + "./Pages/Forum/ForumIndex.jsx": __vite_glob_0_71, + "./Pages/Forum/ForumNewThread.jsx": __vite_glob_0_72, + "./Pages/Forum/ForumSection.jsx": __vite_glob_0_73, + "./Pages/Forum/ForumThread.jsx": __vite_glob_0_74, + "./Pages/Group/GroupChallengeShow.jsx": __vite_glob_0_75, + "./Pages/Group/GroupEventShow.jsx": __vite_glob_0_76, + "./Pages/Group/GroupFaqPage.jsx": __vite_glob_0_77, + "./Pages/Group/GroupHelpPage.jsx": __vite_glob_0_78, + "./Pages/Group/GroupIndex.jsx": __vite_glob_0_79, + "./Pages/Group/GroupPostShow.jsx": __vite_glob_0_80, + "./Pages/Group/GroupProjectShow.jsx": __vite_glob_0_81, + "./Pages/Group/GroupQuickstartPage.jsx": __vite_glob_0_82, + "./Pages/Group/GroupReleaseShow.jsx": __vite_glob_0_83, + "./Pages/Group/GroupShow.jsx": __vite_glob_0_84, + "./Pages/Help/AccountHelpPage.jsx": __vite_glob_0_85, + "./Pages/Help/AuthHelpPage.jsx": __vite_glob_0_86, + "./Pages/Help/CardsHelpPage.jsx": __vite_glob_0_87, + "./Pages/Help/HelpCenterPage.jsx": __vite_glob_0_88, + "./Pages/Help/ProfileHelpPage.jsx": __vite_glob_0_89, + "./Pages/Help/StudioHelpPage.jsx": __vite_glob_0_90, + "./Pages/Help/TroubleshootingHelpPage.jsx": __vite_glob_0_91, + "./Pages/Help/UploadHelpPage.jsx": __vite_glob_0_92, + "./Pages/Help/WorldsHelpPage.jsx": __vite_glob_0_93, + "./Pages/Leaderboard/LeaderboardPage.jsx": __vite_glob_0_94, + "./Pages/Messages/Index.jsx": __vite_glob_0_95, + "./Pages/Moderation/AiBiographyAdmin.jsx": __vite_glob_0_96, + "./Pages/Moderation/ArtworkMaturityQueue.jsx": __vite_glob_0_97, + "./Pages/Moderation/Enhance/Index.jsx": __vite_glob_0_98, + "./Pages/Moderation/Enhance/Show.jsx": __vite_glob_0_99, + "./Pages/Moderation/FeaturedArtworks.jsx": __vite_glob_0_100, + "./Pages/Moderation/StaffApplications/Index.jsx": __vite_glob_0_101, + "./Pages/Moderation/StaffApplications/Show.jsx": __vite_glob_0_102, + "./Pages/Moderation/Stories.jsx": __vite_glob_0_103, + "./Pages/Moderation/UsernameQueue.jsx": __vite_glob_0_104, + "./Pages/Moderation/WorldWebStoriesIndex.jsx": __vite_glob_0_105, + "./Pages/Moderation/WorldWebStoryEditor.jsx": __vite_glob_0_106, + "./Pages/News/NewsComments.jsx": __vite_glob_0_107, + "./Pages/News/NewsImagePreview.jsx": __vite_glob_0_108, + "./Pages/Profile/ProfileGallery.jsx": __vite_glob_0_109, + "./Pages/Profile/ProfileShow.jsx": __vite_glob_0_110, + "./Pages/Settings/ProfileEdit.jsx": __vite_glob_0_111, + "./Pages/Studio/StudioActivity.jsx": __vite_glob_0_112, + "./Pages/Studio/StudioAnalytics.jsx": __vite_glob_0_113, + "./Pages/Studio/StudioArchived.jsx": __vite_glob_0_114, + "./Pages/Studio/StudioArtworkAnalytics.jsx": __vite_glob_0_115, + "./Pages/Studio/StudioArtworkEdit.jsx": __vite_glob_0_116, + "./Pages/Studio/StudioArtworks.jsx": __vite_glob_0_117, + "./Pages/Studio/StudioAssets.jsx": __vite_glob_0_118, + "./Pages/Studio/StudioCalendar.jsx": __vite_glob_0_119, + "./Pages/Studio/StudioCardAnalytics.jsx": __vite_glob_0_120, + "./Pages/Studio/StudioCardEditor.jsx": __vite_glob_0_121, + "./Pages/Studio/StudioCardsIndex.jsx": __vite_glob_0_122, + "./Pages/Studio/StudioChallenges.jsx": __vite_glob_0_123, + "./Pages/Studio/StudioCollections.jsx": __vite_glob_0_124, + "./Pages/Studio/StudioComments.jsx": __vite_glob_0_125, + "./Pages/Studio/StudioContentIndex.jsx": __vite_glob_0_126, + "./Pages/Studio/StudioDashboard.jsx": __vite_glob_0_127, + "./Pages/Studio/StudioDrafts.jsx": __vite_glob_0_128, + "./Pages/Studio/StudioFeatured.jsx": __vite_glob_0_129, + "./Pages/Studio/StudioFollowers.jsx": __vite_glob_0_130, + "./Pages/Studio/StudioGroupActivity.jsx": __vite_glob_0_131, + "./Pages/Studio/StudioGroupArtworks.jsx": __vite_glob_0_132, + "./Pages/Studio/StudioGroupAssets.jsx": __vite_glob_0_133, + "./Pages/Studio/StudioGroupChallengeEditor.jsx": __vite_glob_0_134, + "./Pages/Studio/StudioGroupChallenges.jsx": __vite_glob_0_135, + "./Pages/Studio/StudioGroupCollections.jsx": __vite_glob_0_136, + "./Pages/Studio/StudioGroupCreate.jsx": __vite_glob_0_137, + "./Pages/Studio/StudioGroupDashboard.jsx": __vite_glob_0_138, + "./Pages/Studio/StudioGroupEventEditor.jsx": __vite_glob_0_139, + "./Pages/Studio/StudioGroupEvents.jsx": __vite_glob_0_140, + "./Pages/Studio/StudioGroupInvitations.jsx": __vite_glob_0_141, + "./Pages/Studio/StudioGroupJoinRequests.jsx": __vite_glob_0_142, + "./Pages/Studio/StudioGroupMembers.jsx": __vite_glob_0_143, + "./Pages/Studio/StudioGroupPostEditor.jsx": __vite_glob_0_144, + "./Pages/Studio/StudioGroupPosts.jsx": __vite_glob_0_145, + "./Pages/Studio/StudioGroupProjectEditor.jsx": __vite_glob_0_146, + "./Pages/Studio/StudioGroupProjects.jsx": __vite_glob_0_147, + "./Pages/Studio/StudioGroupRecruitment.jsx": __vite_glob_0_148, + "./Pages/Studio/StudioGroupReleaseEditor.jsx": __vite_glob_0_149, + "./Pages/Studio/StudioGroupReleases.jsx": __vite_glob_0_150, + "./Pages/Studio/StudioGroupReputation.jsx": __vite_glob_0_151, + "./Pages/Studio/StudioGroupReviewQueue.jsx": __vite_glob_0_152, + "./Pages/Studio/StudioGroupSettings.jsx": __vite_glob_0_153, + "./Pages/Studio/StudioGroupsIndex.jsx": __vite_glob_0_154, + "./Pages/Studio/StudioGrowth.jsx": __vite_glob_0_155, + "./Pages/Studio/StudioInbox.jsx": __vite_glob_0_156, + "./Pages/Studio/StudioNewsEditor.jsx": __vite_glob_0_157, + "./Pages/Studio/StudioNewsIndex.jsx": __vite_glob_0_158, + "./Pages/Studio/StudioNewsTaxonomies.jsx": __vite_glob_0_159, + "./Pages/Studio/StudioPreferences.jsx": __vite_glob_0_160, + "./Pages/Studio/StudioProfile.jsx": __vite_glob_0_161, + "./Pages/Studio/StudioScheduled.jsx": __vite_glob_0_162, + "./Pages/Studio/StudioSearch.jsx": __vite_glob_0_163, + "./Pages/Studio/StudioSettings.jsx": __vite_glob_0_164, + "./Pages/Studio/StudioStories.jsx": __vite_glob_0_165, + "./Pages/Studio/StudioUploadQueue.jsx": __vite_glob_0_166, + "./Pages/Studio/StudioWorldEditor.jsx": __vite_glob_0_167, + "./Pages/Studio/StudioWorldsIndex.jsx": __vite_glob_0_168, + "./Pages/Upload/Index.jsx": __vite_glob_0_169, + "./Pages/World/WorldIndex.jsx": __vite_glob_0_170, + "./Pages/World/WorldShow.jsx": __vite_glob_0_171 }); const ClientOnlyPlaceholder = () => null; d( diff --git a/composer.json b/composer.json index 3854709e..a518d5f2 100644 --- a/composer.json +++ b/composer.json @@ -114,6 +114,9 @@ "pestphp/pest-plugin": true, "php-http/discovery": true }, + "audit": { + "abandoned": "report" + }, "platform": { "ext-pcntl": "8.4.0", "ext-posix": "8.4.0" diff --git a/composer.lock b/composer.lock index 06cee776..7c68a95b 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.381.3", + "version": "3.384.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "989f4776aed2a3b184a5b64046542e8fe66e99e2" + "reference": "c7d34f2d60515bd0c307e462268f75877842da4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/989f4776aed2a3b184a5b64046542e8fe66e99e2", - "reference": "989f4776aed2a3b184a5b64046542e8fe66e99e2", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c7d34f2d60515bd0c307e462268f75877842da4a", + "reference": "c7d34f2d60515bd0c307e462268f75877842da4a", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.381.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.384.5" }, - "time": "2026-05-18T18:21:09+00:00" + "time": "2026-06-08T18:25:02+00:00" }, { "name": "brick/math", @@ -1239,25 +1239,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.11.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/5af96f374e0ab4ebd747b8310888c99d3adb0a8c", + "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.11", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1266,8 +1267,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.5", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -1345,7 +1347,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.11.1" }, "funding": [ { @@ -1361,28 +1363,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-06-07T22:54:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -1428,7 +1431,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1444,27 +1447,29 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.9.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1472,9 +1477,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", + "http-interop/http-factory-tests": "1.1.0", "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1545,7 +1550,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.11.0" }, "funding": [ { @@ -1561,20 +1566,20 @@ "type": "tidelift" } ], - "time": "2026-03-10T16:41:02+00:00" + "time": "2026-06-02T12:30:48+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.5", + "version": "v1.0.6", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946", "shasum": "" }, "require": { @@ -1583,7 +1588,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1631,7 +1636,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.6" }, "funding": [ { @@ -1647,7 +1652,7 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:27:06+00:00" + "time": "2026-05-23T22:00:21+00:00" }, { "name": "inertiajs/inertia-laravel", @@ -2148,16 +2153,16 @@ }, { "name": "laravel/framework", - "version": "v12.59.0", + "version": "v12.61.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "70a838b1a6f12abaf5bebb76ea4f28fbd37451fa" + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/70a838b1a6f12abaf5bebb76ea4f28fbd37451fa", - "reference": "70a838b1a6f12abaf5bebb76ea4f28fbd37451fa", + "url": "https://api.github.com/repos/laravel/framework/zipball/e8472ca9774452fe50841d9bdced060679f4d58d", + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d", "shasum": "" }, "require": { @@ -2366,20 +2371,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-14T15:31:40+00:00" + "time": "2026-06-04T14:22:52+00:00" }, { "name": "laravel/horizon", - "version": "v5.46.0", + "version": "v5.47.2", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5" + "reference": "a6ac142293ad02db4d7cccb961dd32f56ef1462d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/bfea968e8aa674fb649d02e55ea0d38bdf5137d5", - "reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5", + "url": "https://api.github.com/repos/laravel/horizon/zipball/a6ac142293ad02db4d7cccb961dd32f56ef1462d", + "reference": "a6ac142293ad02db4d7cccb961dd32f56ef1462d", "shasum": "" }, "require": { @@ -2444,9 +2449,9 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.46.0" + "source": "https://github.com/laravel/horizon/tree/v5.47.2" }, - "time": "2026-04-20T18:08:11+00:00" + "time": "2026-06-03T15:11:37+00:00" }, { "name": "laravel/prompts", @@ -4883,16 +4888,16 @@ }, { "name": "predis/predis", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "2033429520d8997a7815a2485f56abe6d2d0e075" + "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/2033429520d8997a7815a2485f56abe6d2d0e075", - "reference": "2033429520d8997a7815a2485f56abe6d2d0e075", + "url": "https://api.github.com/repos/predis/predis/zipball/8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", + "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", "shasum": "" }, "require": { @@ -4934,7 +4939,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v3.4.2" + "source": "https://github.com/predis/predis/tree/v3.5.0" }, "funding": [ { @@ -4942,7 +4947,7 @@ "type": "github" } ], - "time": "2026-03-09T20:33:04+00:00" + "time": "2026-06-02T19:25:56+00:00" }, { "name": "psr/clock", @@ -5358,16 +5363,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.22", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", - "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", "shasum": "" }, "require": { @@ -5431,9 +5436,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" }, - "time": "2026-03-22T23:03:24+00:00" + "time": "2026-05-23T13:41:31+00:00" }, { "name": "pusher/pusher-php-server", @@ -5695,16 +5700,16 @@ }, { "name": "ratchet/rfc6455", - "version": "v0.4.0", + "version": "v0.4.1", "source": { "type": "git", "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + "reference": "9b05f371219cbaf9748b505f139617dd0715592b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/9b05f371219cbaf9748b505f139617dd0715592b", + "reference": "9b05f371219cbaf9748b505f139617dd0715592b", "shasum": "" }, "require": { @@ -5748,9 +5753,9 @@ "support": { "chat": "https://gitter.im/reactphp/reactphp", "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.1" }, - "time": "2025-02-24T01:18:22+00:00" + "time": "2026-06-06T14:34:23+00:00" }, { "name": "react/cache", @@ -6653,20 +6658,20 @@ }, { "name": "symfony/clock", - "version": "v8.0.8", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3" + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b55a638b189a6faa875e0ccdb00908fb87af95b3", - "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/clock": "^1.0" }, "provide": { @@ -6706,7 +6711,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v8.0.8" + "source": "https://github.com/symfony/clock/tree/v8.1.0" }, "funding": [ { @@ -6726,20 +6731,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/console", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", - "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -6804,7 +6809,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.11" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -6824,24 +6829,24 @@ "type": "tidelift" } ], - "time": "2026-05-13T12:04:42+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/css-selector", - "version": "v8.0.9", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "3665cfade90565430909b906394c73c8739e57d0" + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/3665cfade90565430909b906394c73c8739e57d0", - "reference": "3665cfade90565430909b906394c73c8739e57d0", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.4.1" }, "type": "library", "autoload": { @@ -6873,7 +6878,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.0.9" + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" }, "funding": [ { @@ -6893,7 +6898,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/deprecation-contracts", @@ -7050,20 +7055,21 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.9", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { @@ -7111,7 +7117,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" }, "funding": [ { @@ -7131,7 +7137,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7215,20 +7221,21 @@ }, { "name": "symfony/filesystem", - "version": "v8.0.11", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7" + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/224db910898ce1317b892a9a1338f1f8f17eb7c7", - "reference": "224db910898ce1317b892a9a1338f1f8f17eb7c7", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, @@ -7261,7 +7268,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.11" + "source": "https://github.com/symfony/filesystem/tree/v8.1.0" }, "funding": [ { @@ -7281,7 +7288,7 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:39:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/finder", @@ -7353,16 +7360,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + "reference": "bc354f47c62301e990b7874fa662326368508e2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", "shasum": "" }, "require": { @@ -7411,7 +7418,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" }, "funding": [ { @@ -7431,20 +7438,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "eb9d68199af3fcfb3fb4d2e227367b68f8c1bb88" + "reference": "9df847980c436451f4f51d1284491bb4356dd989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/eb9d68199af3fcfb3fb4d2e227367b68f8c1bb88", - "reference": "eb9d68199af3fcfb3fb4d2e227367b68f8c1bb88", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", "shasum": "" }, "require": { @@ -7530,7 +7537,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.11" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" }, "funding": [ { @@ -7550,20 +7557,20 @@ "type": "tidelift" } ], - "time": "2026-05-13T17:55:00+00:00" + "time": "2026-05-27T08:31:43+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.8", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62" + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62", - "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", "shasum": "" }, "require": { @@ -7614,7 +7621,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.8" + "source": "https://github.com/symfony/mailer/tree/v7.4.12" }, "funding": [ { @@ -7634,20 +7641,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/mime", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2d550c4758ba4c47519a6667c36553d535705b0c" + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2d550c4758ba4c47519a6667c36553d535705b0c", - "reference": "2d550c4758ba4c47519a6667c36553d535705b0c", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", "shasum": "" }, "require": { @@ -7703,7 +7710,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.9" + "source": "https://github.com/symfony/mime/tree/v7.4.13" }, "funding": [ { @@ -7723,24 +7730,24 @@ "type": "tidelift" } ], - "time": "2026-04-29T13:21:53+00:00" + "time": "2026-05-23T16:22:37+00:00" }, { "name": "symfony/options-resolver", - "version": "v8.0.8", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" + "reference": "88f9c561f678a02d54b897014049fa839e33ff82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", - "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -7774,7 +7781,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" + "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" }, "funding": [ { @@ -7794,7 +7801,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/polyfill-ctype", @@ -7881,16 +7888,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -7939,7 +7946,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -7959,20 +7966,20 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.37.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "3510b63d07376b04e57e27e82607d468bb134f78" + "reference": "445c90e341fccda10311019cf82ff73bb7343945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/3510b63d07376b04e57e27e82607d468bb134f78", - "reference": "3510b63d07376b04e57e27e82607d468bb134f78", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/445c90e341fccda10311019cf82ff73bb7343945", + "reference": "445c90e341fccda10311019cf82ff73bb7343945", "shasum": "" }, "require": { @@ -8027,7 +8034,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.38.0" }, "funding": [ { @@ -8047,20 +8054,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:50:15+00:00" + "time": "2026-05-25T11:52:53+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -8114,7 +8121,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -8134,20 +8141,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.37.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -8199,7 +8206,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -8219,20 +8226,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -8284,7 +8291,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -8304,7 +8311,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php80", @@ -8392,16 +8399,16 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -8448,7 +8455,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" }, "funding": [ { @@ -8468,20 +8475,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:45:58+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -8528,7 +8535,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -8548,20 +8555,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -8608,7 +8615,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -8628,20 +8635,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T18:47:49+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { @@ -8688,7 +8695,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -8708,7 +8715,7 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:10:57+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { "name": "symfony/polyfill-uuid", @@ -8795,16 +8802,16 @@ }, { "name": "symfony/process", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", - "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -8836,7 +8843,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.11" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -8856,24 +8863,24 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:55:21+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v8.0.8", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19" + "reference": "67fd34de15ded1763aa1e330fe345f080a94022c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/94facc221260c1d5f20e31ee43cd6c6a824b4a19", - "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/67fd34de15ded1763aa1e330fe345f080a94022c", + "reference": "67fd34de15ded1763aa1e330fe345f080a94022c", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "psr/http-message": "^1.0|^2.0", "symfony/http-foundation": "^7.4|^8.0" }, @@ -8923,7 +8930,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.0.8" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.1.0" }, "funding": [ { @@ -8943,20 +8950,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/routing", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "287771d8bc86eacb30678dd10eda6c64a859951f" + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/287771d8bc86eacb30678dd10eda6c64a859951f", - "reference": "287771d8bc86eacb30678dd10eda6c64a859951f", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", "shasum": "" }, "require": { @@ -9008,7 +9015,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.9" + "source": "https://github.com/symfony/routing/tree/v7.4.13" }, "funding": [ { @@ -9028,7 +9035,7 @@ "type": "tidelift" } ], - "time": "2026-04-22T15:21:55+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/service-contracts", @@ -9119,20 +9126,20 @@ }, { "name": "symfony/string", - "version": "v8.0.11", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", - "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-intl-normalizer": "^1.0", @@ -9185,7 +9192,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.11" + "source": "https://github.com/symfony/string/tree/v8.1.0" }, "funding": [ { @@ -9205,24 +9212,24 @@ "type": "tidelift" } ], - "time": "2026-05-13T12:07:53+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation", - "version": "v8.0.10", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67" + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f63e9342e12646a57c91ef8a366a4f9d8e557b67", - "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-mbstring": "^1.0", "symfony/translation-contracts": "^3.6.1" }, @@ -9278,7 +9285,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.10" + "source": "https://github.com/symfony/translation/tree/v8.1.0" }, "funding": [ { @@ -9298,7 +9305,7 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:30:54+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/translation-contracts", @@ -10334,16 +10341,16 @@ }, { "name": "fruitcake/laravel-debugbar", - "version": "v4.2.8", + "version": "v4.3.0", "source": { "type": "git", "url": "https://github.com/fruitcake/laravel-debugbar.git", - "reference": "799d70c1101d3f8840dd76ff68ff6a78f9352905" + "reference": "3d76ea8d78b82225b92789de65fc630c1cd8e80c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/799d70c1101d3f8840dd76ff68ff6a78f9352905", - "reference": "799d70c1101d3f8840dd76ff68ff6a78f9352905", + "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/3d76ea8d78b82225b92789de65fc630c1cd8e80c", + "reference": "3d76ea8d78b82225b92789de65fc630c1cd8e80c", "shasum": "" }, "require": { @@ -10420,7 +10427,7 @@ ], "support": { "issues": "https://github.com/fruitcake/laravel-debugbar/issues", - "source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.2.8" + "source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.3.0" }, "funding": [ { @@ -10432,7 +10439,7 @@ "type": "github" } ], - "time": "2026-04-20T13:31:29+00:00" + "time": "2026-06-04T07:54:01+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -10487,16 +10494,16 @@ }, { "name": "laravel/boost", - "version": "v2.4.7", + "version": "v2.4.10", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "cebd69eb8ebcefd27653ba95407ef8be965ec239" + "reference": "080189f51c8d27c0792a03483a70adc7770f6eeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/cebd69eb8ebcefd27653ba95407ef8be965ec239", - "reference": "cebd69eb8ebcefd27653ba95407ef8be965ec239", + "url": "https://api.github.com/repos/laravel/boost/zipball/080189f51c8d27c0792a03483a70adc7770f6eeb", + "reference": "080189f51c8d27c0792a03483a70adc7770f6eeb", "shasum": "" }, "require": { @@ -10505,7 +10512,7 @@ "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", "illuminate/routing": "^11.45.3|^12.41.1|^13.0", "illuminate/support": "^11.45.3|^12.41.1|^13.0", - "laravel/mcp": "^0.5.1|^0.6.0|^0.7.0", + "laravel/mcp": "^0.7.1|^0.8.0", "laravel/prompts": "^0.3.10", "laravel/roster": "^0.5.0", "php": "^8.2" @@ -10549,20 +10556,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-05-18T12:23:29+00:00" + "time": "2026-06-09T10:21:08+00:00" }, { "name": "laravel/breeze", - "version": "v2.4.1", + "version": "v2.4.2", "source": { "type": "git", "url": "https://github.com/laravel/breeze.git", - "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f" + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/breeze/zipball/28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", - "reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f", + "url": "https://api.github.com/repos/laravel/breeze/zipball/4f20e7b2cc8d25daa85d8647241a89c8e0930305", + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305", "shasum": "" }, "require": { @@ -10610,20 +10617,20 @@ "issues": "https://github.com/laravel/breeze/issues", "source": "https://github.com/laravel/breeze" }, - "time": "2026-03-10T19:59:01+00:00" + "time": "2026-05-14T16:54:25+00:00" }, { "name": "laravel/mcp", - "version": "v0.7.0", + "version": "v0.8.0", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "3513b4feca5f1678be4d2261dcfa8e456436d02a" + "reference": "18221a07093d84153883bc956e5e213999549a4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/3513b4feca5f1678be4d2261dcfa8e456436d02a", - "reference": "3513b4feca5f1678be4d2261dcfa8e456436d02a", + "url": "https://api.github.com/repos/laravel/mcp/zipball/18221a07093d84153883bc956e5e213999549a4b", + "reference": "18221a07093d84153883bc956e5e213999549a4b", "shasum": "" }, "require": { @@ -10637,7 +10644,8 @@ "illuminate/routing": "^11.45.3|^12.41.1|^13.0", "illuminate/support": "^11.45.3|^12.41.1|^13.0", "illuminate/validation": "^11.45.3|^12.41.1|^13.0", - "php": "^8.2" + "php": "^8.2", + "symfony/process": "^7.4.5|^8.0.5" }, "require-dev": { "laravel/pint": "^1.20", @@ -10650,7 +10658,7 @@ "extra": { "laravel": { "aliases": { - "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + "Mcp": "Laravel\\Mcp\\Facades\\Mcp" }, "providers": [ "Laravel\\Mcp\\Server\\McpServiceProvider" @@ -10683,20 +10691,20 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-04-21T10:23:03+00:00" + "time": "2026-06-08T13:48:51+00:00" }, { "name": "laravel/pail", - "version": "v1.2.6", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", - "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", "shasum": "" }, "require": { @@ -10763,7 +10771,7 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2026-02-09T13:44:54+00:00" + "time": "2026-05-20T22:24:57+00:00" }, { "name": "laravel/pint", @@ -10896,16 +10904,16 @@ }, { "name": "laravel/sail", - "version": "v1.59.0", + "version": "v1.62.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "a41abad557e487eaefde6c9873085ed086fdf47a" + "reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/a41abad557e487eaefde6c9873085ed086fdf47a", - "reference": "a41abad557e487eaefde6c9873085ed086fdf47a", + "url": "https://api.github.com/repos/laravel/sail/zipball/3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", + "reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", "shasum": "" }, "require": { @@ -10955,7 +10963,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2026-05-13T14:02:20+00:00" + "time": "2026-05-27T04:02:01+00:00" }, { "name": "mockery/mockery", @@ -11198,16 +11206,16 @@ }, { "name": "pestphp/pest", - "version": "v4.7.0", + "version": "v4.7.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "2fc75cfcf03c041c804778fa894282234adc3c66" + "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/2fc75cfcf03c041c804778fa894282234adc3c66", - "reference": "2fc75cfcf03c041c804778fa894282234adc3c66", + "url": "https://api.github.com/repos/pestphp/pest/zipball/40b88b62ef8a7c6fcae5fc28f1fa747f601c131b", + "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b", "shasum": "" }, "require": { @@ -11220,21 +11228,21 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.24", - "symfony/process": "^7.4.8|^8.0.8" + "phpunit/phpunit": "^12.5.28", + "symfony/process": "^7.4.13|^8.1.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.24", + "phpunit/phpunit": ">12.5.28", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "mrpunyapal/peststan": "^0.2.9", + "mrpunyapal/peststan": "^0.2.10", "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.1", "pestphp/pest-plugin-type-coverage": "^4.0.4", - "psy/psysh": "^0.12.22" + "psy/psysh": "^0.12.23" }, "bin": [ "bin/pest" @@ -11301,7 +11309,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.7.0" + "source": "https://github.com/pestphp/pest/tree/v4.7.2" }, "funding": [ { @@ -11313,7 +11321,7 @@ "type": "github" } ], - "time": "2026-05-03T16:09:32+00:00" + "time": "2026-06-01T06:08:59+00:00" }, { "name": "pestphp/pest-plugin", @@ -12172,16 +12180,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.6", + "version": "12.5.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "876099a072646c7745f673d7aeab5382c4439691" + "reference": "186dab580576598076de6818596d12b61801880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", - "reference": "876099a072646c7745f673d7aeab5382c4439691", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", "shasum": "" }, "require": { @@ -12192,13 +12200,13 @@ "php": ">=8.3", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0.3", - "sebastian/lines-of-code": "^4.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", "sebastian/version": "^6.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.1" + "phpunit/phpunit": "^12.5.28" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -12236,7 +12244,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" }, "funding": [ { @@ -12256,7 +12264,7 @@ "type": "tidelift" } ], - "time": "2026-04-15T08:23:17+00:00" + "time": "2026-06-01T13:24:19+00:00" }, { "name": "phpunit/php-file-iterator", @@ -12517,16 +12525,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.24", + "version": "12.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" + "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", - "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5895d05f5bf421ed230fbd76e1277e4b8955def4", + "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4", "shasum": "" }, "require": { @@ -12545,15 +12553,15 @@ "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.6", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.0", - "sebastian/exporter": "^7.0.2", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", "sebastian/global-state": "^8.0.2", "sebastian/object-enumerator": "^7.0.0", "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.3", + "sebastian/type": "^6.0.4", "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, @@ -12595,7 +12603,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.28" }, "funding": [ { @@ -12603,7 +12611,7 @@ "type": "other" } ], - "time": "2026-05-01T04:21:04+00:00" + "time": "2026-05-27T14:01:10+00:00" }, { "name": "sebastian/cli-parser", @@ -12676,16 +12684,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.6", + "version": "7.1.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" + "reference": "7c65c1e79836812819705b473a90c12399542485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", - "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", "shasum": "" }, "require": { @@ -12693,10 +12701,10 @@ "ext-mbstring": "*", "php": ">=8.3", "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0" + "sebastian/exporter": "^7.0.3" }, "require-dev": { - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^12.5.25" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -12744,7 +12752,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" }, "funding": [ { @@ -12764,7 +12772,7 @@ "type": "tidelift" } ], - "time": "2026-04-14T08:23:15+00:00" + "time": "2026-05-21T04:45:25+00:00" }, { "name": "sebastian/complexity", @@ -12893,23 +12901,23 @@ }, { "name": "sebastian/environment", - "version": "8.1.0", + "version": "8.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", - "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.26" }, "suggest": { "ext-posix": "*" @@ -12945,7 +12953,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" }, "funding": [ { @@ -12965,29 +12973,29 @@ "type": "tidelift" } ], - "time": "2026-04-15T12:13:01+00:00" + "time": "2026-05-25T13:40:20+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.2", + "version": "7.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", - "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", "shasum": "" }, "require": { "ext-mbstring": "*", "php": ">=8.3", - "sebastian/recursion-context": "^7.0" + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -13035,7 +13043,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" }, "funding": [ { @@ -13055,30 +13063,30 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:16:11+00:00" + "time": "2026-05-20T04:37:17+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.2", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", - "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", "shasum": "" }, "require": { "php": ">=8.3", "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.28" }, "type": "library", "extra": { @@ -13109,7 +13117,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" }, "funding": [ { @@ -13129,28 +13137,28 @@ "type": "tidelift" } ], - "time": "2025-08-29T11:29:25+00:00" + "time": "2026-06-01T15:10:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", - "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -13179,15 +13187,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:28+00:00" + "time": "2026-05-19T16:22:07+00:00" }, { "name": "sebastian/object-enumerator", @@ -13381,23 +13401,23 @@ }, { "name": "sebastian/type", - "version": "6.0.3", + "version": "6.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + "reference": "82ff822c2edc46724be9f7411d3163021f602773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", - "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -13426,7 +13446,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" }, "funding": [ { @@ -13446,7 +13466,7 @@ "type": "tidelift" } ], - "time": "2025-08-09T06:57:12+00:00" + "time": "2026-05-20T06:45:45+00:00" }, { "name": "sebastian/version", @@ -13556,27 +13576,28 @@ }, { "name": "symfony/yaml", - "version": "v8.0.11", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "48046fbd5567bd1717f278eaa2cfc3131f489984" + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/48046fbd5567bd1717f278eaa2cfc3131f489984", - "reference": "48046fbd5567bd1717f278eaa2cfc3131f489984", + "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0", + "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<7.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" }, "bin": [ "Resources/bin/yaml-lint" @@ -13607,7 +13628,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.11" + "source": "https://github.com/symfony/yaml/tree/v8.1.0" }, "funding": [ { @@ -13627,7 +13648,7 @@ "type": "tidelift" } ], - "time": "2026-05-13T12:07:53+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -13740,16 +13761,16 @@ }, { "name": "webmozart/assert", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", "shasum": "" }, "require": { @@ -13765,7 +13786,11 @@ }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { + "dev-master": "2.0-dev", "dev-feature/2-0": "2.0-dev" } }, @@ -13796,9 +13821,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.3.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.0" }, - "time": "2026-04-11T10:33:05+00:00" + "time": "2026-05-20T13:07:01+00:00" } ], "aliases": [], diff --git a/config/security-report.php b/config/security-report.php new file mode 100644 index 00000000..36f909ee --- /dev/null +++ b/config/security-report.php @@ -0,0 +1,30 @@ + env('SECURITY_REPORT_ENABLED', true), + + 'notify_email' => env('SECURITY_REPORT_NOTIFY_EMAIL', env('MAIL_FROM_ADDRESS')), + + 'scan' => [ + 'composer' => env('SECURITY_REPORT_SCAN_COMPOSER', true), + 'npm' => env('SECURITY_REPORT_SCAN_NPM', true), + ], + + 'store_raw' => env('SECURITY_REPORT_STORE_RAW', true), + + 'max_raw_kb' => (int) env('SECURITY_REPORT_MAX_RAW_KB', 512), + + 'fail_on' => [ + 'high' => env('SECURITY_REPORT_FAIL_ON_HIGH', false), + 'critical' => env('SECURITY_REPORT_FAIL_ON_CRITICAL', false), + ], + + 'commands' => [ + 'composer_audit' => ['composer', 'audit', '--format=json', '--locked'], + 'composer_outdated' => ['composer', 'outdated', '--direct', '--format=json'], + 'npm_audit' => ['npm', 'audit', '--json'], + 'npm_outdated' => ['npm', 'outdated', '--json'], + ], + + 'timeout_seconds' => 180, +]; diff --git a/config/sitemaps.php b/config/sitemaps.php index a386bd3d..9704631e 100644 --- a/config/sitemaps.php +++ b/config/sitemaps.php @@ -83,6 +83,12 @@ return [ 'enabled' => [ 'artworks', + 'academy-pages', + 'academy-courses', + 'academy-lessons', + 'academy-prompts', + 'academy-packs', + 'academy-challenges', 'users', 'tags', 'categories', @@ -114,4 +120,4 @@ return [ 'legal-privacy', 'legal-cookies', ], -]; \ No newline at end of file +]; diff --git a/config/vision.php b/config/vision.php index 30e8b5fd..4424c9b8 100644 --- a/config/vision.php +++ b/config/vision.php @@ -83,6 +83,19 @@ return [ 'connect_timeout_seconds' => (int) env('VISION_VECTOR_GATEWAY_CONNECT_TIMEOUT', 5), 'retries' => (int) env('VISION_VECTOR_GATEWAY_RETRIES', 1), 'retry_delay_ms' => (int) env('VISION_VECTOR_GATEWAY_RETRY_DELAY_MS', 250), + + // Tighter budget for the read/search path, which runs synchronously inside + // web requests (unlike upsert/delete, which only run from queued/console + // indexing jobs). Keeps a slow or unreachable gateway from pinning FPM + // workers for 20s+ per request. + 'search_timeout_seconds' => (int) env('VISION_VECTOR_GATEWAY_SEARCH_TIMEOUT', 6), + 'search_connect_timeout_seconds' => (int) env('VISION_VECTOR_GATEWAY_SEARCH_CONNECT_TIMEOUT', 2), + 'search_retries' => (int) env('VISION_VECTOR_GATEWAY_SEARCH_RETRIES', 0), + + // Once a search call fails, stop attempting new ones for this many seconds + // (circuit breaker) — avoids every concurrent request for a different + // artwork independently blocking on the same downed/slow gateway. + 'circuit_breaker_seconds' => (int) env('VISION_VECTOR_GATEWAY_CIRCUIT_SECONDS', 30), 'upsert_endpoint' => env('VISION_VECTOR_GATEWAY_UPSERT_ENDPOINT', '/vectors/upsert'), 'upsert_file_endpoint' => env('VISION_VECTOR_GATEWAY_UPSERT_FILE_ENDPOINT', '/vectors/upsert/file'), 'search_endpoint' => env('VISION_VECTOR_GATEWAY_SEARCH_ENDPOINT', '/vectors/search'), diff --git a/database/migrations/2026_06_09_120000_create_security_reports_table.php b/database/migrations/2026_06_09_120000_create_security_reports_table.php new file mode 100644 index 00000000..1446c89c --- /dev/null +++ b/database/migrations/2026_06_09_120000_create_security_reports_table.php @@ -0,0 +1,64 @@ +id(); + $table->string('status', 32)->default('completed'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('finished_at')->nullable(); + + $table->unsignedInteger('composer_critical')->default(0); + $table->unsignedInteger('composer_high')->default(0); + $table->unsignedInteger('composer_medium')->default(0); + $table->unsignedInteger('composer_low')->default(0); + $table->unsignedInteger('composer_unknown')->default(0); + + $table->unsignedInteger('npm_critical')->default(0); + $table->unsignedInteger('npm_high')->default(0); + $table->unsignedInteger('npm_moderate')->default(0); + $table->unsignedInteger('npm_low')->default(0); + $table->unsignedInteger('npm_info')->default(0); + $table->unsignedInteger('npm_unknown')->default(0); + + $table->unsignedInteger('total_critical')->default(0); + $table->unsignedInteger('total_high')->default(0); + $table->unsignedInteger('total_medium')->default(0); + $table->unsignedInteger('total_low')->default(0); + $table->unsignedInteger('total_unknown')->default(0); + + $table->unsignedInteger('composer_outdated_count')->default(0); + $table->unsignedInteger('npm_outdated_count')->default(0); + + $table->json('summary')->nullable(); + $table->json('composer_audit')->nullable(); + $table->json('composer_outdated')->nullable(); + $table->json('npm_audit')->nullable(); + $table->json('npm_outdated')->nullable(); + + $table->longText('error_message')->nullable(); + + $table->string('triggered_by', 64)->nullable(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + + $table->timestamps(); + + $table->index('status'); + $table->index('finished_at'); + $table->index(['total_critical', 'total_high']); + }); + } + + public function down(): void + { + Schema::dropIfExists('security_reports'); + } +}; diff --git a/database/migrations/2026_08_01_000001_add_featured_thumbnail_audit_columns_to_artworks_table.php b/database/migrations/2026_08_01_000001_add_featured_thumbnail_audit_columns_to_artworks_table.php new file mode 100644 index 00000000..0eb1da1c --- /dev/null +++ b/database/migrations/2026_08_01_000001_add_featured_thumbnail_audit_columns_to_artworks_table.php @@ -0,0 +1,53 @@ +json('featured_thumbnail_variants_json') + ->nullable() + ->after('thumbnails_checked_at'); + } + + if (! Schema::hasColumn('artworks', 'featured_thumbnails_checked_at')) { + $table->timestamp('featured_thumbnails_checked_at') + ->nullable() + ->after('featured_thumbnail_variants_json'); + } + }); + + Schema::table('artworks', function (Blueprint $table): void { + if (Schema::hasColumn('artworks', 'featured_thumbnails_checked_at')) { + $table->index('featured_thumbnails_checked_at', 'artworks_featured_thumbnails_checked_idx'); + } + }); + } + + public function down(): void + { + Schema::table('artworks', function (Blueprint $table): void { + try { + $table->dropIndex('artworks_featured_thumbnails_checked_idx'); + } catch (Throwable) { + } + + $columns = []; + + foreach (['featured_thumbnail_variants_json', 'featured_thumbnails_checked_at'] as $column) { + if (Schema::hasColumn('artworks', $column)) { + $columns[] = $column; + } + } + + if ($columns !== []) { + $table->dropColumn($columns); + } + }); + } +}; diff --git a/deploy/nginx/search-rate-limit.conf b/deploy/nginx/search-rate-limit.conf new file mode 100644 index 00000000..8c8b4a3f --- /dev/null +++ b/deploy/nginx/search-rate-limit.conf @@ -0,0 +1,43 @@ +# ----------------------------------------------------------------------- +# Rate limiting for /search and the AI vector-search endpoints +# +# Laravel already throttles these routes at the application layer +# (see App\Providers\AppServiceProvider::configureSearchRateLimiter / +# configureVectorSearchRateLimiter), but that still costs one PHP-FPM +# worker per request just to run the rate limiter and reject the +# request. This nginx-level limiter rejects floods with a 503 before +# they ever reach FPM, which is what actually protects worker capacity +# during a scripted flood or bot storm. +# +# Setup: +# 1. Add the `limit_req_zone` and `limit_req_status` lines to the +# `http {}` block (nginx.conf or conf.d/00-rate-limit-zones.conf) — +# zones MUST be declared at http level, not inside server {}. +# 2. Include the `location` blocks below inside the relevant +# `server {}` block, ABOVE the general `location ~ \.php$` / +# PHP-FPM passthrough. +# ----------------------------------------------------------------------- + +# --- Add to the http {} block --------------------------------------------- +# limit_req_zone $binary_remote_addr zone=search_zone:10m rate=20r/m; +# limit_req_zone $binary_remote_addr zone=ai_search_zone:10m rate=10r/m; +# limit_req_status 429; +# limit_req_log_level warn; +# --------------------------------------------------------------------------- + +# Human search traffic: /search page + /api/search/* (Meilisearch-backed, +# cheap once app-level caching is warm — burst allowance covers pagination +# clicks / autocomplete without tripping on normal use). +location ~ ^/(search|api/search) { + limit_req zone=search_zone burst=15 nodelay; + try_files $uri $uri/ /index.php?$query_string; +} + +# AI similarity / image-search endpoints: each request can trigger an +# outbound HTTP call to the vision vector gateway (see +# App\Services\Vision\VectorGatewayClient), so keep the burst tight — +# a flood here is the "consuming FPM workers" scenario from the slow log. +location ~ ^/api/(art/[0-9]+/similar-ai|search/image) { + limit_req zone=ai_search_zone burst=5 nodelay; + try_files $uri $uri/ /index.php?$query_string; +} diff --git a/docs/skinbase-optimization-audit.md b/docs/skinbase-optimization-audit.md new file mode 100644 index 00000000..1067d515 --- /dev/null +++ b/docs/skinbase-optimization-audit.md @@ -0,0 +1,1130 @@ +# Skinbase.org — Optimization Discovery Audit + +**Audit type:** read-only discovery (no application, schema, or configuration changes) +**Date:** 2026-06-05 +**Repository:** `D:\Sites\Skinbase26` +**Branch:** `develop` +**Commit:** `5af95f6533a129e6c22f051c632ab821240e8317` (`5af95f65 Optimize academy`) +**Working tree at start:** already dirty (many modified and untracked files). Those files were not reset, stashed, or cleaned. This audit inspected the on-disk tree as it existed, including dirty work. + +--- + +## 1. Executive Summary + +```text +Backend: Laravel 12.61.1, PHP ^8.2 (composer platform hints 8.4), Horizon 5.47.2, Reverb 1.10.2 +Frontend: Hybrid MPA: Blade (Nova homepage/gallery) + Inertia React 19 + Vite 7 SSR +Database: MySQL (env example: DB_CONNECTION=mysql, DB_DATABASE=skinbase26); legacy MySQL also configured +Cache: default CACHE_STORE=database; homepage uses failover store redis→database→array +Sessions: SESSION_DRIVER=database, 120 min; ConditionalStartSession skips anonymous public GET +Queues: QUEUE_CONNECTION=redis (.env.example); Horizon + supervisor/systemd examples +Search: Laravel Scout 10.25 + Meilisearch PHP 1.16; SQL LIKE fallbacks remain (news search) +Storage: local disks + S3-compatible object storage for artworks; originals also on local/backup roots +CDN: FILES_CDN_URL / AVATAR_CDN_URL default https://cdn.skinbase.org; Cloudflare purge optional +Rendering: Blade for public homepage/browse/search; Inertia+SSR for studio/admin/profile/upload/feed +Approx. size: ~1361 PHP in app/, 511 JSX, 248 Blade views, 308 migrations, 376 services, 317 controllers, 205 models +Primary content: Public artworks (skins, wallpapers, photography, digital art) plus academy, forum, feed, collections +Primary suspected bottleneck areas: + 1) High-write counters (views/downloads) and scheduled ranking/metric scans + 2) Default database cache/session vs Redis already used for queues + 3) Media pipeline (sync derivatives by default) and PHP-streamed downloads unless nginx accel is live + 4) Search/filter URL surfaces + sitemap live-build fallback + 5) Queue worker timeout 90s vs recommendation jobs timeout 900s +Audit confidence: HIGH for architecture/code paths; MEDIUM for production runtime (no live server/.env); HIGH for historical slow-query evidence in docs/slow-query-optimization-plan.md +``` + +```text +P0 count: 4 +P1 count: 16 +P2 count: 12 +P3 count: 8 +``` + +This audit does **not** recommend an implementation order. It classifies evidence so a later milestone can design a measured roadmap. + +--- + +## 2. Repository State + +| Item | Value | +| ---- | ----- | +| Branch | `develop` (up to date with `origin/develop`) | +| HEAD | `5af95f6533a129e6c22f051c632ab821240e8317` | +| Last commit | `5af95f65 Optimize academy` | +| Working tree before audit | **Dirty** | +| Audit modifications to app | **None** | + +Dirty at start (non-exhaustive; git status at start of audit): + +- Modified: sitemap pipeline, academy controllers/views, SearchController, Artwork model/observers, vision vector search, composer/package.json, robots.txt, routes, tests, SSR bootstrap, featured thumbnail generator. +- Deleted: `public/sitemap.xml` +- Untracked: security-report feature, academy pages sitemap builder, featured thumbnail audit migration, nginx search-rate-limit.conf, admin system pages. + +**Safety:** the audit did not reset, stash, checkout, or revert any of that work. + +--- + +## 3. Architecture + +### Architecture map (as coded) + +```text +Browser + ↓ +nginx (repo snippets: static-cache, sitemaps, download-accel, search-rate-limit, upstream-error-pages) + ├── hashed /build/assets (1y Cache-Control) + ├── public/sitemaps/*.xml (try_files then PHP) + └── PHP-FPM (unknown production pool) + ↓ + Laravel 12 + ├── MySQL (artworks, users, stats, sessions, cache table, ControlPanel) + ├── Redis (queues, Horizon, presence, optional homepage cache, artwork_stats deltas) + ├── Meilisearch (Scout indexes: artworks, users, groups, posts, messages) + ├── Object storage / CDN (cdn.skinbase.org, S3 disk) + ├── Vision / CLIP / YOLO / vector gateway (optional HTTP) + ├── Stripe / Cashier (Academy billing; disabled by default in .env.example) + ├── Sentry + ├── Reverb + Echo (messaging) + └── Queue workers (Horizon supervisors + deploy/supervisor example) + Node SSR (Inertia) on 127.0.0.1:13714 [config/inertia.php] + Python enhance-worker (services/enhance-worker) optional +``` + +### Web request lifecycle + +1. nginx (if snippets are included in production vhost — **UNKNOWN whether live**). +2. `public/index.php` → Laravel 12 bootstrap. +3. Web middleware replacements: `ConditionalStartSession`, `ConditionalShareErrorsFromSession`, `ConditionalValidateCsrfToken`. +4. Appended: `SecurityHeaders`, `RedirectLegacyProfileSubdomain`, `TrackOnlineVisitor`, `UpdateLastVisit`, `HandleInertiaRequests`, onboarding/email-upgrade middleware. +5. Route → controller/service → Blade **or** Inertia. +6. Guest homepage sets `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600`. Authenticated responses set `private, no-store`. + +### Confirmed vs unknown + +| Layer | Status | +| ----- | ------ | +| Application architecture | CONFIRMED from repo | +| Production nginx/PHP-FPM/OPcache | UNKNOWN — only snippets in `deploy/nginx/` | +| Production `.env` drivers | UNKNOWN — `.env.example` only | +| Historical MySQL slow log | CONFIRMED in `docs/slow-query-optimization-plan.md` (server3 / db `skinbase`, 2026-04-04→04-26) | + +--- + +## 4. Technology Stack + +| Area | Evidence | Version / default | +| ---- | -------- | ----------------- | +| PHP | `composer.json` require | `^8.2`; platform `ext-pcntl/posix` 8.4.0 | +| Laravel | `composer.lock` | **v12.61.1** | +| Inertia | lock | inertia-laravel **v1.3.4**; JS `@inertiajs/react` ^1.0.4 | +| Horizon | lock | **v5.47.2** | +| Reverb | lock | **v1.10.2** | +| Scout | lock | **v10.25.0** | +| Meilisearch PHP | lock | **v1.16.1** | +| Intervention Image | lock | **3.11.8** | +| Predis | lock | **v3.5.0**; `.env.example` `REDIS_CLIENT=phpredis` | +| Sentry | lock | **4.25.1** | +| Cashier | composer.json | `^16.5` | +| Debugbar | require-dev | fruitcake/laravel-debugbar **v4.3.0** | +| Pest | require-dev | `^4.3` | +| Node/Vite | package.json | Vite **^7.0.7**, React **^19.2.4**, Tailwind **^3.1.0** (also `@tailwindcss/vite` ^4) | +| Package manager | lockfiles | Composer + npm (`package-lock.json`) | +| CSS | resources | Tailwind + SCSS `nova.scss` + `nova-grid.css` | +| Auth | Breeze-style + Socialite | session guard `web`; extra `controlpanel` guard | +| Roles | middleware aliases | admin/moderator/staff/creator | +| Mail | `.env.example` | `MAIL_MAILER=log` | +| Analytics | code | internal academy/feed/discovery events; no GA package found in composer | +| Error monitoring | Sentry Integration in `bootstrap/app.php` | DSN from env | +| Realtime | Reverb + Echo + pusher-js | messaging | +| ControlPanel | `packages/klevze` | ~1436 PHP files, ~510 `/cp` routes | +| Legacy site | `oldSite/` | historical PHP/assets; not the live app | + +**No Docker Compose / GitHub Actions** found at repo root. + +--- + +## 5. Application Domains + +Domains that **exist** in code (not assumed): + +- Artworks / gallery / downloads / views / favourites / likes / reactions / awards / medals +- Categories / content types / tags +- Users / profiles / avatars / covers / XP / achievements / followers +- Discover / explore / ranking / heat / trending / for-you / recommendations / CLIP vectors +- Collections / groups / worlds / web stories +- Studio / dashboard / upload v1+v2 / enhance +- Comments (artwork, profile, news, collection, nova cards, posts) +- Forum +- Feed / posts / hashtags +- News / blog / pages / interviews +- Academy (courses, lessons, prompts, packs, challenges, billing) +- Nova Cards +- Messaging (Reverb) +- Search (Meilisearch + SQL) +- Sitemaps / robots / SEO +- Moderation / staff / traffic online visitors +- ControlPanel (`/cp`) +- Security reports (untracked at audit time) + +### Size inventory (application tree, excluding vendor/node_modules) + +| Kind | Count | +| ---- | ----- | +| PHP `app/` | 1361 | +| Blade `resources/views` | 248 | +| PHP `database/` | 330 | +| PHP tests | 274 | +| Config PHP | 63 | +| JSX | 511 | +| JS in `resources/js` | 50 | +| Controllers | 317 | +| Models | 205 | +| Services | 376 | +| Jobs | 45 | +| Listeners | 6 | +| Commands | 124 | +| Middleware | 24 | +| Policies | 17 | +| Events | 33 | +| Notifications | 13 | +| Observers | 10 | +| Repositories | 3 | +| Mail | 6 | +| HTTP Resources | 2 | +| React pages | 207 | +| React components | 288 | +| Blade components | 29 | +| Migrations | 308 | +| Feature tests | 227 | +| Unit tests | 45 | +| Playwright e2e | 11 | +| Packages PHP | 1436 | +| public/build JS | 260 | +| public/build CSS | 5 | + +--- + +## 6. Route Inventory + +`php artisan route:list --json` succeeded. `var/routes.json` (cached dump) reports **1518** routes. + +Approximate mix from `var/routes.json`: + +| Bucket | Approx count | Notes | +| ------ | ------------ | ----- | +| GET\|HEAD | 770 | | +| POST | 572 | | +| Auth-ish middleware | 669 | | +| `api/*` | 303 | Many still use `web` middleware group | +| `cp` ControlPanel | 510 | Dominant admin surface | +| `studio` | 155 | | +| `settings` | 75 | | +| `moderation` / admin | included in admin-ish 569 | | +| `_debugbar` | 5 | Present in this dump — debug tooling registered in this environment | +| Horizon | 22 | | + +Source-level `Route::` calls: `web.php` ~660, `api.php` ~272, `auth.php` 27, `legacy.php` 34. + +### Important public page types (benchmark candidates) + +| Page Type | Example Route | Handler | Public? | Cache candidate? | +| --------- | ------------- | ------- | ------- | ---------------- | +| Homepage | `GET /` | `Web\HomeController@index` | yes | **already** guest flexible cache | +| Homepage alias | `GET /home` | same | yes | duplicate URL | +| Featured | `GET /featured` | `FeaturedArtworksController` | yes | yes | +| Latest | `GET /uploads/latest` | `Community\LatestController` | yes | yes | +| Discover trending | `GET /discover/trending` | `DiscoverController@trending` | yes | Meili + Cache::remember | +| Discover rising | `GET /discover/rising` | `DiscoverController@rising` | yes | yes | +| Discover fresh | `GET /discover/fresh` | `DiscoverController@fresh` | yes | yes | +| Explore | `GET /explore` | `ExploreController@index` | yes | Meili TTL map | +| Content-type browse | `GET /{contentTypeSlug}/{path?}` | `BrowseGalleryController@content` | yes | versioned cache | +| Artwork detail | `GET /art/{id}/{slug?}` | `ArtworkPageController@show` | yes | per-id candidate | +| Alternate artwork URL | `GET /{type}/{categoryPath}/{artwork}` | `BrowseGalleryController@showArtwork` | yes | duplicate of `/art/{id}` | +| Download | `GET /download/artwork/{id}` | `ArtworkDownloadController` | yes | no (write + file) | +| Search | `GET /search` | `Web\SearchController@index` | yes | Meili + news LIKE; noindex | +| Tags index | `GET /tags` | `Web\TagController@index` | yes | yes | +| Tag show | `GET /tags/{tag}` (two route shapes) | TagController vs HashtagFeed | yes | collision risk | +| Categories | `GET /categories` | `CategoryController@index` | yes | yes | +| Profile | `GET /@{username}` | `ProfileController@showByUsername` | yes | per-user | +| Sitemap | `GET /sitemap.xml` | `SitemapController@index` | yes | static file preferred | +| Robots | `GET /robots.txt` | `RobotsTxtController` | yes | generated | +| For-you | `GET /discover/for-you` | auth | no | personalized | + +--- + +## 7. High-Traffic Request Paths + +### Homepage `GET /` + +```text +Route / (web) +→ ConditionalStartSession (skip anonymous GET if configured) +→ TrackOnlineVisitor (Redis presence) +→ HomeController::index +→ HomepageService::all() [guest] or allForUser($user) +→ view web.home (Blade, not Inertia) +``` + +- Guest: `Cache::store(homepage failover)->flexible(...)` TTL default 1800s, fresh 30s (`HomepageService`, `config/homepage.php`). +- Authenticated: **no payload cache**; personalization + same public rails. +- Sections: hero, announcement, community favorites, hall of fame, rising, trending, fresh, collections, world spotlight, groups, tags, creators, news. +- Cache-Control set on response (guest public, user no-store). +- Obvious DB/Meili sources: many (each section method); mitigated for guests by payload cache. +- External APIs: none on this path unless world/news fail internally. +- **CONFIRMED cache candidate already implemented for guests.** Authenticated homepage is still a **potential cache candidate** (partial). + +### Browse / explore / discover + +- Discover trending: `ArtworkSearchService::discoverTrending` (Meili cached) with SQL fallback `fallbackTrendingFromDatabase`. +- Explore/browse: Meilisearch sort maps + `Cache::remember` keyed by sort/page/viewer segment (`BrowseGalleryController` CACHE_VERSION `v4`). +- Content-type catch-all `/{contentTypeSlug}/{path?}` is a high-crawl surface. + +### Artwork detail `GET /art/{id}/{slug?}` + +```text +ArtworkPageController::show +→ Artwork::withTrashed lookup +→ 404/410/403 suggestion queries (ErrorSuggestionService) +→ full with([user.profile, group..., categories..., tags, stats, awardStat]) +→ 301 if slug mismatch +→ ArtworkResource (extra Schema::hasTable + per-request counts) +→ related query with orWhereHas(categories|tags) limit 12 +→ ArtworkComment::limit(500) with user.profile +→ Blade/Inertia mix (seo via SeoFactory::artwork) +``` + +Query sources: 2 artwork loads + related `orWhereHas` + comments up to 500 + resource extras (followers, likes, bookmarks, favourites). +View increment is **not** on this GET; client POSTs `/api/art/{id}/view`. + +### Profile `GET /@{username}` + +`ProfileController::renderProfilePage`: artworks pagination, featured join `artwork_features`, favourites, `user_statistics`, social links, plus later tabs. Multiple queries; some eager loads present. + +### Search `GET /search` (throttle:search) + +Meilisearch via `ArtworkSearchService::search` **or** `popular()`. Additional SQL `NewsArticle` `LIKE '%q%'` on title/excerpt/content/meta_title. Groups via `GroupDiscoveryService`. Canonical query 301. `page_robots = noindex,follow`. + +### Download `GET /download/artwork/{id}` (throttle:downloads) + +Resolves original via `ArtworkOriginalFileLocator`; writes `artwork_downloads` row; increments counters (possibly **twice** — `incrementDownloadCountIfAvailable` + `ArtworkStatsService::incrementDownloads(..., defer: false)`). X-Accel-Redirect only if `app.download_accel_enabled`. + +### View write `POST /api/art/{id}/view` (throttle:120,1) + +Inserts `artwork_view_events` **and** `incrementViews(..., defer: false)` (immediate MySQL, not Redis delta). XP award may fire. + +--- + +## 8. Database Architecture + +**Engine:** MySQL (`DB_CONNECTION=mysql` in `.env.example`). Laravel also defines sqlite/pgsql. Tests use sqlite `:memory:`. + +**Complexity:** High. 308 migrations. `Schema::create` appears across 90+ migration files; unique table count is well over 100 (artworks, users, academy, collections, groups, worlds, forum, feed, rec, nova cards, ControlPanel, etc.). Exact production table count requires `information_schema`. + +### High-traffic / large-growth tables (from code + slow-query doc) + +| Table | Purpose | Important columns | Indexes (from migrations) | Likely usage | +| ----- | ------- | ----------------- | ------------------------- | ------------ | +| `artworks` | primary content | user_id, slug, is_public, is_approved, published_at, deleted_at, trending_score_*, visibility, maturity_* | PK; slug; browse `(is_public,is_approved,published_at)`; **batch1** `(deleted_at,is_public,is_approved,published_at,id)` and `(deleted_at,is_public,is_approved,user_id)`; FULLTEXT `(title,description)` | catalog, sitemaps, ranking jobs | +| `artwork_stats` | denormalized counters | views, downloads, favorites, windowed cols added later | PK artwork_id | joins on list/detail | +| `artwork_metric_snapshots_hourly` | hourly metrics | artwork_id, bucket_hour, counts | unique (artwork_id,bucket_hour); idx_bucket_hour; **batch1 idx_bucket_artwork (bucket_hour,artwork_id)** | rising/heat jobs | +| `artwork_view_events` | per-view log | artwork_id, user_id, viewed_at | (inspect later) | insert on every view; prune 90d | +| `artwork_downloads` | download log | artwork_id, user_id, ip | | insert on download | +| `artwork_favourites` / likes / comments / shares / reactions | engagement | FKs + timestamps | various | counts, rec jobs | +| `artwork_tag` / `artwork_category` | pivots | | | filters, sitemaps | +| `tags` | taxonomy | usage_count, **artworks_count** (batch1) | idx_tags_artworks_count | popular tags | +| `users` / `user_profiles` / `user_statistics` | accounts | | | profiles, toolbars | +| `rank_artwork_scores` | ranking | model_version, score_* | batch1 idx_mv_trending/new_hot/best | explore | +| `sessions` | session store | | | logged-in + leftover cookies | +| `cache` / `cache_locks` | default cache | | | if CACHE_STORE=database | +| `jobs` / `failed_jobs` | queues if database driver | payload | | LIKE payload scans historically | +| `rec_item_pairs` / `rec_artwork_recs` | recommenders | | | nightly jobs | +| `user_discovery_events` | personalization | | | queued ingest | +| ControlPanel tables | `/cp` | | | admin | + +Legacy connection `projekti_old_skinbase` is configured for import, not request path. + +--- + +## 9. Existing Indexes + +**Artworks (core):** + +- PK `id` +- `user_id` +- `slug` (unique dropped later; non-unique index remains) +- `is_public`, `is_approved`, `published_at` single-column +- compound `idx_artworks_browse (is_public, is_approved, published_at)` +- compound `idx_public_approved_published_id (deleted_at, is_public, is_approved, published_at, id)` — **added 2026-04-26 batch1** +- compound `idx_public_approved_user_id` +- FULLTEXT `artworks_title_description_fulltext` (2026-04-26) +- many maturity/feature/group indexes + +**Snapshots:** unique `(artwork_id, bucket_hour)`; `idx_bucket_hour`; `idx_artwork_bucket`; **batch1 `idx_bucket_artwork (bucket_hour, artwork_id)`**. + +**Rank scores:** `(model_version, score_trending|new_hot|best)` batch1. + +**Tags:** `artworks_count` column + index (batch1) to replace correlated `count(*)` subquery. + +### Index vs query mismatches (EXPLAIN candidates — do not add yet) + +1. **Related artworks on detail** (`ArtworkPageController` ~157–184): `WHERE user_id = ? OR group_id = ? OR whereHas categories OR whereHas tags` + `ORDER BY published_at`. Existing indexes do not match this OR/whereHas pattern. **Candidate requiring EXPLAIN.** +2. **Public count** (`count(*)` on public+approved+published): covering index exists after batch1; still a **counter-cache candidate**. +3. **News search LIKE '%q%'** on `content`: no FULLTEXT observed on news in this pass. **EXPLAIN + FULLTEXT/Meili candidate.** +4. **Profile username** `whereRaw('LOWER(username) = ?')`: functional lookup may skip normal unique index. **EXPLAIN candidate.** +5. **Toolbar subqueries** counting artworks/favourites/notifications per user (cached 30s): not a missing index issue if `user_id` indexed; still correlated. +6. **Historical Q1/Q2 aggregate listing** joining derived fav/comment/share counts: batch1 index helps outer scan; inline derived joins still need EXPLAIN on current ranking commands. +7. **Jobs payload LIKE** (historical AutoTag dedupe): no index possible; verify code no longer scans `jobs.payload`. + +--- + +## 10. Query Findings + +### QF-001 — Artwork detail related `orWhereHas` + +```text +File: app/Http/Controllers/Web/ArtworkPageController.php +Line/range: ~157–184 +Function: show() +Query behavior: related artworks via OR of user_id, group_id, whereHas(categories), whereHas(tags), latest published_at, limit 12 +Why expensive: whereHas exists-subqueries + OR prevents single index +Evidence: source +Confidence: high +``` + +### QF-002 — Comments unbounded-ish load + +```text +File: ArtworkPageController.php +Line/range: ~209–215 +Query behavior: ArtworkComment where artwork_id, is_approved, order created_at, limit 500 +Why expensive: hydrates up to 500 comment+user.profile rows on every detail view +Evidence: source +Confidence: high +``` + +### QF-003 — Search news leading-wildcard LIKE + +```text +File: app/Http/Controllers/Web/SearchController.php +Line/range: ~66–78 +Query behavior: NewsArticle title/excerpt/content/meta_title LIKE '%q%' +Why expensive: cannot use B-tree; content column scan +Evidence: source; public /search is throttled but anonymous +Confidence: high +``` + +### QF-004 — Meilisearch visibility fallback over-fetch + +```text +File: app/Services/ArtworkSearchService.php +Line/range: search() ~85–106; searchWithThumbnailPreference ~109–128 +Query behavior: paginate candidate pool up to 240 then filter in PHP +Why expensive: large Meili page + extra round trip when maturity filtering required +Evidence: SEARCH_CANDIDATE_POOL_MAX = 240 +Confidence: medium +``` + +### QF-005 — ArtworkResource extra queries on detail serialize + +```text +File: app/Http/Resources/ArtworkResource.php +Line/range: ~23–80+ +Query behavior: loadMissing relations; Schema::hasTable; user_statistics lookup; exists() on likes/bookmarks/favourites +Why expensive: several extra queries per detail; Schema::hasTable on hot path +Evidence: source +Confidence: high for detail; would be N+1 if used in lists (list uses ArtworkListResource) +``` + +### QF-006 — Guest homepage aggregation (mitigated) + +```text +File: app/Services/HomepageService.php +Function: buildGuestPayload / all +Query behavior: many section queries +Why expensive on miss: large fan-out +Evidence: flexible cache + homepage:warm-guest-cache schedule +Confidence: high that miss is expensive; guest hit is mitigated +``` + +### QF-007 — Authenticated homepage uncached + +```text +File: HomepageService::allForUser +Why expensive: repeats public rails + user_data counts + for_you + following +Evidence: no Cache::remember wrapping allForUser (only some subcalls) +Confidence: high +``` + +### QF-008 — Toolbar correlated counts + +```text +File: app/Providers/AppServiceProvider.php View::composer layouts.nova +Line/range: ~226–258 +Query behavior: selectSub COUNT artworks, favourites, unread notifications + messages join +Cached: Cache::remember toolbar:{userId} TTL config toolbar.cache_ttl_seconds default 30 +Confidence: high +``` + +### QF-009 — Historical production slow log (server3) + +`docs/slow-query-optimization-plan.md`: 68,950 slow queries / 22 days / 15.39B rows examined. Top class: artwork aggregate SELECT with derived joins (~117,834s, 78% of slow time). Batch1 indexes and tag `artworks_count` were added in repo **the same day as that analysis**. **Production re-measure required** to know residual cost. + +### QF-010 — `inRandomOrder()` + +Interview/user controllers (`InterviewController`, `UserController`). Low traffic vs gallery. Confidence: medium, likely P3. + +### QF-011 — Ranking/heat scheduled scans + +Commands `nova:recalculate-rankings`, `nova:metrics-snapshot-hourly`, `nova:recalculate-heat`, `skinbase:recalculate-trending` every 15–30 minutes. Historical Q1/Q2 look like this class. Confidence: high they are heavy; medium they still match old SQL. + +--- + +## 11. N+1 Findings + +| Source | Relation | Amplification | Eager load? | +| ------ | -------- | ------------- | ----------- | +| Gallery mapping using `$a->user?->profile` after `loadMissing(['user.profile','categories.contentType'])` | user/profile/categories | none if loadMissing ran | **present** on search/tag/discover | +| `ArtworkListResource` `$user?->profile?->avatar_url` | profile | 1 per card if profile not loaded | list resource checks `relationLoaded('user')` but may still lazy-load profile | +| ArtworkResource `contributors.user.profile` | nested | per contributor | loadMissing on detail only | +| Comments loop uses `$c->user` | user.profile | 500 max | **with('user.profile') present** | +| 404 `ErrorSuggestionService` trending artworks/tags/creators | various | extra queries on 404 | unknown eager | +| ControlPanel / DataTables | unknown | `/cp` 510 routes | not fully traced | +| Nova toolbar content types | ContentTypeSlugResolver | every Blade nova layout | cached-ish via resolver | + +No smoking-gun `foreach ($items as $item) { $item->user }` without eager load was confirmed on the main gallery path after `loadMissing`. Residual N+1 risk is **profile avatar accessors** and **ControlPanel**. Confidence: medium. + +--- + +## 12. Cache Architecture + +| Item | Value | +| ---- | ----- | +| Default store | `env('CACHE_STORE', 'database')` — **database in .env.example** | +| Redis store | defined; homepage failover `redis, database, array` | +| File store | `storage/framework/cache/data` | +| Tags | not a first-class pattern in app Cache:: calls reviewed | +| Key examples | `homepage.payload.guest`, `homepage.tags.{n}`, `search.popular.{segment}.page.{n}`, `toolbar:{userId}`, `explore.cache.version`, `search.related.{id}.{segment}` | +| TTL | 120–1800s typical; toolbar 30s; search 300s | +| Invalidation | ArtworkObserver increments `explore.cache.version`; homepage clear methods; some forget() on world stories | +| Production Redis for default cache | **UNKNOWN** | + +**Potential cache candidates (not implemented as a change here):** + +- Authenticated homepage modules +- Artwork detail related + comment trees +- Public artwork count +- Tag directory stats +- Error-page suggestion rails +- Ranking top-N lists (partially via RankBuildListsJob) +- Category directory +- `/search` empty-state popular (already remembered) + +--- + +## 13. Session Architecture + +| Item | Value | +| ---- | ----- | +| Driver | `database` (`.env.example` / `config/session.php`) | +| Table | `sessions` | +| Lifetime | 120 minutes | +| Lottery | 2/100 sweeps | +| Conditional sessions | `config/skinbase-sessions.php` enabled; skip anonymous public GET; skip bots | +| Always-session paths | login, dashboard, studio, upload, admin, messages, etc. | +| Public skip list | `/`, featured, discover, explore, art, tags, sitemaps, news, … | + +If a session cookie already exists, session is **not** skipped even on public GET. That limits anonymous CDN/full-page cache for returning users. + +`HandleInertiaRequests` avoids reading auth when session skipped. + +**Impact:** full-page HTTP cache for anonymous first-time crawlers is feasible; cookie holders still hit origin. + +--- + +## 14. Queue Architecture + +| Item | Value | +| ---- | ----- | +| Default connection | `.env.example` **redis**; config fallback `database` | +| Horizon | name `skinbase-nova`; path `/horizon`; prefix `skinbase_nova_horizon:` | +| Horizon supervisors | `search,default` (timeout **960s**, production maxProcesses 5); `broadcasts,notifications` (timeout 90s, maxProcesses 3) | +| Supervisor example | `--timeout=90` queues `search,forum-security,forum-moderation,vision,recommendations,discovery,mail,default` **numprocs=1** | +| systemd example | same `--timeout=90` | +| Job timeout example | `RecComputeSimilarHybridJob` **$timeout = 900**, `$tries = 1` | + +**CONFIRMED mismatch:** deploy worker timeout 90s vs recommendation jobs 900s vs Horizon 960s. If production uses supervisor/systemd snippet rather than Horizon, 900s jobs will be killed. + +Horizon **does not** list `vision`, `recommendations`, `discovery`, `mail`, `forum-*` as dedicated supervisors; those names only appear on the supervisor example. If Horizon is the production worker, those queues must be consumed via `default` or they stall. `docs/QUEUE.md` already warns Scout `search` queue must be consumed. + +Job classes include: AutoTag, embeddings/vectors, derivatives, featured thumbs, IncrementArtworkView (queued variant exists but view controller uses sync increment), indexing, discovery ingest, rec compute, sitemaps, enhance, security report, nova cards, leaderboards, etc. + +Failed jobs: Laravel `failed_jobs` table; Horizon trim failed 10080 minutes. + +--- + +## 15. Scheduler + +Defined in `routes/console.php` (Laravel 11+ style). `app/Console/Kernel.php` registers commands; **schedules live in console.php**. + +| Task | Frequency | Purpose | Potential cost | +| ---- | --------- | ------- | -------------- | +| `skinbase:recalculate-trending` 24h | :06/:36 | trending scores | HIGH (historical slow SQL class) | +| trending 7d | :19/:49 | | HIGH | +| reset-windowed-stats | daily/weekly 03:30/03:50 | counters | MEDIUM–HIGH | +| uploads/enhance cleanup | daily 03:00–03:30 | files | MEDIUM | +| analytics aggregates | 03:10–03:35 | discovery/feed/tags | MEDIUM | +| academy analytics | hourly + daily | | MEDIUM | +| `skinbase:flush-redis-stats` | every 10 min | drain deltas | LOW–MED (if views still sync, less used) | +| prune view events | Sunday 04:00 | delete old events | HIGH I/O if table large | +| rec pair/tag/behavior/hybrid jobs | 4h / 02:00–02:30 | similarity | HIGH (900s timeouts) | +| publish-scheduled posts/artworks/news/cards | every minute | publish | LOW–MED | +| collection lifecycle | every 10 min | | MED | +| homepage:warm-guest-cache | every 10 min offset | warm cache | MED on miss | +| artworks:search-reconcile | hourly :28 | Meili drift | MED | +| posts:warm-trending | every 2 min | | LOW–MED | +| nova:recalculate-rankings | :07/:37 | ranking v2 | HIGH | +| metrics-snapshot-hourly | :02 | snapshots | HIGH write | +| nova:recalculate-heat | every 15 min | heat | HIGH | +| sitemaps generate | 10:30 and 22:30 | XML | HIGH | +| sitemaps publish | every 6h :08 | | MED | +| sitemaps validate | 04:45 | | MED | +| security:scan | weekly Mon 05:15 | | MED | +| leaderboards / rank lists / nova card caches | hourly | | MED | +| prune metric snapshots keep 7d | daily 04:00 | | HIGH I/O | +| forum AI/bot/post/firewall scans | hourly | | MED | +| health:tick | every minute | | LOW | +| horizon:snapshot | hourly :45 | | LOW | + +Night window 02:00–05:00 is densely packed. + +--- + +## 16. Search Architecture + +```text +/search?q= + → throttle:search (20/user/min, 30/IP/min) + → canonical 301 + → Artwork::search() Scout/Meilisearch + → optional PHP visibility filter / candidate pool + → NewsArticle LIKE '%q%' + → GroupDiscoveryService + → Blade search.index, robots noindex,follow +``` + +- Driver default **meilisearch**. Queue: `search` on Redis. +- Indexes: artworks (rich filterable/sortable attrs), messages, plus Searchable on User, Group, Post. +- Autocomplete: React `SearchBar.jsx` / overlay (separate API; not fully traced). +- Image/vector search: `/api/art/{id}/similar-ai`, `/api/search/image` with `throttle:vector-search`; nginx snippet `search-rate-limit.conf`. +- Fallback SQL LIKE remains for news and historical artwork LIKE (slow-query doc); Scout path is primary for artworks. +- Scale constraint: Meilisearch cluster size **UNKNOWN**; PHP LIKE on news `content` will not scale. +- Search is noindex (good) but `robots.txt` still `Allow: /` so crawlers may hit it until they honor robots meta. + +--- + +## 17. Statistics / Counters + +| Signal | Mechanism | Sync? | Location | +| ------ | --------- | ----- | -------- | +| Artwork views | insert `artwork_view_events` + increment `artwork_stats` views/24h/7d | **sync, defer:false** | `ArtworkViewController`, `ArtworkStatsService` | +| Queued IncrementArtworkView | exists | not used by this controller | `app/Jobs/IncrementArtworkView.php` | +| Redis deltas | `artwork_stats:deltas` + `skinbase:flush-redis-stats` | unused when defer=false | ArtworkStatsService | +| Downloads | row in `artwork_downloads` + increment (possibly duplicated) | sync | `ArtworkDownloadController` | +| Favourites/likes/comments | tables + observers | write path | observers/jobs medal stats | +| Ranking/heat | scheduled recompute | async | commands | +| XP | XPService on view | sync on view POST | | +| Presence | Redis | after response | TrackOnlineVisitor | +| Profile views | not confirmed as a dedicated counter | | | + +**High-write path (P0/P1):** every public artwork view POST = at least one INSERT + one UPDATE, throttle 120/min/IP, CSRF excepted for `api/art/*/view`. + +--- + +## 18. Media Pipeline + +| Item | Actual | +| ---- | ------ | +| Upload max | 50 MB image, 200 MB archive (`config/uploads.php`) | +| MIME | jpeg/png/webp; GIF off by default | +| Derivatives | xs 320, sm 680, md 1024, lg 1920, xl 2560, sq 512 | +| Featured variants | mobile_xs 400 … desktop_xl 2200×1238 | +| Quality | 85 default; sq 82 | +| Queue derivatives | `UPLOAD_QUEUE_DERIVATIVES` **default false** → **synchronous** publish pipeline unless enabled | +| GenerateDerivativesJob | exists; used when queued | +| Presenter widths | xs 160 / sm 320 / md 640 / lg 1280 / xl 1920 / sq 400 — **mismatch vs derivative max sizes** | +| Format | WebP thumbs typical; originals jpeg/png/webp | +| AVIF | not a first-class upload MIME | +| CDN | `https://cdn.skinbase.org`; missing thumbs `https://files.skinbase.org/default` | +| srcset | ThumbnailPresenter builds srcset; Blade galleries pass `thumb_srcset`; homepage hero preload + fetchpriority high | +| lazy | widespread `loading="lazy"`; homepage hero is preloaded (good LCP) | +| Lighthouse (skinbase.top, 2026-03-23) | LCP 0.8s, FCP 0.7s, CLS 0.017, TBT 0ms, performance 0.99 — **one historical homepage lab run**, not production skinbase.org proof | + +--- + +## 19. Filesystem / CDN + +Disks (`config/filesystems.php`): `local` (private), `public`, `sitemaps_public` (public_path), `s3`. + +Artwork roots (`config/uploads.php`): + +- `storage/app/artworks` +- local originals `storage/app/originals/artworks` +- readonly backup `/opt/www/virtual/files/cdn/artworks/original` +- object disk `s3` prefix `artworks` + +Download may stream through PHP if accel disabled (`deploy/nginx/download-accel.conf` documents FPM buffering problem). + +CDN purge webhook optional (`CDN_PURGE_URL`, Cloudflare zone/token). + +--- + +## 20. Frontend Architecture + +Hybrid: + +- **Blade Nova MPA** for homepage, discover, explore, search, many public galleries. +- **Inertia + React 19** for studio, admin, profile, upload, feed, forum, academy pages, collections manage. +- **SSR** enabled (`config/inertia.php` `ssr.enabled=true`, url `http://127.0.0.1:13714`); `deploy/supervisor/skinbase-ssr.conf` runs `bootstrap/ssr/ssr.js`. Homepage excluded from SSR graph (`resources/js/ssr.jsx`). +- Vite 7, Tailwind 3 (+ vite v4 plugin present), Sass, Alpine in devDependencies. +- Notable JS: TipTap, highlight.js/lowlight, emoji-mart + data, framer-motion, Echo/pusher-js, marked, react-markdown, turndown. + +--- + +## 21. Frontend Payload Findings + +| Candidate | Evidence | +| --------- | -------- | +| Guest homepage JSON-ish `props` | HomepageService serializes many rails (10 items × many sections + collections + tags + creators + news). Cached. | +| Authenticated homepage | same plus for_you / following / user_data — uncached | +| Artwork detail | ArtworkResource large graph + related 12 + comments ≤500 | +| Profile page | artworks page + featured + favourites + stats in one Inertia render | +| Shared Inertia | auth user flags + `studio_groups` via GroupService for every Inertia page when logged in (`HandleInertiaRequests::share`) | +| Search | full paginator + news + groups | +| TipTap / emoji-data chunks | 481 KB + 423 KB — studio/editor, not homepage | + +`ArtworkListResource` is relatively slim (good). Detail resource is fat (expected). + +--- + +## 22. Build Assets + +Existing `public/build` inspected (build **not** re-run, to avoid dirtying tree). + +| Metric | Value | +| ------ | ----- | +| JS files | 260, **5.73 MB** total | +| CSS files | 5, **500 KB** total | +| Largest JS | `vendor-tiptap` 481 KB; `emoji-data` 423 KB; `vendor-syntax` 307 KB | +| Largest CSS | `app-oMtr0NO-.css` 429 KB | +| Manual chunks | tiptap, syntax, tooltip, motion, realtime, emoji (vite.config.mjs) | + +`npm run build` was **not** executed (node_modules present; existing hashed assets sufficient for size evidence). + +--- + +## 23. SEO + +Shared system: `config/seo.php`, `SeoFactory` / `SeoDataBuilder`, Blade `partials/seo/head.blade.php`, Inertia `SeoHead.jsx` (`docs/seo-audit-system-v1-summary.md`). + +| Page type | Title | Description | Canonical | Indexability | Structured data | Potential issue | +| --------- | ----- | ----------- | --------- | ------------ | --------------- | --------------- | +| Homepage | Skinbase – Digital Art & Wallpapers | yes | `url('/')` | index,follow | WebSite + SearchAction | `/home` duplicate | +| Artwork | `{title} by {author} — Skinbase` | stripped desc | `route('art.show')` 301 slug | index | ImageObject + CreativeWork | second URL via content-type path | +| Discover | page_title set | yes | canonicalRoute | index | via unified seo if wired | sort/pagination params | +| Search | Search: q | yes | canonicalized query | **noindex,follow** | | still Allow:/ in robots | +| Tags | Browse Tags | yes | tags.index | index | | two `/tags/{tag}` route patterns | +| Profile | via SeoFactory | | lowercase username 301 | index | | `tab=` redirected to path | +| 404 | errors layout | | | **noindex,nofollow** | | extra suggestion queries | + +OG/Twitter: `summary_large_image`; fallback `/gfx/skinbase_back_001.webp`. + +--- + +## 24. Canonicals / Duplicate URLs + +Flagged surfaces (no redirects implemented in this audit): + +| Surface | Evidence | +| ------- | -------- | +| `/` and `/home` | both HomeController | +| `/art/{id}/{slug}` vs `/{type}/{category}/{artwork}` | both public artwork URLs | +| `/explore/top-rated` 301 to `explore?sort=` | good | +| `/discover` 301 to `/discover/trending` | good | +| Search extra params 301 stripped | good | +| Profile case / username_redirects | 301 | +| `/contact` defined twice | PageController marketing **and** ApplicationController `contact.show` (later wins in Laravel) | +| `/tags/{tag}` vs feed hashtag regex | possible overlap | +| Explore sort querystrings | multiple indexable sorts unless canonicalized | +| Legacy `gallery.php`, `/lost-password` | 301 | +| www/HTTPS | not in app; **server UNKNOWN** | +| `public/robots.txt` vs dynamic `RobotsTxtController` | both exist; route `GET /robots.txt` uses controller | + +--- + +## 25. Sitemaps + +- Routes: `/sitemap.xml`, `/sitemaps/{name}.xml` +- Config: shard size 10k; many families (artworks, users, tags, academy-*, news-google, forum, …) +- Scheduler: generate 10:30/22:30, publish every 6h, validate daily, cleanup job +- nginx snippet serves static files first +- **Defaults:** `SITEMAPS_BUILD_ON_REQUEST=true`, `SITEMAPS_FALLBACK_TO_LIVE_BUILD=true` — if static/published missing, **PHP builds sitemap on crawler request** +- `public/sitemap.xml` is **deleted in dirty tree**; static files under `public/sitemaps/` exist +- Cache-Control on PHP/file responses uses `sitemaps.cache_ttl_seconds` default 900; nginx snippet 6h + +Potentially expensive: live-build of sharded artworks sitemap on cache miss. + +--- + +## 26. robots.txt + +Controller output: + +```text +User-agent: * +Allow: / +Sitemap: {APP_URL}/sitemap.xml +``` + +Static `public/robots.txt` matches (sitemap hard-coded `https://skinbase.org/sitemap.xml`). + +- No `Disallow` for `/search`, `/explore?*`, `/cp`, `/studio`, `/horizon`, APIs +- Relies on meta robots / auth for private areas +- Parameter crawl explosion: **POSSIBLE** for sort/filter/pagination +- Search is noindex in HTML (good) but still allowed + +--- + +## 27. Web Server / HTTP + +Repo provides **snippets only**, not a full production vhost: + +- `deploy/nginx/static-cache.conf` — 1y hashed assets; 7d `/images` +- `sitemaps.conf` — try_files then PHP +- `download-accel.conf` — internal X-Accel +- `search-rate-limit.conf` — untracked in dirty tree; 20r/m search, 10r/m AI search +- `upstream-error-pages.conf` — 502/504 static HTML + +**Production web-server configuration not available in repository** (no live `nginx.conf`, TLS, HTTP/2/3, Brotli, gzip, FPM socket, `client_max_body_size`). + +App security headers middleware: X-Frame-Options SAMEORIGIN, nosniff, Referrer-Policy, Permissions-Policy. CSP not set here. + +--- + +## 28. PHP Runtime + +| Item | Repo evidence | +| ---- | ------------- | +| Version | ^8.2; platform 8.4 extensions | +| OPcache / memory_limit / FPM | **not in repo** | +| CLI | 124 commands; Horizon memory 128MB workers; master 64MB | +| Upload | app 50MB; PHP `upload_max_filesize` UNKNOWN | + +--- + +## 29. Dependencies + +**Performance-relevant:** Horizon, Scout/Meilisearch, Redis/predis, Intervention Image, Flysystem S3, Reverb. + +**Image:** intervention/image, gumlet/php-image-resize. + +**Debug:** fruitcake/laravel-debugbar (require-dev). `bootstrap/cache/packages.php` currently lists debugbar — **this local environment has it discovered**. Production `composer install --no-dev` (docs/deployment.md) should omit it. `var/routes.json` includes `_debugbar` routes. + +**composer audit (read-only):** advisories reported for transitive `guzzlehttp/guzzle` (9), `guzzlehttp/psr7` (2), `league/commonmark` (6), `mtdowling/jmespath.php` (1), `phpseclib/phpseclib` (1). Abandoned: none. **Do not treat this as a full CVE triage; versions/severity need human review. No secrets printed.** + +**npm audit:** not completed in this pass. + +--- + +## 30. Security / Abuse-Sensitive Paths + +Not a pentest. Performance-adjacent: + +| Path | Control | Residual | +| ---- | ------- | -------- | +| `/search` | throttle 20/30 per min; query param cap 15 / 500 chars | still Meili+SQL LIKE | +| `/api/art/{id}/view` | 120/min; CSRF excepted | cheap to write DB | +| `/api/art/{id}/similar-ai` | vector-search limiter; nginx snippet | outbound vision HTTP | +| Downloads | 60/120 per min | originals via PHP if no accel | +| Uploads | per-user/IP chunk limits; draft quotas | sync image work if queue flag false | +| Forum | dedicated firewall/bot/AI middleware | scheduled scans | +| Registration | Turnstile optional, disposable domains, rate limits | | +| Horizon | `web` middleware only in config — **auth gating UNKNOWN** | +| `/cp` | ControlPanel package | large attack/admin surface | +| Stripe webhooks | CSRF except `stripe/*` | | + +--- + +## 31. Error Handling + +`bootstrap/app.php`: Sentry; 404 → `ErrorController::handleNotFound` (pattern-specific suggestion queries); ModelNotFound same; 403/other HTTP → Blade `errors.{status}` or `errors.http`; 500 logs correlation id via `NotFoundLogger` unless `APP_DEBUG`. + +404 pages run extra DB (trending artworks, tags, creators). Confidence: medium they are cached internally (not verified). + +JSON/Inertia 404 returns minimal JSON. + +--- + +## 32. Tests + +Inventory: Pest Feature 227 + Unit 45 + Playwright e2e 11 + Vitest script. + +`php artisan test` was started. Partial results before log truncation: + +- Many unit tests PASS (academy, collections, discovery, early growth, enhance, sanitizer, …) +- FAIL observed: + - `Tests\Unit\ForumRateLimitRouteTest` + - `Tests\Unit\ForumRestrictedCategoryAccessTest` (2) + - `Tests\Unit\HomepageAnnouncementModuleTest` (2: homepage payload/render) + +**Full suite did not finish in the captured log** (output truncated; duration incomplete). Tests were **not** modified to pass. + +Playwright / Vitest / `npm run build` not re-run. + +--- + +## 33. Existing Performance Tooling + +| Artifact | What it is | +| -------- | ---------- | +| `docs/slow-query-optimization-plan.md` | Production slow.log analysis + index plan | +| `database/migrations/2026_04_26_082019_add_performance_indexes_batch1.php` | Indexes from that plan | +| `lighthouse_metrics.json` | Lighthouse 13 lab run vs **https://skinbase.top/** 2026-03-23 | +| `scripts/parse-lighthouse.js`, `parse-lcp.cjs` | parsers | +| Debugbar storage `storage/debugbar/*.json` | local query traces | +| Horizon metrics snapshots | scheduled | +| Playwright e2e | functional, not load | +| No k6/ab/wrk/Artillery found | | + +--- + +## 34. Optimization Opportunity Matrix + +| ID | Area | Finding | Evidence | Impact | Confidence | Priority | Production measurement needed | +| -- | ---- | ------- | -------- | ------ | ---------- | -------- | ----------------------------- | +| DB-001 | DB | Historical 78% slow time on artwork aggregate listing/joins | slow-query doc Q1/Q2 | very high | high historical; residual UNKNOWN | P0 | EXPLAIN current ranking SQL; table sizes | +| DB-002 | DB | Detail related `orWhereHas` OR query | ArtworkPageController | high TTFB on detail | high | P1 | EXPLAIN + p95 art.show | +| DB-003 | DB | Comments load 500 | ArtworkPageController | payload/TTFB | high | P1 | p95 comment counts | +| DB-004 | DB | News `LIKE %q%` on search | SearchController | search tail latency | high | P1 | slow log / EXPLAIN | +| DB-005 | DB | `LOWER(username)` profile lookup | ProfileController | profile TTFB | medium | P2 | EXPLAIN | +| CACHE-001 | Cache | Default CACHE_STORE=database | .env.example / config/cache.php | global | medium (prod unknown) | P1 | prod CACHE_STORE, cache table size | +| CACHE-002 | Cache | Authenticated homepage uncached | HomepageService::allForUser | TTFB logged-in | high | P1 | logged-in homepage trace | +| QUEUE-001 | Queue | Worker timeout 90s vs jobs 900s | deploy supervisor/systemd vs RecComputeSimilar* | failed jobs / stuck recs | high | P0 | which worker actually runs in prod | +| QUEUE-002 | Queue | Horizon supervisors omit vision/discovery/mail names | horizon.php vs QUEUE.md | stalled AI jobs | medium | P1 | Horizon dashboard queues | +| STAT-001 | Stats | View POST writes event+counter sync | ArtworkViewController defer:false | write amplification | high | P0 | views/sec, innodb row ops | +| STAT-002 | Stats | Download may double-increment | ArtworkDownloadController | incorrect + extra writes | medium | P1 | code path test + QPS | +| SITEMAP-001 | SEO/Infra | Live-build fallback on request | config/sitemaps.php defaults | crawler-triggered heavy PHP | high | P0 | whether static files always present | +| SEO-001 | SEO | robots Allow:/ no Disallow search/filters | RobotsTxtController | crawl budget | medium | P1 | GSC crawl stats | +| SEO-002 | SEO | Duplicate artwork URLs | art.show vs browse showArtwork | ranking dilution | high | P1 | GSC duplicates | +| MEDIA-001 | Media | Derivatives sync by default | UPLOAD_QUEUE_DERIVATIVES=false | upload latency / FPM | high | P1 | upload p95 | +| MEDIA-002 | Media | Presenter sizes ≠ derivative sizes | ThumbnailPresenter vs uploads.php | wrong srcset / overfetch | high | P2 | RUM image bytes | +| MEDIA-003 | Media | PHP download without accel | download-accel + controller | FPM blocked on large files | high if accel off | P1 | confirm DOWNLOAD_ACCEL | +| SEARCH-001 | Search | Meili candidate pool 240 | ArtworkSearchService | extra Meili/PHP work | medium | P2 | Meili latency | +| SEARCH-002 | Search | nginx search limiter is snippet/untracked | search-rate-limit.conf | FPM flood | medium | P1 | whether included in vhost | +| FE-001 | Frontend | Large CSS 429KB + editor chunks | public/build | studio CWV not homepage | high | P2 | page-type coverage | +| FE-002 | Frontend | Inertia share studio_groups | HandleInertiaRequests | extra query logged-in | medium | P2 | Inertia traces | +| INFRA-001 | Infra | Production nginx/FPM/OPcache unknown | deploy snippets only | — | — | P0 data | collect server config | +| SEC-001 | Security | composer audit advisories | composer audit | supply chain | medium | P2 | version pin review | +| SEC-002 | Security | Debugbar discovered locally; _debugbar routes in dump | packages.php, routes.json | must not be in prod | medium | P1 | prod composer --no-dev | +| ERR-001 | Errors | 404 runs suggestion queries | ErrorController | bot 404 load | medium | P2 | 404 QPS | + +--- + +## 35. P0 Findings + +1. **QUEUE-001** — Deployed example workers use `--timeout=90` while recommendation jobs declare `$timeout = 900`. Horizon allows 960s. CONFIRMED in repo; production worker type UNKNOWN. +2. **STAT-001** — Artwork views persist an event row and increment MySQL counters on the request (`defer: false`), CSRF-excepted, 120/min. CONFIRMED. +3. **SITEMAP-001** — Config defaults rebuild sitemaps in-request if published/static missing. Dirty tree deleted `public/sitemap.xml`. CONFIRMED default; production file presence UNKNOWN. +4. **DB-001** — Production slow log (Apr 2026) showed artwork aggregate scans as ~78% of slow time. Batch1 indexes exist in migrations; **residual production cost UNKNOWN / requires re-EXPLAIN**. Treated P0 until measured clear. + +--- + +## 36. P1 Findings + +CACHE-001, CACHE-002, QUEUE-002, STAT-002, SEO-001, SEO-002, MEDIA-001, MEDIA-003, SEARCH-002, SEC-002, DB-002, DB-003, DB-004, plus: + +- TrackOnlineVisitor Redis write after almost every public GET. +- Toolbar correlated counts every 30s per user on Nova layouts. +- Historical jobs-table LIKE dedupe pattern (verify gone). +- Download original serving vs CDN derivatives. + +--- + +## 37. P2 Findings + +DB-005, MEDIA-002, SEARCH-001, FE-001, FE-002, ERR-001, SEC-001, inRandomOrder interviews, comment/N+1 residuals, presenter vs files.skinbase.org fallback host mismatch vs cdn.skinbase.org, duplicate `/contact` routes, packed 02:00–05:00 scheduler, academy/forum hourly jobs, Lighthouse only for skinbase.top. + +--- + +## 38. P3 Findings + +- gumlet/php-image-resize alongside Intervention. +- Tailwind v3 + v4 vite plugin coexistence. +- Default Laravel README still in repo. +- `oldSite/` large binary tree in repository. +- Debugbar JSON in `storage/debugbar` (local clutter). +- yajra/datatables unbounded version `*`. +- jenssegers/agent `*`. +- ControlPanel menu composer cost not profiled. + +--- + +## 39. Risky Areas + +| Area | Why sensitive | +| ---- | ------------- | +| Ranking / heat / trending SQL | historically the slow-log majority; changing formulas affects homepage/discover | +| View/download counters | product metrics, medals, trending inputs | +| Upload/derivative pipeline | data loss / hash-addressed CDN | +| Meilisearch index settings | search relevance; Scout observer disabled on Artwork by design | +| Billing / Cashier / Academy | money | +| Maturity / moderation | legal/safety | +| Conditional sessions | auth bugs if skip logic wrong | +| Sitemap publish | SEO indexing | +| ControlPanel `/cp` | admin | +| Vector/CLIP/YOLO gateways | latency and cost | +| Legacy routes + oldSite | accidental dual-write / dead links | + +--- + +## 40. Production Data Required + +Collect values only — **never passwords, tokens, keys**. + +- OS, CPU, RAM, disk type/latency +- nginx version + **full vhost** (gzip/brotli, HTTP/2/3, body size, includes of deploy snippets) +- PHP-FPM version, pool (`pm`, max_children), `memory_limit`, `max_execution_time`, `upload_max_filesize`, `post_max_size`, OPcache +- MySQL version, buffer pool, connections, slow_query_log threshold, **table sizes/row estimates** +- Redis version, memory, eviction, Horizon vs supervisor actually running, queue depths, failed jobs +- Meilisearch host, index sizes, RAM +- CDN provider, cache hit ratio, bandwidth +- Production `.env` **non-secret** drivers: `CACHE_STORE`, `SESSION_DRIVER`, `QUEUE_CONNECTION`, `SCOUT_DRIVER`, `FILESYSTEM_DISK`, `UPLOAD_QUEUE_DERIVATIVES`, `DOWNLOAD_ACCEL_*`, `SITEMAPS_*`, `APP_DEBUG` +- Request QPS, peak, top URLs, 404 rate +- Whether `composer install --no-dev` is used +- Whether debugbar/horizon are publicly reachable +- Confirm skinbase.org vs skinbase.top relationship + +--- + +## 41. Recommended Production SQL Diagnostics + +Read-only. Prefer `information_schema` estimates over `COUNT(*)` on huge tables. + +```sql +-- Database size +SELECT table_schema, + ROUND(SUM(data_length+index_length)/1024/1024,1) AS mb +FROM information_schema.tables +WHERE table_schema = DATABASE(); + +-- Table sizes (no full counts) +SELECT table_name, + table_rows, + ROUND(data_length/1024/1024,1) AS data_mb, + ROUND(index_length/1024/1024,1) AS index_mb +FROM information_schema.tables +WHERE table_schema = DATABASE() +ORDER BY data_length+index_length DESC +LIMIT 40; + +SHOW INDEX FROM artworks; +SHOW INDEX FROM artwork_metric_snapshots_hourly; +SHOW INDEX FROM rank_artwork_scores; +SHOW INDEX FROM tags; + +SHOW GLOBAL STATUS LIKE 'Threads_connected'; +SHOW GLOBAL STATUS LIKE 'Slow_queries'; +SHOW VARIABLES LIKE 'slow_query%'; +SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; +``` + +EXPLAIN candidates (use production-realistic binds, avoid long locks): + +- Current `nova:recalculate-rankings` / trending SQL (compare to old Q1/Q2) +- Artwork related `orWhereHas` pattern +- News LIKE search +- `LOWER(username)` profile lookup +- `artwork_view_events` insert rate vs table size + +Do **not** `SELECT *` dump `artwork_metric_snapshots_hourly`. + +--- + +## 42. Recommended Server Diagnostics + +Read-only: + +```text +uptime; free -h; df -h +iostat -x 1 5 +ps aux | egrep 'php-fpm|nginx|redis|meili|horizon|queue:work|ssr.js' +nginx -T | egrep 'gzip|brotli|http2|client_max_body|limit_req|include' +php -i | egrep 'opcache|memory_limit|max_execution|upload_max|post_max' +redis-cli INFO memory +php artisan horizon:status +php artisan queue:failed --json | head +``` + +--- + +## 43. Benchmark URL Set + +Use templates; do not invent IDs. + +1. `GET /` (anonymous, cold and warm) +2. `GET /` (authenticated) +3. `GET /discover/trending` +4. `GET /discover/fresh` +5. `GET /explore` +6. `GET /explore/{type}/trending` (e.g. wallpapers) +7. `GET /{contentType}/{category}` page 1 and page 5 +8. `GET /art/{id}/{slug}` typical +9. `GET /art/{id}/{slug}` high-comment artwork +10. `GET /@{username}` +11. `GET /search?q={common-term}` +12. `GET /search` (empty) +13. `GET /tags/{popular-tag}` +14. `GET /download/artwork/{id}` (auth and guest) +15. `POST /api/art/{id}/view` +16. `GET /sitemap.xml` +17. `GET /sitemaps/artworks-0001.xml` (or first shard) +18. `GET /news/{slug}` +19. `GET /categories` +20. Unknown URL 404 (`/no-such-page-xyz`) + +--- + +## 44. Unknowns / Limitations + +- Production `.env` and live nginx/PHP/MySQL not inspected. +- Dirty working tree means some findings (sitemaps, robots, search, academy) include **uncommitted** work. +- Full `php artisan test` log truncated; not a complete pass/fail census. +- `npm audit` / production build not re-run. +- ControlPanel internals not query-audited in depth. +- Subagent fan-out expired; this report is from direct inspection. +- Lighthouse file is skinbase.top, March 2026 — may not match skinbase.org today. +- Slow-query document predates or coincides with batch1 indexes; residual UNKNOWN. + +--- + +## 45. Repository Changes Made + +```text +Created: +- docs/skinbase-optimization-audit.md + +Modified: +- none (by this audit) + +Application source changes: +- none + +Database changes: +- none + +Configuration changes: +- none +``` + +Pre-existing dirty files were left untouched. + +Verify after write with `git status --short` / `git diff --stat` (audit file should be the only new path from this milestone). diff --git a/package.json b/package.json index 0b14f042..9ea2f1c4 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "build": "vite build && vite build --ssr", "build:ssr": "vite build --ssr", "dev": "vite", + "security:audit": "npm audit --audit-level=moderate", + "security:audit-json": "npm audit --json", + "security:outdated": "npm outdated --json", "test:ui": "vitest run", "test:e2e": "playwright test", "test:routes": "playwright test tests/e2e/routes.spec.ts", diff --git a/public/robots.txt b/public/robots.txt index ff1996ba..1aad7d7e 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,4 @@ User-agent: * -Disallow: +Allow: / -Sitemap: https://skinbase.top/sitemap.xml \ No newline at end of file +Sitemap: https://skinbase.org/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 6dbf475b..00000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - http://skinbase26.test/sitemaps/artworks.xml - - - http://skinbase26.test/sitemaps/academy-lessons.xml - 2026-05-03T19:03:23+02:00 - - - http://skinbase26.test/sitemaps/academy-prompts.xml - 2026-05-03T19:03:23+02:00 - - - http://skinbase26.test/sitemaps/academy-packs.xml - 2026-05-03T19:03:23+02:00 - - - http://skinbase26.test/sitemaps/academy-challenges.xml - 2026-05-03T19:03:23+02:00 - - - http://skinbase26.test/sitemaps/users.xml - - - http://skinbase26.test/sitemaps/tags.xml - - - http://skinbase26.test/sitemaps/categories.xml - - - http://skinbase26.test/sitemaps/collections.xml - - - http://skinbase26.test/sitemaps/cards.xml - - - http://skinbase26.test/sitemaps/stories.xml - - - http://skinbase26.test/sitemaps/news.xml - - - http://skinbase26.test/sitemaps/news-google.xml - - - http://skinbase26.test/sitemaps/forum-index.xml - - - http://skinbase26.test/sitemaps/forum-categories.xml - 2026-05-03T19:00:04+02:00 - - - http://skinbase26.test/sitemaps/forum-threads.xml - - - http://skinbase26.test/sitemaps/static-pages.xml - - \ No newline at end of file diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index 5694895c..5849c221 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -48,6 +48,7 @@ const buildAdminNavGroups = (isAdmin) => [ label: 'System', items: [ ...(isAdmin ? [{ label: 'Auth Audit', href: '/moderation/auth-audit', icon: 'fa-solid fa-user-shield' }] : []), + ...(isAdmin ? [{ label: 'Security Report', href: '/moderation/system/security-report', icon: 'fa-solid fa-shield-virus' }] : []), { label: 'Settings', href: '/moderation/settings', icon: 'fa-solid fa-gear' }, ], }, diff --git a/resources/js/Pages/Academy/Billing/Account.jsx b/resources/js/Pages/Academy/Billing/Account.jsx index 6d797f39..c4275fcc 100644 --- a/resources/js/Pages/Academy/Billing/Account.jsx +++ b/resources/js/Pages/Academy/Billing/Account.jsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from 'react' -import { Head, Link, useForm, usePage } from '@inertiajs/react' +import { Link, useForm, usePage } from '@inertiajs/react' import AccessBadge from '../../../components/academy/billing/AccessBadge' +import SeoHead from '../../../components/seo/SeoHead' function formatDate(iso) { if (!iso) return null @@ -11,7 +12,7 @@ function formatDate(iso) { } } -export default function AcademyBillingAccount({ currentTier, isSubscribed, subscription, activePlan = null, links = {} }) { +export default function AcademyBillingAccount({ seo, currentTier, isSubscribed, subscription, activePlan = null, links = {} }) { const { flash, auth } = usePage().props const { data, setData, post, processing } = useForm({ issue_type: 'billing', @@ -84,7 +85,7 @@ export default function AcademyBillingAccount({ currentTier, isSubscribed, subsc return (
- +
{flash?.error ? ( @@ -280,4 +281,4 @@ export default function AcademyBillingAccount({ currentTier, isSubscribed, subsc
) -} \ No newline at end of file +} diff --git a/resources/js/Pages/Academy/Billing/Cancel.jsx b/resources/js/Pages/Academy/Billing/Cancel.jsx index c0171175..bb871551 100644 --- a/resources/js/Pages/Academy/Billing/Cancel.jsx +++ b/resources/js/Pages/Academy/Billing/Cancel.jsx @@ -1,10 +1,11 @@ import React from 'react' -import { Head, Link } from '@inertiajs/react' +import { Link } from '@inertiajs/react' +import SeoHead from '../../../components/seo/SeoHead' -export default function AcademyBillingCancel({ message, links = {} }) { +export default function AcademyBillingCancel({ seo, message, links = {} }) { return (
- +
@@ -20,4 +21,4 @@ export default function AcademyBillingCancel({ message, links = {} }) {
) -} \ No newline at end of file +} diff --git a/resources/js/Pages/Academy/Billing/Success.jsx b/resources/js/Pages/Academy/Billing/Success.jsx index dae06eef..e39c48f6 100644 --- a/resources/js/Pages/Academy/Billing/Success.jsx +++ b/resources/js/Pages/Academy/Billing/Success.jsx @@ -1,15 +1,16 @@ import React from 'react' -import { Head, Link, usePage, useForm } from '@inertiajs/react' +import { Link, usePage, useForm } from '@inertiajs/react' import AccessBadge from '../../../components/academy/billing/AccessBadge' +import SeoHead from '../../../components/seo/SeoHead' -export default function AcademyBillingSuccess({ currentTier, isSubscribed, links = {} }) { +export default function AcademyBillingSuccess({ seo, currentTier, isSubscribed, links = {} }) { const { auth } = usePage().props const sessionId = usePage().props.sessionId || null const userEmail = auth?.user?.email ?? null const { data, setData, post, processing } = useForm({ message: '', session_id: sessionId }) return (
- +
@@ -65,4 +66,4 @@ export default function AcademyBillingSuccess({ currentTier, isSubscribed, links
) -} \ No newline at end of file +} diff --git a/resources/js/Pages/Academy/ChallengeSubmit.jsx b/resources/js/Pages/Academy/ChallengeSubmit.jsx index 93583eb5..35dabd0a 100644 --- a/resources/js/Pages/Academy/ChallengeSubmit.jsx +++ b/resources/js/Pages/Academy/ChallengeSubmit.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { Head, useForm } from '@inertiajs/react' +import { useForm } from '@inertiajs/react' import SeoHead from '../../components/seo/SeoHead' import NovaSelect from '../../components/ui/NovaSelect' @@ -17,7 +17,6 @@ export default function AcademyChallengeSubmit({ seo, challenge, artworks, submi return (
-
@@ -65,4 +64,4 @@ export default function AcademyChallengeSubmit({ seo, challenge, artworks, submi
) -} \ No newline at end of file +} diff --git a/resources/js/Pages/Academy/Index.jsx b/resources/js/Pages/Academy/Index.jsx index ccc9a134..ac42a8f1 100644 --- a/resources/js/Pages/Academy/Index.jsx +++ b/resources/js/Pages/Academy/Index.jsx @@ -106,13 +106,26 @@ function MetricCard({ label, value, accent }) { } function FeaturedCourseCard({ course }) { - const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || '' + const cover = course?.cover_image_thumb_url || course?.cover_image_url || course?.teaser_image_thumb_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || '' + const coverSrcSet = course?.cover_image_srcset || course?.teaser_image_srcset || '' return (
- {cover ? : null} + {cover ? ( + + ) : null}
{course.difficulty} @@ -552,4 +565,4 @@ export default function AcademyIndex({ seo, pricingUrl, academyAccess = null, li
) -} \ No newline at end of file +} diff --git a/resources/js/Pages/Academy/Show.jsx b/resources/js/Pages/Academy/Show.jsx index 542590c2..392dffed 100644 --- a/resources/js/Pages/Academy/Show.jsx +++ b/resources/js/Pages/Academy/Show.jsx @@ -628,7 +628,7 @@ function PromptFilledExampleCard({ example, analytics, contentId, index }) {
) : null} - {example?.prompt ?
{example.prompt}
: null} + {example?.prompt ?
{example.prompt}
: null} {example?.negative_prompt ? (
@@ -803,7 +803,7 @@ function PromptHelperPromptCard({ helperPrompt, analytics, contentId }) {

Prompt text

-
{helperPrompt.prompt}
+
{helperPrompt.prompt}
) @@ -839,7 +839,7 @@ function PromptVariantCard({ variant, analytics, contentId }) {

Variant prompt

-
{variant.prompt}
+
{variant.prompt}
{variant.negative_prompt ? ( @@ -848,7 +848,7 @@ function PromptVariantCard({ variant, analytics, contentId }) {

Negative prompt

-
{variant.negative_prompt}
+
{variant.negative_prompt}
) : null} @@ -1082,7 +1082,7 @@ function AiComparisonSection({ block }) { -
{payload.prompt}
+
{payload.prompt}
{hasNegativePrompt ? (

Negative prompt

@@ -1735,7 +1735,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
-
+
{flash.success ?
{flash.success}
: null} {flash.error ?
{flash.error}
: null} @@ -2017,13 +2017,13 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
) : pageType === 'prompt' ? ( -
-
+
+
-
+
{academyBreadcrumbs.length ? (
@@ -2092,7 +2092,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
-
+

Preview artwork

@@ -2102,7 +2102,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
-
-
+
+
{!promptHasFullAccess && (promptPreviewImage || promptPublicExamples.length) ? (
@@ -2228,7 +2228,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat /> ) : null}
-
{promptBody || 'Prompt text is not available yet.'}
+
{promptBody || 'Prompt text is not available yet.'}
{!promptHasFullAccess ? (

{promptUnlockTitle || 'Unlock the full prompt'}

@@ -2251,7 +2251,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat metadata={{ copy_type: 'negative_prompt', source: 'prompt_body' }} />
-
{item.negative_prompt}
+
{item.negative_prompt}
) : null}
@@ -2401,7 +2401,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat ) : null}
-
+ @if(config('academy.enabled', true)) + + @endif + {{-- COMMUNITY --}}
+ @if(config('academy.enabled', true)) +
+ + +
+ @endif +