diff --git a/.env.example b/.env.example index 75415212..32cf826a 100644 --- a/.env.example +++ b/.env.example @@ -329,6 +329,30 @@ TURNSTILE_FAIL_OPEN=false TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify TURNSTILE_TIMEOUT=5 +ENHANCE_DISK=public +ENHANCE_SOURCE_PREFIX=enhance/sources +ENHANCE_OUTPUT_PREFIX=enhance/outputs +ENHANCE_PREVIEW_PREFIX=enhance/previews +ENHANCE_ENGINE=stub +ENHANCE_MAX_UPLOAD_MB=20 +ENHANCE_MAX_INPUT_WIDTH=4096 +ENHANCE_MAX_INPUT_HEIGHT=4096 +ENHANCE_MAX_OUTPUT_WIDTH=8192 +ENHANCE_MAX_OUTPUT_HEIGHT=8192 +ENHANCE_DAILY_LIMIT=10 +ENHANCE_QUEUE=default +ENHANCE_COMPLETED_EXPIRES_AFTER_DAYS=30 +ENHANCE_FAILED_EXPIRES_AFTER_DAYS=7 +ENHANCE_DELETED_FILE_GRACE_DAYS=1 +ENHANCE_CLEANUP_CHUNK_SIZE=100 +ENHANCE_STUCK_PROCESSING_AFTER_MINUTES=30 +ENHANCE_STUCK_QUEUED_AFTER_MINUTES=60 +ENHANCE_STUB_SHOW_WARNING=true +ENHANCE_WORKER_URL= +ENHANCE_WORKER_TIMEOUT=300 +ENHANCE_WORKER_TOKEN= +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 + AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 diff --git a/app/Console/Commands/Enhance/CleanupEnhanceJobsCommand.php b/app/Console/Commands/Enhance/CleanupEnhanceJobsCommand.php new file mode 100644 index 00000000..3629dff6 --- /dev/null +++ b/app/Console/Commands/Enhance/CleanupEnhanceJobsCommand.php @@ -0,0 +1,290 @@ +option('only'))); + $validTargets = ['', 'expired', 'deleted', 'failed', 'orphaned']; + + if (! in_array($target, $validTargets, true)) { + $this->error('The --only option must be one of: expired, deleted, failed, orphaned.'); + + return self::FAILURE; + } + + if ((bool) $this->option('dry-run') && (bool) $this->option('force')) { + $this->error('Use either --dry-run or --force, not both.'); + + return self::FAILURE; + } + + $dryRun = (bool) $this->option('dry-run') || ! (bool) $this->option('force'); + $daysOverride = $this->option('days'); + $selectedTarget = $target !== '' ? $target : 'all'; + + Log::info('enhance.cleanup.started', [ + 'dry_run' => $dryRun, + 'target' => $selectedTarget, + 'days_override' => $daysOverride, + ]); + + if ($dryRun) { + $this->warn('Running in dry-run mode. No files will be deleted.'); + } + + $rows = []; + + if ($target === '' || $target === 'expired') { + $expired = $this->cleanupExpiredCompletedJobs($dryRun); + $rows[] = ['expired', $expired['jobs'], $expired['files'], $dryRun ? 'dry-run' : 'cleaned']; + } + + if ($target === '' || $target === 'failed') { + $failed = $this->cleanupFailedJobs($dryRun, $daysOverride); + $rows[] = ['failed', $failed['jobs'], $failed['files'], $dryRun ? 'dry-run' : 'cleaned']; + } + + if ($target === '' || $target === 'deleted') { + $deleted = $this->cleanupSoftDeletedJobs($dryRun, $daysOverride); + $rows[] = ['deleted', $deleted['jobs'], $deleted['files'], $dryRun ? 'dry-run' : 'cleaned']; + } + + if ($target === 'orphaned') { + $orphaned = $this->scanOrphanedFiles($dryRun); + $rows[] = ['orphaned', $orphaned['files'], $orphaned['deleted'], $dryRun ? 'dry-run' : 'deleted']; + + if ($orphaned['unsupported']) { + $this->warn('Orphaned file scan was skipped because the configured disk does not support safe listing.'); + } + + foreach ($orphaned['sample'] as $path) { + $this->line(' - ' . $path); + } + } + + if ($rows !== []) { + $this->table(['Target', 'Jobs/Files', 'Files deleted', 'Mode'], $rows); + } + + Log::info('enhance.cleanup.completed', [ + 'dry_run' => $dryRun, + 'target' => $selectedTarget, + 'rows' => $rows, + ]); + + $this->info('Enhance cleanup finished.'); + + return self::SUCCESS; + } + + private function cleanupExpiredCompletedJobs(bool $dryRun): array + { + $query = EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_COMPLETED) + ->whereNotNull('expires_at') + ->where('expires_at', '<=', now()); + + return $this->cleanupJobs($query, $dryRun, 'expired', fn (): array => [ + 'status' => EnhanceJob::STATUS_EXPIRED, + ]); + } + + private function cleanupFailedJobs(bool $dryRun, mixed $daysOverride): array + { + $days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.failed_expires_after_days', 7)); + $cutoff = now()->subDays($days); + + $query = EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_FAILED) + ->where(function (Builder $builder) use ($cutoff): void { + $builder + ->where('finished_at', '<=', $cutoff) + ->orWhere(function (Builder $fallback) use ($cutoff): void { + $fallback->whereNull('finished_at')->where('created_at', '<=', $cutoff); + }); + }); + + return $this->cleanupJobs($query, $dryRun, 'failed-expired'); + } + + private function cleanupSoftDeletedJobs(bool $dryRun, mixed $daysOverride): array + { + $days = $this->resolveDays($daysOverride, (int) config('enhance.lifecycle.deleted_file_grace_days', 1)); + $cutoff = now()->subDays($days); + + $query = EnhanceJob::withTrashed() + ->whereNotNull('deleted_at') + ->where('deleted_at', '<=', $cutoff); + + return $this->cleanupJobs($query, $dryRun, 'deleted-grace'); + } + + private function cleanupJobs(Builder $query, bool $dryRun, string $reason, ?callable $attributes = null): array + { + $result = ['jobs' => 0, 'files' => 0]; + $chunkSize = max(1, (int) config('enhance.lifecycle.cleanup_chunk_size', 100)); + + $query->chunkById($chunkSize, function ($jobs) use (&$result, $dryRun, $reason, $attributes): void { + foreach ($jobs as $job) { + $result['jobs']++; + $result['files'] += count($this->enhanceJobPaths($job)); + + if ($dryRun) { + continue; + } + + $deleteResult = $this->storage->deleteFilesForJob($job); + $metadata = is_array($job->metadata) ? $job->metadata : []; + + $job->forceFill(array_merge( + $this->cleanupAttributesForJob($job), + $attributes ? $attributes($job) : [], + [ + 'metadata' => array_merge($metadata, [ + 'cleanup' => [ + 'files_removed_at' => now()->toIso8601String(), + 'reason' => $reason, + 'deleted' => $deleteResult['deleted'], + ], + ]), + ], + ))->save(); + } + }); + + return $result; + } + + private function scanOrphanedFiles(bool $dryRun): array + { + $disk = $this->storage->diskName(); + $knownPaths = array_fill_keys(array_map( + static fn (string $path): string => ltrim($path, '/'), + $this->storage->listKnownJobPaths(), + ), true); + $sample = []; + $result = [ + 'files' => 0, + 'deleted' => 0, + 'unsupported' => false, + 'sample' => [], + ]; + + foreach ($this->enhancePrefixes() as $prefix) { + try { + $files = Storage::disk($disk)->allFiles($prefix); + } catch (Throwable) { + $result['unsupported'] = true; + + return $result; + } + + foreach ($files as $file) { + $normalized = ltrim($file, '/'); + + if (isset($knownPaths[$normalized])) { + continue; + } + + $result['files']++; + + if (count($sample) < 20) { + $sample[] = $normalized; + } + + if (! $dryRun && $this->storage->safeDelete($disk, $normalized)) { + $result['deleted']++; + } + } + } + + $result['sample'] = $sample; + + return $result; + } + + private function cleanupAttributesForJob(EnhanceJob $job): array + { + $attributes = []; + + if ($this->storage->isEnhancePath($job->source_path)) { + $attributes['source_disk'] = null; + $attributes['source_path'] = null; + $attributes['source_hash'] = null; + } + + if ($this->storage->isEnhancePath($job->output_path)) { + $attributes['output_disk'] = null; + $attributes['output_path'] = null; + $attributes['output_hash'] = null; + $attributes['output_width'] = null; + $attributes['output_height'] = null; + $attributes['output_filesize'] = null; + $attributes['output_mime'] = null; + } + + if ($this->storage->isEnhancePath($job->preview_path)) { + $attributes['preview_disk'] = null; + $attributes['preview_path'] = null; + } + + return $attributes; + } + + private function enhanceJobPaths(EnhanceJob $job): array + { + return array_values(array_filter([ + $this->storage->isEnhancePath($job->source_path) ? trim((string) $job->source_path) : null, + $this->storage->isEnhancePath($job->output_path) ? trim((string) $job->output_path) : null, + $this->storage->isEnhancePath($job->preview_path) ? trim((string) $job->preview_path) : null, + ])); + } + + private function enhancePrefixes(): array + { + return array_values(array_filter(array_unique(array_map( + static fn (string $prefix): string => trim($prefix, '/'), + [ + (string) config('enhance.source_prefix', 'enhance/sources'), + (string) config('enhance.output_prefix', 'enhance/outputs'), + (string) config('enhance.preview_prefix', 'enhance/previews'), + ], + )))); + } + + private function resolveDays(mixed $daysOverride, int $default): int + { + if ($daysOverride === null || $daysOverride === '') { + return max(0, $default); + } + + return max(0, (int) $daysOverride); + } +} \ No newline at end of file diff --git a/app/Console/Commands/Enhance/EnhanceHealthCommand.php b/app/Console/Commands/Enhance/EnhanceHealthCommand.php new file mode 100644 index 00000000..769e99c9 --- /dev/null +++ b/app/Console/Commands/Enhance/EnhanceHealthCommand.php @@ -0,0 +1,108 @@ +payload(); + + if ((bool) $this->option('json')) { + $this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return self::SUCCESS; + } + + $this->info('Enhance health'); + $this->newLine(); + + $this->table(['Metric', 'Value'], [ + ['Configured engine', $payload['engine']], + ['Configured queue', $payload['queue']], + ['Worker URL configured', $payload['worker_configured'] ? 'yes' : 'no'], + ['Storage disk', $payload['storage_disk']], + ['Total jobs', $payload['counts']['total']], + ['Pending jobs', $payload['counts']['pending']], + ['Queued jobs', $payload['counts']['queued']], + ['Processing jobs', $payload['counts']['processing']], + ['Completed jobs', $payload['counts']['completed']], + ['Failed jobs', $payload['counts']['failed']], + ['Cancelled jobs', $payload['counts']['cancelled']], + ['Expired jobs', $payload['counts']['expired']], + ['Stuck queued jobs', $payload['health']['stuck_queued']], + ['Stuck processing jobs', $payload['health']['stuck_processing']], + ['Jobs created today', $payload['today']['created']], + ['Jobs completed today', $payload['today']['completed']], + ['Jobs failed today', $payload['today']['failed']], + ['Average processing time today', $payload['today']['average_processing_seconds'] ?? '—'], + ]); + + return self::SUCCESS; + } + + private function payload(): array + { + $todayStart = now()->startOfDay(); + $todayEnd = now()->endOfDay(); + $stuckQueuedCutoff = now()->subMinutes((int) config('enhance.health.stuck_queued_after_minutes', 60)); + $stuckProcessingCutoff = now()->subMinutes((int) config('enhance.health.stuck_processing_after_minutes', 30)); + + $counts = [ + 'total' => EnhanceJob::query()->count(), + 'pending' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_PENDING)->count(), + 'queued' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_QUEUED)->count(), + 'processing' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_PROCESSING)->count(), + 'completed' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_COMPLETED)->count(), + 'failed' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_FAILED)->count(), + 'cancelled' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_CANCELLED)->count(), + 'expired' => EnhanceJob::query()->where('status', EnhanceJob::STATUS_EXPIRED)->count(), + ]; + + return [ + 'engine' => (string) config('enhance.default_engine', EnhanceJob::ENGINE_STUB), + 'queue' => (string) config('enhance.queue', 'default'), + 'worker_configured' => trim((string) config('enhance.external_worker.url', '')) !== '', + 'storage_disk' => (string) config('enhance.disk', 'public'), + 'counts' => $counts, + 'health' => [ + 'stuck_queued' => EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_QUEUED) + ->whereNotNull('queued_at') + ->where('queued_at', '<=', $stuckQueuedCutoff) + ->count(), + 'stuck_processing' => EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_PROCESSING) + ->whereNotNull('started_at') + ->where('started_at', '<=', $stuckProcessingCutoff) + ->count(), + ], + 'today' => [ + 'created' => EnhanceJob::query()->whereBetween('created_at', [$todayStart, $todayEnd])->count(), + 'completed' => EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_COMPLETED) + ->whereBetween('finished_at', [$todayStart, $todayEnd]) + ->count(), + 'failed' => EnhanceJob::query() + ->where('status', EnhanceJob::STATUS_FAILED) + ->whereBetween('finished_at', [$todayStart, $todayEnd]) + ->count(), + 'average_processing_seconds' => ($average = EnhanceJob::query() + ->whereNotNull('processing_seconds') + ->whereBetween('finished_at', [$todayStart, $todayEnd]) + ->avg('processing_seconds')) !== null + ? round((float) $average, 2) + : null, + ], + ]; + } +} \ No newline at end of file diff --git a/app/Console/Commands/Enhance/EnhanceRunCommand.php b/app/Console/Commands/Enhance/EnhanceRunCommand.php new file mode 100644 index 00000000..d48fe7d8 --- /dev/null +++ b/app/Console/Commands/Enhance/EnhanceRunCommand.php @@ -0,0 +1,290 @@ +option('dry-run'); + $engineOverride = trim((string) $this->option('engine')); + $idOption = trim((string) $this->option('id')); + $limit = max(0, (int) $this->option('limit')); + $includeFailed = (bool) $this->option('failed'); + + if ($engineOverride !== '' && ! in_array($engineOverride, [EnhanceJob::ENGINE_STUB, EnhanceJob::ENGINE_EXTERNAL_WORKER], true)) { + $this->error("Unknown engine override: {$engineOverride}. Use 'stub' or 'external_worker'."); + + return self::FAILURE; + } + + $jobs = $this->resolveJobs($idOption, $limit, $includeFailed); + + if ($jobs->isEmpty()) { + $this->info('No eligible enhance jobs found.'); + + return self::SUCCESS; + } + + $this->info(sprintf('Found %d job(s) to process.', $jobs->count())); + $this->newLine(); + + if ($dryRun) { + $this->warn('Dry-run mode — no jobs will be processed.'); + + foreach ($jobs as $job) { + $engine = $engineOverride !== '' ? "{$engineOverride} (overridden)" : $job->engine; + $this->line(sprintf( + ' [dry-run] Job #%d status=%-12s engine=%-18s scale=%dx mode=%-14s user_id=%d', + $job->id, + $job->status, + $engine, + $job->scale, + $job->mode, + $job->user_id, + )); + } + + return self::SUCCESS; + } + + $processed = 0; + $failed = 0; + + foreach ($jobs as $job) { + if ($this->processJob($job, $engineOverride)) { + $processed++; + } else { + $failed++; + } + + $this->newLine(); + } + + $this->info(sprintf('Done: %d completed, %d failed.', $processed, $failed)); + + return $failed > 0 ? self::FAILURE : self::SUCCESS; + } + + private function resolveJobs(string $idOption, int $limit, bool $includeFailed): Collection + { + if ($idOption !== '') { + $ids = array_filter(array_map('intval', explode(',', $idOption))); + + return EnhanceJob::query() + ->whereIn('id', $ids) + ->whereIn('status', self::PROCESSABLE_STATUSES) + ->get(); + } + + $statuses = [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING]; + + if ($includeFailed) { + $statuses[] = EnhanceJob::STATUS_FAILED; + } + + $query = EnhanceJob::query()->whereIn('status', $statuses)->oldest(); + + if ($limit > 0) { + $query->limit($limit); + } + + return $query->get(); + } + + private function processJob(EnhanceJob $job, string $engineOverride): bool + { + $engine = $engineOverride !== '' ? $engineOverride : (string) $job->engine; + + $this->line(sprintf('--- Job #%d ---', $job->id)); + $this->line(sprintf(' Status : %s', $job->status)); + $this->line(sprintf(' Engine : %s%s', $engine, $engineOverride !== '' ? ' (overridden)' : '')); + $this->line(sprintf(' Scale : %dx', $job->scale)); + $this->line(sprintf(' Mode : %s', $job->mode)); + $this->line(sprintf(' User : #%d', $job->user_id)); + + if ($this->output->isVerbose()) { + $this->line(sprintf( + ' Source : disk=%-10s path=%s', + $job->source_disk ?: '(default)', + $job->source_path ?: '—', + )); + $this->line(sprintf( + ' Input : %dx%d size=%s mime=%s', + (int) $job->input_width, + (int) $job->input_height, + $this->formatBytes((int) $job->input_filesize), + $job->input_mime ?: '—', + )); + + if ($job->error_message !== null) { + $this->warn(sprintf(' Previous error: %s', $job->error_message)); + } + } + + if (! in_array($job->status, self::PROCESSABLE_STATUSES, true)) { + $this->warn(sprintf(' Skipping: status "%s" is not processable.', $job->status)); + + return false; + } + + $job->forceFill([ + 'status' => EnhanceJob::STATUS_PROCESSING, + 'started_at' => now(), + 'finished_at' => null, + 'error_message' => null, + ])->save(); + + $started = microtime(true); + $completedExpiryDays = (int) config('enhance.lifecycle.completed_expires_after_days', 30); + + try { + $this->line(' Processing...'); + + $processor = $this->processorFactory->make($engine); + $result = $processor->process($job); + + if ($this->output->isVerbose()) { + $this->line(sprintf( + ' Output : %dx%d size=%s mime=%s', + $result->width, + $result->height, + $this->formatBytes($result->filesize), + $result->mime, + )); + $this->line(sprintf( + ' Stored : disk=%-10s path=%s', + $result->disk, + $result->path, + )); + } + + $this->line(' Generating preview...'); + $preview = $this->storage->createPreviewFromStoredOutput($job, $result->disk, $result->path) ?? []; + + $outputHash = null; + $outputContents = Storage::disk($result->disk)->get($result->path); + + if (is_string($outputContents) && $outputContents !== '') { + $outputHash = hash('sha256', $outputContents); + } + + $job->forceFill([ + 'status' => EnhanceJob::STATUS_COMPLETED, + 'output_disk' => $result->disk, + 'output_path' => $result->path, + 'output_hash' => $outputHash, + 'output_width' => $result->width, + 'output_height' => $result->height, + 'output_filesize' => $result->filesize, + 'output_mime' => $result->mime, + 'metadata' => array_merge($job->metadata ?? [], $result->metadata ?? []), + 'processing_seconds' => (int) round(microtime(true) - $started), + 'finished_at' => now(), + 'expires_at' => $completedExpiryDays > 0 ? now()->addDays($completedExpiryDays) : null, + ] + $preview)->save(); + + $elapsed = round(microtime(true) - $started, 2); + $this->info(sprintf(' Completed in %.2fs', $elapsed)); + + if ($this->output->isVerbose() && ! empty($result->metadata)) { + $this->line(' Metadata:'); + + foreach ($result->metadata as $key => $value) { + $display = is_scalar($value) ? (string) $value : json_encode($value); + $this->line(sprintf(' %-30s %s', $key . ':', $display)); + } + } + + return true; + } catch (Throwable $exception) { + $elapsed = round(microtime(true) - $started, 2); + + $job->forceFill([ + 'status' => EnhanceJob::STATUS_FAILED, + 'error_message' => Str::limit($exception->getMessage(), 1000), + 'processing_seconds' => (int) round(microtime(true) - $started), + 'finished_at' => now(), + ])->save(); + + $this->error(sprintf(' Failed in %.2fs: %s', $elapsed, $exception->getMessage())); + + if ($this->output->isVerbose()) { + $this->line(sprintf(' Exception : %s', get_class($exception))); + $this->line(sprintf(' At : %s:%d', $exception->getFile(), $exception->getLine())); + + $previous = $exception->getPrevious(); + + if ($previous !== null) { + $this->line(sprintf(' Caused by : %s: %s', get_class($previous), $previous->getMessage())); + } + } + + if ($this->output->isVeryVerbose()) { + $this->line(' Stack trace:'); + $frames = array_slice(explode("\n", $exception->getTraceAsString()), 0, 25); + + foreach ($frames as $frame) { + $this->line(' ' . $frame); + } + } + + Log::warning('enhance.run.command.failed', [ + 'enhance_job_id' => $job->id, + 'engine' => $engine, + 'message' => $exception->getMessage(), + 'exception' => get_class($exception), + ]); + + return false; + } + } + + private function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . 'B'; + } + + if ($bytes < 1_048_576) { + return round($bytes / 1024, 1) . 'KB'; + } + + return round($bytes / 1_048_576, 1) . 'MB'; + } +} diff --git a/app/Console/Commands/PublishScheduledNewsCommand.php b/app/Console/Commands/PublishScheduledNewsCommand.php index 27109a3c..6ec3094c 100644 --- a/app/Console/Commands/PublishScheduledNewsCommand.php +++ b/app/Console/Commands/PublishScheduledNewsCommand.php @@ -7,6 +7,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use App\Services\News\NewsService; use cPad\Plugins\News\Models\NewsArticle; final class PublishScheduledNewsCommand extends Command @@ -17,6 +18,11 @@ final class PublishScheduledNewsCommand extends Command protected $description = 'Publish scheduled News articles whose publish time has passed.'; + public function __construct(private readonly NewsService $news) + { + parent::__construct(); + } + public function handle(): int { $dryRun = (bool) $this->option('dry-run'); @@ -60,11 +66,7 @@ final class PublishScheduledNewsCommand extends Command return; } - $article->forceFill([ - 'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED, - 'status' => 'published', - 'published_at' => $article->published_at ?? $now, - ])->save(); + $this->news->publish($article); $published++; $this->line(sprintf('Published News article #%d: "%s"', $article->id, $article->title)); diff --git a/app/Http/Controllers/Academy/AcademyCourseController.php b/app/Http/Controllers/Academy/AcademyCourseController.php index 0c5ae638..7e6d1569 100644 --- a/app/Http/Controllers/Academy/AcademyCourseController.php +++ b/app/Http/Controllers/Academy/AcademyCourseController.php @@ -85,6 +85,13 @@ final class AcademyCourseController extends Controller 'featuredCourses' => $featuredCourses->all(), 'filters' => $filters, 'pricingUrl' => route('academy.pricing'), + 'lessonsUrl' => route('academy.lessons.index'), + 'promptLibraryUrl' => route('academy.prompts.index'), + 'academyAccess' => array_merge($this->access->accessSummary($request->user()), [ + 'billingUrl' => $request->user() && (bool) config('academy_billing.enabled', false) + ? route('academy.billing.account') + : route('academy.pricing'), + ]), 'analytics' => [ 'enabled' => true, 'contentType' => null, diff --git a/app/Http/Controllers/Academy/AcademyHomeController.php b/app/Http/Controllers/Academy/AcademyHomeController.php index 2bb1e329..94849757 100644 --- a/app/Http/Controllers/Academy/AcademyHomeController.php +++ b/app/Http/Controllers/Academy/AcademyHomeController.php @@ -60,10 +60,16 @@ final class AcademyHomeController extends Controller return Inertia::render('Academy/Index', [ 'seo' => $seo, 'pricingUrl' => route('academy.pricing'), + 'academyAccess' => array_merge($this->access->accessSummary($request->user()), [ + 'billingUrl' => $request->user() && (bool) config('academy_billing.enabled', false) + ? route('academy.billing.account') + : route('academy.pricing'), + ]), 'links' => [ 'lessons' => route('academy.lessons.index'), 'courses' => route('academy.courses.index'), 'prompts' => route('academy.prompts.index'), + 'promptPopular' => route('academy.prompts.popular'), 'packs' => route('academy.packs.index'), 'challenges' => route('academy.challenges.index'), ], diff --git a/app/Http/Controllers/Academy/AcademyLessonController.php b/app/Http/Controllers/Academy/AcademyLessonController.php index 1872745e..80749c7a 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 Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; use Inertia\Inertia; @@ -27,7 +28,7 @@ final class AcademyLessonController extends Controller private readonly AcademyInteractionService $interactions, ) {} - public function index(Request $request): Response + public function index(Request $request): Response|JsonResponse { abort_unless((bool) config('academy.enabled', true), 404); @@ -65,6 +66,10 @@ final class AcademyLessonController extends Controller $this->analytics->trackSearch((string) $filters['q'], (int) $lessons->total(), array_filter($filters), $request); } + if ($request->expectsJson()) { + return response()->json($lessons); + } + $seo = app(SeoFactory::class) ->collectionListing( 'Academy Lessons — Skinbase', @@ -78,10 +83,21 @@ final class AcademyLessonController extends Controller 'title' => 'Academy lessons', 'description' => 'Step-by-step tutorials and workflow guides for AI-assisted creative work on Skinbase.', 'seo' => $seo, + 'breadcrumbs' => [ + ['label' => 'Academy', 'href' => route('academy.index')], + ['label' => 'Lessons', 'href' => route('academy.lessons.index')], + ], 'items' => $lessons, 'filters' => $filters, 'categories' => $this->cache->categoriesByType('lesson'), 'pricingUrl' => route('academy.pricing'), + 'coursesUrl' => route('academy.courses.index'), + 'promptLibraryUrl' => route('academy.prompts.index'), + 'academyAccess' => array_merge($this->access->accessSummary($request->user()), [ + 'billingUrl' => $request->user() && (bool) config('academy_billing.enabled', false) + ? route('academy.billing.account') + : route('academy.pricing'), + ]), 'analytics' => [ 'enabled' => true, 'contentType' => filled($filters['q'] ?? null) ? AcademyAnalyticsContentType::SEARCH : null, diff --git a/app/Http/Controllers/Academy/AcademyPromptController.php b/app/Http/Controllers/Academy/AcademyPromptController.php index c0862276..9b842445 100644 --- a/app/Http/Controllers/Academy/AcademyPromptController.php +++ b/app/Http/Controllers/Academy/AcademyPromptController.php @@ -10,11 +10,13 @@ use App\Services\Academy\AcademyAccessService; use App\Services\Academy\AcademyAnalyticsService; use App\Services\Academy\AcademyCacheService; use App\Services\Academy\AcademyInteractionService; +use App\Services\Academy\AcademyPopularityService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; use App\Support\Seo\SeoFactory; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Support\Str; +use Illuminate\Support\Facades\DB; use Inertia\Inertia; use Inertia\Response; @@ -25,6 +27,7 @@ final class AcademyPromptController extends Controller private readonly AcademyCacheService $cache, private readonly AcademyAnalyticsService $analytics, private readonly AcademyInteractionService $interactions, + private readonly AcademyPopularityService $popularity, ) { } @@ -86,16 +89,32 @@ final class AcademyPromptController extends Controller return Inertia::render('Academy/List', [ 'pageType' => 'prompts', + 'promptView' => 'library', 'title' => 'Prompt library', 'description' => 'Reusable prompt templates for wallpapers, worlds, mascots, covers, and digital art workflows.', 'seo' => $seo, + 'breadcrumbs' => [ + ['label' => 'Academy', 'href' => route('academy.index')], + ['label' => 'Prompt Library', 'href' => route('academy.prompts.index')], + ], 'items' => $prompts, 'filters' => $filters, 'categories' => $this->cache->categoriesByType('prompt'), 'pricingUrl' => route('academy.pricing'), + 'coursesUrl' => route('academy.courses.index'), + 'packsUrl' => route('academy.packs.index'), + 'promptPopularUrl' => route('academy.prompts.popular'), + 'promptLibraryUrl' => route('academy.prompts.index'), + 'academyAccess' => array_merge($this->access->accessSummary($request->user()), [ + 'billingUrl' => $request->user() && (bool) config('academy_billing.enabled', false) + ? route('academy.billing.account') + : route('academy.pricing'), + ]), + 'featuredPrompts' => $this->featuredPromptPayloads($request->user()), + 'popularPrompts' => $this->popularPromptPayloads($request->user()), 'analytics' => [ 'enabled' => true, - 'contentType' => filled($filters['q'] ?? null) ? AcademyAnalyticsContentType::SEARCH : null, + 'contentType' => AcademyAnalyticsContentType::PROMPT_LIBRARY, 'contentId' => null, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_prompts_index', @@ -110,6 +129,186 @@ final class AcademyPromptController extends Controller ])->rootView('collections'); } + public function popular(Request $request): Response + { + abort_unless((bool) config('academy.enabled', true), 404); + + $validated = $request->validate([ + 'period' => ['nullable', 'string', 'in:7d,30d,90d'], + ]); + + $selectedPeriod = $this->selectedPopularPromptPeriod($validated['period'] ?? null); + $from = now()->subDays($selectedPeriod['days'] - 1)->startOfDay(); + $to = now()->endOfDay(); + + $rows = DB::query() + ->fromSub( + $this->popularity->queryBetween($from, $to) + ->where('content_type', AcademyAnalyticsContentType::PROMPT) + ->whereNotNull('content_id') + ->selectRaw('content_id, sum(views) as views, sum(prompt_copies) as prompt_copies, sum(popularity_score) as popularity_score') + ->groupBy('content_id'), + 'prompt_rankings' + ) + ->orderByDesc('popularity_score') + ->orderByDesc('prompt_copies') + ->orderByDesc('views') + ->paginate(12) + ->withQueryString(); + + $prompts = AcademyPromptTemplate::query() + ->with('category') + ->active() + ->published() + ->whereIn('id', $rows->pluck('content_id')->map(static fn ($value): int => (int) $value)->all()) + ->get() + ->keyBy('id'); + + $baseRank = (($rows->currentPage() - 1) * $rows->perPage()); + + $rows->setCollection( + $rows->getCollection() + ->values() + ->map(function (object $row, int $index) use ($prompts, $request, $baseRank, $selectedPeriod): ?array { + $prompt = $prompts->get((int) $row->content_id); + + if (! $prompt instanceof AcademyPromptTemplate) { + return null; + } + + $payload = $this->access->promptPayload($prompt, $request->user()); + $payload['ranking'] = [ + 'rank' => $baseRank + $index + 1, + 'views' => max(0, (int) ($row->views ?? 0)), + 'prompt_copies' => max(0, (int) ($row->prompt_copies ?? 0)), + 'popularity_score' => round((float) ($row->popularity_score ?? 0), 2), + ]; + $payload['spotlight'] = [ + 'eyebrow' => max(0, (int) ($row->prompt_copies ?? 0)) > 0 + ? sprintf('%d copies %s', (int) $row->prompt_copies, $selectedPeriod['eyebrow_suffix']) + : sprintf('%d views %s', (int) $row->views, $selectedPeriod['eyebrow_suffix']), + ]; + + return $payload; + }) + ->filter() + ->values() + ); + + $seo = app(SeoFactory::class) + ->collectionListing( + sprintf('%s Prompts — Skinbase Academy', $selectedPeriod['title_prefix']), + sprintf('See which Skinbase Academy prompt templates are driving the most views and copies %s.', $selectedPeriod['description_suffix']), + route('academy.prompts.popular', $request->query()), + ) + ->toArray(); + + return Inertia::render('Academy/List', [ + 'pageType' => 'prompts', + 'promptView' => 'popular', + 'title' => sprintf('%s prompts', $selectedPeriod['title_prefix']), + 'description' => sprintf('The prompt templates getting the most momentum from views and copies across the Academy %s.', $selectedPeriod['description_suffix']), + 'seo' => $seo, + 'breadcrumbs' => [ + ['label' => 'Academy', 'href' => route('academy.index')], + ['label' => 'Prompt Library', 'href' => route('academy.prompts.index')], + ['label' => 'Popular Prompts', 'href' => route('academy.prompts.popular')], + ], + 'items' => $rows, + 'filters' => [], + 'categories' => [], + 'pricingUrl' => route('academy.pricing'), + 'coursesUrl' => route('academy.courses.index'), + 'packsUrl' => route('academy.packs.index'), + 'promptPopularUrl' => route('academy.prompts.popular'), + 'promptLibraryUrl' => route('academy.prompts.index'), + 'academyAccess' => array_merge($this->access->accessSummary($request->user()), [ + 'billingUrl' => $request->user() && (bool) config('academy_billing.enabled', false) + ? route('academy.billing.account') + : route('academy.pricing'), + ]), + 'popularPeriod' => [ + 'value' => $selectedPeriod['value'], + 'label' => $selectedPeriod['label'], + 'description' => $selectedPeriod['description'], + ], + 'popularPeriods' => collect($this->popularPromptPeriods()) + ->map(fn (array $period): array => [ + 'value' => $period['value'], + 'label' => $period['label'], + 'description' => $period['description'], + 'href' => route('academy.prompts.popular', ['period' => $period['value']]), + 'active' => $period['value'] === $selectedPeriod['value'], + ]) + ->values() + ->all(), + 'featuredPrompts' => $this->featuredPromptPayloads($request->user()), + 'popularPrompts' => [], + 'analytics' => [ + 'enabled' => true, + 'contentType' => AcademyAnalyticsContentType::PROMPT_POPULAR, + 'contentId' => null, + 'eventUrl' => route('academy.analytics.events.store'), + 'pageName' => 'academy_prompts_popular', + 'trackingKey' => sprintf('period:%s', $selectedPeriod['value']), + 'metadata' => [ + 'period' => $selectedPeriod['value'], + 'period_days' => $selectedPeriod['days'], + ], + 'search' => null, + 'isPremium' => false, + 'isGuest' => $request->user() === null, + 'isSubscriber' => $request->user()?->hasAcademyCreatorAccess() || $request->user()?->hasAcademyProAccess(), + ], + ])->rootView('collections'); + } + + /** + * @return array> + */ + private function popularPromptPeriods(): array + { + return [ + [ + 'value' => '7d', + 'days' => 7, + 'label' => '7 days', + 'description' => 'Fresh momentum from the last 7 days.', + 'title_prefix' => 'Top 7-day', + 'description_suffix' => 'in the last 7 days', + 'eyebrow_suffix' => 'in the last 7 days', + ], + [ + 'value' => '30d', + 'days' => 30, + 'label' => '30 days', + 'description' => 'The default monthly view of prompt momentum.', + 'title_prefix' => 'Popular', + 'description_suffix' => 'this month', + 'eyebrow_suffix' => 'this month', + ], + [ + 'value' => '90d', + 'days' => 90, + 'label' => '90 days', + 'description' => 'Longer-running prompt momentum across the quarter.', + 'title_prefix' => 'Top 90-day', + 'description_suffix' => 'in the last 90 days', + 'eyebrow_suffix' => 'in the last 90 days', + ], + ]; + } + + /** + * @return array + */ + private function selectedPopularPromptPeriod(?string $value): array + { + return collect($this->popularPromptPeriods()) + ->first(fn (array $period): bool => $period['value'] === $value) + ?? $this->popularPromptPeriods()[1]; + } + public function show(Request $request, string $slug): Response { abort_unless((bool) config('academy.enabled', true), 404); @@ -201,4 +400,70 @@ final class AcademyPromptController extends Controller ], ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []); } + + /** + * @return array> + */ + private function featuredPromptPayloads(mixed $viewer, int $limit = 4): array + { + return collect($this->cache->featuredPrompts()) + ->take($limit) + ->map(function (AcademyPromptTemplate $prompt) use ($viewer): array { + $payload = $this->access->promptPayload($prompt, $viewer); + $payload['spotlight'] = [ + 'eyebrow' => $prompt->prompt_of_week ? 'Prompt of the week' : 'Featured pick', + ]; + + return $payload; + }) + ->values() + ->all(); + } + + /** + * @return array> + */ + private function popularPromptPayloads(mixed $viewer, int $limit = 4): array + { + $rows = $this->popularity->queryBetween(now()->subDays(29)->startOfDay(), now()->endOfDay()) + ->where('content_type', AcademyAnalyticsContentType::PROMPT) + ->whereNotNull('content_id') + ->selectRaw('content_id, sum(views) as views, sum(prompt_copies) as prompt_copies, sum(popularity_score) as popularity_score') + ->groupBy('content_id') + ->orderByDesc('popularity_score') + ->limit($limit) + ->get(); + + if ($rows->isEmpty()) { + return []; + } + + $prompts = AcademyPromptTemplate::query() + ->with('category') + ->active() + ->published() + ->whereIn('id', $rows->pluck('content_id')->all()) + ->get() + ->keyBy('id'); + + return $rows->map(function ($row) use ($prompts, $viewer): ?array { + $prompt = $prompts->get((int) $row->content_id); + + if (! $prompt instanceof AcademyPromptTemplate) { + return null; + } + + $payload = $this->access->promptPayload($prompt, $viewer); + $copies = max(0, (int) ($row->prompt_copies ?? 0)); + $views = max(0, (int) ($row->views ?? 0)); + $payload['spotlight'] = [ + 'eyebrow' => $copies > 0 ? sprintf('%d copies this month', $copies) : sprintf('%d views this month', $views), + ]; + + return $payload; + }) + ->filter() + ->values() + ->all(); + } } \ No newline at end of file diff --git a/app/Http/Controllers/Academy/AcademyPromptPackController.php b/app/Http/Controllers/Academy/AcademyPromptPackController.php index 8a2e8bef..4db0c5be 100644 --- a/app/Http/Controllers/Academy/AcademyPromptPackController.php +++ b/app/Http/Controllers/Academy/AcademyPromptPackController.php @@ -29,7 +29,6 @@ final class AcademyPromptPackController extends Controller abort_unless((bool) config('academy.enabled', true), 404); $packs = AcademyPromptPack::query() - ->with('prompts') ->active() ->published() ->latest('published_at') @@ -57,7 +56,7 @@ final class AcademyPromptPackController extends Controller 'pricingUrl' => route('academy.pricing'), 'analytics' => [ 'enabled' => true, - 'contentType' => null, + 'contentType' => AcademyAnalyticsContentType::PROMPT_PACK_LIBRARY, 'contentId' => null, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_packs_index', diff --git a/app/Http/Controllers/Api/LinkPreviewController.php b/app/Http/Controllers/Api/LinkPreviewController.php index a0d7a724..46c56d38 100644 --- a/app/Http/Controllers/Api/LinkPreviewController.php +++ b/app/Http/Controllers/Api/LinkPreviewController.php @@ -53,7 +53,12 @@ class LinkPreviewController extends Controller return response()->json(['error' => 'Invalid URL.'], 422); } - // Resolve hostname and block private/loopback IPs (SSRF protection) + // Resolve hostname and block private/loopback IPs (SSRF protection). + // NOTE: This check is not atomic with Guzzle's own DNS resolution — a + // DNS rebinding attack could theoretically pass this check and then + // resolve to an internal IP when Guzzle makes the actual request. + // Risk is low (requires attacker-controlled DNS with very short TTL), + // but this is a known limitation of the current approach. $resolved = gethostbyname($host); if ($this->isBlockedIp($resolved)) { return response()->json(['error' => 'URL not allowed.'], 422); diff --git a/app/Http/Controllers/Api/UploadController.php b/app/Http/Controllers/Api/UploadController.php index 67fdafb4..24e0b8d8 100644 --- a/app/Http/Controllers/Api/UploadController.php +++ b/app/Http/Controllers/Api/UploadController.php @@ -47,7 +47,9 @@ use App\Uploads\Exceptions\DraftQuotaException; use App\Models\Artwork; use App\Models\Group; use App\Services\GroupArtworkReviewService; +use App\Support\ArtworkDescriptionContentValidator; use App\Services\Worlds\WorldSubmissionService; +use Illuminate\Validation\ValidationException; use Illuminate\Support\Str; final class UploadController extends Controller @@ -534,6 +536,8 @@ final class UploadController extends Controller 'nsfw' => ['nullable', 'boolean'], ]); + $this->ensureValidArtworkDescription($validated); + $updates = []; foreach (['title', 'category_id', 'description', 'tags', 'license', 'nsfw'] as $field) { if (array_key_exists($field, $validated)) { @@ -635,6 +639,8 @@ final class UploadController extends Controller 'world_submissions.*.source_surface' => ['nullable', 'string', 'max:80'], ]); + $this->ensureValidArtworkDescription($validated); + $mode = $validated['mode'] ?? 'now'; $visibility = $validated['visibility'] ?? 'public'; @@ -814,6 +820,8 @@ final class UploadController extends Controller 'world_submissions.*.source_surface' => ['nullable', 'string', 'max:80'], ]); + $this->ensureValidArtworkDescription($validated); + if (! ctype_digit($id)) { return response()->json(['message' => 'Artwork review submission requires an artwork draft id.'], Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -842,4 +850,13 @@ final class UploadController extends Controller 'group_review_status' => (string) $artwork->group_review_status, ], Response::HTTP_OK); } + + private function ensureValidArtworkDescription(array $validated): void + { + foreach (ArtworkDescriptionContentValidator::errors($validated['description'] ?? null) as $message) { + throw ValidationException::withMessages([ + 'description' => [$message], + ]); + } + } } diff --git a/app/Http/Controllers/ArtworkEnhanceController.php b/app/Http/Controllers/ArtworkEnhanceController.php new file mode 100644 index 00000000..281db7fd --- /dev/null +++ b/app/Http/Controllers/ArtworkEnhanceController.php @@ -0,0 +1,52 @@ +findOrFail($artwork); + + $actor = $request->user(); + abort_unless($actor !== null, 403); + + $isOwner = (int) $artwork->user_id === (int) $actor->id; + $isStaff = $actor->isAdmin() || $actor->isModerator(); + + abort_unless($isOwner || $isStaff, 403); + + $validated = $request->validate([ + 'scale' => ['required', 'integer', Rule::in((array) config('enhance.allowed_scales', [2, 4]))], + 'mode' => ['required', 'string', Rule::in((array) config('enhance.allowed_modes', ['standard', 'artwork', 'photo', 'illustration']))], + ]); + + try { + $job = $this->enhanceService->createFromArtwork($actor, $artwork, $validated); + } catch (RuntimeException $exception) { + return redirect() + ->route('enhance.create', ['artwork' => $artwork->id]) + ->withErrors([ + 'source' => $exception->getMessage(), + ]); + } + + return redirect() + ->route('enhance.show', ['enhanceJob' => $job]) + ->with('success', 'Artwork enhance job created.'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/EnhanceController.php b/app/Http/Controllers/EnhanceController.php new file mode 100644 index 00000000..581af61f --- /dev/null +++ b/app/Http/Controllers/EnhanceController.php @@ -0,0 +1,208 @@ +authorize('viewAny', EnhanceJob::class); + + $jobs = EnhanceJob::query() + ->where('user_id', (int) $request->user()->id) + ->with('artwork:id,title,slug') + ->latest('id') + ->paginate(12) + ->withQueryString() + ->through(fn (EnhanceJob $job): array => $this->serializeJobListItem($job)); + + $latestCompleted = EnhanceJob::query() + ->where('user_id', (int) $request->user()->id) + ->where('status', EnhanceJob::STATUS_COMPLETED) + ->latest('finished_at') + ->limit(4) + ->get() + ->map(fn (EnhanceJob $job): array => $this->serializeJobListItem($job)) + ->all(); + + return Inertia::render('Enhance/Index', [ + 'title' => 'Skinbase Enhance', + 'jobs' => $jobs, + 'latestCompleted' => $latestCompleted, + 'createUrl' => route('enhance.create'), + 'indexUrl' => route('enhance.index'), + 'dailyLimit' => (int) config('enhance.daily_limit', 10), + 'enhanceConfig' => $this->enhanceService->frontendConfig(), + ]); + } + + public function create(Request $request): Response + { + $this->authorize('create', EnhanceJob::class); + + $selectedArtwork = null; + + if (($artworkId = (int) $request->integer('artwork')) > 0) { + $artwork = Artwork::query() + ->select(['id', 'user_id', 'title', 'slug']) + ->findOrFail($artworkId); + + $actor = $request->user(); + abort_unless($actor !== null, 403); + + $isOwner = (int) $artwork->user_id === (int) $actor->id; + $isStaff = $actor->isAdmin() || $actor->isModerator(); + + abort_unless($isOwner || $isStaff, 403); + + $selectedArtwork = [ + 'id' => $artwork->id, + 'title' => $artwork->title, + 'show_url' => route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]), + 'store_url' => route('artworks.enhance.store', ['artwork' => $artwork->id]), + ]; + } + + return Inertia::render('Enhance/Create', [ + 'title' => 'Skinbase Enhance', + 'options' => $this->optionsPayload(), + 'storeUrl' => route('enhance.store'), + 'indexUrl' => route('enhance.index'), + 'maxUploadMb' => (int) config('enhance.max_upload_mb', 20), + 'selectedArtwork' => $selectedArtwork, + 'enhanceConfig' => $this->enhanceService->frontendConfig(), + ]); + } + + public function store(Request $request): RedirectResponse + { + $this->authorize('create', EnhanceJob::class); + + $validated = $request->validate([ + 'image' => ['required', 'file', 'mimetypes:image/jpeg,image/png,image/webp', 'max:' . ((int) config('enhance.max_upload_mb', 20) * 1024)], + 'scale' => ['required', 'integer', Rule::in((array) config('enhance.allowed_scales', [2, 4]))], + 'mode' => ['required', 'string', Rule::in((array) config('enhance.allowed_modes', ['standard', 'artwork', 'photo', 'illustration']))], + ]); + + $job = $this->enhanceService->createFromUpload($request->user(), $request->file('image'), $validated); + + return redirect() + ->route('enhance.show', ['enhanceJob' => $job]) + ->with('success', 'Enhance job created.'); + } + + public function show(EnhanceJob $enhanceJob): Response + { + $this->authorize('view', $enhanceJob); + $enhanceJob->loadMissing('artwork:id,title,slug'); + + return Inertia::render('Enhance/Show', [ + 'title' => 'Enhance Job', + 'job' => $this->serializeJobDetail($enhanceJob), + 'indexUrl' => route('enhance.index'), + 'createUrl' => route('enhance.create'), + 'enhanceConfig' => $this->enhanceService->frontendConfig(), + ]); + } + + public function retry(EnhanceJob $enhanceJob): RedirectResponse + { + $this->authorize('retry', $enhanceJob); + + $job = $this->enhanceService->retry($enhanceJob); + + return redirect() + ->route('enhance.show', ['enhanceJob' => $job]) + ->with('success', 'Enhance job queued again.'); + } + + public function destroy(EnhanceJob $enhanceJob): RedirectResponse + { + $this->authorize('delete', $enhanceJob); + + $this->enhanceService->delete($enhanceJob); + + return redirect() + ->route('enhance.index') + ->with('success', 'Enhance job deleted.'); + } + + private function optionsPayload(): array + { + return [ + 'modes' => array_map(fn (string $mode): array => [ + 'value' => $mode, + 'label' => ucfirst($mode), + ], (array) config('enhance.allowed_modes', [])), + 'scales' => array_map(fn (int $scale): array => [ + 'value' => $scale, + 'label' => $scale . 'x', + ], array_map('intval', (array) config('enhance.allowed_scales', []))), + ]; + } + + private function serializeJobListItem(EnhanceJob $job): array + { + return [ + 'id' => $job->id, + 'status' => (string) $job->status, + 'engine' => (string) $job->engine, + 'mode' => (string) $job->mode, + 'scale' => (int) $job->scale, + 'source_url' => $job->sourceUrl(), + 'output_url' => $job->outputUrl(), + 'preview_url' => $job->previewUrl(), + 'input_width' => (int) ($job->input_width ?? 0), + 'input_height' => (int) ($job->input_height ?? 0), + 'output_width' => (int) ($job->output_width ?? 0), + 'output_height' => (int) ($job->output_height ?? 0), + 'error_message' => $job->error_message, + 'processing_seconds' => $job->processing_seconds, + 'created_at' => optional($job->created_at)?->toIso8601String(), + 'finished_at' => optional($job->finished_at)?->toIso8601String(), + 'show_url' => route('enhance.show', ['enhanceJob' => $job]), + 'artwork' => $job->artwork ? [ + 'id' => $job->artwork->id, + 'title' => $job->artwork->title, + 'slug' => $job->artwork->slug, + 'url' => route('art.show', ['id' => $job->artwork->id, 'slug' => $job->artwork->slug]), + ] : null, + ]; + } + + private function serializeJobDetail(EnhanceJob $job): array + { + return $this->serializeJobListItem($job) + [ + 'input_filesize' => (int) ($job->input_filesize ?? 0), + 'input_mime' => $job->input_mime, + 'output_filesize' => (int) ($job->output_filesize ?? 0), + 'output_mime' => $job->output_mime, + 'metadata' => $job->metadata ?? [], + 'queued_at' => optional($job->queued_at)?->toIso8601String(), + 'started_at' => optional($job->started_at)?->toIso8601String(), + 'deleted_at' => optional($job->deleted_at)?->toIso8601String(), + 'expires_at' => optional($job->expires_at)?->toIso8601String(), + 'retry_url' => route('enhance.retry', ['enhanceJob' => $job]), + 'delete_url' => route('enhance.destroy', ['enhanceJob' => $job]), + 'download_url' => $job->outputUrl(), + 'can_retry' => auth()->user()?->can('retry', $job) ?? false, + 'can_delete' => auth()->user()?->can('delete', $job) ?? false, + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Internal/EnhanceSourceController.php b/app/Http/Controllers/Internal/EnhanceSourceController.php new file mode 100644 index 00000000..487dd236 --- /dev/null +++ b/app/Http/Controllers/Internal/EnhanceSourceController.php @@ -0,0 +1,39 @@ +hasValidSignature(), 403); + abort_unless($this->storage->isEnhancePath($enhanceJob->source_path), 404); + + try { + $binary = $this->storage->fetchSourceBinary($enhanceJob); + } catch (Throwable) { + abort(404); + } + + return response($binary, 200, [ + 'Content-Type' => trim((string) ($enhanceJob->input_mime ?: 'application/octet-stream')), + 'Content-Length' => (string) strlen($binary), + 'Cache-Control' => 'private, max-age=60', + 'Content-Disposition' => 'inline; filename="enhance-source-' . $enhanceJob->id . '"', + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Legacy/UserController.php b/app/Http/Controllers/Legacy/UserController.php index b2580c1d..b17b9677 100644 --- a/app/Http/Controllers/Legacy/UserController.php +++ b/app/Http/Controllers/Legacy/UserController.php @@ -79,19 +79,27 @@ class UserController extends Controller } } + $allowedLegacyMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if ($request->hasFile('personal_picture')) { $f = $request->file('personal_picture'); - $name = $user->id . '.' . $f->getClientOriginalExtension(); - $f->move(public_path('user-picture'), $name); - $profileUpdates['cover_image'] = $name; - $user->picture = $name; + if (in_array($f->getMimeType(), $allowedLegacyMimes, true)) { + $ext = $f->guessExtension() ?: 'jpg'; + $name = $user->id . '.' . $ext; + $f->move(public_path('user-picture'), $name); + $profileUpdates['cover_image'] = $name; + $user->picture = $name; + } } if ($request->hasFile('emotion_icon')) { $f = $request->file('emotion_icon'); - $name = $user->id . '.' . $f->getClientOriginalExtension(); - $f->move(public_path('emotion'), $name); - $user->eicon = $name; + if (in_array($f->getMimeType(), $allowedLegacyMimes, true)) { + $ext = $f->guessExtension() ?: 'jpg'; + $name = $user->id . '.' . $ext; + $f->move(public_path('emotion'), $name); + $user->eicon = $name; + } } // Save core user fields diff --git a/app/Http/Controllers/Moderation/ModerationEnhanceController.php b/app/Http/Controllers/Moderation/ModerationEnhanceController.php new file mode 100644 index 00000000..c94b88a7 --- /dev/null +++ b/app/Http/Controllers/Moderation/ModerationEnhanceController.php @@ -0,0 +1,162 @@ + trim((string) $request->query('status', 'all')), + 'engine' => trim((string) $request->query('engine', 'all')), + 'mode' => trim((string) $request->query('mode', 'all')), + 'scale' => trim((string) $request->query('scale', 'all')), + 'user' => trim((string) $request->query('user', '')), + 'date_from' => trim((string) $request->query('date_from', '')), + 'date_to' => trim((string) $request->query('date_to', '')), + ]; + + $jobs = EnhanceJob::query() + ->with(['user:id,name,username', 'artwork:id,title,slug']) + ->when($filters['status'] !== '' && $filters['status'] !== 'all', fn ($query) => $query->where('status', $filters['status'])) + ->when($filters['engine'] !== '' && $filters['engine'] !== 'all', fn ($query) => $query->where('engine', $filters['engine'])) + ->when($filters['mode'] !== '' && $filters['mode'] !== 'all', fn ($query) => $query->where('mode', $filters['mode'])) + ->when($filters['scale'] !== '' && $filters['scale'] !== 'all', fn ($query) => $query->where('scale', (int) $filters['scale'])) + ->when($filters['user'] !== '', function ($query) use ($filters): void { + $query->whereHas('user', function ($userQuery) use ($filters): void { + $userQuery + ->where('name', 'like', '%' . $filters['user'] . '%') + ->orWhere('username', 'like', '%' . $filters['user'] . '%'); + }); + }) + ->when($filters['date_from'] !== '', fn ($query) => $query->whereDate('created_at', '>=', $filters['date_from'])) + ->when($filters['date_to'] !== '', fn ($query) => $query->whereDate('created_at', '<=', $filters['date_to'])) + ->latest('id') + ->paginate(20) + ->withQueryString() + ->through(fn (EnhanceJob $job): array => $this->serializeJob($job)); + + return Inertia::render('Moderation/Enhance/Index', [ + 'title' => 'Enhance Jobs', + 'jobs' => $jobs, + 'filters' => $filters, + 'options' => [ + 'statuses' => ['all', 'pending', 'queued', 'processing', 'completed', 'failed', 'cancelled', 'expired'], + 'engines' => ['all', 'stub', 'external_worker'], + 'modes' => array_merge(['all'], (array) config('enhance.allowed_modes', [])), + 'scales' => array_merge(['all'], array_map('intval', (array) config('enhance.allowed_scales', []))), + ], + 'indexUrl' => route('admin.enhance.index'), + 'enhanceConfig' => $this->enhanceService->frontendConfig(), + ])->rootView('moderation'); + } + + public function show(EnhanceJob $enhanceJob): Response + { + $enhanceJob->loadMissing(['user:id,name,username', 'artwork:id,title,slug']); + + return Inertia::render('Moderation/Enhance/Show', [ + 'title' => 'Enhance Job #' . $enhanceJob->id, + 'job' => $this->serializeJob($enhanceJob, true), + 'indexUrl' => route('admin.enhance.index'), + 'enhanceConfig' => $this->enhanceService->frontendConfig(), + ])->rootView('moderation'); + } + + public function retry(EnhanceJob $enhanceJob): RedirectResponse + { + $this->authorize('retry', $enhanceJob); + + $job = $this->enhanceService->retry($enhanceJob); + + return redirect() + ->route('admin.enhance.show', ['enhanceJob' => $job]) + ->with('success', 'Enhance job queued again.'); + } + + public function markFailed(Request $request, EnhanceJob $enhanceJob): RedirectResponse + { + $this->authorize('markFailed', $enhanceJob); + + $job = $this->enhanceService->markFailedByModerator($enhanceJob, $request->user()); + + return redirect() + ->route('admin.enhance.show', ['enhanceJob' => $job]) + ->with('success', 'Enhance job marked as failed.'); + } + + public function destroy(EnhanceJob $enhanceJob): RedirectResponse + { + $this->authorize('delete', $enhanceJob); + + $this->enhanceService->delete($enhanceJob); + + return redirect() + ->route('admin.enhance.index') + ->with('success', 'Enhance job deleted.'); + } + + private function serializeJob(EnhanceJob $job, bool $detailed = false): array + { + return [ + 'id' => $job->id, + 'status' => (string) $job->status, + 'engine' => (string) $job->engine, + 'mode' => (string) $job->mode, + 'scale' => (int) $job->scale, + 'source_url' => $job->sourceUrl(), + 'output_url' => $job->outputUrl(), + 'preview_url' => $job->previewUrl(), + 'input_width' => (int) ($job->input_width ?? 0), + 'input_height' => (int) ($job->input_height ?? 0), + 'input_filesize' => (int) ($job->input_filesize ?? 0), + 'input_mime' => $job->input_mime, + 'output_width' => (int) ($job->output_width ?? 0), + 'output_height' => (int) ($job->output_height ?? 0), + 'output_filesize' => (int) ($job->output_filesize ?? 0), + 'output_mime' => $job->output_mime, + 'processing_seconds' => $job->processing_seconds, + 'error_message' => $job->error_message, + 'metadata' => $job->metadata ?? [], + 'created_at' => optional($job->created_at)?->toIso8601String(), + 'queued_at' => optional($job->queued_at)?->toIso8601String(), + 'started_at' => optional($job->started_at)?->toIso8601String(), + 'finished_at' => optional($job->finished_at)?->toIso8601String(), + 'expires_at' => optional($job->expires_at)?->toIso8601String(), + 'user' => $job->user ? [ + 'id' => $job->user->id, + 'name' => $job->user->name, + 'username' => $job->user->username, + ] : null, + 'artwork' => $job->artwork ? [ + 'id' => $job->artwork->id, + 'title' => $job->artwork->title, + 'slug' => $job->artwork->slug, + 'url' => route('art.show', ['id' => $job->artwork->id, 'slug' => $job->artwork->slug]), + ] : null, + 'show_url' => route('admin.enhance.show', ['enhanceJob' => $job]), + 'download_url' => $job->outputUrl(), + 'retry_url' => route('admin.enhance.retry', ['enhanceJob' => $job]), + 'mark_failed_url' => route('admin.enhance.mark-failed', ['enhanceJob' => $job]), + 'delete_url' => route('admin.enhance.destroy', ['enhanceJob' => $job]), + 'can_retry' => $job->status === EnhanceJob::STATUS_FAILED, + 'can_mark_failed' => in_array($job->status, [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING], true), + 'detailed' => $detailed, + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/News/NewsController.php b/app/Http/Controllers/News/NewsController.php index 8bc66520..cce534a4 100644 --- a/app/Http/Controllers/News/NewsController.php +++ b/app/Http/Controllers/News/NewsController.php @@ -132,6 +132,32 @@ class NewsController extends Controller ] + $this->sidebarData()); } + // ----------------------------------------------------------------------- + // Type page — /news/type/{type} + // ----------------------------------------------------------------------- + + public function type(Request $request, string $type): View + { + $typeLabels = \cPad\Plugins\News\Models\NewsArticle::TYPE_LABELS; + + abort_unless(array_key_exists($type, $typeLabels), 404); + + $label = $typeLabels[$type]; + $perPage = config('news.articles_per_page', 12); + + $articles = NewsArticle::with('author', 'category') + ->published() + ->where('type', $type) + ->editorialOrder() + ->paginate($perPage); + + return view('news.type', [ + 'type' => $type, + 'typeLabel' => $label, + 'articles' => $articles, + ] + $this->sidebarData()); + } + // ----------------------------------------------------------------------- // Article page — /news/{slug} // ----------------------------------------------------------------------- @@ -173,14 +199,21 @@ class NewsController extends Controller return; } - NewsView::create([ - 'article_id' => $article->id, - 'user_id' => $userId, - 'ip' => $ip, - 'created_at' => now(), - ]); + try { + NewsView::create([ + 'article_id' => $article->id, + 'user_id' => $userId, + 'ip' => $ip, + 'created_at' => now(), + ]); - $article->incrementViews(); + $article->incrementViews(); + } catch (\Illuminate\Database\QueryException $e) { + // Unique constraint violation — duplicate view, skip silently. + if (($e->errorInfo[1] ?? 0) !== 1062) { + throw $e; + } + } if ($canReadSession) { $request->session()->put($session, true); diff --git a/app/Http/Controllers/News/NewsRssController.php b/app/Http/Controllers/News/NewsRssController.php index 8532d463..4b2ea160 100644 --- a/app/Http/Controllers/News/NewsRssController.php +++ b/app/Http/Controllers/News/NewsRssController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\News; use App\Http\Controllers\Controller; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Cache; use cPad\Plugins\News\Models\NewsArticle; class NewsRssController extends Controller @@ -14,13 +15,17 @@ class NewsRssController extends Controller */ public function feed(): Response { - $articles = NewsArticle::with('author', 'category') - ->published() - ->orderByDesc('published_at') - ->limit(config('news.rss_limit', 25)) - ->get(); + $ttl = max(60, (int) config('news.rss_cache_ttl', 300)); - $xml = $this->buildRss($articles); + $xml = Cache::remember('news.rss.feed', $ttl, function (): string { + $articles = NewsArticle::with('author', 'category') + ->published() + ->orderByDesc('published_at') + ->limit(config('news.rss_limit', 25)) + ->get(); + + return $this->buildRss($articles); + }); return response($xml, 200, [ 'Content-Type' => 'application/rss+xml; charset=UTF-8', diff --git a/app/Http/Controllers/Settings/AcademyAdminAnalyticsController.php b/app/Http/Controllers/Settings/AcademyAdminAnalyticsController.php index 470283e1..4869469c 100644 --- a/app/Http/Controllers/Settings/AcademyAdminAnalyticsController.php +++ b/app/Http/Controllers/Settings/AcademyAdminAnalyticsController.php @@ -6,11 +6,13 @@ namespace App\Http\Controllers\Settings; use App\Http\Controllers\Controller; use App\Models\AcademyContentMetricDaily; +use App\Models\AcademyEvent; use App\Models\AcademySearchLog; use App\Services\Academy\AcademyAnalyticsContentResolver; use App\Services\Academy\AcademyContentIntelligenceService; use App\Services\Academy\AcademyPopularityService; use App\Support\AcademyAnalytics\AcademyAnalyticsContentType; +use App\Support\AcademyAnalytics\AcademyAnalyticsEventType; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; @@ -29,6 +31,9 @@ final class AcademyAdminAnalyticsController extends Controller public function overview(Request $request): Response { [$from, $to, $range] = $this->resolveDateRange($request); + $promptLibraryCurrent = $this->contentSummary(AcademyAnalyticsContentType::PROMPT_LIBRARY, $from, $to); + [$previousFrom, $previousTo] = $this->previousRange($from, $to); + $promptLibraryPrevious = $this->contentSummary(AcademyAnalyticsContentType::PROMPT_LIBRARY, $previousFrom, $previousTo); $summary = $this->metricsQuery($from, $to) ->selectRaw('sum(views) as views, sum(unique_visitors) as unique_visitors, sum(user_views) as user_views, sum(guest_views) as guest_views, sum(subscriber_views) as subscriber_views, sum(prompt_copies) as prompt_copies, sum(likes) as likes, sum(saves) as saves, sum(completions) as completions, sum(starts) as starts, sum(upgrade_clicks) as upgrade_clicks') @@ -50,6 +55,21 @@ final class AcademyAdminAnalyticsController extends Controller 'courseStarts' => (int) ($summary?->starts ?? 0), 'upgradeClicks' => (int) ($summary?->upgrade_clicks ?? 0), ], + 'promptLibraryTrend' => [ + 'current' => $promptLibraryCurrent, + 'previous' => $promptLibraryPrevious, + 'deltas' => [ + 'views' => $this->percentDelta((int) $promptLibraryCurrent['views'], (int) $promptLibraryPrevious['views']), + 'uniqueVisitors' => $this->percentDelta((int) $promptLibraryCurrent['uniqueVisitors'], (int) $promptLibraryPrevious['uniqueVisitors']), + 'engagedViews' => $this->percentDelta((int) $promptLibraryCurrent['engagedViews'], (int) $promptLibraryPrevious['engagedViews']), + 'engagementRate' => $this->percentDelta((float) $promptLibraryCurrent['engagementRate'], (float) $promptLibraryPrevious['engagementRate']), + ], + 'range' => [ + 'current' => ['from' => $from->toDateString(), 'to' => $to->toDateString()], + 'previous' => ['from' => $previousFrom->toDateString(), 'to' => $previousTo->toDateString()], + ], + ], + 'popularPromptPeriodUsage' => $this->popularPromptPeriodUsage($from, $to), 'topContent' => $this->serializeContentRows($this->popularity->topContent($from, $to, 8)), 'topWeek' => $this->serializeContentRows($this->popularity->topContent(now()->subDays(6)->startOfDay(), now()->endOfDay(), 8)), ]); @@ -65,6 +85,11 @@ final class AcademyAdminAnalyticsController extends Controller return $this->renderContentPage($request, AcademyAnalyticsContentType::PROMPT, 'Prompt analytics', 'Copy-heavy prompt performance, save rates, and upgrade interest.'); } + public function promptLibrary(Request $request): Response + { + return $this->renderContentPage($request, AcademyAnalyticsContentType::PROMPT_LIBRARY, 'Prompt library analytics', 'Discovery and engagement on the public /academy/prompts library page.'); + } + public function lessons(Request $request): Response { return $this->renderContentPage($request, AcademyAnalyticsContentType::LESSON, 'Lesson analytics', 'Lesson engagement, starts, completions, and drop-off signals.'); @@ -333,9 +358,14 @@ final class AcademyAdminAnalyticsController extends Controller 'access' => $access, 'content_type' => $contentType, ], + 'summary' => $contentType === AcademyAnalyticsContentType::PROMPT_LIBRARY + ? $this->contentSummary(AcademyAnalyticsContentType::PROMPT_LIBRARY, $from, $to) + : null, 'rows' => $serializedRows, 'contentTypeOptions' => [ ['value' => '', 'label' => 'All content'], + ['value' => AcademyAnalyticsContentType::PROMPT_LIBRARY, 'label' => 'Prompt library'], + ['value' => AcademyAnalyticsContentType::PROMPT_PACK_LIBRARY, 'label' => 'Prompt pack library'], ['value' => AcademyAnalyticsContentType::PROMPT, 'label' => 'Prompts'], ['value' => AcademyAnalyticsContentType::LESSON, 'label' => 'Lessons'], ['value' => AcademyAnalyticsContentType::COURSE, 'label' => 'Courses'], @@ -359,7 +389,134 @@ final class AcademyAdminAnalyticsController extends Controller private function metricsQuery(Carbon $from, Carbon $to) { return AcademyContentMetricDaily::query() - ->whereBetween('date', [$from->toDateString(), $to->toDateString()]); + ->whereBetween('date', [$from->copy()->startOfDay(), $to->copy()->endOfDay()]); + } + + /** + * @return array + */ + private function contentSummary(string $contentType, Carbon $from, Carbon $to): array + { + $query = $this->metricsQuery($from, $to) + ->where('content_type', $contentType); + + if (! AcademyAnalyticsContentType::requiresContentId($contentType)) { + $query->whereNull('content_id'); + } + + $summary = $query + ->selectRaw('sum(views) as views, sum(unique_visitors) as unique_visitors, sum(engaged_views) as engaged_views, sum(scroll_50) as scroll_50, sum(scroll_75) as scroll_75, sum(scroll_100) as scroll_100, avg(avg_engaged_seconds) as avg_engaged_seconds, sum(popularity_score) as popularity_score') + ->first(); + + $uniqueVisitors = max(0, (int) ($summary?->unique_visitors ?? 0)); + $engagedViews = max(0, (int) ($summary?->engaged_views ?? 0)); + $scroll100 = max(0, (int) ($summary?->scroll_100 ?? 0)); + + return [ + 'views' => max(0, (int) ($summary?->views ?? 0)), + 'uniqueVisitors' => $uniqueVisitors, + 'engagedViews' => $engagedViews, + 'scroll50' => max(0, (int) ($summary?->scroll_50 ?? 0)), + 'scroll75' => max(0, (int) ($summary?->scroll_75 ?? 0)), + 'scroll100' => $scroll100, + 'avgEngagedSeconds' => round((float) ($summary?->avg_engaged_seconds ?? 0), 1), + 'popularityScore' => round((float) ($summary?->popularity_score ?? 0), 2), + 'engagementRate' => $uniqueVisitors > 0 ? round(($engagedViews / $uniqueVisitors) * 100, 1) : 0.0, + 'deepScrollRate' => $uniqueVisitors > 0 ? round(($scroll100 / $uniqueVisitors) * 100, 1) : 0.0, + ]; + } + + /** + * @return array{0: Carbon, 1: Carbon} + */ + private function previousRange(Carbon $from, Carbon $to): array + { + $days = $from->copy()->startOfDay()->diffInDays($to->copy()->startOfDay()) + 1; + + return [ + $from->copy()->subDays($days)->startOfDay(), + $from->copy()->subDay()->endOfDay(), + ]; + } + + private function percentDelta(int|float $current, int|float $previous): ?float + { + if ((float) $previous === 0.0) { + return (float) $current === 0.0 ? 0.0 : null; + } + + return round((((float) $current - (float) $previous) / (float) $previous) * 100, 1); + } + + /** + * @return array{totalViews:int,totalVisitors:int,periods:list>} + */ + private function popularPromptPeriodUsage(Carbon $from, Carbon $to): array + { + $events = AcademyEvent::query() + ->whereBetween('occurred_at', [$from, $to]) + ->where('event_type', AcademyAnalyticsEventType::PAGE_VIEW) + ->where('content_type', AcademyAnalyticsContentType::PROMPT_POPULAR) + ->get(['visitor_id', 'metadata']); + + $summary = []; + $totalViews = 0; + $visitorBuckets = []; + + foreach ($events as $event) { + $metadata = is_array($event->metadata) ? $event->metadata : []; + $period = trim((string) ($metadata['period'] ?? '')); + + if ($period === '') { + continue; + } + + $days = max(0, (int) ($metadata['period_days'] ?? 0)); + + if (! isset($summary[$period])) { + $summary[$period] = [ + 'period' => $period, + 'label' => sprintf('%s days', $days > 0 ? $days : (int) preg_replace('/\D+/', '', $period)), + 'views' => 0, + 'uniqueVisitors' => 0, + 'share' => 0.0, + 'days' => $days, + ]; + $visitorBuckets[$period] = []; + } + + $summary[$period]['views']++; + $totalViews++; + + $visitorId = trim((string) ($event->visitor_id ?? '')); + if ($visitorId !== '') { + $visitorBuckets[$period][$visitorId] = true; + } + } + + $totalVisitors = 0; + + foreach ($summary as $period => &$row) { + $uniqueVisitors = count($visitorBuckets[$period] ?? []); + $row['uniqueVisitors'] = $uniqueVisitors; + $row['share'] = $totalViews > 0 ? round((((int) $row['views']) / $totalViews) * 100, 1) : 0.0; + $totalVisitors += $uniqueVisitors; + } + unset($row); + + usort($summary, static function (array $left, array $right): int { + if ((int) $right['views'] === (int) $left['views']) { + return ((int) $left['days']) <=> ((int) $right['days']); + } + + return ((int) $right['views']) <=> ((int) $left['views']); + }); + + return [ + 'totalViews' => $totalViews, + 'totalVisitors' => $totalVisitors, + 'periods' => array_values($summary), + ]; } /** @@ -440,6 +597,7 @@ final class AcademyAdminAnalyticsController extends Controller ['label' => 'Overview', 'href' => route('admin.academy.analytics.overview')], ['label' => 'Intelligence', 'href' => route('admin.academy.analytics.intelligence')], ['label' => 'Content', 'href' => route('admin.academy.analytics.content')], + ['label' => 'Prompt Library', 'href' => route('admin.academy.analytics.prompt-library')], ['label' => 'Prompts', 'href' => route('admin.academy.analytics.prompts')], ['label' => 'Lessons', 'href' => route('admin.academy.analytics.lessons')], ['label' => 'Courses', 'href' => route('admin.academy.analytics.courses')], diff --git a/app/Http/Controllers/Studio/StudioArtworksApiController.php b/app/Http/Controllers/Studio/StudioArtworksApiController.php index dff13422..b4bab332 100644 --- a/app/Http/Controllers/Studio/StudioArtworksApiController.php +++ b/app/Http/Controllers/Studio/StudioArtworksApiController.php @@ -18,6 +18,7 @@ use App\Services\TagService; use App\Services\ArtworkVersioningService; use App\Services\Studio\StudioArtworkQueryService; use App\Services\Studio\StudioBulkActionService; +use App\Support\ArtworkDescriptionContentValidator; use App\Services\Tags\TagDiscoveryService; use App\Services\Worlds\WorldSubmissionService; use Carbon\Carbon; @@ -164,6 +165,8 @@ final class StudioArtworksApiController extends Controller 'evolution_note' => 'sometimes|nullable|string|max:1200', ]); + $this->ensureValidArtworkDescription($validated); + $hasAttributionUpdates = array_key_exists('group', $validated) || array_key_exists('primary_author_user_id', $validated) || array_key_exists('contributor_user_ids', $validated) @@ -326,6 +329,15 @@ final class StudioArtworksApiController extends Controller ]); } + private function ensureValidArtworkDescription(array $validated): void + { + foreach (ArtworkDescriptionContentValidator::errors($validated['description'] ?? null) as $message) { + throw ValidationException::withMessages([ + 'description' => [$message], + ]); + } + } + public function evolutionOptions(Request $request, int $id): JsonResponse { $artwork = $request->user()->artworks()->findOrFail($id); diff --git a/app/Http/Controllers/Studio/StudioController.php b/app/Http/Controllers/Studio/StudioController.php index cf516cf8..5e11bf30 100644 --- a/app/Http/Controllers/Studio/StudioController.php +++ b/app/Http/Controllers/Studio/StudioController.php @@ -95,7 +95,13 @@ final class StudioController extends Controller { $provider = $this->content->provider('artworks'); $prefs = $this->preferences->forUser($request->user()); - $listing = $this->content->list($request->user(), $request->only(['q', 'sort', 'bucket', 'page', 'per_page', 'content_type', 'category', 'tag']), null, 'artworks'); + $filters = $request->only(['q', 'sort', 'bucket', 'page', 'per_page', 'content_type', 'category', 'tag']); + + if (! $request->filled('sort')) { + $filters['sort'] = 'published_desc'; + } + + $listing = $this->content->list($request->user(), $filters, null, 'artworks'); $listing['default_view'] = $prefs['default_content_view']; return Inertia::render('Studio/StudioArtworks', [ diff --git a/app/Http/Controllers/Studio/StudioNewsController.php b/app/Http/Controllers/Studio/StudioNewsController.php index 7c6373cf..d1106014 100644 --- a/app/Http/Controllers/Studio/StudioNewsController.php +++ b/app/Http/Controllers/Studio/StudioNewsController.php @@ -377,10 +377,41 @@ final class StudioNewsController extends Controller 'og_image' => ['nullable', 'string', 'max:2048'], 'relations' => ['nullable', 'array', 'max:12'], 'relations.*.entity_type' => ['required_with:relations', Rule::in(array_column($this->news->relationTypeOptions(), 'value'))], - 'relations.*.entity_id' => ['required_with:relations', 'integer', 'min:1'], + 'relations.*.entity_id' => ['nullable', 'integer', 'min:1'], + 'relations.*.external_url' => ['nullable', 'string', 'max:2048'], 'relations.*.context_label' => ['nullable', 'string', 'max:120'], ]); + $relationErrors = []; + + foreach ((array) ($validated['relations'] ?? []) as $index => $relation) { + $entityType = trim(Str::lower((string) ($relation['entity_type'] ?? ''))); + + if ($entityType === NewsService::RELATION_SOURCE) { + $externalUrl = $this->normalizeExternalRelationUrl($relation['external_url'] ?? null); + + if ($externalUrl === null) { + $relationErrors["relations.{$index}.external_url"] = 'Source relations need a valid URL.'; + continue; + } + + $validated['relations'][$index]['entity_id'] = null; + $validated['relations'][$index]['external_url'] = $externalUrl; + + continue; + } + + if ((int) ($relation['entity_id'] ?? 0) < 1) { + $relationErrors["relations.{$index}.entity_id"] = 'Select a related entity.'; + } + + $validated['relations'][$index]['external_url'] = null; + } + + if ($relationErrors !== []) { + throw ValidationException::withMessages($relationErrors); + } + if (($validated['editorial_status'] ?? null) === NewsArticle::EDITORIAL_STATUS_SCHEDULED && empty($validated['published_at'])) { throw ValidationException::withMessages([ 'published_at' => 'Scheduled articles need a publish date and time.', @@ -390,6 +421,25 @@ final class StudioNewsController extends Controller return $validated; } + private function normalizeExternalRelationUrl(mixed $value): ?string + { + $url = trim((string) ($value ?? '')); + + if ($url === '') { + return null; + } + + if (preg_match('/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i', $url, $matches) === 1) { + $url = trim((string) ($matches[1] ?? '')); + } + + if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) { + return null; + } + + return Str::limit($url, 2048, ''); + } + private function tagPayload(): array { return NewsTag::query() diff --git a/app/Http/Controllers/Studio/StudioNewsMediaApiController.php b/app/Http/Controllers/Studio/StudioNewsMediaApiController.php index c7861177..8fbf70d4 100644 --- a/app/Http/Controllers/Studio/StudioNewsMediaApiController.php +++ b/app/Http/Controllers/Studio/StudioNewsMediaApiController.php @@ -46,6 +46,7 @@ final class StudioNewsMediaApiController extends Controller 'size_bytes' => $stored['size_bytes'], 'mobile_url' => $stored['mobile_url'], 'desktop_url' => $stored['desktop_url'], + 'large_url' => $stored['large_url'], 'srcset' => $stored['srcset'], ]); } catch (RuntimeException $e) { diff --git a/app/Http/Controllers/User/ProfileController.php b/app/Http/Controllers/User/ProfileController.php index b401480e..1a516b4e 100644 --- a/app/Http/Controllers/User/ProfileController.php +++ b/app/Http/Controllers/User/ProfileController.php @@ -855,25 +855,33 @@ class ProfileController extends Controller } } + $allowedImageMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if ($request->hasFile('emoticon')) { $file = $request->file('emoticon'); - $fname = $file->getClientOriginalName(); - $path = \Illuminate\Support\Facades\Storage::disk('public')->putFileAs('user-emoticons/'.$user->id, $file, $fname); - try { - \Illuminate\Support\Facades\DB::table('users')->where('id', $user->id)->update(['eicon' => $fname]); - } catch (\Exception $e) {} + if (in_array($file->getMimeType(), $allowedImageMimes, true)) { + $ext = $file->guessExtension() ?: 'jpg'; + $fname = $user->id . '_emoticon_' . time() . '.' . $ext; + \Illuminate\Support\Facades\Storage::disk('public')->putFileAs('user-emoticons/'.$user->id, $file, $fname); + try { + \Illuminate\Support\Facades\DB::table('users')->where('id', $user->id)->update(['eicon' => $fname]); + } catch (\Exception $e) {} + } } if ($request->hasFile('photo')) { $file = $request->file('photo'); - $fname = $file->getClientOriginalName(); - $path = \Illuminate\Support\Facades\Storage::disk('public')->putFileAs('user-picture/'.$user->id, $file, $fname); - if (\Illuminate\Support\Facades\Schema::hasTable('user_profiles')) { - $profileUpdates['cover_image'] = $fname; - } else { - try { - \Illuminate\Support\Facades\DB::table('users')->where('id', $user->id)->update(['picture' => $fname]); - } catch (\Exception $e) {} + if (in_array($file->getMimeType(), $allowedImageMimes, true)) { + $ext = $file->guessExtension() ?: 'jpg'; + $fname = $user->id . '_photo_' . time() . '.' . $ext; + \Illuminate\Support\Facades\Storage::disk('public')->putFileAs('user-picture/'.$user->id, $file, $fname); + if (\Illuminate\Support\Facades\Schema::hasTable('user_profiles')) { + $profileUpdates['cover_image'] = $fname; + } else { + try { + \Illuminate\Support\Facades\DB::table('users')->where('id', $user->id)->update(['picture' => $fname]); + } catch (\Exception $e) {} + } } } diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php new file mode 100644 index 00000000..ed8b6fe6 --- /dev/null +++ b/app/Http/Middleware/SecurityHeaders.php @@ -0,0 +1,22 @@ +headers->set('X-Frame-Options', 'SAMEORIGIN'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + + return $response; + } +} diff --git a/app/Http/Requests/Academy/UpsertAcademyLessonRequest.php b/app/Http/Requests/Academy/UpsertAcademyLessonRequest.php index 466444b8..9c3e6cf8 100644 --- a/app/Http/Requests/Academy/UpsertAcademyLessonRequest.php +++ b/app/Http/Requests/Academy/UpsertAcademyLessonRequest.php @@ -111,7 +111,7 @@ class UpsertAcademyLessonRequest extends FormRequest 'cover_image' => ['nullable', 'string', 'max:2048'], 'article_cover_image' => ['nullable', 'string', 'max:2048'], 'tags' => ['nullable', 'array'], - 'tags.*' => ['string', 'max:100'], + 'tags.*' => ['string', 'max:200'], 'video_url' => ['nullable', 'string', 'max:2048'], 'reading_minutes' => ['required', 'integer', 'min:1', 'max:999'], 'featured' => ['required', 'boolean'], diff --git a/app/Http/Requests/Artworks/ArtworkCreateRequest.php b/app/Http/Requests/Artworks/ArtworkCreateRequest.php index 58bf8e0d..ddfe0478 100644 --- a/app/Http/Requests/Artworks/ArtworkCreateRequest.php +++ b/app/Http/Requests/Artworks/ArtworkCreateRequest.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace App\Http\Requests\Artworks; +use App\Support\ArtworkDescriptionContentValidator; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Validator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; final class ArtworkCreateRequest extends FormRequest @@ -32,6 +34,15 @@ final class ArtworkCreateRequest extends FormRequest ]; } + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator): void { + foreach (ArtworkDescriptionContentValidator::errors($this->input('description')) as $message) { + $validator->errors()->add('description', $message); + } + }); + } + private function denyAsNotFound(): void { throw new NotFoundHttpException(); diff --git a/app/Http/Requests/Dashboard/UpdateArtworkRequest.php b/app/Http/Requests/Dashboard/UpdateArtworkRequest.php index f36c68ec..a43be9d9 100644 --- a/app/Http/Requests/Dashboard/UpdateArtworkRequest.php +++ b/app/Http/Requests/Dashboard/UpdateArtworkRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests\Dashboard; use App\Models\Artwork; +use App\Support\ArtworkDescriptionContentValidator; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Validator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class UpdateArtworkRequest extends FormRequest @@ -45,6 +47,15 @@ class UpdateArtworkRequest extends FormRequest ]; } + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator): void { + foreach (ArtworkDescriptionContentValidator::errors($this->input('description')) as $message) { + $validator->errors()->add('description', $message); + } + }); + } + public function artwork(): Artwork { if (! $this->artwork) { diff --git a/app/Http/Requests/Manage/ManageArtworkUpdateRequest.php b/app/Http/Requests/Manage/ManageArtworkUpdateRequest.php index ade76dfb..96477057 100644 --- a/app/Http/Requests/Manage/ManageArtworkUpdateRequest.php +++ b/app/Http/Requests/Manage/ManageArtworkUpdateRequest.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace App\Http\Requests\Manage; +use App\Support\ArtworkDescriptionContentValidator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Validator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; final class ManageArtworkUpdateRequest extends FormRequest @@ -48,6 +50,15 @@ final class ManageArtworkUpdateRequest extends FormRequest ]; } + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator): void { + foreach (ArtworkDescriptionContentValidator::errors($this->input('description')) as $message) { + $validator->errors()->add('description', $message); + } + }); + } + public function artwork(): object { if (! $this->artwork) { diff --git a/app/Http/Requests/Studio/ApplyArtworkAiAssistRequest.php b/app/Http/Requests/Studio/ApplyArtworkAiAssistRequest.php index e5c7ab9c..de16ee00 100644 --- a/app/Http/Requests/Studio/ApplyArtworkAiAssistRequest.php +++ b/app/Http/Requests/Studio/ApplyArtworkAiAssistRequest.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace App\Http\Requests\Studio; +use App\Support\ArtworkDescriptionContentValidator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; +use Illuminate\Validation\Validator; final class ApplyArtworkAiAssistRequest extends FormRequest { @@ -31,4 +33,13 @@ final class ApplyArtworkAiAssistRequest extends FormRequest 'similar_actions.*.state' => ['required_with:similar_actions', Rule::in(['ignored', 'reviewed'])], ]; } + + public function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator): void { + foreach (ArtworkDescriptionContentValidator::errors($this->input('description')) as $message) { + $validator->errors()->add('description', $message); + } + }); + } } \ No newline at end of file diff --git a/app/Jobs/Enhance/ProcessEnhanceJob.php b/app/Jobs/Enhance/ProcessEnhanceJob.php new file mode 100644 index 00000000..7503a306 --- /dev/null +++ b/app/Jobs/Enhance/ProcessEnhanceJob.php @@ -0,0 +1,119 @@ +onQueue($queue); + } + } + + public function handle(EnhanceProcessorFactory $factory, EnhanceStorageService $storage): void + { + $enhanceJob = EnhanceJob::query()->find($this->enhanceJobId); + + if (! $enhanceJob instanceof EnhanceJob) { + return; + } + + if (! in_array($enhanceJob->status, [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING, EnhanceJob::STATUS_FAILED], true)) { + return; + } + + $enhanceJob->forceFill([ + 'status' => EnhanceJob::STATUS_PROCESSING, + 'started_at' => now(), + 'finished_at' => null, + 'error_message' => null, + ])->save(); + + Log::info('enhance.job.processing', [ + 'enhance_job_id' => $enhanceJob->id, + 'user_id' => $enhanceJob->user_id, + 'engine' => $enhanceJob->engine, + ]); + + $started = microtime(true); + $completedExpiryDays = (int) config('enhance.lifecycle.completed_expires_after_days', 30); + + try { + $processor = $factory->make((string) $enhanceJob->engine); + $result = $processor->process($enhanceJob); + $preview = $storage->createPreviewFromStoredOutput($enhanceJob, $result->disk, $result->path) ?? []; + $outputHash = null; + $outputContents = Storage::disk($result->disk)->get($result->path); + + if (is_string($outputContents) && $outputContents !== '') { + $outputHash = hash('sha256', $outputContents); + } + + $enhanceJob->forceFill([ + 'status' => EnhanceJob::STATUS_COMPLETED, + 'output_disk' => $result->disk, + 'output_path' => $result->path, + 'output_hash' => $outputHash, + 'output_width' => $result->width, + 'output_height' => $result->height, + 'output_filesize' => $result->filesize, + 'output_mime' => $result->mime, + 'metadata' => array_merge($enhanceJob->metadata ?? [], $result->metadata ?? []), + 'processing_seconds' => (int) round(microtime(true) - $started), + 'finished_at' => now(), + 'expires_at' => $completedExpiryDays > 0 ? now()->addDays($completedExpiryDays) : null, + ] + $preview)->save(); + + Log::info('enhance.job.completed', [ + 'enhance_job_id' => $enhanceJob->id, + 'user_id' => $enhanceJob->user_id, + 'processing_seconds' => $enhanceJob->processing_seconds, + ]); + } catch (Throwable $exception) { + report($exception); + + $enhanceJob->forceFill([ + 'status' => EnhanceJob::STATUS_FAILED, + 'error_message' => Str::limit($exception->getMessage(), 1000), + 'processing_seconds' => (int) round(microtime(true) - $started), + 'finished_at' => now(), + ])->save(); + + Log::warning('enhance.job.failed', [ + 'enhance_job_id' => $enhanceJob->id, + 'user_id' => $enhanceJob->user_id, + 'message' => $exception->getMessage(), + ]); + + throw $exception; + } + } +} \ No newline at end of file diff --git a/app/Models/EnhanceJob.php b/app/Models/EnhanceJob.php new file mode 100644 index 00000000..d95e8709 --- /dev/null +++ b/app/Models/EnhanceJob.php @@ -0,0 +1,157 @@ + 'array', + 'queued_at' => 'datetime', + 'started_at' => 'datetime', + 'finished_at' => 'datetime', + 'expires_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function artwork(): BelongsTo + { + return $this->belongsTo(Artwork::class); + } + + public function isPending(): bool + { + return $this->status === self::STATUS_PENDING; + } + + public function isQueued(): bool + { + return $this->status === self::STATUS_QUEUED; + } + + public function isProcessing(): bool + { + return $this->status === self::STATUS_PROCESSING; + } + + public function isCompleted(): bool + { + return $this->status === self::STATUS_COMPLETED; + } + + public function isFailed(): bool + { + return $this->status === self::STATUS_FAILED; + } + + public function canBeDeletedBy(User $user): bool + { + if ($user->isAdmin() || $user->isModerator()) { + return true; + } + + return (int) $this->user_id === (int) $user->id + && in_array($this->status, [self::STATUS_PENDING, self::STATUS_FAILED, self::STATUS_COMPLETED, self::STATUS_CANCELLED, self::STATUS_EXPIRED], true); + } + + public function sourceUrl(): ?string + { + return $this->resolveDiskUrl($this->source_disk, $this->source_path); + } + + public function outputUrl(): ?string + { + return $this->resolveDiskUrl($this->output_disk, $this->output_path); + } + + public function previewUrl(): ?string + { + return $this->resolveDiskUrl($this->preview_disk, $this->preview_path); + } + + private function resolveDiskUrl(?string $disk, ?string $path): ?string + { + $trimmedPath = ltrim(trim((string) $path), '/'); + + if ($trimmedPath === '') { + return null; + } + + $configuredDisk = trim((string) config('enhance.disk', 'public')); + $targetDisk = trim((string) $disk) ?: $configuredDisk ?: 'public'; + + // For non-local disks (e.g. S3-backed), construct the CDN URL directly. + // For local disks ('public', 'local') fall through to Storage::disk()->url() + // so that the correct APP_URL-based path is returned in non-CDN environments. + $base = rtrim((string) config('cdn.files_url', ''), '/'); + if ($base !== '' && $targetDisk === $configuredDisk && ! in_array($targetDisk, ['public', 'local'], true)) { + return $base . '/' . $trimmedPath; + } + + $url = Storage::disk($targetDisk)->url($trimmedPath); + + if (str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) { + return $url; + } + + return url($url); + } +} \ No newline at end of file diff --git a/app/Policies/EnhanceJobPolicy.php b/app/Policies/EnhanceJobPolicy.php new file mode 100644 index 00000000..9f5e9fd6 --- /dev/null +++ b/app/Policies/EnhanceJobPolicy.php @@ -0,0 +1,55 @@ +isAdmin() || $user->isModerator())) { + return true; + } + + return null; + } + + public function viewAny(?User $user): bool + { + return $user !== null; + } + + public function view(User $user, EnhanceJob $enhanceJob): bool + { + return (int) $enhanceJob->user_id === (int) $user->id; + } + + public function create(?User $user): bool + { + if ($user === null) { + return false; + } + + return ! method_exists($user, 'hasVerifiedEmail') || $user->hasVerifiedEmail(); + } + + public function delete(User $user, EnhanceJob $enhanceJob): bool + { + return $enhanceJob->canBeDeletedBy($user); + } + + public function retry(User $user, EnhanceJob $enhanceJob): bool + { + return (int) $enhanceJob->user_id === (int) $user->id + && $enhanceJob->isFailed(); + } + + public function markFailed(User $user, EnhanceJob $enhanceJob): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 1a482e89..6e40ce44 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -11,6 +11,7 @@ use App\Models\AcademyChallengeSubmission; use App\Models\AcademyLesson; use App\Models\AcademyPromptPack; use App\Models\AcademyPromptTemplate; +use App\Models\EnhanceJob; use App\Models\Collection; use App\Models\Group; use App\Models\NovaCard; @@ -25,6 +26,7 @@ use App\Policies\AcademyChallengeSubmissionPolicy; use App\Policies\AcademyLessonPolicy; use App\Policies\AcademyPromptPackPolicy; use App\Policies\AcademyPromptTemplatePolicy; +use App\Policies\EnhanceJobPolicy; use App\Policies\CollectionPolicy; use App\Policies\GroupPolicy; use App\Policies\NovaCardPolicy; @@ -43,6 +45,7 @@ class AuthServiceProvider extends ServiceProvider AcademyLesson::class => AcademyLessonPolicy::class, AcademyPromptPack::class => AcademyPromptPackPolicy::class, AcademyPromptTemplate::class => AcademyPromptTemplatePolicy::class, + EnhanceJob::class => EnhanceJobPolicy::class, Collection::class => CollectionPolicy::class, Group::class => GroupPolicy::class, NovaCard::class => NovaCardPolicy::class, diff --git a/app/Services/Academy/AcademyAccessService.php b/app/Services/Academy/AcademyAccessService.php index db61e905..8b321153 100644 --- a/app/Services/Academy/AcademyAccessService.php +++ b/app/Services/Academy/AcademyAccessService.php @@ -92,6 +92,121 @@ final class AcademyAccessService return $this->activeAcademySubscription($user) instanceof Subscription; } + /** + * @return array + */ + public function accessSummary(?User $user): array + { + if (! $user instanceof User) { + return [ + 'signedIn' => false, + 'tier' => 'free', + 'tierLabel' => 'Guest', + 'hasPaidAccess' => false, + 'status' => 'guest', + 'statusLabel' => 'Preview access only', + 'expiresAt' => null, + 'dateLabel' => null, + 'renewsAutomatically' => false, + 'source' => 'none', + ]; + } + + if ($this->isAcademyAdmin($user)) { + return [ + 'signedIn' => true, + 'tier' => 'admin', + 'tierLabel' => 'Admin', + 'hasPaidAccess' => true, + 'status' => 'staff_access', + 'statusLabel' => 'Full staff access', + 'expiresAt' => null, + 'dateLabel' => null, + 'renewsAutomatically' => false, + 'source' => 'admin', + ]; + } + + $tier = $this->currentTier($user); + $subscription = $this->activeAcademySubscription($user); + + if ($subscription instanceof Subscription) { + $trialEndsAt = $subscription->trial_ends_at?->toISOString(); + $endsAt = $subscription->ends_at?->toISOString(); + + if ($subscription->onGracePeriod()) { + return [ + 'signedIn' => true, + 'tier' => $tier, + 'tierLabel' => $this->tierLabel($tier), + 'hasPaidAccess' => $tier !== 'free', + 'status' => 'grace_period', + 'statusLabel' => 'Cancels soon', + 'expiresAt' => $endsAt, + 'dateLabel' => 'Access ends', + 'renewsAutomatically' => false, + 'source' => 'subscription', + ]; + } + + if ($subscription->onTrial()) { + return [ + 'signedIn' => true, + 'tier' => $tier, + 'tierLabel' => $this->tierLabel($tier), + 'hasPaidAccess' => $tier !== 'free', + 'status' => 'trialing', + 'statusLabel' => 'Trial active', + 'expiresAt' => $trialEndsAt, + 'dateLabel' => 'Trial ends', + 'renewsAutomatically' => ! $subscription->cancelled(), + 'source' => 'subscription', + ]; + } + + return [ + 'signedIn' => true, + 'tier' => $tier, + 'tierLabel' => $this->tierLabel($tier), + 'hasPaidAccess' => $tier !== 'free', + 'status' => 'active', + 'statusLabel' => 'Renews automatically', + 'expiresAt' => null, + 'dateLabel' => null, + 'renewsAutomatically' => true, + 'source' => 'subscription', + ]; + } + + if ($tier !== 'free') { + return [ + 'signedIn' => true, + 'tier' => $tier, + 'tierLabel' => $this->tierLabel($tier), + 'hasPaidAccess' => true, + 'status' => 'active', + 'statusLabel' => 'Full access active', + 'expiresAt' => null, + 'dateLabel' => null, + 'renewsAutomatically' => false, + 'source' => 'legacy_role', + ]; + } + + return [ + 'signedIn' => true, + 'tier' => 'free', + 'tierLabel' => 'Free', + 'hasPaidAccess' => false, + 'status' => 'free', + 'statusLabel' => 'Free access', + 'expiresAt' => null, + 'dateLabel' => null, + 'renewsAutomatically' => false, + 'source' => 'none', + ]; + } + public function canAccessLesson(?User $user, AcademyLesson $lesson): bool { return $this->canAccessContent($user, (string) $lesson->access_level); @@ -633,6 +748,16 @@ final class AcademyAccessService }; } + private function tierLabel(string $tier): string + { + return match ($this->normalizeAccessLevel($tier)) { + 'admin' => 'Admin', + 'pro' => 'Pro', + 'creator' => 'Creator', + default => 'Free', + }; + } + private function isAcademyAdmin(User $user): bool { return $user->hasStaffAccess() || $user->isModerator(); diff --git a/app/Services/Academy/AcademyAnalyticsContentResolver.php b/app/Services/Academy/AcademyAnalyticsContentResolver.php index b9420460..0464e73c 100644 --- a/app/Services/Academy/AcademyAnalyticsContentResolver.php +++ b/app/Services/Academy/AcademyAnalyticsContentResolver.php @@ -42,6 +42,9 @@ final class AcademyAnalyticsContentResolver if (! $contentId) { return match ($contentType) { AcademyAnalyticsContentType::HOME => 'Academy Home', + AcademyAnalyticsContentType::PROMPT_LIBRARY => 'Prompt Library', + AcademyAnalyticsContentType::PROMPT_POPULAR => 'Popular Prompts', + AcademyAnalyticsContentType::PROMPT_PACK_LIBRARY => 'Prompt Pack Library', AcademyAnalyticsContentType::SEARCH => 'Academy Search', AcademyAnalyticsContentType::UPGRADE => 'Academy Upgrade', default => 'Unknown Academy Content', diff --git a/app/Services/Academy/AcademyPopularityService.php b/app/Services/Academy/AcademyPopularityService.php index 78333120..c6a3ce10 100644 --- a/app/Services/Academy/AcademyPopularityService.php +++ b/app/Services/Academy/AcademyPopularityService.php @@ -45,7 +45,7 @@ final class AcademyPopularityService public function queryBetween(Carbon $from, Carbon $to): Builder { return AcademyContentMetricDaily::query() - ->whereBetween('date', [$from->toDateString(), $to->toDateString()]); + ->whereBetween('date', [$from->copy()->startOfDay(), $to->copy()->endOfDay()]); } public function topContent(Carbon $from, Carbon $to, int $limit = 10): Collection diff --git a/app/Services/ContentSanitizer.php b/app/Services/ContentSanitizer.php index b45f7283..03f2cc31 100644 --- a/app/Services/ContentSanitizer.php +++ b/app/Services/ContentSanitizer.php @@ -32,9 +32,11 @@ class ContentSanitizer public const EMOJI_DENSITY_MAX = 0.40; // HTML tags we allow in the final rendered output + // Include heading tags so editor-produced headings (h1-h6) are preserved. private const ALLOWED_TAGS = [ 'p', 'br', 'strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'blockquote', 'del', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ]; // Allowed attributes per tag diff --git a/app/Services/Enhance/EnhanceProcessor.php b/app/Services/Enhance/EnhanceProcessor.php new file mode 100644 index 00000000..88aa2cf8 --- /dev/null +++ b/app/Services/Enhance/EnhanceProcessor.php @@ -0,0 +1,12 @@ + $this->stubProcessor, + EnhanceJob::ENGINE_EXTERNAL_WORKER => $this->externalWorkerProcessor, + default => throw new RuntimeException('Unknown enhance processor engine.'), + }; + } +} \ No newline at end of file diff --git a/app/Services/Enhance/EnhanceProcessorResult.php b/app/Services/Enhance/EnhanceProcessorResult.php new file mode 100644 index 00000000..97d53f1c --- /dev/null +++ b/app/Services/Enhance/EnhanceProcessorResult.php @@ -0,0 +1,19 @@ +assertCreationAllowed($user); + $this->assertDailyLimit($user); + + $validated = $this->validator->validateUpload($file, $options); + $source = $this->storage->storeUploadedSource($user, $file); + + $job = DB::transaction(function () use ($user, $validated, $source): EnhanceJob { + $enhanceJob = EnhanceJob::query()->create($validated + $source + [ + 'user_id' => (int) $user->id, + 'status' => EnhanceJob::STATUS_PENDING, + ]); + + $this->queue($enhanceJob); + + return $enhanceJob->fresh(); + }); + + Log::info('enhance.job.created', [ + 'enhance_job_id' => $job->id, + 'user_id' => $user->id, + 'type' => 'upload', + 'engine' => $job->engine, + ]); + + return $job; + } + + public function createFromArtwork(User $user, Artwork $artwork, array $options): EnhanceJob + { + $this->assertCreationAllowed($user); + $this->assertDailyLimit($user); + + $artworkSource = $this->storage->fetchArtworkSource($artwork); + $validated = $this->validator->validateBinary( + $artworkSource['binary'], + $options, + (int) ($artwork->file_size ?? strlen((string) $artworkSource['binary'])), + ); + $source = $this->storage->storeSourceBinary($user, (string) $artworkSource['binary'], (string) $artworkSource['extension']); + + $job = DB::transaction(function () use ($user, $artwork, $validated, $source): EnhanceJob { + $enhanceJob = EnhanceJob::query()->create($validated + $source + [ + 'user_id' => (int) $user->id, + 'artwork_id' => (int) $artwork->id, + 'status' => EnhanceJob::STATUS_PENDING, + ]); + + $this->queue($enhanceJob); + + return $enhanceJob->fresh(); + }); + + Log::info('enhance.job.created', [ + 'enhance_job_id' => $job->id, + 'user_id' => $user->id, + 'artwork_id' => $artwork->id, + 'type' => 'artwork', + 'engine' => $job->engine, + ]); + + return $job; + } + + public function retry(EnhanceJob $job): EnhanceJob + { + Log::info('enhance.retry.started', [ + 'enhance_job_id' => $job->id, + 'user_id' => $job->user_id, + 'status' => $job->status, + ]); + + if (! $job->isFailed()) { + throw ValidationException::withMessages([ + 'job' => 'Only failed enhance jobs can be retried.', + ]); + } + + if (! $this->sourceExists($job)) { + Log::warning('enhance.retry.failed_missing_source', [ + 'enhance_job_id' => $job->id, + 'user_id' => $job->user_id, + ]); + + throw ValidationException::withMessages([ + 'job' => 'This enhance job can no longer be retried because the original source file was cleaned up.', + ]); + } + + DB::transaction(function () use ($job): void { + $this->storage->deleteGeneratedFiles($job); + + $metadata = is_array($job->metadata) ? $job->metadata : []; + $retryCount = max(0, (int) ($metadata['retry_count'] ?? 0)) + 1; + + $job->forceFill([ + 'status' => EnhanceJob::STATUS_QUEUED, + 'output_disk' => null, + 'output_path' => null, + 'output_hash' => null, + 'output_width' => null, + 'output_height' => null, + 'output_filesize' => null, + 'output_mime' => null, + 'preview_disk' => null, + 'preview_path' => null, + 'processing_seconds' => null, + 'error_message' => null, + 'started_at' => null, + 'finished_at' => null, + 'queued_at' => now(), + 'metadata' => array_merge($metadata, [ + 'retry_count' => $retryCount, + 'last_retried_at' => now()->toIso8601String(), + ]), + ])->save(); + + ProcessEnhanceJob::dispatch((int) $job->id)->afterCommit(); + }); + + Log::info('enhance.retry.dispatched', [ + 'enhance_job_id' => $job->id, + 'user_id' => $job->user_id, + 'retry_count' => (int) (($job->fresh()?->metadata['retry_count'] ?? 0)), + ]); + + return $job->fresh(); + } + + public function markFailedByModerator(EnhanceJob $job, User $actor): EnhanceJob + { + if (! in_array($job->status, [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING], true)) { + throw ValidationException::withMessages([ + 'job' => 'Only pending, queued, or processing jobs can be marked as failed.', + ]); + } + + $metadata = is_array($job->metadata) ? $job->metadata : []; + + DB::transaction(function () use ($job, $actor, $metadata): void { + $job->forceFill([ + 'status' => EnhanceJob::STATUS_FAILED, + 'error_message' => 'Marked as failed by moderator.', + 'finished_at' => now(), + 'processing_seconds' => $job->started_at ? max(0, now()->diffInSeconds($job->started_at)) : $job->processing_seconds, + 'metadata' => array_merge($metadata, [ + 'moderation' => [ + 'marked_failed_at' => now()->toIso8601String(), + 'marked_failed_by' => (int) $actor->id, + ], + ]), + ])->save(); + }); + + Log::info('enhance.moderation.mark_failed', [ + 'enhance_job_id' => $job->id, + 'moderator_id' => $actor->id, + ]); + + return $job->fresh(); + } + + public function delete(EnhanceJob $job): void + { + DB::transaction(function () use ($job): void { + $this->storage->deleteFiles($job); + $job->delete(); + }); + + Log::info('enhance.job.deleted', [ + 'enhance_job_id' => $job->id, + 'user_id' => $job->user_id, + ]); + } + + private function assertCreationAllowed(User $user): void + { + if (method_exists($user, 'hasVerifiedEmail') && ! $user->hasVerifiedEmail()) { + throw ValidationException::withMessages([ + 'image' => 'Please verify your email address before using Skinbase Enhance.', + ]); + } + } + + private function assertDailyLimit(User $user): void + { + $limit = max(0, (int) config('enhance.daily_limit', 10)); + + if ($limit === 0) { + return; + } + + $count = EnhanceJob::query() + ->where('user_id', (int) $user->id) + ->whereBetween('created_at', [now()->startOfDay(), now()->endOfDay()]) + ->count(); + + if ($count >= $limit) { + throw ValidationException::withMessages([ + 'image' => 'You have reached your daily enhance limit. Please try again tomorrow.', + ]); + } + } + + private function queue(EnhanceJob $job): void + { + $job->forceFill([ + 'status' => EnhanceJob::STATUS_QUEUED, + 'queued_at' => now(), + ])->save(); + + ProcessEnhanceJob::dispatch((int) $job->id)->afterCommit(); + } + + public function frontendConfig(): array + { + $engine = (string) config('enhance.default_engine', EnhanceJob::ENGINE_STUB); + $showStubWarning = (bool) config('enhance.stub.show_warning', true) && $engine === EnhanceJob::ENGINE_STUB; + + return [ + 'engine' => $engine, + 'isStub' => $engine === EnhanceJob::ENGINE_STUB, + 'showStubWarning' => $showStubWarning, + 'maxUploadMb' => (int) config('enhance.max_upload_mb', 20), + 'allowedModes' => array_values((array) config('enhance.allowed_modes', [])), + 'allowedScales' => array_map('intval', (array) config('enhance.allowed_scales', [])), + ]; + } + + private function sourceExists(EnhanceJob $job): bool + { + $path = ltrim(trim((string) $job->source_path), '/'); + + if ($path === '') { + return false; + } + + return Storage::disk($job->source_disk ?: $this->storage->diskName())->exists($path); + } +} \ No newline at end of file diff --git a/app/Services/Enhance/EnhanceStorageService.php b/app/Services/Enhance/EnhanceStorageService.php new file mode 100644 index 00000000..5445c126 --- /dev/null +++ b/app/Services/Enhance/EnhanceStorageService.php @@ -0,0 +1,366 @@ +manager = extension_loaded('gd') + ? new ImageManager(new GdDriver()) + : new ImageManager(new ImagickDriver()); + } catch (\Throwable) { + $this->manager = null; + } + } + + public function diskName(): string + { + return (string) config('enhance.disk', 'public'); + } + + public function fetchSourceBinary(EnhanceJob $job): string + { + $path = trim((string) $job->source_path); + + if ($path === '') { + throw new RuntimeException('Enhance source image is missing.'); + } + + $contents = Storage::disk($job->source_disk ?: $this->diskName())->get($path); + + if (! is_string($contents) || $contents === '') { + throw new RuntimeException('Unable to read enhance source image.'); + } + + return $contents; + } + + public function fetchArtworkSource(Artwork $artwork): array + { + $objectPath = $this->artworkOriginalFileLocator->resolveObjectPath($artwork); + + if ($objectPath === '') { + throw new RuntimeException('Artwork source file is unavailable for enhance.'); + } + + $disk = (string) config('uploads.object_storage.disk', 's3'); + $contents = Storage::disk($disk)->get($objectPath); + + if (! is_string($contents) || $contents === '') { + throw new RuntimeException('Unable to read the original artwork source.'); + } + + $extension = strtolower(ltrim((string) ($artwork->file_ext ?? pathinfo($objectPath, PATHINFO_EXTENSION)), '.')); + $mime = trim(strtolower((string) ($artwork->mime_type ?? ''))); + + if ($mime === '') { + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mime = strtolower((string) $finfo->buffer($contents)); + } + + return [ + 'disk' => $disk, + 'path' => $objectPath, + 'binary' => $contents, + 'mime' => $mime, + 'extension' => $extension !== '' ? $extension : $this->extensionFromMime($mime), + ]; + } + + public function storeUploadedSource(User $user, UploadedFile $file): array + { + $path = (string) ($file->getRealPath() ?: $file->getPathname()); + + if ($path === '' || ! is_readable($path)) { + throw new RuntimeException('Unable to resolve uploaded source path.'); + } + + $binary = file_get_contents($path); + + if (! is_string($binary) || $binary === '') { + throw new RuntimeException('Unable to read uploaded source image.'); + } + + $extension = strtolower(ltrim((string) ($file->getClientOriginalExtension() ?: $file->extension()), '.')); + + return $this->storeSourceBinary($user, $binary, $extension !== '' ? $extension : 'bin'); + } + + public function storeSourceBinary(User $user, string $binary, string $extension): array + { + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mime = strtolower((string) $finfo->buffer($binary)); + $normalizedExtension = $extension !== '' ? $extension : $this->extensionFromMime($mime); + $relativePath = $this->buildPath((string) config('enhance.source_prefix', 'enhance/sources'), (int) $user->id, sprintf('%s.%s', Str::uuid()->toString(), $normalizedExtension)); + + $this->writeBinary($this->diskName(), $relativePath, $binary, $mime); + + return [ + 'source_disk' => $this->diskName(), + 'source_path' => $relativePath, + 'source_hash' => hash('sha256', $binary), + ]; + } + + public function putOutputBinary(EnhanceJob $job, string $binary, string $mime, ?string $extension = null): array + { + $normalizedMime = strtolower(trim($mime)); + $ext = $extension !== null && $extension !== '' ? strtolower(ltrim($extension, '.')) : $this->extensionFromMime($normalizedMime); + $filename = sprintf('%s_x%d.%s', Str::uuid()->toString(), (int) $job->scale, $ext); + $relativePath = $this->buildPath((string) config('enhance.output_prefix', 'enhance/outputs'), (int) $job->user_id, $filename); + + $this->writeBinary($this->diskName(), $relativePath, $binary, $normalizedMime); + $dimensions = @getimagesizefromstring($binary) ?: [0, 0]; + + return [ + 'disk' => $this->diskName(), + 'path' => $relativePath, + 'hash' => hash('sha256', $binary), + 'width' => (int) ($dimensions[0] ?? 0), + 'height' => (int) ($dimensions[1] ?? 0), + 'filesize' => strlen($binary), + 'mime' => $normalizedMime, + ]; + } + + public function storePreviewFromBinary(EnhanceJob $job, string $binary): ?array + { + $previewBinary = $binary; + $previewMime = 'image/webp'; + + if ($this->manager !== null) { + try { + $previewBinary = (string) $this->manager + ->read($binary) + ->scaleDown(width: 1600, height: 1600) + ->encode(new WebpEncoder(82)); + } catch (\Throwable) { + $previewMime = strtolower((string) ((new \finfo(FILEINFO_MIME_TYPE))->buffer($binary) ?: 'image/jpeg')); + $previewBinary = $binary; + } + } else { + $previewMime = strtolower((string) ((new \finfo(FILEINFO_MIME_TYPE))->buffer($binary) ?: 'image/jpeg')); + } + + $extension = $this->extensionFromMime($previewMime); + $relativePath = $this->buildPath( + (string) config('enhance.preview_prefix', 'enhance/previews'), + (int) $job->user_id, + sprintf('%s_preview.%s', Str::uuid()->toString(), $extension), + ); + + $this->writeBinary($this->diskName(), $relativePath, $previewBinary, $previewMime); + + return [ + 'preview_disk' => $this->diskName(), + 'preview_path' => $relativePath, + ]; + } + + public function createPreviewFromStoredOutput(EnhanceJob $job, string $disk, string $path): ?array + { + $contents = Storage::disk($disk)->get($path); + + if (! is_string($contents) || $contents === '') { + return null; + } + + return $this->storePreviewFromBinary($job, $contents); + } + + public function deleteFiles(EnhanceJob $job): void + { + $this->deleteFilesForJob($job); + } + + public function deleteGeneratedFiles(EnhanceJob $job): void + { + foreach ([ + [$job->output_disk, $job->output_path], + [$job->preview_disk, $job->preview_path], + ] as [$disk, $path]) { + $this->safeDelete($disk, $path); + } + } + + public function deleteFilesForJob(EnhanceJob $job): array + { + $result = [ + 'deleted' => [ + 'source' => false, + 'output' => false, + 'preview' => false, + ], + 'skipped' => [], + 'errors' => [], + ]; + + foreach ([ + 'source' => [$job->source_disk, $job->source_path], + 'output' => [$job->output_disk, $job->output_path], + 'preview' => [$job->preview_disk, $job->preview_path], + ] as $key => [$disk, $path]) { + try { + $deleted = $this->safeDelete($disk, $path); + $result['deleted'][$key] = $deleted; + + if (! $deleted && trim((string) $path) !== '') { + $result['skipped'][] = $key; + } + } catch (\Throwable $exception) { + $result['errors'][$key] = $exception->getMessage(); + + Log::warning('enhance.cleanup.file_delete_failed', [ + 'path' => trim((string) $path), + 'disk' => $disk ?: $this->diskName(), + 'message' => $exception->getMessage(), + ]); + } + } + + return $result; + } + + public function isEnhancePath(?string $path): bool + { + $trimmedPath = ltrim(trim((string) $path), '/'); + + if ($trimmedPath === '') { + return false; + } + + foreach ($this->enhancePrefixes() as $prefix) { + if ($trimmedPath === $prefix || str_starts_with($trimmedPath, $prefix . '/')) { + return true; + } + } + + return false; + } + + public function safeDelete(?string $disk, ?string $path): bool + { + $trimmedPath = ltrim(trim((string) $path), '/'); + + if ($trimmedPath === '') { + return false; + } + + if (! $this->isEnhancePath($trimmedPath)) { + Log::warning('enhance.cleanup.file_skipped', [ + 'path' => $trimmedPath, + 'disk' => $disk ?: $this->diskName(), + 'reason' => 'outside-enhance-prefixes', + ]); + + return false; + } + + $targetDisk = $disk ?: $this->diskName(); + + if (! Storage::disk($targetDisk)->exists($trimmedPath)) { + return false; + } + + $deleted = Storage::disk($targetDisk)->delete($trimmedPath); + + if ($deleted) { + Log::info('enhance.cleanup.file_deleted', [ + 'path' => $trimmedPath, + 'disk' => $targetDisk, + ]); + + return true; + } + + Log::warning('enhance.cleanup.file_delete_failed', [ + 'path' => $trimmedPath, + 'disk' => $targetDisk, + 'message' => 'Storage delete returned false.', + ]); + + return false; + } + + public function listKnownJobPaths(): array + { + return EnhanceJob::withTrashed() + ->get(['source_path', 'output_path', 'preview_path']) + ->flatMap(fn (EnhanceJob $job): array => array_values(array_filter([ + ltrim(trim((string) $job->source_path), '/'), + ltrim(trim((string) $job->output_path), '/'), + ltrim(trim((string) $job->preview_path), '/'), + ]))) + ->unique() + ->values() + ->all(); + } + + private function buildPath(string $prefix, int $userId, string $filename): string + { + return sprintf( + '%s/%d/%s/%s/%s', + trim($prefix, '/'), + $userId, + now()->format('Y'), + now()->format('m'), + ltrim($filename, '/'), + ); + } + + private function enhancePrefixes(): array + { + return array_values(array_filter(array_unique(array_map( + static fn (string $prefix): string => trim($prefix, '/'), + [ + (string) config('enhance.source_prefix', 'enhance/sources'), + (string) config('enhance.output_prefix', 'enhance/outputs'), + (string) config('enhance.preview_prefix', 'enhance/previews'), + ], + )))); + } + + private function extensionFromMime(string $mime): string + { + return match ($mime) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/webp' => 'webp', + default => 'bin', + }; + } + + private function writeBinary(string $disk, string $path, string $binary, string $mime): void + { + $written = Storage::disk($disk)->put($path, $binary, [ + 'visibility' => 'public', + 'CacheControl' => 'public, max-age=31536000, immutable', + 'ContentType' => $mime, + ]); + + if ($written !== true) { + throw new RuntimeException('Unable to store enhance image in storage.'); + } + } +} \ No newline at end of file diff --git a/app/Services/Enhance/EnhanceValidator.php b/app/Services/Enhance/EnhanceValidator.php new file mode 100644 index 00000000..90ef6fb9 --- /dev/null +++ b/app/Services/Enhance/EnhanceValidator.php @@ -0,0 +1,128 @@ +getRealPath() ?: $file->getPathname()); + + if ($path === '' || ! is_readable($path)) { + throw ValidationException::withMessages([ + 'image' => 'Unable to read the uploaded image.', + ]); + } + + $binary = file_get_contents($path); + + if (! is_string($binary) || $binary === '') { + throw ValidationException::withMessages([ + 'image' => 'Unable to read the uploaded image.', + ]); + } + + return $this->validateBinary($binary, $options, (int) ($file->getSize() ?? strlen($binary))); + } + + public function validateBinary(string $binary, array $options, ?int $filesize = null): array + { + $normalized = $this->normalizeOptions($options); + $size = $filesize ?? strlen($binary); + + if ($size <= 0) { + throw ValidationException::withMessages([ + 'image' => 'Uploaded image is empty.', + ]); + } + + $maxBytes = (int) config('enhance.max_upload_mb', 20) * 1024 * 1024; + if ($maxBytes > 0 && $size > $maxBytes) { + throw ValidationException::withMessages([ + 'image' => sprintf('The image may not be greater than %d MB.', (int) config('enhance.max_upload_mb', 20)), + ]); + } + + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mime = strtolower((string) $finfo->buffer($binary)); + + if (! in_array($mime, (array) config('enhance.allowed_mimes', []), true)) { + throw ValidationException::withMessages([ + 'image' => 'Unsupported image format. Upload a JPEG, PNG, or WebP image.', + ]); + } + + $dimensions = @getimagesizefromstring($binary); + + if (! is_array($dimensions) || (int) ($dimensions[0] ?? 0) < 1 || (int) ($dimensions[1] ?? 0) < 1) { + throw ValidationException::withMessages([ + 'image' => 'Uploaded file is not a valid image.', + ]); + } + + $width = (int) ($dimensions[0] ?? 0); + $height = (int) ($dimensions[1] ?? 0); + + if ($width > (int) config('enhance.max_input_width', 4096)) { + throw ValidationException::withMessages([ + 'image' => sprintf('Image width may not exceed %d pixels.', (int) config('enhance.max_input_width', 4096)), + ]); + } + + if ($height > (int) config('enhance.max_input_height', 4096)) { + throw ValidationException::withMessages([ + 'image' => sprintf('Image height may not exceed %d pixels.', (int) config('enhance.max_input_height', 4096)), + ]); + } + + return $normalized + [ + 'input_width' => $width, + 'input_height' => $height, + 'input_filesize' => $size, + 'input_mime' => $mime, + ]; + } + + public function normalizeOptions(array $options): array + { + $allowedScales = array_map('intval', (array) config('enhance.allowed_scales', [2, 4])); + $allowedModes = array_map('strval', (array) config('enhance.allowed_modes', ['standard', 'artwork', 'photo', 'illustration'])); + $allowedEngines = [ + \App\Models\EnhanceJob::ENGINE_STUB, + \App\Models\EnhanceJob::ENGINE_EXTERNAL_WORKER, + ]; + + $scale = (int) ($options['scale'] ?? config('enhance.allowed_scales.0', 2)); + $mode = trim((string) ($options['mode'] ?? 'standard')); + $engine = trim((string) ($options['engine'] ?? config('enhance.default_engine', \App\Models\EnhanceJob::ENGINE_STUB))); + + if (! in_array($scale, $allowedScales, true)) { + throw ValidationException::withMessages([ + 'scale' => 'Please select a supported scale.', + ]); + } + + if (! in_array($mode, $allowedModes, true)) { + throw ValidationException::withMessages([ + 'mode' => 'Please select a supported enhance mode.', + ]); + } + + if (! in_array($engine, $allowedEngines, true)) { + throw ValidationException::withMessages([ + 'engine' => 'Please select a supported enhance engine.', + ]); + } + + return [ + 'scale' => $scale, + 'mode' => $mode, + 'engine' => $engine, + ]; + } +} \ No newline at end of file diff --git a/app/Services/Enhance/Processors/ExternalWorkerEnhanceProcessor.php b/app/Services/Enhance/Processors/ExternalWorkerEnhanceProcessor.php new file mode 100644 index 00000000..082c85b5 --- /dev/null +++ b/app/Services/Enhance/Processors/ExternalWorkerEnhanceProcessor.php @@ -0,0 +1,304 @@ +sourceUrlForWorker($job); + + try { + $response = $this->http($timeout) + ->post($this->workerEndpoint($workerUrl, '/v1/upscale'), [ + 'job_id' => (int) $job->id, + 'source_url' => $sourceUrl, + 'scale' => (int) $job->scale, + 'mode' => (string) $job->mode, + 'output_format' => 'webp', + ]); + } catch (ConnectionException $exception) { + throw $this->wrapHttpException($exception, $job, 'upscale'); + } + + $payload = $this->decodeWorkerPayload($response); + [$binary, $cleanupFilename] = $this->resolveWorkerOutputBinary($payload, $workerUrl, $token, $timeout, $job); + $validated = $this->validateOutputBinary($binary); + $stored = $this->storage->putOutputBinary($job, $binary, $validated['mime']); + + if ($cleanupFilename !== null) { + $this->deleteWorkerResult($workerUrl, $cleanupFilename, $token, $timeout, $job); + } + + $metadata = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : []; + $metadata['source_transport'] = str_contains($sourceUrl, '/internal/enhance/source/') ? 'signed-route' : 'temporary-url'; + + return new EnhanceProcessorResult( + disk: $stored['disk'], + path: $stored['path'], + width: (int) $validated['width'], + height: (int) $validated['height'], + filesize: (int) $validated['filesize'], + mime: (string) $validated['mime'], + metadata: $metadata, + ); + } + + private function http(int $timeout): PendingRequest + { + return Http::timeout($timeout) + ->acceptJson() + ->asJson() + ->withToken((string) config('enhance.external_worker.token')); + } + + private function decodeWorkerPayload(Response $response): array + { + if (! $response->successful()) { + $payload = $response->json(); + + throw new RuntimeException( + $response->status() >= 500 + ? 'Worker is unavailable.' + : $this->normalizeWorkerError(is_array($payload) ? ($payload['error'] ?? null) : null, 'Worker rejected the image.'), + ); + } + + $payload = $response->json(); + + if (! is_array($payload) || ! ($payload['success'] ?? false)) { + throw new RuntimeException( + $this->normalizeWorkerError(is_array($payload) ? ($payload['error'] ?? null) : null, 'Worker returned an invalid response.'), + ); + } + + return $payload; + } + + private function resolveWorkerOutputBinary(array $payload, string $workerUrl, string $token, int $timeout, EnhanceJob $job): array + { + $base64 = trim((string) ($payload['output_base64'] ?? '')); + + if ($base64 !== '') { + $binary = base64_decode($base64, true); + + if (! is_string($binary) || $binary === '') { + throw new RuntimeException('Worker returned an invalid response.'); + } + + return [$binary, null]; + } + + $outputUrl = trim((string) ($payload['output_url'] ?? '')); + + if ($outputUrl === '') { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $safeOutputUrl = $this->normalizeWorkerOutputUrl($workerUrl, $outputUrl); + + try { + $outputResponse = Http::timeout($timeout) + ->withToken($token) + ->get($safeOutputUrl); + } catch (ConnectionException $exception) { + throw $this->wrapHttpException($exception, $job, 'download'); + } + + if (! $outputResponse->successful()) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $binary = $outputResponse->body(); + + if ($binary === '') { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $path = trim((string) parse_url($safeOutputUrl, PHP_URL_PATH)); + $filename = basename($path); + + return [$binary, $filename !== '' ? $filename : null]; + } + + private function validateOutputBinary(string $binary): array + { + $maxBytes = max(1, (int) config('enhance.external_worker.max_download_mb', 60)) * 1024 * 1024; + + if (strlen($binary) > $maxBytes) { + throw new RuntimeException('The upscaled output exceeded the maximum allowed size.'); + } + + $dimensions = @getimagesizefromstring($binary); + + if (! is_array($dimensions)) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $width = (int) ($dimensions[0] ?? 0); + $height = (int) ($dimensions[1] ?? 0); + $maxWidth = max(1, (int) config('enhance.max_output_width', 8192)); + $maxHeight = max(1, (int) config('enhance.max_output_height', 8192)); + + if ($width < 1 || $height < 1 || $width > $maxWidth || $height > $maxHeight) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $mime = strtolower((string) ((new \finfo(FILEINFO_MIME_TYPE))->buffer($binary) ?: '')); + + if (! in_array($mime, (array) config('enhance.allowed_mimes', []), true)) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + return [ + 'width' => $width, + 'height' => $height, + 'filesize' => strlen($binary), + 'mime' => $mime, + ]; + } + + private function sourceUrlForWorker(EnhanceJob $job): string + { + $disk = Storage::disk($job->source_disk ?: $this->storage->diskName()); + $path = ltrim(trim((string) $job->source_path), '/'); + + if ($path === '') { + throw new RuntimeException('The source file could not be downloaded by the worker.'); + } + + try { + if (method_exists($disk, 'providesTemporaryUrls') && $disk->providesTemporaryUrls()) { + return $disk->temporaryUrl($path, now()->addMinutes(15)); + } + } catch (Throwable) { + } + + return URL::temporarySignedRoute( + 'enhance.source.download', + now()->addMinutes(15), + ['enhanceJob' => $job->id], + ); + } + + private function normalizeWorkerOutputUrl(string $workerUrl, string $outputUrl): string + { + if (str_starts_with($outputUrl, '/')) { + return rtrim($workerUrl, '/') . $outputUrl; + } + + $workerParts = parse_url($workerUrl); + $outputParts = parse_url($outputUrl); + + if (! is_array($workerParts) || ! is_array($outputParts)) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + $sameHost = ($workerParts['scheme'] ?? null) === ($outputParts['scheme'] ?? null) + && ($workerParts['host'] ?? null) === ($outputParts['host'] ?? null) + && (($workerParts['port'] ?? null) === ($outputParts['port'] ?? null)); + + if (! $sameHost) { + throw new RuntimeException('Worker returned an invalid response.'); + } + + return $outputUrl; + } + + private function deleteWorkerResult(string $workerUrl, string $filename, string $token, int $timeout, EnhanceJob $job): void + { + $safeFilename = basename($filename); + + if ($safeFilename === '') { + return; + } + + try { + Http::timeout(min($timeout, 30)) + ->acceptJson() + ->withToken($token) + ->delete($this->workerEndpoint($workerUrl, '/v1/results/' . rawurlencode($safeFilename))); + } catch (ConnectionException $exception) { + Log::warning('enhance.external_worker.cleanup_failed', [ + 'enhance_job_id' => $job->id, + 'message' => $exception->getMessage(), + ]); + } + } + + private function workerEndpoint(string $workerUrl, string $path): string + { + return rtrim($workerUrl, '/') . $path; + } + + private function normalizeWorkerError(mixed $error, string $fallback): string + { + $message = trim((string) $error); + + if (in_array($message, self::SAFE_WORKER_ERRORS, true)) { + return $message; + } + + return $fallback; + } + + private function wrapHttpException(ConnectionException $exception, EnhanceJob $job, string $stage): RuntimeException + { + $message = str_contains(strtolower($exception->getMessage()), 'timed out') + ? 'The enhance worker timed out while processing this image.' + : 'Worker is unavailable.'; + + Log::warning('enhance.external_worker.http_failed', [ + 'enhance_job_id' => $job->id, + 'stage' => $stage, + 'message' => $exception->getMessage(), + ]); + + return new RuntimeException($message, 0, $exception); + } +} \ No newline at end of file diff --git a/app/Services/Enhance/Processors/StubEnhanceProcessor.php b/app/Services/Enhance/Processors/StubEnhanceProcessor.php new file mode 100644 index 00000000..469a614f --- /dev/null +++ b/app/Services/Enhance/Processors/StubEnhanceProcessor.php @@ -0,0 +1,75 @@ +manager = extension_loaded('gd') + ? new ImageManager(new GdDriver()) + : new ImageManager(new ImagickDriver()); + } catch (\Throwable) { + $this->manager = null; + } + } + + public function process(EnhanceJob $job): EnhanceProcessorResult + { + $sourceBinary = $this->storage->fetchSourceBinary($job); + $outputBinary = $sourceBinary; + $outputMime = (string) ($job->input_mime ?: 'image/jpeg'); + $scale = max(1, (int) $job->scale); + $metadata = [ + 'stub' => true, + 'engine' => EnhanceJob::ENGINE_STUB, + 'requested_scale' => $scale, + ]; + + if ($this->manager !== null) { + try { + $image = $this->manager->read($sourceBinary); + $targetWidth = max((int) $image->width(), (int) $image->width() * $scale); + $targetHeight = max((int) $image->height(), (int) $image->height() * $scale); + $outputBinary = (string) $image + ->resize($targetWidth, $targetHeight) + ->encode(new WebpEncoder(88)); + $outputMime = 'image/webp'; + $metadata['actual_scale'] = $scale; + } catch (\Throwable) { + $metadata['actual_scale'] = 1; + $metadata['fallback'] = 'source-copy'; + } + } else { + $metadata['actual_scale'] = 1; + $metadata['fallback'] = 'source-copy'; + } + + $stored = $this->storage->putOutputBinary($job, $outputBinary, $outputMime); + + return new EnhanceProcessorResult( + disk: $stored['disk'], + path: $stored['path'], + width: (int) $stored['width'], + height: (int) $stored['height'], + filesize: (int) $stored['filesize'], + mime: (string) $stored['mime'], + metadata: $metadata, + ); + } +} \ No newline at end of file diff --git a/app/Services/News/NewsCoverImageService.php b/app/Services/News/NewsCoverImageService.php index dc025129..5da9aab6 100644 --- a/app/Services/News/NewsCoverImageService.php +++ b/app/Services/News/NewsCoverImageService.php @@ -75,6 +75,7 @@ final class NewsCoverImageService 'size_bytes' => strlen($masterEncoded), 'mobile_url' => NewsCoverImage::variantUrl($path, 'mobile'), 'desktop_url' => NewsCoverImage::variantUrl($path, 'desktop'), + 'large_url' => NewsCoverImage::variantUrl($path, 'large'), 'srcset' => NewsCoverImage::srcset($path), ]; } diff --git a/app/Services/News/NewsService.php b/app/Services/News/NewsService.php index be9f7d11..92c23ad5 100644 --- a/app/Services/News/NewsService.php +++ b/app/Services/News/NewsService.php @@ -26,6 +26,7 @@ use cPad\Plugins\News\Models\NewsArticle; use cPad\Plugins\News\Models\NewsArticleRelation; use cPad\Plugins\News\Models\NewsCategory; use cPad\Plugins\News\Models\NewsTag; +use cPad\Plugins\News\Services\NewsArticleService; final class NewsService { @@ -39,6 +40,7 @@ final class NewsService public const RELATION_CHALLENGE = 'challenge'; public const RELATION_EVENT = 'event'; public const RELATION_USER = 'user'; + public const RELATION_SOURCE = 'source'; public const RELATION_LABELS = [ self::RELATION_GROUP => 'Group', @@ -49,10 +51,15 @@ final class NewsService self::RELATION_CHALLENGE => 'Challenge', self::RELATION_EVENT => 'Event', self::RELATION_USER => 'Profile', + self::RELATION_SOURCE => 'Source', ]; private ?bool $artworkStatsViewsColumnExists = null; + public function __construct(private readonly NewsArticleService $articleService) + { + } + public function articleTypeOptions(): array { return \collect(NewsArticle::TYPE_LABELS) @@ -224,12 +231,22 @@ final class NewsService 'og_description' => (string) ($article->og_description ?? ''), 'og_image' => (string) ($article->og_image ?? ''), 'relations' => $article->relatedEntities - ->map(fn (NewsArticleRelation $relation): array => [ - 'entity_type' => (string) $relation->entity_type, - 'entity_id' => (int) $relation->entity_id, - 'context_label' => (string) ($relation->context_label ?? ''), - 'preview' => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer), - ]) + ->map(function (NewsArticleRelation $relation) use ($viewer): array { + $entityType = (string) $relation->entity_type; + $externalUrl = $entityType === self::RELATION_SOURCE + ? (string) ($relation->external_url ?? '') + : ''; + + return [ + 'entity_type' => $entityType, + 'entity_id' => $entityType === self::RELATION_SOURCE ? '' : (int) $relation->entity_id, + 'external_url' => $externalUrl, + 'context_label' => (string) ($relation->context_label ?? ''), + 'preview' => $entityType === self::RELATION_SOURCE + ? $this->resolveSourcePreview($externalUrl, (string) ($relation->context_label ?? '')) + : $this->resolveEntityPreview($entityType, (int) $relation->entity_id, $viewer), + ]; + }) ->values() ->all(), ]; @@ -263,6 +280,8 @@ final class NewsService 'published_at' => $article->published_at ?? \now(), ])->save(); + $this->articleService->createForumThread($article); + $this->invalidatePublicCache(); return $article->fresh(['author', 'category', 'tags', 'relatedEntities']); @@ -312,6 +331,7 @@ final class NewsService self::RELATION_CHALLENGE => $this->searchChallenges($query, $viewer), self::RELATION_EVENT => $this->searchEvents($query, $viewer), self::RELATION_USER => $this->searchUsers($query), + self::RELATION_SOURCE => [], default => [], }; } @@ -321,7 +341,15 @@ final class NewsService $article->loadMissing('relatedEntities'); return $article->relatedEntities - ->map(fn (NewsArticleRelation $relation): ?array => $this->resolveEntityPreview((string) $relation->entity_type, (int) $relation->entity_id, $viewer, (string) ($relation->context_label ?? ''))) + ->map(function (NewsArticleRelation $relation) use ($viewer): ?array { + $entityType = (string) $relation->entity_type; + + if ($entityType === self::RELATION_SOURCE) { + return $this->resolveSourcePreview((string) ($relation->external_url ?? ''), (string) ($relation->context_label ?? '')); + } + + return $this->resolveEntityPreview($entityType, (int) $relation->entity_id, $viewer, (string) ($relation->context_label ?? '')); + }) ->filter() ->values() ->all(); @@ -380,6 +408,7 @@ final class NewsService public function invalidatePublicCache(): void { Cache::forever(self::PUBLIC_CACHE_VERSION_KEY, $this->publicCacheVersion() + 1); + Cache::forget('news.rss.feed'); } public function syncRelations(NewsArticle $article, array $relations): void @@ -387,20 +416,32 @@ final class NewsService $normalized = \collect($relations) ->map(function (array $relation): ?array { $entityType = trim(Str::lower((string) ($relation['entity_type'] ?? ''))); - $entityId = (int) ($relation['entity_id'] ?? 0); + $externalUrl = $entityType === self::RELATION_SOURCE + ? $this->normalizeExternalRelationUrl($relation['external_url'] ?? $relation['entity_id'] ?? null) + : null; + $entityId = $entityType === self::RELATION_SOURCE ? null : (int) ($relation['entity_id'] ?? 0); - if (! array_key_exists($entityType, self::RELATION_LABELS) || $entityId < 1) { + if (! array_key_exists($entityType, self::RELATION_LABELS)) { + return null; + } + + if ($entityType === self::RELATION_SOURCE && $externalUrl === null) { + return null; + } + + if ($entityType !== self::RELATION_SOURCE && $entityId < 1) { return null; } return [ 'entity_type' => $entityType, 'entity_id' => $entityId, + 'external_url' => $externalUrl, 'context_label' => Str::limit(trim((string) ($relation['context_label'] ?? '')), 120, ''), ]; }) ->filter() - ->unique(fn (array $relation): string => $relation['entity_type'] . ':' . $relation['entity_id']) + ->unique(fn (array $relation): string => $relation['entity_type'] . ':' . ($relation['entity_type'] === self::RELATION_SOURCE ? ($relation['external_url'] ?? '') : $relation['entity_id'])) ->values(); $article->relatedEntities()->delete(); @@ -409,6 +450,7 @@ final class NewsService $article->relatedEntities()->create([ 'entity_type' => $relation['entity_type'], 'entity_id' => $relation['entity_id'], + 'external_url' => $relation['external_url'], 'context_label' => $relation['context_label'] !== '' ? $relation['context_label'] : null, 'sort_order' => $index, ]); @@ -808,6 +850,34 @@ final class NewsService }; } + private function resolveSourcePreview(string $externalUrl, string $contextLabel): ?array + { + $normalizedUrl = $this->normalizeExternalRelationUrl($externalUrl); + + if ($normalizedUrl === null) { + return null; + } + + $host = \parse_url($normalizedUrl, PHP_URL_HOST); + $host = \is_string($host) ? preg_replace('/^www\./i', '', $host) : null; + + return [ + 'id' => $normalizedUrl, + 'entity_type' => self::RELATION_SOURCE, + 'entity_label' => self::RELATION_LABELS[self::RELATION_SOURCE], + 'title' => $host ?: 'External source', + 'subtitle' => 'Reference link', + 'description' => Str::limit($normalizedUrl, 140), + 'url' => $normalizedUrl, + 'image' => null, + 'avatar' => null, + 'context_label' => $contextLabel !== '' ? $contextLabel : 'Source link', + 'meta' => array_values(array_filter([ + $host, + ])), + ]; + } + private function resolveGroupPreview(int $entityId, ?User $viewer, string $contextLabel): ?array { $group = Group::query()->with('owner')->find($entityId); @@ -1017,4 +1087,23 @@ final class NewsService 'meta' => [], ]; } + + private function normalizeExternalRelationUrl(mixed $value): ?string + { + $url = trim((string) ($value ?? '')); + + if ($url === '') { + return null; + } + + if (preg_match('/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i', $url, $matches) === 1) { + $url = trim((string) ($matches[1] ?? '')); + } + + if ($url === '') { + return null; + } + + return Str::limit($url, 2048, ''); + } } \ No newline at end of file diff --git a/app/Support/AcademyAnalytics/AcademyAnalyticsContentType.php b/app/Support/AcademyAnalytics/AcademyAnalyticsContentType.php index f61a91b6..7ee2bff9 100644 --- a/app/Support/AcademyAnalytics/AcademyAnalyticsContentType.php +++ b/app/Support/AcademyAnalytics/AcademyAnalyticsContentType.php @@ -7,6 +7,9 @@ namespace App\Support\AcademyAnalytics; final class AcademyAnalyticsContentType { public const HOME = 'academy_home'; + public const PROMPT_LIBRARY = 'academy_prompt_library'; + public const PROMPT_POPULAR = 'academy_prompt_popular'; + public const PROMPT_PACK_LIBRARY = 'academy_prompt_pack_library'; public const PROMPT = 'academy_prompt'; public const LESSON = 'academy_lesson'; public const COURSE = 'academy_course'; @@ -22,6 +25,9 @@ final class AcademyAnalyticsContentType { return [ self::HOME, + self::PROMPT_LIBRARY, + self::PROMPT_POPULAR, + self::PROMPT_PACK_LIBRARY, self::PROMPT, self::LESSON, self::COURSE, diff --git a/app/Support/ArtworkDescriptionContentValidator.php b/app/Support/ArtworkDescriptionContentValidator.php new file mode 100644 index 00000000..f6a24f4e --- /dev/null +++ b/app/Support/ArtworkDescriptionContentValidator.php @@ -0,0 +1,24 @@ + + */ + public static function errors(null|string $value): array + { + $normalized = trim((string) ($value ?? '')); + + if ($normalized === '') { + return []; + } + + return ContentSanitizer::validate($normalized); + } +} \ No newline at end of file diff --git a/app/Support/News/NewsCoverImage.php b/app/Support/News/NewsCoverImage.php index 034aafd4..f256475f 100644 --- a/app/Support/News/NewsCoverImage.php +++ b/app/Support/News/NewsCoverImage.php @@ -19,6 +19,11 @@ final class NewsCoverImage 'quality' => 76, 'suffix' => 'desktop', ], + 'large' => [ + 'width' => 1280, + 'quality' => 80, + 'suffix' => 'large', + ], ]; public static function isManagedPath(?string $path): bool diff --git a/bootstrap/app.php b/bootstrap/app.php index a4318553..0fc20f06 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -29,13 +29,12 @@ return Application::configure(basePath: dirname(__DIR__)) ]); $middleware->validateCsrfTokens(except: [ - 'chat_post', - 'chat_post/*', 'api/art/*/view', 'stripe/*', ]); $middleware->web(append: [ + \App\Http\Middleware\SecurityHeaders::class, \App\Http\Middleware\RedirectLegacyProfileSubdomain::class, \App\Http\Middleware\TrackOnlineVisitor::class, \App\Http\Middleware\UpdateLastVisit::class, diff --git a/bootstrap/ssr/ssr-manifest.json b/bootstrap/ssr/ssr-manifest.json index 4a07b65a..e1204998 100644 --- a/bootstrap/ssr/ssr-manifest.json +++ b/bootstrap/ssr/ssr-manifest.json @@ -2096,6 +2096,9 @@ "resources/js/Pages/Collection/SavedCollections.jsx": [], "resources/js/Pages/Community/CommunityActivityPage.jsx": [], "resources/js/Pages/Community/LatestCommentsPage.jsx": [], + "resources/js/Pages/Enhance/Create.jsx": [], + "resources/js/Pages/Enhance/Index.jsx": [], + "resources/js/Pages/Enhance/Show.jsx": [], "resources/js/Pages/Feed/FollowingFeed.jsx": [], "resources/js/Pages/Feed/HashtagFeed.jsx": [], "resources/js/Pages/Feed/SavedFeed.jsx": [], @@ -2142,6 +2145,8 @@ "resources/js/Pages/Messages/Index.jsx": [], "resources/js/Pages/Moderation/AiBiographyAdmin.jsx": [], "resources/js/Pages/Moderation/ArtworkMaturityQueue.jsx": [], + "resources/js/Pages/Moderation/Enhance/Index.jsx": [], + "resources/js/Pages/Moderation/Enhance/Show.jsx": [], "resources/js/Pages/Moderation/WorldWebStoriesIndex.jsx": [], "resources/js/Pages/Moderation/WorldWebStoryEditor.jsx": [], "resources/js/Pages/News/NewsComments.jsx": [], @@ -2275,6 +2280,9 @@ "resources/js/components/docs/FaqSearchInput.jsx": [], "resources/js/components/docs/QuickstartChecklist.jsx": [], "resources/js/components/docs/QuickstartNextSteps.jsx": [], + "resources/js/components/enhance/BeforeAfterSlider.jsx": [], + "resources/js/components/enhance/EnhanceStatusBadge.jsx": [], + "resources/js/components/enhance/EnhanceStubWarning.jsx": [], "resources/js/components/forum/AuthorBadge.jsx": [], "resources/js/components/forum/Breadcrumbs.jsx": [], "resources/js/components/forum/CategoryCard.jsx": [], @@ -2377,6 +2385,7 @@ "resources/js/components/upload/ScreenshotUploader.jsx": [], "resources/js/components/upload/StudioStatusBar.jsx": [], "resources/js/components/upload/UploadActions.jsx": [], + "resources/js/components/upload/UploadDescriptionEditor.jsx": [], "resources/js/components/upload/UploadDropzone.jsx": [], "resources/js/components/upload/UploadOverlay.jsx": [], "resources/js/components/upload/UploadSidebar.jsx": [], @@ -2457,7 +2466,9 @@ "resources/js/lib/useNavContext.js": [], "resources/js/lib/worldAnalytics.js": [], "resources/js/ssr.jsx": [], + "resources/js/utils/contentValidation.js": [], "resources/js/utils/emojiFlood.js": [], + "resources/js/utils/enhanceFormatting.js": [], "resources/js/utils/flagUrl.js": [], "resources/js/utils/scheduleCountdown.js": [], "resources/js/utils/studioEvents.js": [] diff --git a/bootstrap/ssr/ssr.js b/bootstrap/ssr/ssr.js index 07c95248..debe58a7 100644 --- a/bootstrap/ssr/ssr.js +++ b/bootstrap/ssr/ssr.js @@ -12360,7 +12360,7 @@ function X$1() { return l3; } var At = Ne$1; -const LABELS$1 = { +const LABELS$2 = { free: "Free", creator: "Creator", pro: "Pro", @@ -12374,11 +12374,11 @@ const CLASSES = { }; function AccessBadge({ tier = "free", className = "" }) { const normalizedTier = typeof tier === "string" ? tier.toLowerCase() : "free"; - const label = LABELS$1[normalizedTier] || "Free"; + const label = LABELS$2[normalizedTier] || "Free"; 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 formatDate$f(iso) { +function formatDate$j(iso) { if (!iso) return null; try { return new Date(iso).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" }); @@ -12387,7 +12387,7 @@ function formatDate$f(iso) { } } function AcademyBillingAccount({ currentTier, isSubscribed, subscription, activePlan = null, links = {} }) { - const endsAt = formatDate$f(subscription?.endsAt); + 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" }, /* @__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( @@ -12667,6 +12667,22 @@ function contentViewEventType(contentType) { if (contentType === "academy_challenge") return "academy_challenge_view"; return "academy_content_view"; } +function analyticsMetadata(analytics, extra = {}) { + const metadata = analytics?.metadata && typeof analytics.metadata === "object" ? analytics.metadata : {}; + return { + page_name: analytics?.pageName, + ...metadata, + ...extra + }; +} +function analyticsTrackingKey(analytics) { + if (analytics?.trackingKey) { + return String(analytics.trackingKey); + } + const metadata = analytics?.metadata && typeof analytics.metadata === "object" ? analytics.metadata : {}; + const pairs = Object.entries(metadata).filter(([, value]) => value !== void 0 && value !== null && value !== "").sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}:${String(value)}`); + return pairs.join("|"); +} function trackUpgradeClick(analytics, metadata = {}) { if (!analytics?.eventUrl) { return; @@ -12682,37 +12698,31 @@ function useAcademyPageAnalytics(analytics) { if (!analytics?.enabled || !analytics?.eventUrl || typeof window === "undefined") { return void 0; } - const baseKey = `${analytics.pageName || window.location.pathname}:${analytics.contentType || "page"}:${analytics.contentId || "none"}`; - void trackAcademyEvent("academy_page_view", analytics.contentType || null, analytics.contentId || null, { - page_name: analytics.pageName - }, { + const trackingKey = analyticsTrackingKey(analytics); + const baseKey = `${analytics.pageName || window.location.pathname}:${analytics.contentType || "page"}:${analytics.contentId || "none"}:${trackingKey || "default"}`; + void trackAcademyEvent("academy_page_view", analytics.contentType || null, analytics.contentId || null, analyticsMetadata(analytics), { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:page-view` }); if (analytics.contentType || analytics.contentId) { - void trackAcademyEvent(contentViewEventType(analytics.contentType), analytics.contentType || null, analytics.contentId || null, { - page_name: analytics.pageName - }, { + void trackAcademyEvent(contentViewEventType(analytics.contentType), analytics.contentType || null, analytics.contentId || null, analyticsMetadata(analytics), { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:content-view` }); } if (analytics.isPremium && analytics.isLocked) { - void trackAcademyEvent("academy_premium_preview_view", analytics.contentType || null, analytics.contentId || null, { - page_name: analytics.pageName - }, { + void trackAcademyEvent("academy_premium_preview_view", analytics.contentType || null, analytics.contentId || null, analyticsMetadata(analytics), { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:premium-preview` }); } const engagedTimer = window.setTimeout(() => { - void trackAcademyEvent("academy_engaged_view", analytics.contentType || null, analytics.contentId || null, { - page_name: analytics.pageName, + void trackAcademyEvent("academy_engaged_view", analytics.contentType || null, analytics.contentId || null, analyticsMetadata(analytics, { engaged_seconds: 15 - }, { + }), { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:engaged` @@ -12732,10 +12742,9 @@ function useAcademyPageAnalytics(analytics) { return; } sentMilestones.add(milestone.threshold); - void trackAcademyEvent(milestone.eventType, analytics.contentType || null, analytics.contentId || null, { - page_name: analytics.pageName, + void trackAcademyEvent(milestone.eventType, analytics.contentType || null, analytics.contentId || null, analyticsMetadata(analytics, { scroll_percent: milestone.threshold - }, { + }), { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:scroll-${milestone.threshold}` @@ -12747,7 +12756,7 @@ function useAcademyPageAnalytics(analytics) { window.clearTimeout(engagedTimer); window.removeEventListener("scroll", onScroll); }; - }, [analytics?.contentId, analytics?.contentType, analytics?.enabled, analytics?.eventUrl, analytics?.isLocked, analytics?.isPremium, analytics?.pageName]); + }, [analytics?.contentId, analytics?.contentType, analytics?.enabled, analytics?.eventUrl, analytics?.isLocked, analytics?.isPremium, analytics?.pageName, analytics?.trackingKey, JSON.stringify(analytics?.metadata || {})]); } function getCsrfToken$j() { return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || ""; @@ -13201,8 +13210,64 @@ const __vite_glob_0_4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.def __proto__: null, default: AcademyChallengeSubmit }, Symbol.toStringTag, { value: "Module" })); -function CourseCard({ course, variant = "default", analytics = null, searchContext = null, position: position2 = null }) { - const isFeatured = variant === "featured"; +function Breadcrumbs$2({ items = [] }) { + if (!items.length) { + return null; + } + return /* @__PURE__ */ React.createElement("nav", { "aria-label": "Breadcrumb", className: "flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, items.map((item, index2) => { + const isLast = index2 === items.length - 1; + return /* @__PURE__ */ React.createElement(React.Fragment, { key: `${item.label}-${index2}` }, isLast ? /* @__PURE__ */ React.createElement("span", { className: "text-white/80" }, item.label) : /* @__PURE__ */ React.createElement(xe, { href: item.href, className: "transition hover:text-white" }, item.label), !isLast ? /* @__PURE__ */ React.createElement("span", { className: "text-slate-600" }, "/") : null); + })); +} +function formatAccessDate$2(value) { + if (!value) { + return null; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return null; + } + return new Intl.DateTimeFormat(void 0, { + year: "numeric", + month: "short", + day: "numeric" + }).format(parsed); +} +function academyAccessHeading$2(access) { + switch (access?.status) { + case "staff_access": + return "You currently have full staff access to the Academy."; + case "grace_period": + return `${access.tierLabel} access is still active.`; + case "trialing": + return `${access.tierLabel} trial is active right now.`; + case "active": + return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : "Your Academy access is active."; + case "free": + return "You currently have Free access to the Academy."; + default: + return null; + } +} +function academyAccessMeta$2(access) { + if (!access?.signedIn) { + return []; + } + const items = [ + { label: "Current tier", value: access?.tierLabel || "Free" }, + { label: "Status", value: access?.statusLabel || "Free access" } + ]; + const formattedDate = formatAccessDate$2(access?.expiresAt); + if (formattedDate && access?.dateLabel) { + items.push({ label: access.dateLabel, value: formattedDate }); + } else if (access?.renewsAutomatically) { + items.push({ label: "Billing", value: "Renews automatically" }); + } else if (!access?.hasPaidAccess) { + items.push({ label: "Upgrade", value: "Creator and Pro unlock premium lessons and courses" }); + } + return items; +} +function CourseCard({ course, analytics = null, searchContext = null, position: position2 = null }) { const progress = course?.progress || null; const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ""; const trackSearchClick = () => { @@ -13225,18 +13290,31 @@ function CourseCard({ course, variant = "default", analytics = null, searchConte "data-academy-search-query": searchContext?.query || void 0, "data-academy-search-results-count": searchContext?.resultsCount || void 0, "data-academy-search-position": position2 || void 0, - className: [ - "group overflow-hidden rounded-[30px] border border-white/10 transition hover:border-sky-300/25 hover:bg-white/[0.06]", - isFeatured ? "bg-[linear-gradient(135deg,rgba(14,165,233,0.14),rgba(15,23,42,0.92))]" : "bg-white/[0.04]" - ].join(" ") + className: "group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-amber-200/24 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]" }, - /* @__PURE__ */ React.createElement("div", { className: "relative" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: "", "aria-hidden": "true", className: `w-full object-cover ${isFeatured ? "h-56" : "h-44"}` }) : /* @__PURE__ */ React.createElement("div", { className: `w-full bg-[linear-gradient(135deg,rgba(14,165,233,0.22),rgba(15,23,42,0.92))] ${isFeatured ? "h-56" : "h-44"}` }), /* @__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-5 top-5 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, course.difficulty), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-200" }, course.access_level), course.is_featured ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/15 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100" }, "Featured") : null)), - /* @__PURE__ */ React.createElement("div", { className: "p-6" }, /* @__PURE__ */ React.createElement("h2", { className: `font-semibold tracking-[-0.05em] text-white transition group-hover:text-sky-100 ${isFeatured ? "text-3xl" : "text-2xl"}` }, course.title), course.subtitle ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-medium uppercase tracking-[0.18em] text-slate-400" }, course.subtitle) : null, /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-300" }, course.excerpt || course.description || "Structured Academy course."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-3 sm:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500" }, "Lessons"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, course.lessons_count || 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500" }, "Duration"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, course.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible")), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500" }, "Progress"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, progress ? `${progress.percent}%` : "Start fresh")))) + /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, 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-amber-100" }, course.difficulty) : null, course?.access_level ? /* @__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-white" }, course.access_level) : null, course.is_featured ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-300/20 bg-amber-300/15 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100" }, "Featured") : null), /* @__PURE__ */ React.createElement("div", { className: "absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "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-white" }, course?.lessons_count || 0, " lessons"), /* @__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-white" }, course?.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible pace")))), + /* @__PURE__ */ React.createElement("div", { className: "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.22em] text-amber-100/80" }, "Academy course"), course?.subtitle ? /* @__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" }, course.subtitle) : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-amber-50" }, course.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, course.excerpt || course.description || "Structured Academy course."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, progress ? `${progress.percent}% complete` : "Start fresh", course?.access_level ? ` · ${course.access_level}` : "")) ); } -function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, analytics }) { +function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, lessonsUrl, promptLibraryUrl, academyAccess = null, analytics }) { const flash = X$1().props.flash || {}; useAcademyPageAnalytics(analytics); + const breadcrumbs = [ + { label: "Academy", href: "/academy" }, + { label: "Courses", href: "/academy/courses" } + ]; + const visibleItems = Array.isArray(items?.data) ? items.data : []; + const totalCourses = Number(items?.total || items?.data?.length || 0); + featuredCourses.length; + const featuredCourse = featuredCourses.find((course) => course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image) || featuredCourses[0] || visibleItems[0] || null; + const featuredCover = featuredCourse?.cover_image_url || featuredCourse?.teaser_image_url || featuredCourse?.cover_image || featuredCourse?.teaser_image || ""; + const showSignedInAccess = Boolean(academyAccess?.signedIn); + const accessHeading = academyAccessHeading$2(academyAccess); + const accessMeta = academyAccessMeta$2(academyAccess); + const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl; + const primaryActionLabel = useBillingAction ? academyAccess?.status === "grace_period" ? "Renew now" : "Manage billing" : "See plans"; + const primaryActionIcon = useBillingAction ? academyAccess?.status === "grace_period" ? "fa-solid fa-rotate-right" : "fa-solid fa-sliders" : "fa-solid fa-arrow-up-right-from-square"; + const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl; const searchContext = analytics?.search ? { query: analytics.search.query, normalizedQuery: analytics.search.normalizedQuery, @@ -13255,7 +13333,12 @@ function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = { value: "premium", label: "Premium" }, { value: "mixed", label: "Mixed" } ]; - 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(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, description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1400px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.96),rgba(14,165,233,0.12))] p-8 shadow-[0_24px_90px_rgba(2,6,23,0.36)] md:p-10 lg:p-12" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-6" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-6xl" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-5 text-base leading-8 text-slate-300 md:text-lg" }, description)), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: "academy_courses_index_hero" }), className: "rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100" }, "See Academy plans"))), 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, featuredCourses.length ? /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 xl:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]" }, /* @__PURE__ */ React.createElement(CourseCard, { course: featuredCourses[0], variant: "featured", analytics, searchContext, position: 1 }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5" }, featuredCourses.slice(1, 3).map((course, index2) => /* @__PURE__ */ React.createElement(CourseCard, { key: course.id, course, analytics, searchContext, position: index2 + 2 })))) : null, /* @__PURE__ */ React.createElement("section", { className: "grid gap-3 rounded-[30px] border border-white/10 bg-black/20 p-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement( + const handlePrimaryAction = () => { + if (!useBillingAction) { + trackUpgradeClick(analytics, { source: "academy_courses_index_hero_primary" }); + } + }; + 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(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, description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1400px] space-y-6" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[40px] border border-amber-200/12 bg-[linear-gradient(155deg,rgba(251,191,36,0.14),rgba(15,23,42,0.96)_36%,rgba(14,165,233,0.14))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(253,230,138,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-75" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-12 h-36 w-36 rounded-full bg-amber-300/18 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-sky-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement(Breadcrumbs$2, { items: breadcrumbs }), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-200/18 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-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" }, "Courses"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, totalCourses, " guided paths")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("h1", { className: "max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95" }, description)), /* @__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-amber-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-route" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-3 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75" }, "Library"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, totalCourses, " guided courses")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75" }, "Focus"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, "Sequenced learning + tracked completion"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Structured progression"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Tracked completion"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Reusable lesson paths")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: lessonsUrl || "/academy/lessons", className: "inline-flex items-center gap-2 rounded-full border border-amber-200/26 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:border-amber-200/36 hover:bg-amber-300/18" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader text-xs" }), "Browse lessons"), /* @__PURE__ */ React.createElement(xe, { href: promptLibraryUrl || "/academy/prompts", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-wand-magic-sparkles text-xs" }), "Prompt library"), /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-sky-200/20 bg-sky-300/10 px-5 py-3 text-sm font-semibold text-sky-50 transition hover:border-sky-200/30 hover:bg-sky-300/16" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] 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.2em] text-slate-400" }, "Featured course"), featuredCourse?.difficulty ? /* @__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-white/80" }, featuredCourse.difficulty) : null), /* @__PURE__ */ React.createElement(xe, { href: featuredCourse?.public_url || "#academy-courses-grid", className: "group mt-4 block overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04] transition hover:border-amber-200/24 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]" }, featuredCover ? /* @__PURE__ */ React.createElement("img", { src: featuredCover, alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.78))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, featuredCourse?.access_level ? /* @__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-amber-100" }, featuredCourse.access_level) : null, featuredCourse?.is_featured ? /* @__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-white" }, "Spotlight") : null)), /* @__PURE__ */ React.createElement("div", { className: "p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, featuredCourse?.subtitle || "Guided learning path"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-amber-50" }, featuredCourse?.title || "Explore courses"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, featuredCourse?.excerpt || featuredCourse?.description || "Open a structured Academy course built from reusable lessons."))))), /* @__PURE__ */ React.createElement("div", { className: "xl:col-span-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-crown text-sm" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80" }, showSignedInAccess ? "Your Academy access" : "Upgrade for full access"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]" }, showSignedInAccess ? accessHeading : "Unlock the full course library, premium lesson paths, and the broader Academy learning track."))), showSignedInAccess ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, accessMeta.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70" }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white md:text-base" }, item.value)))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel), /* @__PURE__ */ React.createElement(xe, { href: lessonsUrl || "/academy/lessons", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader text-xs" }), "Browse lessons")), academyAccess?.status === "grace_period" ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-sky-100/75" }, "Opens billing account to restore renewal before access ends.") : null)))), 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, /* @__PURE__ */ React.createElement("section", { className: "grid gap-3 rounded-[30px] border border-white/10 bg-black/20 p-5 md:grid-cols-2" }, /* @__PURE__ */ React.createElement( NovaSelect, { label: "Difficulty", @@ -13275,7 +13358,7 @@ function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = searchable: false, className: "rounded-2xl bg-white/[0.04]" } - )), (items?.data || []).length === 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400" }, "No published Academy courses matched these filters.") : /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 md:grid-cols-2 xl:grid-cols-3" }, items.data.map((course, index2) => /* @__PURE__ */ React.createElement(CourseCard, { key: course.id, course, analytics, searchContext, position: index2 + 1 }))))); + )), visibleItems.length === 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400" }, "No published Academy courses matched these filters.") : /* @__PURE__ */ React.createElement("section", { id: "academy-courses-grid", className: "grid gap-5 md:grid-cols-2 xl:grid-cols-3" }, visibleItems.map((course, index2) => /* @__PURE__ */ React.createElement(CourseCard, { key: course.id, course, analytics, searchContext, position: index2 + 1 }))))); } const __vite_glob_0_5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -13305,38 +13388,51 @@ function LessonChip({ lesson }) { const isCompleted = Boolean(lesson?.completed); const readingMinutes = Number(lesson?.reading_minutes || 0); const ctaLabel = isCompleted ? "Review lesson" : "Open lesson"; + const difficultyLabel = lesson?.difficulty || "lesson"; + const accessLabel = lesson?.access_level || "free"; + const lessonTypeLabel = lesson?.lesson_type || "article"; + const statusLabel2 = isCompleted ? "Completed" : lesson?.is_required ? "Required next" : "Optional read"; + const supportCopy = isCompleted ? "You already finished this lesson." : lesson?.is_required ? "Recommended as the next required step in this course." : "Optional depth you can take at your own pace."; return /* @__PURE__ */ React.createElement( xe, { href: lesson.course_url || `/academy/lessons/${lesson.slug}`, className: [ - "group relative overflow-hidden rounded-[32px] border bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.64))] shadow-[0_24px_50px_rgba(2,6,23,0.2)] transition duration-200 hover:-translate-y-1 hover:border-sky-300/30 hover:shadow-[0_28px_70px_rgba(14,165,233,0.12)]", + "group relative overflow-hidden rounded-[34px] border bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(15,23,42,0.78))] shadow-[0_24px_54px_rgba(2,6,23,0.22)] transition duration-200 hover:-translate-y-1 hover:border-sky-300/30 hover:shadow-[0_28px_74px_rgba(14,165,233,0.16)]", isCompleted ? "border-emerald-300/25" : "border-white/10" ].join(" ") }, - /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.09),transparent_24%),linear-gradient(135deg,transparent,rgba(255,255,255,0.02))] opacity-70 transition duration-200 group-hover:opacity-100" }), - /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-0 lg:grid-cols-[172px_minmax(0,1fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "relative border-b border-white/10 bg-slate-950 lg:border-b-0 lg:border-r" }, thumbnail ? /* @__PURE__ */ React.createElement("img", { src: thumbnail, alt: "", "aria-hidden": "true", className: "h-40 w-full object-cover lg:h-full" }) : /* @__PURE__ */ React.createElement("div", { className: "h-40 w-full bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] lg:h-full" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.04),rgba(2,6,23,0.84))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-3 top-3 flex items-start justify-between gap-3" }, lesson.is_required ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/35 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/80 backdrop-blur" }, "Required") : /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/35 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/65 backdrop-blur" }, "Optional"), isCompleted ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-emerald-300/25 bg-emerald-300/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100 backdrop-blur" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", className: "h-3.5 w-3.5" }, /* @__PURE__ */ React.createElement("circle", { cx: "8", cy: "8", r: "7", stroke: "currentColor", strokeWidth: "1.2" }), /* @__PURE__ */ React.createElement("path", { d: "M4.75 8.2 7 10.4l4.25-4.8", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" })), "Done") : null), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-3 bottom-3 flex items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, stepLabel ? /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.24em] text-sky-100/80" }, stepLabel) : null, stepNumber > 0 ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-5xl font-semibold tracking-[-0.1em] text-white" }, String(stepNumber).padStart(2, "0")) : null))), /* @__PURE__ */ React.createElement("div", { className: "p-5 md:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 xl:grid-cols-[minmax(0,1fr)_200px] xl:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2.5" }, stepLabel ? /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, stepLabel) : null, lesson.formatted_lesson_number ? /* @__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-400" }, lesson.formatted_lesson_number) : null, /* @__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-400" }, lesson.difficulty || "lesson"), /* @__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-400" }, lesson.access_level || "free"), readingMinutes > 0 ? /* @__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-400" }, readingMinutes, " min") : null), /* @__PURE__ */ React.createElement("h3", { className: "mt-3 max-w-3xl text-[1.65rem] font-semibold tracking-[-0.05em] text-white transition group-hover:text-sky-100" }, lesson.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, isCompleted ? "You already finished this lesson." : "Follow this step next in the course path."), /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm leading-7 text-slate-300" }, lesson.excerpt || lesson.content_preview || "Open this lesson inside the course.")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5" }, lesson.lesson_type || "article"), lesson.category_name ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5" }, lesson.category_name) : null)), /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col justify-between gap-4 xl:border-l xl:border-white/10 xl:pl-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-3 xl:grid-cols-1" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Status"), /* @__PURE__ */ React.createElement("p", { className: `mt-2 text-sm font-semibold ${isCompleted ? "text-emerald-100" : "text-white"}` }, isCompleted ? "Completed" : "Up next")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Access"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, lesson.access_level || "Free")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-4 py-3" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Read time"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, readingMinutes > 0 ? `${readingMinutes} min` : "Quick read"))), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 xl:justify-end" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.16em] text-slate-500" }, "Continue path"), /* @__PURE__ */ React.createElement("span", { 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 transition group-hover:border-sky-300/35 group-hover:bg-sky-300/14 group-hover:text-white" }, ctaLabel, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", className: "h-4 w-4" }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 8h9m0 0-3.5-3.5M12.5 8l-3.5 3.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" })))))))) + /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.12),transparent_24%),radial-gradient(circle_at_bottom_left,rgba(251,191,36,0.08),transparent_28%),linear-gradient(135deg,transparent,rgba(255,255,255,0.02))] opacity-80 transition duration-200 group-hover:opacity-100" }), + /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(125,211,252,0.42),transparent)]" }), + /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-0 lg:grid-cols-[188px_minmax(0,1fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "relative border-b border-white/10 bg-slate-950/90 lg:border-b-0 lg:border-r lg:border-white/10" }, thumbnail ? /* @__PURE__ */ React.createElement("img", { src: thumbnail, alt: "", "aria-hidden": "true", className: "h-44 w-full object-cover lg:h-full" }) : /* @__PURE__ */ React.createElement("div", { className: "h-44 w-full bg-[linear-gradient(135deg,rgba(56,189,248,0.2),rgba(15,23,42,0.96))] lg:h-full" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.08),rgba(2,6,23,0.42)_42%,rgba(2,6,23,0.9))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-4 top-4 flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] backdrop-blur ${lesson.is_required ? "border-white/10 bg-black/40 text-white/82" : "border-white/10 bg-black/30 text-white/62"}` }, lesson.is_required ? "Required" : "Optional"), isCompleted ? /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-emerald-300/25 bg-emerald-300/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100 backdrop-blur" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", className: "h-3.5 w-3.5" }, /* @__PURE__ */ React.createElement("circle", { cx: "8", cy: "8", r: "7", stroke: "currentColor", strokeWidth: "1.2" }), /* @__PURE__ */ React.createElement("path", { d: "M4.75 8.2 7 10.4l4.25-4.8", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" })), "Done") : null), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-4 bottom-4 flex items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/30 px-3 py-2 backdrop-blur-sm" }, stepLabel ? /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.24em] text-sky-100/80" }, stepLabel) : null, stepNumber > 0 ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-5xl font-semibold tracking-[-0.1em] text-white" }, String(stepNumber).padStart(2, "0")) : null, !stepNumber && lesson.formatted_lesson_number ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm font-semibold uppercase tracking-[0.16em] text-white/80" }, lesson.formatted_lesson_number) : null))), /* @__PURE__ */ React.createElement("div", { className: "p-5 md:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 xl:grid-cols-[minmax(0,1fr)_240px] xl:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2.5" }, stepLabel ? /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, stepLabel) : null, lesson.formatted_lesson_number ? /* @__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-400" }, lesson.formatted_lesson_number) : null, /* @__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-400" }, difficultyLabel), /* @__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-400" }, accessLabel), readingMinutes > 0 ? /* @__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-400" }, readingMinutes, " min") : null), /* @__PURE__ */ React.createElement("h3", { className: "mt-3 max-w-3xl text-[1.65rem] font-semibold tracking-[-0.05em] text-white transition group-hover:text-sky-100" }, lesson.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-400" }, supportCopy), /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,rgba(2,6,23,0.36),rgba(2,6,23,0.18))] px-4 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm leading-7 text-slate-300" }, lesson.excerpt || lesson.content_preview || "Open this lesson inside the course.")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap items-center gap-2.5 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5" }, lessonTypeLabel), lesson.category_name ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5" }, lesson.category_name) : null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Course flow"))), /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col justify-between gap-4 xl:border-l xl:border-white/10 xl:pl-6" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500" }, "Lesson path"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Status"), /* @__PURE__ */ React.createElement("span", { className: `text-sm font-semibold ${isCompleted ? "text-emerald-100" : "text-white"}` }, statusLabel2)), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Access"), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, accessLabel)), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Read time"), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, readingMinutes > 0 ? `${readingMinutes} min` : "Quick read")))), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-3 xl:justify-end" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-[0.16em] text-slate-500" }, "Continue path"), /* @__PURE__ */ React.createElement("span", { 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 transition group-hover:border-sky-300/35 group-hover:bg-sky-300/14 group-hover:text-white" }, ctaLabel, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", className: "h-4 w-4" }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 8h9m0 0-3.5-3.5M12.5 8l-3.5 3.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" })))))))) ); } function SectionBlock({ section, isActive = false }) { if (!section?.is_visible) return null; - return /* @__PURE__ */ React.createElement("section", { className: `rounded-[32px] border p-6 transition md:p-7 ${isActive ? "border-sky-300/25 bg-[linear-gradient(180deg,rgba(14,165,233,0.08),rgba(255,255,255,0.04))] shadow-[0_22px_50px_rgba(14,165,233,0.08)]" : "border-white/10 bg-white/[0.04]"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap 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("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Course section"), /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${isActive ? "border-sky-300/20 bg-sky-300/12 text-sky-100" : "border-white/10 bg-black/20 text-slate-300"}` }, section.order_num + 1)), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, section.title), section.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-7 text-slate-300" }, section.description) : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-300" }, section.lessons?.length || 0, " lessons"), isActive ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Reading now") : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-6" }, (section.lessons || []).map((lesson) => /* @__PURE__ */ React.createElement(LessonChip, { key: lesson.course_lesson_id || lesson.id, lesson })))); + const lessonCount = section.lessons?.length || 0; + const requiredCount = (section.lessons || []).filter((lesson) => lesson?.is_required).length; + return /* @__PURE__ */ React.createElement("section", { className: `relative overflow-hidden rounded-[34px] border p-6 transition md:p-7 ${isActive ? "border-sky-300/25 bg-[linear-gradient(180deg,rgba(14,165,233,0.08),rgba(255,255,255,0.04))] shadow-[0_24px_56px_rgba(14,165,233,0.08)]" : "border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"}` }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.08),transparent_28%),linear-gradient(135deg,transparent,rgba(255,255,255,0.015))] opacity-80" }), /* @__PURE__ */ React.createElement("div", { className: "relative flex flex-wrap items-start justify-between gap-5" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2.5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Course section"), /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${isActive ? "border-sky-300/20 bg-sky-300/12 text-sky-100" : "border-white/10 bg-black/20 text-slate-300"}` }, section.order_num + 1), requiredCount > 0 ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300" }, requiredCount, " required") : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.05em] text-white md:text-[2rem]" }, section.title), section.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-7 text-slate-300" }, section.description) : null, /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, lessonCount, " lessons mapped in this section")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-300" }, lessonCount, " lessons"), isActive ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100" }, "Reading now") : null)), /* @__PURE__ */ React.createElement("div", { className: "relative mt-6 space-y-6" }, (section.lessons || []).map((lesson) => /* @__PURE__ */ React.createElement(LessonChip, { key: lesson.course_lesson_id || lesson.id, lesson })))); } function AcademyCoursesShow({ seo, course, sections = [], unsectionedLessons = [], pricingUrl, startUrl = null, interaction = null, interactionRoutes = null, loginUrl = null, analytics = null }) { const flash = X$1().props.flash || {}; useAcademyPageAnalytics(analytics); const cover = course?.cover_image_url || course?.cover_image || course?.teaser_image_url || course?.teaser_image || ""; + const heroBackground = course?.teaser_image_url || course?.teaser_image || course?.cover_image_url || course?.cover_image || ""; const progress = course?.progress || null; const [liked, setLiked] = reactExports.useState(Boolean(interaction?.liked)); const [saved, setSaved] = reactExports.useState(Boolean(interaction?.saved)); const [likesCount, setLikesCount] = reactExports.useState(Number(interaction?.likes_count || 0)); const [savesCount, setSavesCount] = reactExports.useState(Number(interaction?.saves_count || 0)); + const visibleSections = sections.filter((section) => section?.is_visible); + const totalLessons = Number(course?.lessons_count || unsectionedLessons.length + visibleSections.reduce((sum, section) => sum + (section.lessons || []).length, 0)); + const totalSections = visibleSections.length + (unsectionedLessons.length ? 1 : 0); + const estimatedMinutes = course?.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible pace"; const sectionJumpItems = reactExports.useMemo( () => [ ...unsectionedLessons.length ? [{ id: "course-outline-core", label: "Core lessons", count: unsectionedLessons.length }] : [], - ...sections.filter((section) => section?.is_visible).map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })) + ...visibleSections.map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })) ], - [sections, unsectionedLessons] + [unsectionedLessons, visibleSections] ); const [activeJumpId, setActiveJumpId] = reactExports.useState(sectionJumpItems[0]?.id || null); const breadcrumbs = [ @@ -13409,7 +13505,7 @@ function AcademyCoursesShow({ seo, course, sections = [], unsectionedLessons = [ setSavesCount(Number(payload.saves_count || 0)); } }; - 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(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: course?.title, description: course?.excerpt || course?.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1400px] 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, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[40px] border border-white/10 bg-black/20 shadow-[0_24px_90px_rgba(2,6,23,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-0 xl:grid-cols-[minmax(0,1.2fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden p-6 md:p-8 lg:p-10 xl:p-12" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: "", "aria-hidden": "true", className: "absolute inset-0 h-full w-full object-cover opacity-[0.18]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(125,211,252,0.18),_transparent_28%),radial-gradient(circle_at_78%_26%,_rgba(251,191,36,0.12),_transparent_20%),linear-gradient(135deg,_rgba(2,6,23,0.98),_rgba(15,23,42,0.85))]" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-5xl" }, /* @__PURE__ */ React.createElement(CourseBreadcrumbs, { items: breadcrumbs }), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap items-center gap-2.5" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100" }, "Academy course"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, course?.difficulty), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, course?.access_level), progress?.percent ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-emerald-300/20 bg-emerald-300/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-100" }, progress.percent, "% complete") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("h1", { className: "text-4xl font-semibold tracking-[-0.06em] text-white md:text-5xl lg:text-[3.75rem]" }, course?.title), course?.subtitle ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm font-semibold uppercase tracking-[0.24em] text-amber-100/90" }, course.subtitle) : null, /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg" }, course?.excerpt || course?.description), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: startCourse, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, progress?.percent ? "Continue course" : "Start course"), /* @__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-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]" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: "academy_course_header" }), className: "rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100" }, "See plans")), /* @__PURE__ */ React.createElement("div", { className: "mt-7 overflow-hidden rounded-[32px] border border-white/10 bg-slate-950/80 shadow-[0_24px_60px_rgba(2,6,23,0.32)]" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: "", "aria-hidden": "true", className: "w-full object-contain" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-[360px] items-center justify-center bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] px-6 text-center text-sm text-slate-400" }, "No course cover image yet"))))), /* @__PURE__ */ React.createElement("aside", { className: "border-t border-white/10 bg-white/[0.03] p-6 xl:border-l xl:border-t-0 xl:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4 xl:sticky xl:top-6" }, /* @__PURE__ */ React.createElement(ProgressMeter, { progress }), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Jump through the course"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, sectionJumpItems.length ? sectionJumpItems.map((item) => /* @__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(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: course?.title, description: course?.excerpt || course?.description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1400px] 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, /* @__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(2,6,23,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 xl:grid-cols-[minmax(0,1fr)_360px] xl:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, heroBackground ? /* @__PURE__ */ React.createElement("img", { src: heroBackground, alt: "", "aria-hidden": "true", className: "absolute inset-0 h-full w-full object-cover opacity-[0.08]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-5xl" }, /* @__PURE__ */ React.createElement(CourseBreadcrumbs, { items: breadcrumbs }), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap items-center gap-2.5" }, /* @__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.22em] text-sky-50/90" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-200" }, "Course path"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, course?.difficulty), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, course?.access_level), progress?.percent ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-emerald-300/20 bg-emerald-300/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-100" }, progress.percent, "% complete") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, course?.subtitle ? /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold uppercase tracking-[0.24em] text-amber-100/90" }, course.subtitle) : null, /* @__PURE__ */ React.createElement("h1", { className: "mt-4 text-4xl font-semibold tracking-[-0.06em] text-white md:text-5xl lg:text-[3.9rem]" }, course?.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95 md:text-lg" }, course?.excerpt || course?.description)), /* @__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-route" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: startCourse, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, progress?.percent ? "Continue course" : "Start course"), /* @__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-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]" }, saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: "academy_course_header" }), className: "rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100" }, "See plans")), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid gap-3 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75" }, "Library"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, totalLessons, " lessons")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75" }, "Structure"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, totalSections, " sections")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75" }, "Pace"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, estimatedMinutes)), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75" }, "Status"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold text-white" }, progress?.percent ? `${progress.percent}% complete` : "Ready to start"))))), /* @__PURE__ */ React.createElement("aside", { className: "grid gap-4 self-start xl:pt-2" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[30px] border border-white/10 bg-black/20 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm" }, cover ? /* @__PURE__ */ React.createElement("img", { src: cover, alt: course?.title, className: "h-56 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-56 items-center justify-center bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] px-6 text-center text-sm text-slate-400" }, "No course cover image yet")), /* @__PURE__ */ React.createElement(ProgressMeter, { progress }), /* @__PURE__ */ React.createElement("div", { className: "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("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Jump through the course"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-2" }, sectionJumpItems.length ? sectionJumpItems.map((item) => /* @__PURE__ */ React.createElement( "a", { key: item.id, @@ -13419,7 +13515,7 @@ function AcademyCoursesShow({ seo, course, sections = [], unsectionedLessons = [ }, /* @__PURE__ */ React.createElement("span", { className: "font-medium" }, item.label), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400" }, item.count) - )) : /* @__PURE__ */ React.createElement("p", { className: "rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400" }, "No course outline items are available yet."))))))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, unsectionedLessons.length ? /* @__PURE__ */ React.createElement( + )) : /* @__PURE__ */ React.createElement("p", { className: "rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400" }, "No course outline items are available yet.")))))), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, unsectionedLessons.length ? /* @__PURE__ */ React.createElement( SectionBlock, { section: { @@ -13440,15 +13536,278 @@ const __vite_glob_0_6 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.def function academyHref$1(section, slug) { return `/academy/${section}/${encodeURIComponent(slug)}`; } -function FeatureCard({ title, description, href, cta: cta2 }) { - return /* @__PURE__ */ React.createElement(xe, { href, className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6 transition hover:border-white/20 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80" }, "Academy"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, description), /* @__PURE__ */ React.createElement("span", { className: "mt-5 inline-flex rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2 text-sm font-semibold text-sky-100" }, cta2)); +function formatStatValue(value, singular, plural = `${singular}s`) { + const numericValue = Number(value || 0); + return `${numericValue.toLocaleString()} ${numericValue === 1 ? singular : plural}`; +} +function FeatureCard({ title, description, href, cta: cta2, icon, eyebrow, highlights = [], tags = [], meta, theme }) { + return /* @__PURE__ */ React.createElement(xe, { href, className: `group relative overflow-hidden rounded-[32px] border p-6 shadow-[0_24px_80px_rgba(2,6,23,0.22)] transition hover:-translate-y-1 hover:shadow-[0_30px_95px_rgba(2,6,23,0.32)] ${theme.shell}` }, /* @__PURE__ */ React.createElement("div", { className: `absolute inset-0 ${theme.backdrop}` }), /* @__PURE__ */ React.createElement("div", { className: `absolute inset-0 opacity-60 ${theme.pattern}` }), /* @__PURE__ */ React.createElement("div", { className: `absolute -right-14 top-6 h-32 w-32 rounded-full blur-3xl ${theme.glow}` }), /* @__PURE__ */ React.createElement("div", { className: "relative flex min-h-[290px] flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: `text-[11px] font-semibold uppercase tracking-[0.24em] ${theme.eyebrow}` }, eyebrow), /* @__PURE__ */ React.createElement("h2", { className: "mt-4 text-[2rem] font-semibold tracking-[-0.05em] text-white" }, title)), /* @__PURE__ */ React.createElement("span", { className: `flex h-14 w-14 shrink-0 items-center justify-center rounded-[18px] border text-lg shadow-[0_14px_34px_rgba(2,6,23,0.28)] transition group-hover:scale-105 ${theme.iconWrap}` }, /* @__PURE__ */ React.createElement("i", { className: icon }))), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-[34ch] text-sm leading-7 text-slate-200/95" }, description), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-3 sm:grid-cols-2" }, highlights.map((item) => /* @__PURE__ */ React.createElement("div", { key: `${title}-${item.label}`, className: `rounded-[22px] border px-4 py-3 backdrop-blur-sm ${theme.highlightCard}` }, /* @__PURE__ */ React.createElement("p", { className: `text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.highlightLabel}` }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm font-semibold text-white" }, item.value)))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, tags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: `${title}-${tag}`, className: `rounded-full border px-3 py-1 text-[11px] font-semibold tracking-[0.12em] ${theme.tag}` }, tag))), /* @__PURE__ */ React.createElement("div", { className: "mt-auto flex items-center justify-between gap-4 pt-6" }, /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-4 py-2 text-sm font-semibold transition group-hover:translate-x-1 ${theme.cta}` }, cta2), /* @__PURE__ */ React.createElement("span", { className: `text-right text-[11px] font-semibold uppercase tracking-[0.2em] ${theme.meta}` }, meta)))); +} +function FeatureRailCard({ eyebrow, title, description, icon, items = [], emptyText, actionHref = null, actionLabel = null, theme, renderItem }) { + return /* @__PURE__ */ React.createElement("section", { className: `relative overflow-hidden rounded-[30px] border p-6 shadow-[0_22px_70px_rgba(2,6,23,0.26)] ${theme.shell}` }, /* @__PURE__ */ React.createElement("div", { className: `absolute inset-0 ${theme.backdrop}` }), /* @__PURE__ */ React.createElement("div", { className: `absolute inset-0 opacity-60 ${theme.pattern}` }), /* @__PURE__ */ React.createElement("div", { className: `absolute right-0 top-0 h-28 w-28 translate-x-8 -translate-y-6 rounded-full blur-3xl ${theme.glow}` }), /* @__PURE__ */ React.createElement("div", { className: "relative" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-[30ch]" }, /* @__PURE__ */ React.createElement("p", { className: `text-[11px] font-semibold uppercase tracking-[0.24em] ${theme.eyebrow}` }, eyebrow), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex items-center gap-3" }, /* @__PURE__ */ React.createElement("span", { className: `flex h-11 w-11 items-center justify-center rounded-[16px] border text-sm ${theme.iconWrap}` }, /* @__PURE__ */ React.createElement("i", { className: icon })), /* @__PURE__ */ React.createElement("h3", { className: "text-2xl font-semibold tracking-[-0.04em] text-white" }, title)), /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-sm leading-7 text-slate-200/92" }, description)), actionHref && actionLabel ? /* @__PURE__ */ React.createElement(xe, { href: actionHref, className: `inline-flex shrink-0 rounded-full border px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.18em] transition ${theme.action}` }, actionLabel) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-3" }, items.length > 0 ? items.map((item, index2) => renderItem(item, index2)) : /* @__PURE__ */ React.createElement("div", { className: `rounded-[22px] border px-4 py-4 text-sm ${theme.empty}` }, emptyText)))); +} +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 overflow-hidden rounded-[28px] border border-white/10 bg-white/[0.04] transition hover:border-sky-300/25 hover:bg-white/[0.06]" }, /* @__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: "p-5" }, /* @__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("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, course.lessons_count || 0, " lessons · ", course.estimated_minutes ? `${course.estimated_minutes} min` : "Flexible duration"))); + 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")))); } -function AcademyIndex({ seo, pricingUrl, links, featureFlags, stats, featuredCourses, featuredLessons, featuredPrompts, featuredChallenges, analytics }) { +function formatAccessDate$1(value) { + if (!value) { + return null; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return null; + } + return new Intl.DateTimeFormat(void 0, { + year: "numeric", + month: "short", + day: "numeric" + }).format(parsed); +} +function academyAccessHeading$1(access) { + switch (access?.status) { + case "staff_access": + return "You currently have full staff access to the Academy."; + case "grace_period": + return `${access.tierLabel} access is still active.`; + case "trialing": + return `${access.tierLabel} trial is active right now.`; + case "active": + return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : "Your Academy access is active."; + case "free": + return "You currently have Free access to the Academy."; + default: + return "Preview the Academy before you upgrade."; + } +} +function academyAccessMeta$1(access) { + const items = [ + { label: "Current tier", value: access?.tierLabel || "Guest" }, + { label: "Status", value: access?.statusLabel || "Preview access only" } + ]; + const formattedDate = formatAccessDate$1(access?.expiresAt); + if (formattedDate && access?.dateLabel) { + items.push({ label: access.dateLabel, value: formattedDate }); + } else if (access?.renewsAutomatically) { + items.push({ label: "Billing", value: "Renews automatically" }); + } else if (access?.signedIn && !access?.hasPaidAccess) { + items.push({ label: "Upgrade", value: "Creator and Pro unlock premium workflows" }); + } else if (!access?.signedIn) { + items.push({ label: "Upgrade", value: "Sign in to track access and unlock premium content" }); + } + return items; +} +function AcademyIndex({ seo, pricingUrl, academyAccess = null, links, featureFlags, stats, featuredCourses, featuredLessons, featuredPrompts, featuredChallenges, analytics }) { useAcademyPageAnalytics(analytics); + const accessHeading = academyAccessHeading$1(academyAccess); + const accessMeta = academyAccessMeta$1(academyAccess); + const useBillingAction = academyAccess?.signedIn && academyAccess?.hasPaidAccess && academyAccess?.billingUrl; + const accessActionLabel = useBillingAction ? academyAccess?.status === "grace_period" ? "Renew now" : "Manage billing" : "See plans"; + const accessActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl; + const academySections = [ + { + title: "Courses", + description: "Follow guided learning paths that stitch reusable Academy lessons into a clean progression with completion tracking.", + href: links.courses, + cta: "Browse courses", + icon: "fa-solid fa-route", + eyebrow: "Academy paths", + highlights: [ + { label: "Library", value: formatStatValue(stats?.courseCount, "course") }, + { label: "Includes", value: formatStatValue(stats?.lessonCount, "lesson") } + ], + tags: ["Progress tracked", "Learning paths", "Skill ladders"], + meta: "Structured progression", + theme: { + shell: "border-sky-300/18 bg-slate-950/40 hover:border-sky-300/30", + backdrop: "bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_42%,rgba(16,185,129,0.18))]", + pattern: "bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.16),transparent_30%),linear-gradient(125deg,transparent_0%,transparent_45%,rgba(125,211,252,0.08)_45%,rgba(125,211,252,0.08)_52%,transparent_52%,transparent_100%)]", + glow: "bg-sky-300/25", + eyebrow: "text-sky-100/80", + iconWrap: "border-sky-200/20 bg-sky-300/12 text-sky-100", + highlightCard: "border-sky-200/12 bg-slate-950/40", + highlightLabel: "text-sky-100/75", + tag: "border-sky-200/12 bg-sky-300/10 text-sky-100", + cta: "border-sky-300/25 bg-sky-300/12 text-sky-100", + meta: "text-sky-100/75" + } + }, + { + title: "Lessons", + description: "Structured tutorials for prompt writing, cleanup workflows, AI ethics, and Skinbase-native publishing habits.", + href: links.lessons, + cta: "Open lessons", + icon: "fa-solid fa-book-open-reader", + eyebrow: "Focused tutorials", + highlights: [ + { label: "Depth", value: formatStatValue(stats?.lessonCount, "lesson") }, + { label: "Coverage", value: "Prompt craft + workflow cleanup" } + ], + tags: ["Short wins", "Creative habits", "Practical steps"], + meta: "Skill-by-skill learning", + theme: { + shell: "border-amber-300/18 bg-slate-950/40 hover:border-amber-300/30", + backdrop: "bg-[linear-gradient(160deg,rgba(251,191,36,0.18),rgba(15,23,42,0.95)_40%,rgba(249,115,22,0.14))]", + pattern: "bg-[radial-gradient(circle_at_top_right,rgba(253,230,138,0.14),transparent_28%),linear-gradient(180deg,transparent_0%,transparent_54%,rgba(251,191,36,0.08)_54%,rgba(251,191,36,0.08)_58%,transparent_58%,transparent_100%)]", + glow: "bg-amber-300/20", + eyebrow: "text-amber-100/85", + iconWrap: "border-amber-200/20 bg-amber-300/12 text-amber-100", + highlightCard: "border-amber-200/12 bg-slate-950/42", + highlightLabel: "text-amber-100/75", + tag: "border-amber-200/12 bg-amber-300/10 text-amber-100", + cta: "border-amber-300/25 bg-amber-300/12 text-amber-100", + meta: "text-amber-100/75" + } + }, + { + title: "Prompt Library", + description: "Discover reusable prompt templates, locked premium previews, and creator-focused visual workflows.", + href: links.prompts, + cta: "Explore prompts", + icon: "fa-solid fa-wand-magic-sparkles", + eyebrow: "Reusable prompt kits", + highlights: [ + { label: "Templates", value: formatStatValue(stats?.promptCount, "prompt") }, + { label: "Use case", value: "Reusable systems + premium previews" } + ], + tags: ["Fast starts", "Visual workflows", "Copy + adapt"], + meta: "High-speed ideation", + theme: { + shell: "border-rose-300/18 bg-slate-950/40 hover:border-rose-300/30", + backdrop: "bg-[linear-gradient(150deg,rgba(244,63,94,0.16),rgba(15,23,42,0.95)_38%,rgba(45,212,191,0.16))]", + pattern: "bg-[radial-gradient(circle_at_20%_15%,rgba(251,113,133,0.16),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,22px_22px,22px_22px]", + glow: "bg-rose-300/20", + eyebrow: "text-rose-100/85", + iconWrap: "border-rose-200/20 bg-rose-300/12 text-rose-100", + highlightCard: "border-rose-200/12 bg-slate-950/42", + highlightLabel: "text-rose-100/75", + tag: "border-rose-200/12 bg-rose-300/10 text-rose-100", + cta: "border-rose-300/25 bg-rose-300/12 text-rose-100", + meta: "text-rose-100/75" + } + } + ]; + const academyFeatureRails = [ + { + key: "lessons", + eyebrow: "Featured lessons", + title: "Jump-in tutorials", + description: "Shorter Academy pieces for specific prompt problems, cleanup workflows, and publishing habits.", + icon: "fa-solid fa-book-open-reader", + actionHref: links.lessons, + actionLabel: "All lessons", + items: (featuredLessons || []).slice(0, 3), + emptyText: "Featured lessons will appear here when the Academy team highlights a new tutorial.", + theme: { + shell: "border-amber-300/16 bg-slate-950/45", + backdrop: "bg-[linear-gradient(160deg,rgba(251,191,36,0.15),rgba(15,23,42,0.96)_42%,rgba(249,115,22,0.14))]", + pattern: "bg-[radial-gradient(circle_at_top_right,rgba(253,230,138,0.12),transparent_24%),linear-gradient(180deg,transparent_0%,transparent_52%,rgba(251,191,36,0.08)_52%,rgba(251,191,36,0.08)_56%,transparent_56%,transparent_100%)]", + glow: "bg-amber-300/18", + eyebrow: "text-amber-100/82", + iconWrap: "border-amber-200/20 bg-amber-300/12 text-amber-100", + action: "border-amber-300/22 bg-amber-300/10 text-amber-100 hover:border-amber-300/34 hover:bg-amber-300/16", + item: "border-amber-200/10 bg-slate-950/38 hover:border-amber-200/18 hover:bg-slate-950/52", + itemEyebrow: "text-amber-100/75", + itemMeta: "text-amber-100/70", + empty: "border-amber-200/10 bg-slate-950/30 text-amber-50/80" + }, + renderItem: (item, index2, theme) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("lessons", item.slug), className: `group block rounded-[22px] border px-4 py-4 transition ${theme.item}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: `mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-[10px] font-semibold ${theme.iconWrap}` }, index2 + 1), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("span", { className: `block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}` }, item.lesson_label || "Featured lesson"), /* @__PURE__ */ React.createElement("span", { className: "mt-2 block text-sm font-semibold text-white transition group-hover:text-amber-50" }, item.title), /* @__PURE__ */ React.createElement("span", { className: `mt-2 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}` }, "Practical tutorial")))) + }, + { + key: "prompts", + eyebrow: "Featured prompts", + title: "Reusable prompt packs", + description: "Template-driven prompt entries designed for fast reuse, remixing, and premium workflow previews.", + icon: "fa-solid fa-wand-magic-sparkles", + actionHref: links.promptPopular, + actionLabel: "Top prompts", + items: (featuredPrompts || []).slice(0, 3), + emptyText: "Featured prompts will appear here when reusable prompt templates are promoted on the homepage.", + theme: { + shell: "border-rose-300/16 bg-slate-950/45", + backdrop: "bg-[linear-gradient(155deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_40%,rgba(45,212,191,0.14))]", + pattern: "bg-[radial-gradient(circle_at_20%_18%,rgba(251,113,133,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,22px_22px,22px_22px]", + glow: "bg-rose-300/18", + eyebrow: "text-rose-100/82", + iconWrap: "border-rose-200/20 bg-rose-300/12 text-rose-100", + action: "border-rose-300/22 bg-rose-300/10 text-rose-100 hover:border-rose-300/34 hover:bg-rose-300/16", + item: "border-rose-200/10 bg-slate-950/38 hover:border-rose-200/18 hover:bg-slate-950/52", + itemEyebrow: "text-rose-100/75", + itemMeta: "text-rose-100/70", + empty: "border-rose-200/10 bg-slate-950/30 text-rose-50/80" + }, + renderItem: (item, index2, theme) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("prompts", item.slug), className: `group block rounded-[22px] border px-4 py-4 transition ${theme.item}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("span", { className: `block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}` }, "Prompt template #", index2 + 1), /* @__PURE__ */ React.createElement("span", { className: "mt-2 block text-sm font-semibold text-white transition group-hover:text-rose-50" }, item.title)), /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${theme.iconWrap}` }, "Template")), /* @__PURE__ */ React.createElement("span", { className: `mt-3 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}` }, "Reusable workflow")) + }, + { + key: "challenges", + eyebrow: "Current challenges", + title: "Build around a brief", + description: "Academy challenges turn lessons and prompt systems into practical output with a clear creative objective.", + icon: "fa-solid fa-trophy", + items: (featuredChallenges || []).slice(0, 3), + emptyText: "Current challenges will appear here when the Academy team launches a new guided brief.", + theme: { + shell: "border-emerald-300/16 bg-slate-950/45", + backdrop: "bg-[linear-gradient(155deg,rgba(16,185,129,0.14),rgba(15,23,42,0.96)_42%,rgba(56,189,248,0.12))]", + pattern: "bg-[radial-gradient(circle_at_top_left,rgba(110,231,183,0.14),transparent_24%),linear-gradient(135deg,transparent_0%,transparent_48%,rgba(16,185,129,0.08)_48%,rgba(16,185,129,0.08)_56%,transparent_56%,transparent_100%)]", + glow: "bg-emerald-300/18", + eyebrow: "text-emerald-100/82", + iconWrap: "border-emerald-200/20 bg-emerald-300/12 text-emerald-100", + action: "border-emerald-300/22 bg-emerald-300/10 text-emerald-100 hover:border-emerald-300/34 hover:bg-emerald-300/16", + item: "border-emerald-200/10 bg-slate-950/38 hover:border-emerald-200/18 hover:bg-slate-950/52", + itemEyebrow: "text-emerald-100/75", + itemMeta: "text-emerald-100/70", + empty: "border-emerald-200/10 bg-slate-950/30 text-emerald-50/80" + }, + renderItem: (item, index2, theme) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("challenges", item.slug), className: `group block rounded-[22px] border px-4 py-4 transition ${theme.item}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: `mt-0.5 flex h-8 min-w-8 items-center justify-center rounded-full border px-2 text-[10px] font-semibold ${theme.iconWrap}` }, "#", index2 + 1), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("span", { className: `block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}` }, "Active brief"), /* @__PURE__ */ React.createElement("span", { className: "mt-2 block text-sm font-semibold text-white transition group-hover:text-emerald-50" }, item.title), /* @__PURE__ */ React.createElement("span", { className: `mt-2 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}` }, "Apply what you learned")))) + } + ]; + const handleAccessAction = () => { + if (!useBillingAction) { + trackUpgradeClick(analytics, { source: "academy_home_hero" }); + } + }; + const academyMetrics = [ + { + key: "courses", + label: "Courses", + value: stats?.courseCount || 0, + accent: { + shell: "border-sky-300/14 bg-sky-300/[0.08]", + label: "text-sky-100/78" + } + }, + { + key: "lessons", + label: "Lessons", + value: stats?.lessonCount || 0, + accent: { + shell: "border-amber-300/14 bg-amber-300/[0.08]", + label: "text-amber-100/78" + } + }, + { + key: "prompts", + label: "Prompts", + value: stats?.promptCount || 0, + accent: { + shell: "border-rose-300/14 bg-rose-300/[0.08]", + label: "text-rose-100/78" + } + }, + { + key: "challenges", + label: "Challenges", + value: stats?.challengeCount || 0, + accent: { + shell: "border-emerald-300/14 bg-emerald-300/[0.08]", + label: "text-emerald-100/78" + } + } + ]; const jsonLd = [{ "@context": "https://schema.org", "@type": "WebPage", @@ -13456,7 +13815,22 @@ function AcademyIndex({ seo, pricingUrl, links, featureFlags, stats, featuredCou description: seo?.description, url: seo?.canonical }]; - return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.18),_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: "Skinbase AI Academy", description: seo?.description, jsonLd }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1440px] space-y-8" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.94),rgba(28,25,23,0.88)),radial-gradient(circle_at_top_right,rgba(251,191,36,0.2),transparent_26%)] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.38)] md:p-10 lg:p-12" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1fr)_360px] xl:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-200/80" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("h1", { className: "mt-4 max-w-4xl text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl xl:text-6xl" }, "Learn how to turn prompts into wallpapers, digital art, skins, covers, and visual worlds."), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg" }, "Skinbase AI Academy is the creative learning hub for AI-assisted art on Skinbase. Start with free lessons, explore prompt templates, and unlock premium workflows later."), /* @__PURE__ */ React.createElement("div", { className: "mt-7 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: links.courses, 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" }, "Browse courses"), /* @__PURE__ */ React.createElement(xe, { href: links.lessons, className: "rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-300/40 hover:bg-amber-300/18" }, "Browse lessons"), /* @__PURE__ */ React.createElement(xe, { href: links.prompts, className: "rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]" }, "Open prompt library"), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: "academy_home_hero" }), 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" }, "See plans"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-black/20 p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, "Launch status"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3" }, /* @__PURE__ */ React.createElement("span", null, "Challenges"), /* @__PURE__ */ React.createElement("span", null, featureFlags?.challengesEnabled ? "Enabled" : "Disabled")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3" }, /* @__PURE__ */ React.createElement("span", null, "Badges"), /* @__PURE__ */ React.createElement("span", null, featureFlags?.badgesEnabled ? "Enabled" : "Disabled")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3" }, /* @__PURE__ */ React.createElement("span", null, "Payments"), /* @__PURE__ */ React.createElement("span", null, featureFlags?.paymentsEnabled ? "Preview only" : "Disabled")))))), /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 lg:grid-cols-3" }, /* @__PURE__ */ React.createElement(FeatureCard, { title: "Courses", description: "Follow guided learning paths that stitch reusable Academy lessons into a clean progression with completion tracking.", href: links.courses, cta: "Browse courses" }), /* @__PURE__ */ React.createElement(FeatureCard, { title: "Lessons", description: "Structured tutorials for prompt writing, cleanup workflows, AI ethics, and Skinbase-native publishing habits.", href: links.lessons, cta: "Open lessons" }), /* @__PURE__ */ React.createElement(FeatureCard, { title: "Prompt Library", description: "Discover reusable prompt templates, locked premium previews, and creator-focused visual workflows.", href: links.prompts, cta: "Explore prompts" })), /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 lg:grid-cols-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Courses"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white" }, stats?.courseCount || 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Lessons"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white" }, stats?.lessonCount || 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Prompts"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white" }, stats?.promptCount || 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Challenges"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-4xl font-semibold tracking-[-0.05em] text-white" }, stats?.challengeCount || 0))), featuredCourses?.length ? /* @__PURE__ */ React.createElement("section", { className: "space-y-5" }, /* @__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-slate-400" }, "Featured courses"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-3xl font-semibold tracking-[-0.045em] text-white" }, "Guided Academy paths")), /* @__PURE__ */ React.createElement(xe, { href: links.courses, className: "rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white" }, "All courses")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 xl:grid-cols-3" }, featuredCourses.slice(0, 3).map((course) => /* @__PURE__ */ React.createElement(FeaturedCourseCard, { key: course.id, course })))) : null, /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 xl:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Featured lessons"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (featuredLessons || []).slice(0, 3).map((item) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("lessons", item.slug), className: "block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, /* @__PURE__ */ React.createElement("span", { className: "block text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100" }, item.lesson_label || "Featured lesson"), /* @__PURE__ */ React.createElement("span", { className: "mt-1 block" }, item.title))))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Featured prompts"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (featuredPrompts || []).slice(0, 3).map((item) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("prompts", item.slug), className: "block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, item.title)))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-6" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Current challenges"), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, (featuredChallenges || []).slice(0, 3).map((item) => /* @__PURE__ */ React.createElement(xe, { key: item.id, href: academyHref$1("challenges", item.slug), className: "block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white" }, item.title))))))); + return /* @__PURE__ */ React.createElement("main", { className: "min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.18),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-10" }, /* @__PURE__ */ React.createElement(SeoHead, { seo: seo || {}, title: "Skinbase AI Academy", description: seo?.description, jsonLd }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1440px] space-y-6 md:space-y-8 xl:space-y-10" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.94),rgba(28,25,23,0.88)),radial-gradient(circle_at_top_right,rgba(251,191,36,0.2),transparent_26%)] p-6 shadow-[0_32px_100px_rgba(2,6,23,0.38)] md:p-7 lg:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-center" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-200/80" }, "Skinbase AI Academy"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 max-w-[15ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[16ch] md:text-5xl xl:max-w-[19ch] xl:text-[3.2rem]" }, "Learn how to turn prompts into wallpapers, digital art, skins, covers, and visual worlds."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-7 text-slate-300 md:text-lg md:leading-8" }, "Skinbase AI Academy is the creative learning hub for AI-assisted art on Skinbase. Start with free lessons, explore prompt templates, and unlock premium workflows later."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, /* @__PURE__ */ React.createElement(xe, { href: links.courses, 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" }, "Browse courses"), /* @__PURE__ */ React.createElement(xe, { href: links.lessons, className: "rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-300/40 hover:bg-amber-300/18" }, "Browse lessons"), /* @__PURE__ */ React.createElement(xe, { href: links.prompts, className: "rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]" }, "Open prompt library"), /* @__PURE__ */ React.createElement(xe, { href: links.promptPopular, className: "rounded-full border border-rose-300/25 bg-rose-300/12 px-5 py-3 text-sm font-semibold text-rose-100 transition hover:border-rose-300/40 hover:bg-rose-300/18" }, "Top prompts"), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: "academy_home_hero" }), 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" }, "See plans"))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 md:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "flex h-11 w-11 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-crown text-sm" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100/80" }, "Your Academy access"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-lg font-semibold text-white" }, accessHeading))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3" }, accessMeta.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-200" }, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, item.label), /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, item.value)))), /* @__PURE__ */ React.createElement(xe, { href: accessActionHref, onClick: handleAccessAction, className: "mt-4 inline-flex 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" }, accessActionLabel), academyAccess?.status === "grace_period" ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-sky-100/75" }, "Opens billing account to restore renewal before access ends.") : null))), /* @__PURE__ */ React.createElement("section", { className: "space-y-4 md:space-y-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-3 rounded-[30px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.6),rgba(15,23,42,0.22))] px-5 py-4 shadow-[0_16px_48px_rgba(2,6,23,0.18)] md:flex-row md:items-end md:justify-between md:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-2xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/75" }, "Choose your Academy lane"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.045em] text-white md:text-[2rem]" }, "Start with the format that matches how you learn.")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400" }, /* @__PURE__ */ React.createElement("span", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-sky-300/35 to-transparent md:min-w-24" }), /* @__PURE__ */ React.createElement("span", null, "Courses, lessons, prompts"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 lg:grid-cols-3" }, academySections.map((section) => /* @__PURE__ */ React.createElement(FeatureCard, { key: section.title, ...section })))), /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.82),rgba(15,23,42,0.58)),radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_30%),radial-gradient(circle_at_bottom_right,rgba(251,191,36,0.12),transparent_28%)] p-3 shadow-[0_18px_56px_rgba(2,6,23,0.24)] sm:p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 lg:grid-cols-4" }, academyMetrics.map((metric) => /* @__PURE__ */ React.createElement(MetricCard$2, { key: metric.key, label: metric.label, value: metric.value, accent: metric.accent })))), featuredCourses?.length ? /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[36px] border border-sky-200/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.82),rgba(3,7,18,0.94)),radial-gradient(circle_at_top_left,rgba(14,165,233,0.14),transparent_28%),radial-gradient(circle_at_bottom_right,rgba(16,185,129,0.1),transparent_30%)] p-5 shadow-[0_24px_70px_rgba(2,6,23,0.26)] md:p-6 lg:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[length:28px_28px] opacity-25" }), /* @__PURE__ */ React.createElement("div", { className: "relative space-y-4 md:space-y-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-4 rounded-[28px] border border-white/10 bg-black/15 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-2xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/78" }, "Featured courses"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-3xl font-semibold tracking-[-0.045em] text-white" }, "Guided Academy paths"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-[54ch] text-sm leading-7 text-slate-300" }, "Longer learning paths for people who want a clearer start-to-finish route instead of individual tutorials or standalone prompt templates.")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-full border border-sky-300/18 bg-sky-300/10 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/85" }, formatStatValue(featuredCourses.length, "featured path")), /* @__PURE__ */ React.createElement(xe, { href: links.courses, className: "rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]" }, "All courses"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 xl:grid-cols-3" }, featuredCourses.slice(0, 3).map((course) => /* @__PURE__ */ React.createElement(FeaturedCourseCard, { key: course.id, course }))))) : null, /* @__PURE__ */ React.createElement("section", { className: "grid gap-4 xl:grid-cols-3 xl:gap-5" }, academyFeatureRails.map((rail) => /* @__PURE__ */ React.createElement( + FeatureRailCard, + { + key: rail.key, + eyebrow: rail.eyebrow, + title: rail.title, + description: rail.description, + icon: rail.icon, + items: rail.items, + emptyText: rail.emptyText, + actionHref: rail.actionHref, + actionLabel: rail.actionLabel, + theme: rail.theme, + renderItem: (item, index2) => rail.renderItem(item, index2, rail.theme) + } + ))))); } const __vite_glob_0_7 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -13465,6 +13839,15 @@ const __vite_glob_0_7 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.def function academyHref(section, slug) { return `/academy/${section}/${encodeURIComponent(slug)}`; } +function Breadcrumbs$1({ items = [] }) { + if (!items.length) { + return null; + } + return /* @__PURE__ */ React.createElement("nav", { "aria-label": "Breadcrumb", className: "flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, items.map((item, index2) => { + const isLast = index2 === items.length - 1; + return /* @__PURE__ */ React.createElement(React.Fragment, { key: `${item.label}-${index2}` }, isLast ? /* @__PURE__ */ React.createElement("span", { className: "text-white/80" }, item.label) : /* @__PURE__ */ React.createElement(xe, { href: item.href, className: "transition hover:text-white" }, item.label), !isLast ? /* @__PURE__ */ React.createElement("span", { className: "text-slate-600" }, "/") : null); + })); +} function QueryFilters({ pageType, filters, categories }) { if (pageType !== "lessons" && pageType !== "prompts") { return null; @@ -13538,16 +13921,143 @@ function promptPreviewAsset(item) { srcSet: item?.preview_image_srcset || "" }; } -function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }) { - const featuredImages = (items || []).map((item) => promptPreviewAsset(item)).filter(Boolean).slice(0, 3); - const primaryImage = featuredImages[0] || null; - const supportingImages = featuredImages.slice(1, 3); - return /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[38px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.14),transparent_26%),radial-gradient(circle_at_bottom_right,rgba(255,207,191,0.16),transparent_26%),linear-gradient(135deg,rgba(4,9,18,0.98),rgba(15,23,42,0.92))] p-8 shadow-[0_28px_90px_rgba(2,6,23,0.28)] md:p-10 lg:p-12" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_420px] xl:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-[#ffcfbf]/20 bg-[#ffcfbf]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[#fff0ea]" }, "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-300" }, "Prompt Library")), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 max-w-4xl text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl xl:text-6xl" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg" }, description), /* @__PURE__ */ React.createElement("div", { className: "mt-7 grid gap-3 sm:grid-cols-3" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Visual-first"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, "Preview prompt results before opening the detail page.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Reusable"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, "Templates for wallpapers, covers, worlds, portraits, and more.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, "Comparison-ready"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white" }, "See which prompts include provider-specific notes and outputs."))), /* @__PURE__ */ React.createElement("div", { className: "mt-7 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, "Upgrade preview"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white/85" }, totalCount || 0, " prompts available"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, primaryImage ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-[16/10]" }, /* @__PURE__ */ React.createElement("img", { src: primaryImage.src, srcSet: primaryImage.srcSet || void 0, sizes: "(max-width: 1279px) calc(100vw - 4rem), 420px", alt: "", "aria-hidden": "true", className: "h-full w-full object-cover" })), supportingImages.length ? /* @__PURE__ */ React.createElement("div", { className: `grid gap-3 ${supportingImages.length === 1 ? "grid-cols-1" : "grid-cols-2"}` }, supportingImages.map((image2, index2) => /* @__PURE__ */ React.createElement("div", { key: `${image2.src}-${index2}`, className: "overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-square" }, /* @__PURE__ */ React.createElement("img", { src: image2.src, srcSet: image2.srcSet || void 0, sizes: "(max-width: 1279px) calc(50vw - 2rem), 200px", alt: "", "aria-hidden": "true", className: "h-full w-full object-cover" })))) : null) : /* @__PURE__ */ React.createElement("div", { className: "col-span-2 flex aspect-[16/10] items-center justify-center rounded-[28px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.12),rgba(17,24,39,0.92))] px-8 text-center text-sm font-semibold uppercase tracking-[0.24em] text-slate-300" }, "Prompt preview images will appear here")))); +function lessonPreviewAsset(item) { + const src2 = item?.cover_image_url || item?.article_cover_image_url || item?.cover_image || item?.article_cover_image || ""; + if (!src2) { + return null; + } + return { src: src2 }; +} +function PromptSpotlightCard({ item }) { + const preview = promptPreviewAsset(item); + return /* @__PURE__ */ React.createElement(xe, { href: academyHref("prompts", item.slug), className: "group rounded-[28px] border border-white/10 bg-white/[0.04] p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-[104px_minmax(0,1fr)] sm:items-center" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[22px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))] aspect-square" }, preview ? /* @__PURE__ */ React.createElement("img", { src: preview.src, srcSet: preview.srcSet || void 0, sizes: "104px", alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__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-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100" }, item?.spotlight?.eyebrow || "Prompt pick"), item?.difficulty ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300" }, item.difficulty) : null), /* @__PURE__ */ React.createElement("h3", { className: "mt-3 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-sky-100" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-2 line-clamp-2 text-sm leading-6 text-slate-300" }, item.excerpt || item.prompt_preview || item.description || "Reusable prompt template."), /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("span", null, item?.category?.name || "Academy"), item?.tags?.[0] ? /* @__PURE__ */ React.createElement("span", null, item.tags[0]) : null)))); +} +function PromptDiscoverySection({ id, title, description, items = [], href, ctaLabel }) { + if (!items.length) { + return null; + } + return /* @__PURE__ */ React.createElement("section", { id, className: "rounded-[34px] border border-white/10 bg-black/20 p-6 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.2em] text-sky-200/80" }, "Prompt discovery"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-2xl text-sm leading-7 text-slate-300" }, description)), href && ctaLabel ? /* @__PURE__ */ React.createElement(xe, { href, className: "rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.09]" }, ctaLabel) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 xl:grid-cols-2" }, items.map((item) => /* @__PURE__ */ React.createElement(PromptSpotlightCard, { key: `spotlight-${item.id}`, item })))); +} +function PopularPromptPeriodTabs({ currentPeriod, periods = [] }) { + if (!periods.length) { + return null; + } + return /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3" }, periods.map((period) => /* @__PURE__ */ React.createElement( + xe, + { + key: period.value, + href: period.href, + className: `rounded-2xl border px-4 py-3 text-left transition ${period.active ? "border-sky-200/35 bg-sky-200/15 text-sky-50" : "border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"}` + }, + /* @__PURE__ */ React.createElement("span", { className: "block text-[11px] font-semibold uppercase tracking-[0.18em]" }, period.label), + /* @__PURE__ */ React.createElement("span", { className: "mt-1 block text-xs leading-5 text-inherit/80" }, period.description) + )), currentPeriod?.description ? /* @__PURE__ */ React.createElement("div", { className: "flex items-center rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-xs text-slate-300" }, currentPeriod.description) : null); +} +function formatAccessDate(value) { + if (!value) { + return null; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return null; + } + return new Intl.DateTimeFormat(void 0, { + year: "numeric", + month: "short", + day: "numeric" + }).format(parsed); +} +function academyAccessHeading(access) { + switch (access?.status) { + case "staff_access": + return "You currently have full staff access to the Academy."; + case "grace_period": + return `${access.tierLabel} access is still active.`; + case "trialing": + return `${access.tierLabel} trial is active right now.`; + case "active": + return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : "Your Academy access is active."; + case "free": + return "You currently have Free access to the Academy."; + default: + return null; + } +} +function academyAccessMeta(access) { + if (!access?.signedIn) { + return []; + } + const items = [ + { label: "Current tier", value: access?.tierLabel || "Free" }, + { label: "Status", value: access?.statusLabel || "Free access" } + ]; + const formattedDate = formatAccessDate(access?.expiresAt); + if (formattedDate && access?.dateLabel) { + items.push({ label: access.dateLabel, value: formattedDate }); + } else if (access?.renewsAutomatically) { + items.push({ label: "Billing", value: "Renews automatically" }); + } else if (!access?.hasPaidAccess) { + items.push({ label: "Upgrade", value: "Creator and Pro unlock premium prompts" }); + } + return items; +} +function PromptLibraryHero({ promptView = "library", title, description, items, pricingUrl, coursesUrl, packsUrl, promptPopularUrl, promptLibraryUrl, popularPeriod, popularPeriods = [], totalCount, analytics, hasPopularSection, academyAccess = null }) { + const isPopularView = promptView === "popular"; + const statLabel = isPopularView ? `ranked prompts ${currentPeriodStatSuffix(popularPeriod)}` : "prompts available"; + const showSignedInAccess = Boolean(academyAccess?.signedIn); + const accessHeading = academyAccessHeading(academyAccess); + const accessMeta = academyAccessMeta(academyAccess); + const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl; + const primaryActionLabel = useBillingAction ? academyAccess?.status === "grace_period" ? "Renew now" : "Manage billing" : "Upgrade now"; + const primaryActionIcon = useBillingAction ? academyAccess?.status === "grace_period" ? "fa-solid fa-rotate-right" : "fa-solid fa-sliders" : "fa-solid fa-arrow-up-right-from-square"; + const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl; + const secondaryAction = isPopularView ? { href: promptLibraryUrl, label: "Browse full library", icon: "fa-solid fa-grid-2" } : hasPopularSection ? { href: promptPopularUrl, label: "Top prompts", icon: "fa-solid fa-fire" } : { href: coursesUrl, label: "Explore courses", icon: "fa-solid fa-graduation-cap" }; + const heroHighlights = [ + { + label: isPopularView ? "Ranking window" : "Templates", + value: isPopularView ? `${totalCount || 0} ranked prompts` : `${totalCount || 0} prompts` + }, + { + label: "Use case", + value: isPopularView ? "High-performing systems + trend tracking" : "Reusable systems + premium previews" + } + ]; + const heroTags = isPopularView ? ["Momentum picks", "Copy trends", "Compare windows"] : ["Fast starts", "Visual workflows", "Copy + adapt"]; + const handlePrimaryAction = () => { + if (!useBillingAction) { + trackUpgradeClick(analytics, { source: "prompts_library_hero_primary" }); + } + }; + return /* @__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))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7" }, /* @__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-12 h-36 w-36 rounded-full bg-rose-300/18 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-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-rose-200/18 bg-rose-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] 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-[11px] font-semibold uppercase tracking-[0.24em] text-slate-200" }, isPopularView ? "Popular prompts" : "Prompt Library"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, totalCount || 0, " ", statLabel)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("h1", { className: "max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95" }, description)), /* @__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" }))), isPopularView ? /* @__PURE__ */ React.createElement("div", { className: "mt-5" }, /* @__PURE__ */ React.createElement(PopularPromptPeriodTabs, { currentPeriod: popularPeriod, periods: popularPeriods })) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-3 sm:grid-cols-2" }, heroHighlights.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-rose-100/75" }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold leading-8 text-white" }, item.value)))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, heroTags.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-rose-50/90" }, tag))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-rose-200/26 bg-rose-300/12 px-5 py-3 text-sm font-semibold text-rose-50 transition hover:border-rose-200/36 hover:bg-rose-300/18" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel), /* @__PURE__ */ React.createElement(xe, { href: secondaryAction.href, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: `${secondaryAction.icon} text-xs` }), secondaryAction.label), /* @__PURE__ */ React.createElement(xe, { href: packsUrl, className: "inline-flex items-center gap-2 rounded-full border border-cyan-200/20 bg-cyan-300/10 px-5 py-3 text-sm font-semibold text-cyan-50 transition hover:border-cyan-200/30 hover:bg-cyan-300/16" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-box-open text-xs" }), "See prompt packs"))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] 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.2em] text-slate-400" }, "Quick routes"), /* @__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-white/80" }, totalCount || 0, " total")), /* @__PURE__ */ React.createElement("div", { className: "mt-3 grid gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: isPopularView ? promptLibraryUrl : coursesUrl, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, /* @__PURE__ */ React.createElement("span", null, isPopularView ? "Browse full prompt library" : "Browse Academy courses"), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right text-xs text-slate-400" })), /* @__PURE__ */ React.createElement(xe, { href: packsUrl, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, /* @__PURE__ */ React.createElement("span", null, "See prompt packs"), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-box-open text-xs text-slate-400" })), isPopularView ? /* @__PURE__ */ React.createElement(xe, { href: coursesUrl, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, /* @__PURE__ */ React.createElement("span", null, "Explore Academy courses"), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-graduation-cap text-xs text-slate-400" })) : hasPopularSection ? /* @__PURE__ */ React.createElement(xe, { href: promptPopularUrl, className: "flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]" }, /* @__PURE__ */ React.createElement("span", null, "Open top prompts page"), /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-fire text-xs text-slate-400" })) : null, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-[0.9rem] text-sm font-semibold text-white/85" }, isPopularView ? "Use the period tabs to compare momentum windows." : "Jump straight into packs, courses, or ranked prompts.")))), /* @__PURE__ */ React.createElement("div", { className: "xl:col-span-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-crown text-sm" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80" }, showSignedInAccess ? "Your Academy access" : isPopularView ? "Turn rankings into results" : "Upgrade for full access"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]" }, showSignedInAccess ? accessHeading : isPopularView ? "Open the highest-performing prompts, then unlock the full text, helper prompts, variants, and premium workflows." : "Unlock full prompt text, helper prompts, variants, and premium workflows."))), showSignedInAccess ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, accessMeta.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70" }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white md:text-base" }, item.value)))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel), /* @__PURE__ */ React.createElement(xe, { href: secondaryAction.href, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: `${secondaryAction.icon} text-xs` }), secondaryAction.label)), academyAccess?.status === "grace_period" ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-sky-100/75" }, "Opens billing account to restore renewal before access ends.") : null)))); +} +function LessonsLibraryHero({ title, description, items = [], totalCount, pricingUrl, coursesUrl, promptLibraryUrl, academyAccess = null, analytics }) { + const featuredLesson = items.find((item) => lessonPreviewAsset(item)) || items[0] || null; + const featuredPreview = lessonPreviewAsset(featuredLesson); + const showSignedInAccess = Boolean(academyAccess?.signedIn); + const accessHeading = academyAccessHeading(academyAccess); + const accessMeta = academyAccessMeta(academyAccess); + const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl; + const primaryActionLabel = useBillingAction ? academyAccess?.status === "grace_period" ? "Renew now" : "Manage billing" : "See plans"; + const primaryActionIcon = useBillingAction ? academyAccess?.status === "grace_period" ? "fa-solid fa-rotate-right" : "fa-solid fa-sliders" : "fa-solid fa-arrow-up-right-from-square"; + const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl; + const handlePrimaryAction = () => { + if (!useBillingAction) { + trackUpgradeClick(analytics, { source: "lessons_library_hero_primary" }); + } + }; + return /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden rounded-[40px] border border-amber-200/12 bg-[linear-gradient(155deg,rgba(251,191,36,0.14),rgba(15,23,42,0.96)_36%,rgba(14,165,233,0.14))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(253,230,138,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-75" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -left-8 top-12 h-36 w-36 rounded-full bg-amber-300/18 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-sky-300/14 blur-3xl" }), /* @__PURE__ */ React.createElement("div", { className: "relative grid gap-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-amber-200/18 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-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" }, "Lessons"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300" }, totalCount || 0, " tutorials")), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-4xl" }, /* @__PURE__ */ React.createElement("h1", { className: "max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]" }, title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-3xl text-base leading-8 text-slate-200/95" }, description)), /* @__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-amber-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-book-open-reader" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-3 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75" }, "Library"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold leading-8 text-white" }, totalCount || 0, " structured lessons")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75" }, "Focus"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-lg font-semibold leading-8 text-white" }, "Prompt craft + workflow cleanup"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2.5" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Short wins"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Creative habits"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90" }, "Practical steps")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: coursesUrl, className: "inline-flex items-center gap-2 rounded-full border border-amber-200/26 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:border-amber-200/36 hover:bg-amber-300/18" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-route text-xs" }), "Browse courses"), /* @__PURE__ */ React.createElement(xe, { href: promptLibraryUrl || "/academy/prompts", className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-wand-magic-sparkles text-xs" }), "Prompt library"), /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-sky-200/20 bg-sky-300/10 px-5 py-3 text-sm font-semibold text-sky-50 transition hover:border-sky-200/30 hover:bg-sky-300/16" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] 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.2em] text-slate-400" }, "Latest lesson"), featuredLesson?.difficulty ? /* @__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-white/80" }, featuredLesson.difficulty) : null), /* @__PURE__ */ React.createElement(xe, { href: featuredLesson ? academyHref("lessons", featuredLesson.slug) : coursesUrl, className: "group mt-4 block overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04] transition hover:border-amber-200/24 hover:bg-white/[0.06]" }, /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]" }, featuredPreview ? /* @__PURE__ */ React.createElement("img", { src: featuredPreview.src, alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.78))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, featuredLesson?.formatted_lesson_number ? /* @__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-amber-100" }, featuredLesson.formatted_lesson_number) : null, featuredLesson ? /* @__PURE__ */ React.createElement(LockBadge, { item: featuredLesson }) : null)), /* @__PURE__ */ React.createElement("div", { className: "p-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, String(featuredLesson?.series_name || featuredLesson?.category?.name || "Academy lesson").trim()), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-amber-50" }, featuredLesson?.title || "Explore lessons"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-7 text-slate-300" }, featuredLesson?.excerpt || featuredLesson?.content_preview || featuredLesson?.description || "Open a practical Academy lesson."))))), /* @__PURE__ */ React.createElement("div", { className: "xl:col-span-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React.createElement("span", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-crown text-sm" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80" }, showSignedInAccess ? "Your Academy access" : "Upgrade for full access"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]" }, showSignedInAccess ? accessHeading : "Unlock the full lesson library, premium workflows, and the broader Academy learning track."))), showSignedInAccess ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3" }, accessMeta.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70" }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm font-semibold text-white md:text-base" }, item.value)))) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: primaryActionHref, onClick: handlePrimaryAction, className: "inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22" }, /* @__PURE__ */ React.createElement("i", { className: `${primaryActionIcon} text-xs` }), primaryActionLabel), /* @__PURE__ */ React.createElement(xe, { href: coursesUrl, className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-route text-xs" }), "Browse courses")), academyAccess?.status === "grace_period" ? /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-sky-100/75" }, "Opens billing account to restore renewal before access ends.") : null)))); +} +function currentPeriodStatSuffix(popularPeriod) { + if (!popularPeriod?.label) { + return "this month"; + } + return popularPeriod.label === "30 days" ? "this month" : `for ${popularPeriod.label.toLowerCase()}`; } function AcademyCard({ pageType, item, analytics, searchContext, position: position2 }) { const lessonSeries = String(item?.series_name || "").trim(); const promptPreviewImage = item?.preview_image_thumb || item?.preview_image || ""; const promptPreviewSrcSet = item?.preview_image_srcset || ""; + const lessonPreview = lessonPreviewAsset(item); const contentType = searchResultContentType(pageType); const href = itemHref$1(pageType, item); const trackSearchClick = () => { @@ -13573,8 +14083,25 @@ function AcademyCard({ pageType, item, analytics, searchContext, position: posit "data-academy-search-position": position2 || void 0, className: "group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-sky-300/25 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]" }, - /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/11] overflow-hidden bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))]" }, promptPreviewImage ? /* @__PURE__ */ React.createElement("img", { src: promptPreviewImage, srcSet: promptPreviewSrcSet || void 0, sizes: "(max-width: 767px) calc(100vw - 2rem), (max-width: 1279px) calc(50vw - 2rem), 420px", alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" }), /* @__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-[#fff0ea]" }, "Prompt template"), /* @__PURE__ */ React.createElement(LockBadge, { item })), /* @__PURE__ */ React.createElement("div", { className: "absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, item?.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-white" }, item.difficulty) : null, item?.aspect_ratio ? /* @__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-white" }, item.aspect_ratio) : null))), - /* @__PURE__ */ React.createElement("div", { className: "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.22em] text-sky-200/80" }, item?.category?.name || "Academy"), Array.isArray(item?.tool_notes) && item.tool_notes.length ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, item.tool_notes.length, " comparisons") : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.excerpt || item.description || item.prompt_preview || "No description yet."), item.tags?.length ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, item.tags.slice(0, 4).join(" · ")) : null) + /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/11] overflow-hidden bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))]" }, promptPreviewImage ? /* @__PURE__ */ React.createElement("img", { src: promptPreviewImage, srcSet: promptPreviewSrcSet || void 0, sizes: "(max-width: 767px) calc(100vw - 2rem), (max-width: 1279px) calc(50vw - 2rem), 420px", alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" }), /* @__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-[#fff0ea]" }, item?.ranking?.rank ? `#${item.ranking.rank} this month` : "Prompt template"), /* @__PURE__ */ React.createElement(LockBadge, { item })), /* @__PURE__ */ React.createElement("div", { className: "absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, item?.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-white" }, item.difficulty) : null, item?.aspect_ratio ? /* @__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-white" }, item.aspect_ratio) : null))), + /* @__PURE__ */ React.createElement("div", { className: "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.22em] text-sky-200/80" }, item?.category?.name || "Academy"), Array.isArray(item?.tool_notes) && item.tool_notes.length ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, item.tool_notes.length, " comparisons") : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.excerpt || item.description || item.prompt_preview || "No description yet."), item?.ranking ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-400" }, item.ranking.prompt_copies > 0 ? `${item.ranking.prompt_copies} copies` : `${item.ranking.views} views`, " · popularity ", item.ranking.popularity_score) : null, item.tags?.length ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, item.tags.slice(0, 4).join(" · ")) : null) + ); + } + if (pageType === "lessons") { + return /* @__PURE__ */ React.createElement( + xe, + { + href, + onClick: trackSearchClick, + "data-academy-content-type": contentType || void 0, + "data-academy-content-id": item?.id || void 0, + "data-academy-search-query": searchContext?.query || void 0, + "data-academy-search-results-count": searchContext?.resultsCount || void 0, + "data-academy-search-position": position2 || void 0, + className: "group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-amber-200/24 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]" + }, + /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]" }, lessonPreview ? /* @__PURE__ */ React.createElement("img", { src: lessonPreview.src, alt: "", "aria-hidden": "true", className: "h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" }), /* @__PURE__ */ React.createElement("div", { className: "absolute left-4 top-4 flex flex-wrap gap-2" }, item?.formatted_lesson_number ? /* @__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-amber-100" }, item.formatted_lesson_number) : null, /* @__PURE__ */ React.createElement(LockBadge, { item })), /* @__PURE__ */ React.createElement("div", { className: "absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, item?.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-white" }, item.difficulty) : null, item?.reading_minutes ? /* @__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-white" }, item.reading_minutes, " min") : null))), + /* @__PURE__ */ React.createElement("div", { className: "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.22em] text-amber-100/80" }, item?.category?.name || "Academy lesson"), lessonSeries ? /* @__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" }, lessonSeries) : null), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-amber-50" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.excerpt || item.description || item.content_preview || "No description yet."), item.tags?.length ? /* @__PURE__ */ React.createElement("p", { className: "mt-4 text-xs uppercase tracking-[0.18em] text-slate-500" }, item.tags.slice(0, 4).join(" · ")) : null) ); } return /* @__PURE__ */ React.createElement( @@ -13611,7 +14138,7 @@ async function fetchAcademyPage(url) { } return response.json(); } -function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl, analytics }) { +function AcademyList({ pageType, promptView = "library", title, description, seo, breadcrumbs = [], items, filters, categories, pricingUrl, coursesUrl, packsUrl, promptPopularUrl, promptLibraryUrl, popularPeriod = null, popularPeriods = [], featuredPrompts = [], popularPrompts = [], academyAccess = null, analytics }) { const flash = X$1().props.flash || {}; useAcademyPageAnalytics(analytics); const searchContext = analytics?.search ? { @@ -13630,6 +14157,11 @@ function AcademyList({ pageType, title, description, seo, items, filters, catego }); const [loadingMore, setLoadingMore] = React.useState(false); const sentinelRef = React.useRef(null); + const hasActivePromptFilters = pageType === "prompts" && promptView === "library" && Boolean(filters?.q || filters?.category || filters?.difficulty || filters?.tag); + const showPromptDiscovery = pageType === "prompts" && promptView === "library" && !hasActivePromptFilters; + const showPopularFeatured = pageType === "prompts" && promptView === "popular" && featuredPrompts.length > 0; + const infiniteLoadLabel = pageType === "lessons" ? "lessons" : "prompts"; + const usesInfiniteLoad = pageType === "prompts" && promptView === "library" || pageType === "lessons"; React.useEffect(() => { setVisibleItems(initialItems); setPagination({ @@ -13640,10 +14172,10 @@ function AcademyList({ pageType, title, description, seo, items, filters, catego }); setLoadingMore(false); }, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, items?.prev_page_url, pageType]); - const hasMorePages = pageType === "prompts" && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl); - const hasFallbackPagination = pageType === "prompts" && pagination.lastPage > 1; + const hasMorePages = usesInfiniteLoad && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl); + const hasFallbackPagination = usesInfiniteLoad && pagination.lastPage > 1; const loadMore = React.useCallback(async () => { - if (pageType !== "prompts" || loadingMore || !pagination.nextPageUrl) { + if (!usesInfiniteLoad || loadingMore || !pagination.nextPageUrl) { return; } setLoadingMore(true); @@ -13662,7 +14194,7 @@ function AcademyList({ pageType, title, description, seo, items, filters, catego } finally { setLoadingMore(false); } - }, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl]); + }, [loadingMore, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl, usesInfiniteLoad]); React.useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel || !hasMorePages || loadingMore || typeof window === "undefined" || typeof window.IntersectionObserver !== "function") { @@ -13676,7 +14208,7 @@ function AcademyList({ pageType, title, description, seo, items, filters, catego observer.observe(sentinel); return () => observer.disconnect(); }, [hasMorePages, loadMore, loadingMore]); - 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, description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1360px] space-y-6" }, pageType === "prompts" ? /* @__PURE__ */ React.createElement(PromptLibraryHero, { title, description, items: visibleItems, pricingUrl, totalCount: Number(items?.total || visibleItems.length || 0) }) : /* @__PURE__ */ React.createElement("section", { className: "rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Skinbase AI Academy"), /* @__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-base leading-8 text-slate-300" }, description)), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: `${pageType}_list_hero` }), className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, "Upgrade preview"))), 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, /* @__PURE__ */ React.createElement(QueryFilters, { pageType, filters, categories }), visibleItems.length === 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400" }, "Nothing matched this Academy view yet.") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 md:grid-cols-2 xl:grid-cols-3" }, visibleItems.map((item, index2) => /* @__PURE__ */ React.createElement(AcademyCard, { key: `${pageType}-${item.id}`, pageType, item, analytics, searchContext, position: index2 + 1 }))), pageType === "prompts" ? /* @__PURE__ */ React.createElement("div", { className: "pt-2" }, /* @__PURE__ */ React.createElement("div", { ref: sentinelRef, className: "h-10 w-full", "aria-hidden": "true" }), loadingMore ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-300" }, "Loading more prompts...") : null, !hasMorePages && visibleItems.length > initialItems.length ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-400" }, "You have reached the end of the prompt library.") : null, hasFallbackPagination ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3 rounded-[22px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Auto-load is primary. Pagination is available as a backup."), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, pagination.prevPageUrl ? /* @__PURE__ */ React.createElement(xe, { href: pagination.prevPageUrl, 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("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-300" }, "Page ", pagination.currentPage || 1, " of ", pagination.lastPage || 1), pagination.nextPageUrl ? /* @__PURE__ */ React.createElement(xe, { href: pagination.nextPageUrl, 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) : null))); + 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, description }), /* @__PURE__ */ React.createElement("div", { className: "mx-auto max-w-[1360px] space-y-6" }, pageType === "prompts" || pageType === "lessons" ? /* @__PURE__ */ React.createElement(Breadcrumbs$1, { items: breadcrumbs }) : null, pageType === "prompts" ? /* @__PURE__ */ React.createElement(PromptLibraryHero, { promptView, title, description, items: visibleItems, pricingUrl, coursesUrl, packsUrl, promptPopularUrl, promptLibraryUrl, popularPeriod, popularPeriods, totalCount: Number(items?.total || visibleItems.length || 0), analytics, hasPopularSection: popularPrompts.length > 0, academyAccess }) : pageType === "lessons" ? /* @__PURE__ */ React.createElement(LessonsLibraryHero, { title, description, items: visibleItems, totalCount: Number(items?.total || visibleItems.length || 0), pricingUrl, coursesUrl, promptLibraryUrl, academyAccess, analytics }) : /* @__PURE__ */ React.createElement("section", { className: "rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-end justify-between gap-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Skinbase AI Academy"), /* @__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-base leading-8 text-slate-300" }, description)), /* @__PURE__ */ React.createElement(xe, { href: pricingUrl, onClick: () => trackUpgradeClick(analytics, { source: `${pageType}_list_hero` }), className: "rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100" }, "Upgrade preview"))), 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, promptView === "library" ? /* @__PURE__ */ React.createElement(QueryFilters, { pageType, filters, categories }) : null, showPromptDiscovery ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(PromptDiscoverySection, { id: "popular-prompts", title: "Popular prompts right now", description: "See which prompt templates are getting the most momentum from views and copies this month.", items: popularPrompts, href: promptPopularUrl, ctaLabel: "Open rankings" }), /* @__PURE__ */ React.createElement(PromptDiscoverySection, { id: "featured-prompts", title: "Featured prompt picks", description: "Hand-picked templates worth starting from if you want quick wins for wallpapers, worlds, portraits, and creator-style visuals.", items: featuredPrompts, href: coursesUrl, ctaLabel: "Browse courses" })) : null, showPopularFeatured ? /* @__PURE__ */ React.createElement(PromptDiscoverySection, { id: "featured-prompts", title: "Featured picks to try next", description: "Once you have reviewed the top-performing prompts, jump into a few curated templates that are worth adapting into your own workflow.", items: featuredPrompts, href: promptLibraryUrl, ctaLabel: "Browse full library" }) : null, visibleItems.length === 0 ? /* @__PURE__ */ React.createElement("section", { className: "rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400" }, "Nothing matched this Academy view yet.") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("section", { className: "grid gap-5 md:grid-cols-2 xl:grid-cols-3" }, visibleItems.map((item, index2) => /* @__PURE__ */ React.createElement(AcademyCard, { key: `${pageType}-${item.id}`, pageType, item, analytics, searchContext, position: index2 + 1 }))), usesInfiniteLoad ? /* @__PURE__ */ React.createElement("div", { className: "pt-2" }, /* @__PURE__ */ React.createElement("div", { ref: sentinelRef, className: "h-10 w-full", "aria-hidden": "true" }), loadingMore ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-300" }, "Loading more ", infiniteLoadLabel, "...") : null, !hasMorePages && visibleItems.length > initialItems.length ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-400" }, "You have reached the end of the ", pageType === "lessons" ? "lesson library" : "prompt library", ".") : null, hasFallbackPagination ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3 rounded-[22px] border border-white/10 bg-black/20 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Auto-load is primary. Pagination is available as a backup."), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-3" }, pagination.prevPageUrl ? /* @__PURE__ */ React.createElement(xe, { href: pagination.prevPageUrl, 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("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-300" }, "Page ", pagination.currentPage || 1, " of ", pagination.lastPage || 1), pagination.nextPageUrl ? /* @__PURE__ */ React.createElement(xe, { href: pagination.nextPageUrl, 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) : null))); } const __vite_glob_0_8 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -13923,6 +14455,45 @@ function PromptVariantCard({ variant, analytics, contentId }) { function PromptVariantsSection({ variants, analytics, contentId }) { const visibleVariants = Array.isArray(variants) ? variants.filter((variant) => variant && typeof variant === "object") : []; const [activeVariantKey, setActiveVariantKey] = reactExports.useState(""); + const variantsScrollRef = reactExports.useRef(null); + const [canScrollVariantsLeft, setCanScrollVariantsLeft] = reactExports.useState(false); + const [canScrollVariantsRight, setCanScrollVariantsRight] = reactExports.useState(false); + reactExports.useEffect(() => { + if (typeof window === "undefined") { + return void 0; + } + const updateVariantScrollState = () => { + const element22 = variantsScrollRef.current; + if (!element22) { + setCanScrollVariantsLeft(false); + setCanScrollVariantsRight(false); + return; + } + const maxScrollLeft = Math.max(0, element22.scrollWidth - element22.clientWidth); + setCanScrollVariantsLeft(element22.scrollLeft > 6); + setCanScrollVariantsRight(element22.scrollLeft < maxScrollLeft - 6); + }; + updateVariantScrollState(); + const element2 = variantsScrollRef.current; + if (!element2) { + return void 0; + } + element2.addEventListener("scroll", updateVariantScrollState, { passive: true }); + window.addEventListener("resize", updateVariantScrollState, { passive: true }); + return () => { + element2.removeEventListener("scroll", updateVariantScrollState); + window.removeEventListener("resize", updateVariantScrollState); + }; + }, [visibleVariants.length]); + const scrollVariants = (direction) => { + const element2 = variantsScrollRef.current; + if (!element2) return; + const amount = Math.max(260, Math.floor(element2.clientWidth * 0.7)); + element2.scrollBy({ + left: direction === "left" ? -amount : amount, + behavior: "smooth" + }); + }; reactExports.useEffect(() => { if (!visibleVariants.length) { setActiveVariantKey(""); @@ -13939,26 +14510,53 @@ function PromptVariantsSection({ variants, analytics, contentId }) { }, [visibleVariants]); if (!visibleVariants.length) return null; const activeVariant = visibleVariants.find((variant, index2) => String(variant?.slug || variant?.title || `variant-${index2}`) === activeVariantKey) || visibleVariants[0]; - return /* @__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"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Switch between safer, shorter, or more specialized prompt variants without losing the core creative direction.")), /* @__PURE__ */ React.createElement("div", { className: "mt-6 overflow-x-auto pb-2" }, /* @__PURE__ */ React.createElement("div", { className: "inline-flex min-w-full gap-3", role: "tablist", "aria-label": "Prompt variants" }, visibleVariants.map((variant, index2) => { - const variantKey = String(variant?.slug || variant?.title || `variant-${index2}`); - const isActive = activeVariant === variant; - return /* @__PURE__ */ React.createElement( - "button", - { - key: variantKey, - type: "button", - role: "tab", - "aria-selected": isActive, - onClick: () => setActiveVariantKey(variantKey), - className: [ - "min-w-[220px] rounded-[24px] border px-4 py-3 text-left transition", - isActive ? "border-sky-300/30 bg-sky-300/12 shadow-[0_16px_40px_rgba(2,6,23,0.18)]" : "border-white/10 bg-black/20 hover:border-white/20 hover:bg-white/[0.05]" - ].join(" ") - }, - /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "truncate text-sm font-semibold text-white" }, variant.title || `Variant ${index2 + 1}`), variant.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 line-clamp-2 text-xs leading-5 text-slate-300" }, variant.description) : null), variant.recommended ? /* @__PURE__ */ React.createElement("span", { className: "shrink-0 rounded-full border border-[#ffcfbf]/22 bg-[#ffcfbf]/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, "Top pick") : null), - /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, variant.slug ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-2.5 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("span", { className: "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-300" }, variant.recommended_for.length, " use case", variant.recommended_for.length === 1 ? "" : "s") : null) - ); - }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(PromptVariantCard, { variant: activeVariant, analytics, contentId }))); + return /* @__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"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Switch between safer, shorter, or more specialized prompt variants without losing the core creative direction.")), /* @__PURE__ */ React.createElement("div", { className: "relative mt-6" }, /* @__PURE__ */ React.createElement("div", { className: `pointer-events-none absolute inset-y-0 left-0 z-10 w-14 bg-gradient-to-r from-[#182233] via-[#182233]/85 to-transparent transition ${canScrollVariantsLeft ? "opacity-100" : "opacity-0"}`, "aria-hidden": "true" }), /* @__PURE__ */ React.createElement("div", { className: `pointer-events-none absolute inset-y-0 right-0 z-10 w-14 bg-gradient-to-l from-[#182233] via-[#182233]/85 to-transparent transition ${canScrollVariantsRight ? "opacity-100" : "opacity-0"}`, "aria-hidden": "true" }), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + "aria-label": "Scroll prompt variants left", + onClick: () => scrollVariants("left"), + className: `absolute left-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollVariantsLeft ? "opacity-100 hover:scale-105 hover:bg-slate-900/95" : "pointer-events-none opacity-0"}` + }, + /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chevron-left text-sm" }) + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + "aria-label": "Scroll prompt variants right", + onClick: () => scrollVariants("right"), + className: `absolute right-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollVariantsRight ? "opacity-100 hover:scale-105 hover:bg-slate-900/95" : "pointer-events-none opacity-0"}` + }, + /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-chevron-right text-sm" }) + ), /* @__PURE__ */ React.createElement( + "div", + { + ref: variantsScrollRef, + className: "flex gap-3 overflow-x-auto px-1 pb-3 pt-1 snap-x snap-mandatory scroll-smooth scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", + role: "tablist", + "aria-label": "Prompt variants" + }, + visibleVariants.map((variant, index2) => { + const variantKey = String(variant?.slug || variant?.title || `variant-${index2}`); + const isActive = activeVariant === variant; + return /* @__PURE__ */ React.createElement( + "button", + { + key: variantKey, + type: "button", + role: "tab", + "aria-selected": isActive, + onClick: () => setActiveVariantKey(variantKey), + className: [ + "w-[min(360px,calc(100vw-4.5rem))] shrink-0 snap-start rounded-[24px] border px-4 py-3 text-left transition sm:w-[320px]", + isActive ? "border-sky-300/30 bg-sky-300/12 shadow-[0_16px_40px_rgba(2,6,23,0.18)]" : "border-white/10 bg-black/20 hover:border-white/20 hover:bg-white/[0.05]" + ].join(" ") + }, + /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("p", { className: "truncate text-sm font-semibold text-white" }, variant.title || `Variant ${index2 + 1}`), variant.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 line-clamp-2 text-xs leading-5 text-slate-300" }, variant.description) : null), variant.recommended ? /* @__PURE__ */ React.createElement("span", { className: "shrink-0 rounded-full border border-[#ffcfbf]/22 bg-[#ffcfbf]/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, "Top pick") : null), + /* @__PURE__ */ React.createElement("div", { className: "mt-3 flex flex-wrap gap-2" }, variant.slug ? /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.04] px-2.5 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("span", { className: "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-300" }, variant.recommended_for.length, " use case", variant.recommended_for.length === 1 ? "" : "s") : null) + ); + }) + )), /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement(PromptVariantCard, { variant: activeVariant, analytics, contentId }))); } function PromptPublicExampleCard({ example, index: index2, galleryIndex, onOpenImage, className = "", frameClassName }) { if (!example || typeof example !== "object") return null; @@ -14092,7 +14690,6 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], const promptUnlockDetails = item?.unlock_description || promptUnlockDescription(item?.access_level); const promptFeaturedExamples = promptPreviewImage ? promptPublicExamples.slice(0, 2) : promptPublicExamples.slice(0, 4); const promptOverflowExamples = promptPublicExamples.slice(promptFeaturedExamples.length); - const promptModelsCovered = (promptHasFullAccess && promptComparisons.length ? promptComparisons : promptPublicExamples).map((entry, index2) => entry.model_name || entry.provider || entry.title || `Model ${index2 + 1}`); const promptComparisonGalleryImages = promptComparisons.map((note, index2) => { const src2 = note.image_url || note.thumb_url || ""; if (!src2) return null; @@ -14423,7 +15020,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: "overflow-hidden rounded-[40px] border border-white/10 bg-black/20 shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-0 lg:grid-cols-[minmax(0,1fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "relative overflow-hidden p-8 md:p-10 lg:p-12" }, lessonCover ? /* @__PURE__ */ React.createElement("img", { src: lessonCover, alt: "", "aria-hidden": "true", className: "absolute inset-0 h-full w-full object-cover opacity-15" }) : null, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.22),_transparent_34%),linear-gradient(135deg,_rgba(2,6,23,0.96),_rgba(15,23,42,0.78))]" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-3xl" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100" }, "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-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)), item.lesson_label ? /* @__PURE__ */ React.createElement("p", { className: "mt-5 text-sm font-semibold uppercase tracking-[0.24em] text-amber-100" }, item.lesson_label) : null, /* @__PURE__ */ React.createElement("h1", { className: "mt-5 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-6xl" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-2xl text-base leading-8 text-slate-300 md:text-lg" }, lessonSummary), lessonTags.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, lessonTags.map((tag) => /* @__PURE__ */ React.createElement("span", { key: tag, className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-200" }, tag))) : null, courseContext?.title ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-black/25 p-5" }, /* @__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: "border-t border-white/10 bg-white/[0.03] p-6 lg:border-l lg:border-t-0 lg:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-5 lg:sticky lg:top-6" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/10 bg-black/20" }, lessonCover ? /* @__PURE__ */ React.createElement("img", { src: lessonCover, alt: item.title, className: "h-52 w-full object-cover" }) : /* @__PURE__ */ React.createElement("div", { className: "flex h-52 items-center justify-center bg-[linear-gradient(135deg,_rgba(14,165,233,0.18),_rgba(17,24,39,0.94))] text-sm font-semibold uppercase tracking-[0.24em] text-slate-300" }, "Lesson cover")), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__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-[28px] border border-white/10 bg-black/20 p-5" }, /* @__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 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", @@ -14474,17 +15071,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: "overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(4,10,20,0.98),rgba(15,23,42,0.9))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-0 lg:grid-cols-[minmax(340px,0.8fr)_minmax(0,1.2fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "relative border-b border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.18),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(255,183,139,0.18),transparent_32%),linear-gradient(180deg,rgba(5,10,20,0.98),rgba(10,17,30,0.94))] p-5 md:p-6 lg:min-h-[660px] lg:border-b-0 lg:border-r lg:border-white/10 lg:p-8" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(125,211,252,0.12),transparent_24%),radial-gradient(circle_at_80%_75%,rgba(255,207,191,0.12),transparent_28%)]" }), /* @__PURE__ */ React.createElement("div", { className: "relative flex h-full flex-col" }, /* @__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-[#ffd8cd]" }, "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-3 flex-1 overflow-hidden rounded-[32px] 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", - disabled: !promptPreviewImage, - "aria-label": promptPreviewImage ? `Open preview image for ${item.title}` : "Preview image unavailable" - }, - promptPreviewImage ? /* @__PURE__ */ React.createElement("div", { className: "relative h-full min-h-[320px] overflow-hidden lg:min-h-[540px]" }, /* @__PURE__ */ React.createElement("img", { src: promptPreviewThumbImage, srcSet: promptPreviewSrcSet || void 0, sizes: "(max-width: 1023px) calc(100vw - 3rem), 720px", 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.28))]" }), /* @__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/25 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 h-full min-h-[360px] items-center justify-center bg-[linear-gradient(135deg,rgba(251,146,60,0.14),rgba(17,24,39,0.96))] px-8 text-center lg:min-h-[620px]" }, /* @__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: "relative overflow-hidden p-6 md:p-8 lg:p-9" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,183,139,0.14),_transparent_28%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.12),_transparent_28%)]" }), /* @__PURE__ */ React.createElement("div", { className: "relative z-10 max-w-3xl" }, 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-[#ffcfbf]/20 bg-[#ffcfbf]/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-[#fff0ea]" }, "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-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("p", { className: "mt-6 text-xs font-semibold uppercase tracking-[0.22em] text-[#ffd8cd]" }, "Prompt template"), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 max-w-3xl text-[clamp(2.4rem,4.8vw,4.5rem)] font-semibold leading-[0.95] tracking-[-0.05em] text-white" }, item.title), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-[15px] leading-7 text-slate-300 md:text-base" }, lessonSummary), /* @__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: "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( PromptHeaderStat, { label: "Category", @@ -14520,7 +15107,17 @@ function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], accentClassName: "border-white/10 bg-white/[0.05] text-slate-200", valueClassName: "text-white" } - )), lessonTags.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-7 rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-4 md:p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Microtags"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 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("div", { className: "mt-7 grid gap-4 xl:grid-cols-[minmax(0,1.08fr)_minmax(260px,0.92fr)]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/10 bg-black/20 p-4 md:p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400" }, "Prompt status"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, item.locked ? `${promptAccessRequirement ? `${promptAccessRequirement} ` : ""}This page shows the prompt summary and public example results, but the reusable prompt system stays locked until your Academy access level matches the template.` : "This template includes the main prompt, reuse guidance, and model-specific comparison notes in one place.")), promptModelsCovered.length ? /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-[#ffcfbf]/12 bg-[linear-gradient(180deg,rgba(255,207,191,0.08),rgba(255,255,255,0.03))] p-4 md: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.24em] text-[#ffd8cd]" }, "Compared with"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, promptModelsCovered.length, " model", promptModelsCovered.length > 1 ? "s" : "", " documented for this prompt.")), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold text-white" }, promptModelsCovered.length)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, promptModelsCovered.map((model) => /* @__PURE__ */ React.createElement("span", { key: model, className: "rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-[#fff0ea]" }, model)))) : null))))), /* @__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 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]", + 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( "button", { type: "button", @@ -14599,6 +15196,7 @@ const buildAdminNavGroups = (isAdmin) => [ items: [ { label: "Stories", href: "/moderation/stories", icon: "fa-solid fa-feather-pointed" }, { label: "Artworks", href: "/moderation/artworks", icon: "fa-solid fa-images" }, + { label: "Enhance Jobs", href: "/moderation/enhance", icon: "fa-solid fa-up-right-and-down-left-from-center" }, { label: "Featured Artworks", href: "/moderation/artworks/featured", icon: "fa-solid fa-star" }, { label: "Web Stories", href: "/moderation/web-stories", icon: "fa-solid fa-book-open-reader" }, { label: "Homepage Announcements", href: "/moderation/homepage/announcements", icon: "fa-solid fa-bullhorn" }, @@ -14688,8 +15286,11 @@ const __vite_glob_0_13 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de function MetricCell({ value, suffix = "" }) { return /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-white" }, value, suffix); } -function AcademyAnalyticsContent({ nav = [], range: range2, title, subtitle, rows = [] }) { - return /* @__PURE__ */ React.createElement(AdminLayout, { title, subtitle }, /* @__PURE__ */ React.createElement(Se$1, { title: `Admin · ${title}` }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement("div", { 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" }, "Range"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, range2?.from, " to ", range2?.to)), /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/[0.08] bg-white/[0.03]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "min-w-full text-left text-sm" }, /* @__PURE__ */ React.createElement("thead", { className: "border-b border-white/[0.08] bg-black/20 text-[11px] uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Title"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Type"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Access"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Views"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Unique"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Engaged"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Likes"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Saves"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Copies"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Starts"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Completions"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Upgrade Clicks"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Popularity"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Trend"))), /* @__PURE__ */ React.createElement("tbody", null, rows.length ? rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: `${row.content_type}-${row.content_id || "none"}`, className: "border-b border-white/[0.06] align-top text-slate-300" }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, "ID ", row.content_id || "n/a")), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.content_type_label), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.access_level || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.views })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.unique_visitors })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.engaged_views })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.likes })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.saves })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.prompt_copies })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.starts })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.completions })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.upgrade_clicks })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.popularity_score })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.trend))) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 14, className: "px-4 py-10 text-center text-slate-400" }, "No rollup data available yet for this view.")))))))); +function StatCard$g({ label, value, suffix = "" }) { + 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, suffix)); +} +function AcademyAnalyticsContent({ nav = [], range: range2, title, subtitle, summary = null, rows = [] }) { + return /* @__PURE__ */ React.createElement(AdminLayout, { title, subtitle }, /* @__PURE__ */ React.createElement(Se$1, { title: `Admin · ${title}` }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement("div", { 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" }, "Range"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, range2?.from, " to ", range2?.to)), summary ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$g, { label: "Views", value: Number(summary.views || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Unique Visitors", value: Number(summary.uniqueVisitors || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Engaged Views", value: Number(summary.engagedViews || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Engagement Rate", value: Number(summary.engagementRate || 0).toLocaleString(), suffix: "%" }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Avg Engaged Seconds", value: Number(summary.avgEngagedSeconds || 0).toLocaleString(), suffix: "s" }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Scroll 50%", value: Number(summary.scroll50 || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Scroll 100%", value: Number(summary.scroll100 || 0).toLocaleString() }), /* @__PURE__ */ React.createElement(StatCard$g, { label: "Deep Scroll Rate", value: Number(summary.deepScrollRate || 0).toLocaleString(), suffix: "%" })) : null, /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[28px] border border-white/[0.08] bg-white/[0.03]" }, /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement("table", { className: "min-w-full text-left text-sm" }, /* @__PURE__ */ React.createElement("thead", { className: "border-b border-white/[0.08] bg-black/20 text-[11px] uppercase tracking-[0.18em] text-slate-500" }, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Title"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Type"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Access"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Views"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Unique"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Engaged"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Likes"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Saves"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Copies"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Starts"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Completions"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Upgrade Clicks"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Popularity"), /* @__PURE__ */ React.createElement("th", { className: "px-4 py-3" }, "Trend"))), /* @__PURE__ */ React.createElement("tbody", null, rows.length ? rows.map((row) => /* @__PURE__ */ React.createElement("tr", { key: `${row.content_type}-${row.content_id || "none"}`, className: "border-b border-white/[0.06] align-top text-slate-300" }, /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement("p", { className: "font-semibold text-white" }, row.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-slate-500" }, "ID ", row.content_id || "n/a")), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.content_type_label), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.access_level || "n/a"), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.views })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.unique_visitors })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.engaged_views })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.likes })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.saves })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.prompt_copies })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.starts })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.completions })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.upgrade_clicks })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, /* @__PURE__ */ React.createElement(MetricCell, { value: row.popularity_score })), /* @__PURE__ */ React.createElement("td", { className: "px-4 py-4" }, row.trend))) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: 14, className: "px-4 py-10 text-center text-slate-400" }, "No rollup data available yet for this view.")))))))); } const __vite_glob_0_10 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -14780,11 +15381,35 @@ const __vite_glob_0_12 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de function StatCard$e({ label, value }) { 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" }, Number(value || 0).toLocaleString())); } +function formatDelta(delta) { + if (delta === null || delta === void 0) { + return "new"; + } + if (Number(delta) === 0) { + return "0%"; + } + return `${Number(delta) > 0 ? "+" : ""}${Number(delta).toLocaleString()}%`; +} +function PromptLibraryTrend({ trend }) { + const current = trend?.current || {}; + const deltas = trend?.deltas || {}; + const items = [ + { label: "Views", value: Number(current.views || 0).toLocaleString(), delta: deltas.views }, + { label: "Unique Visitors", value: Number(current.uniqueVisitors || 0).toLocaleString(), delta: deltas.uniqueVisitors }, + { label: "Engaged Views", value: Number(current.engagedViews || 0).toLocaleString(), delta: deltas.engagedViews }, + { label: "Engagement Rate", value: `${Number(current.engagementRate || 0).toLocaleString()}%`, delta: deltas.engagementRate } + ]; + return /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] 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.2em] text-slate-500" }, "Prompt Library Trend"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, trend?.range?.current?.from, " to ", trend?.range?.current?.to, " compared with ", trend?.range?.previous?.from, " to ", trend?.range?.previous?.to)), /* @__PURE__ */ React.createElement("div", { className: "rounded-full border border-white/[0.08] bg-black/20 px-4 py-2 text-xs uppercase tracking-[0.18em] text-slate-400" }, "Popularity ", Number(current.popularityScore || 0).toLocaleString())), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, items.map((item) => /* @__PURE__ */ React.createElement("div", { key: item.label, className: "rounded-2xl border border-white/[0.08] bg-black/20 p-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500" }, item.label), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-2xl font-bold text-white" }, item.value), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs uppercase tracking-[0.18em] text-sky-200" }, formatDelta(item.delta), " vs previous"))))); +} +function PopularPromptPeriodUsage({ usage }) { + const periods = usage?.periods || []; + return /* @__PURE__ */ React.createElement("div", { className: "rounded-[28px] border border-white/[0.08] bg-white/[0.03] 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.2em] text-slate-500" }, "Popular Prompt Period Usage"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm text-slate-300" }, "Which ranking window people actually open on the public popular-prompts page.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-full border border-white/[0.08] bg-black/20 px-4 py-2 text-xs uppercase tracking-[0.18em] text-slate-400" }, Number(usage?.totalViews || 0).toLocaleString(), " views · ", Number(usage?.totalVisitors || 0).toLocaleString(), " visitors")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-3" }, periods.length ? periods.map((period) => /* @__PURE__ */ React.createElement("div", { key: period.period, className: "rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-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" }, period.label), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, period.period)), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-right sm:grid-cols-3 sm:gap-6" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-sky-100" }, Number(period.views || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Views")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-sky-100" }, Number(period.uniqueVisitors || 0).toLocaleString()), /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Visitors")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-sky-100" }, Number(period.share || 0).toLocaleString(), "%"), /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "Share")))))) : /* @__PURE__ */ React.createElement("p", { className: "rounded-2xl border border-dashed border-white/[0.08] bg-black/20 px-4 py-6 text-sm text-slate-400" }, "No popular prompt period events have been tracked in this range yet."))); +} function ContentList({ title, items = [] }) { return /* @__PURE__ */ React.createElement("div", { 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" }, title), /* @__PURE__ */ React.createElement("div", { className: "mt-4 space-y-3" }, items.length ? items.map((item) => /* @__PURE__ */ React.createElement("div", { key: `${item.content_type}-${item.content_id || "none"}`, className: "rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-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.title), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs uppercase tracking-[0.18em] text-slate-500" }, item.content_type_label)), /* @__PURE__ */ React.createElement("div", { className: "text-right" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-semibold text-sky-100" }, item.popularity_score), /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.18em] text-slate-500" }, "popularity"))))) : /* @__PURE__ */ React.createElement("p", { className: "rounded-2xl border border-dashed border-white/[0.08] bg-black/20 px-4 py-6 text-sm text-slate-400" }, "No rollup data yet for this range."))); } -function AcademyAnalyticsOverview({ nav = [], range: range2, stats, topContent = [], topWeek = [] }) { - return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Analytics", subtitle: "Daily rollup overview for Academy traffic, engagement, and subscription intent." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Analytics" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement("div", { 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" }, "Range"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, range2?.from, " to ", range2?.to)), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$e, { label: "Views", value: stats.views }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Unique Visitors", value: stats.uniqueVisitors }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Logged-in Views", value: stats.userViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Guest Views", value: stats.guestViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Subscriber Views", value: stats.subscriberViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Prompt Copies", value: stats.promptCopies }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Likes", value: stats.likes }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Saves", value: stats.saves }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Lesson Completions", value: stats.lessonCompletions }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Course Starts", value: stats.courseStarts }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Upgrade Clicks", value: stats.upgradeClicks })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement(ContentList, { title: "Top Content In Range", items: topContent }), /* @__PURE__ */ React.createElement(ContentList, { title: "Top Content This Week", items: topWeek })))); +function AcademyAnalyticsOverview({ nav = [], range: range2, stats, promptLibraryTrend = null, popularPromptPeriodUsage = null, topContent = [], topWeek = [] }) { + return /* @__PURE__ */ React.createElement(AdminLayout, { title: "Academy Analytics", subtitle: "Daily rollup overview for Academy traffic, engagement, and subscription intent." }, /* @__PURE__ */ React.createElement(Se$1, { title: "Admin · Academy Analytics" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement(AnalyticsNav, { items: nav }), /* @__PURE__ */ React.createElement("div", { 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" }, "Range"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm text-slate-300" }, range2?.from, " to ", range2?.to)), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$e, { label: "Views", value: stats.views }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Unique Visitors", value: stats.uniqueVisitors }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Logged-in Views", value: stats.userViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Guest Views", value: stats.guestViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Subscriber Views", value: stats.subscriberViews }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Prompt Copies", value: stats.promptCopies }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Likes", value: stats.likes }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Saves", value: stats.saves }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Lesson Completions", value: stats.lessonCompletions }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Course Starts", value: stats.courseStarts }), /* @__PURE__ */ React.createElement(StatCard$e, { label: "Upgrade Clicks", value: stats.upgradeClicks })), promptLibraryTrend ? /* @__PURE__ */ React.createElement(PromptLibraryTrend, { trend: promptLibraryTrend }) : null, popularPromptPeriodUsage ? /* @__PURE__ */ React.createElement(PopularPromptPeriodUsage, { usage: popularPromptPeriodUsage }) : null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-6 xl:grid-cols-2" }, /* @__PURE__ */ React.createElement(ContentList, { title: "Top Content In Range", items: topContent }), /* @__PURE__ */ React.createElement(ContentList, { title: "Top Content This Week", items: topWeek })))); } const __vite_glob_0_14 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -22467,6 +23092,32 @@ function serializeStructuredJson(value) { return ""; } } +function copyTextToClipboard(text2) { + const source = String(text2 || ""); + if (!source) return Promise.reject(new Error("Nothing to copy")); + if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") { + return navigator.clipboard.writeText(source); + } + if (typeof document === "undefined" || !document.body) { + return Promise.reject(new Error("Clipboard unavailable")); + } + const textarea = document.createElement("textarea"); + textarea.value = source; + textarea.setAttribute("readonly", "true"); + textarea.style.position = "fixed"; + textarea.style.top = "-1000px"; + textarea.style.left = "-1000px"; + document.body.appendChild(textarea); + textarea.select(); + try { + if (document.execCommand("copy")) { + return Promise.resolve(); + } + } finally { + document.body.removeChild(textarea); + } + return Promise.reject(new Error("Clipboard unavailable")); +} function getField(fields, name2) { return fields.find((field) => field.name === name2) || null; } @@ -24367,7 +25018,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$5(record.approved_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Created:"), " ", formatDateTime$5(record.created_at)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("span", { className: "font-semibold text-slate-100" }, "Updated:"), " ", formatDateTime$5(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_90 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_93 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AiBiographyAdmin }, Symbol.toStringTag, { value: "Module" })); @@ -24652,6 +25303,7 @@ function Dashboard({ stats }) { { label: "Upload Queue", href: "/moderation/uploads", icon: "fa-solid fa-cloud-arrow-up", desc: "Moderate pending artwork submissions" }, { label: "Stories", href: "/moderation/stories", icon: "fa-solid fa-feather-pointed", desc: "Browse all creator stories" }, { label: "Artworks", href: "/moderation/artworks", icon: "fa-solid fa-images", desc: "Browse all uploaded artworks" }, + { label: "Enhance Jobs", href: "/moderation/enhance", icon: "fa-solid fa-up-right-and-down-left-from-center", desc: "Inspect queued, failed, and completed image enhance jobs" }, { label: "Featured Artworks", href: "/moderation/artworks/featured", icon: "fa-solid fa-star", desc: "Curate the homepage featured artwork lineup" }, { label: "AI Biography", href: "/moderation/ai-biography", icon: "fa-solid fa-wand-magic-sparkles", desc: "Review generated creator biographies and moderation flags" } ].map((item) => /* @__PURE__ */ React.createElement( @@ -24753,6 +25405,27 @@ const Checkbox = reactExports.forwardRef(function Checkbox2({ label, hint, error (label || hint) && /* @__PURE__ */ React.createElement("span", { className: "flex flex-col gap-0.5" }, label && /* @__PURE__ */ React.createElement("span", { className: "text-sm text-white/90 leading-snug" }, label), hint && /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-500" }, hint)) ), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "text-xs text-red-400", style: { paddingLeft: `calc(${dim} + 0.625rem)` } }, error)); }); +const FILTER_OPTIONS$1 = [ + { value: "all", label: "All rows" }, + { value: "winner", label: "Winner" }, + { value: "active", label: "Active" }, + { value: "eligible", label: "Eligible" }, + { value: "attention", label: "Needs attention" }, + { value: "ineligible", label: "Ineligible" }, + { value: "expired", label: "Expired" }, + { value: "inactive", label: "Inactive" } +]; +const SORT_OPTIONS$2 = [ + { value: "priority", label: "Priority" }, + { value: "featured_at", label: "Featured Since" }, + { value: "expires_at", label: "Expires" }, + { value: "score_30d", label: "Medal Score (30d)" } +]; +const PRIORITY_PRESETS = [60, 100, 180, 260, 340]; +const PAGE_SIZE$1 = 24; +function cn(...values) { + return values.filter(Boolean).join(" "); +} function getCsrfToken$g() { if (typeof document === "undefined") return ""; return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || ""; @@ -24797,46 +25470,44 @@ function formatDateTime$3(value) { timeStyle: "short" }).format(date); } -function Badge$2({ label, tone = "slate" }) { - const toneClasses2 = { - slate: "border-white/10 bg-white/10 text-slate-100", - sky: "border-sky-300/20 bg-sky-400/15 text-sky-100", - emerald: "border-emerald-300/20 bg-emerald-400/15 text-emerald-100", - amber: "border-amber-300/20 bg-amber-400/15 text-amber-100", - rose: "border-rose-300/20 bg-rose-400/15 text-rose-100" - }; - return /* @__PURE__ */ React.createElement("span", { className: `inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] ${toneClasses2[tone] || toneClasses2.slate}` }, label); +function formatShortDate$3(value) { + if (!value) return "No expiry"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "No expiry"; + return new Intl.DateTimeFormat("en", { + month: "short", + day: "numeric" + }).format(date); } -function Field$4({ label, help, children }) { - return /* @__PURE__ */ React.createElement("label", { className: "block space-y-2" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, label), children, help ? /* @__PURE__ */ React.createElement("span", { className: "block text-xs leading-relaxed text-slate-400" }, help) : null); +function formatRelativeExpiry(value) { + if (!value) return "No expiry"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "No expiry"; + const now = /* @__PURE__ */ new Date(); + const diffInDays = Math.ceil((date.getTime() - now.getTime()) / 864e5); + if (diffInDays < 0) return `${Math.abs(diffInDays)}d overdue`; + if (diffInDays === 0) return "Due today"; + if (diffInDays === 1) return "Due tomorrow"; + return `${diffInDays}d left`; } -function StatCard$8({ label, value, tone = "sky" }) { - const toneClasses2 = { - sky: "border-sky-300/15 bg-sky-400/10 text-sky-100", - amber: "border-amber-300/15 bg-amber-400/10 text-amber-100", - emerald: "border-emerald-300/15 bg-emerald-400/10 text-emerald-100", - rose: "border-rose-300/15 bg-rose-400/10 text-rose-100" - }; - return /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: `inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] ${toneClasses2[tone] || toneClasses2.sky}` }, label), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-3xl font-semibold tracking-[-0.04em] text-white" }, value)); +function formatOwner(entry) { + if (!entry?.artwork?.owner) return "Unknown artist"; + if (entry.artwork.owner.type === "group") return `${entry.artwork.owner.display_name || "Unknown"} · Group publisher`; + return `${entry.artwork.owner.display_name || "Unknown"} · @${entry.artwork.owner.username || ""}`; } -function emptyForm() { - return { - artwork_id: "", - priority: 100, - featured_at: isoToLocalInput$3((/* @__PURE__ */ new Date()).toISOString()), - expires_at: "", - is_active: true - }; -} -function mapEntryToCandidate(entry) { - if (!entry) return null; - return { - ...entry.artwork, - medals: entry.medals, - eligibility: entry.eligibility, - existing_feature_count: entry.duplicate_count, - already_featured: entry.duplicate_count > 0 - }; +function compareWinnerOrder(left, right) { + if (Boolean(left?.is_force_hero) !== Boolean(right?.is_force_hero)) { + return Boolean(right?.is_force_hero) - Boolean(left?.is_force_hero); + } + const priorityDiff = Number(right?.priority || 0) - Number(left?.priority || 0); + if (priorityDiff !== 0) return priorityDiff; + const scoreDiff = Number(right?.medals?.score_30d || 0) - Number(left?.medals?.score_30d || 0); + if (scoreDiff !== 0) return scoreDiff; + const featuredDiff = (new Date(right?.featured_at || 0).getTime() || 0) - (new Date(left?.featured_at || 0).getTime() || 0); + if (featuredDiff !== 0) return featuredDiff; + const publishedDiff = (new Date(right?.artwork?.published_at || 0).getTime() || 0) - (new Date(left?.artwork?.published_at || 0).getTime() || 0); + if (publishedDiff !== 0) return publishedDiff; + return Number(right?.id || 0) - Number(left?.id || 0); } function compareEntries(left, right, sortKey, direction) { const dir = direction === "asc" ? 1 : -1; @@ -24857,43 +25528,158 @@ function compareEntries(left, right, sortKey, direction) { if (leftValue !== rightValue) { return (leftValue > rightValue ? 1 : -1) * dir; } - const leftFeatured = new Date(left.featured_at || 0).getTime() || 0; - const rightFeatured = new Date(right.featured_at || 0).getTime() || 0; - if (leftFeatured !== rightFeatured) { - return (leftFeatured > rightFeatured ? 1 : -1) * dir; + return compareWinnerOrder(left, right); +} +function buildEntrySummary(entry) { + if (entry?.is_force_hero) { + return "Force Hero is enabled, so this row overrides the normal homepage winner order until editors switch it off."; } - return Number(right.id || 0) - Number(left.id || 0); + if (entry?.is_winner && entry?.winner_reason) { + return entry.winner_reason; + } + if (!entry?.is_active) { + return "Inactive rows stay visible for planning and editing, but they cannot win the homepage slot."; + } + if (entry?.is_expired) { + return "This featured slot has expired and is excluded from the live homepage rotation."; + } + if (!entry?.eligibility?.is_eligible) { + const reasons = Array.isArray(entry?.eligibility?.reasons) && entry.eligibility.reasons.length > 0 ? entry.eligibility.reasons.join(", ") : "Eligibility checks failed."; + return `Currently blocked from winning: ${reasons}`; + } + return "Eligible and in the active selection pool. Priority leads first, then medal score, featured time, and publish date."; +} +function Badge$2({ label, tone = "slate" }) { + const toneClasses2 = { + slate: "border-white/10 bg-white/[0.07] text-slate-100", + sky: "border-sky-300/20 bg-sky-400/15 text-sky-100", + emerald: "border-emerald-300/20 bg-emerald-400/15 text-emerald-100", + amber: "border-amber-300/20 bg-amber-400/15 text-amber-100", + rose: "border-rose-300/20 bg-rose-400/15 text-rose-100" + }; + return /* @__PURE__ */ React.createElement("span", { className: cn("inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em]", toneClasses2[tone] || toneClasses2.slate) }, label); +} +function Field$4({ label, help, children }) { + return /* @__PURE__ */ React.createElement("label", { className: "block space-y-2" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, label), children, help ? /* @__PURE__ */ React.createElement("span", { className: "block text-xs leading-relaxed text-slate-400" }, help) : null); +} +function NoticeBanner({ notice }) { + if (!notice?.message) return null; + const toneClasses2 = { + success: "border-emerald-300/20 bg-emerald-400/10 text-emerald-50", + error: "border-rose-300/20 bg-rose-400/10 text-rose-50", + info: "border-sky-300/20 bg-sky-400/10 text-sky-50" + }; + return /* @__PURE__ */ React.createElement("div", { className: cn("rounded-[22px] border px-4 py-3 text-sm leading-6 shadow-[0_10px_30px_rgba(2,6,23,0.18)]", toneClasses2[notice.tone] || toneClasses2.info) }, notice.message); +} +function StatCard$8({ label, value, detail, tone = "sky" }) { + const toneClasses2 = { + sky: "border-sky-300/15 bg-sky-400/10 text-sky-100", + amber: "border-amber-300/15 bg-amber-400/10 text-amber-100", + emerald: "border-emerald-300/15 bg-emerald-400/10 text-emerald-100", + rose: "border-rose-300/15 bg-rose-400/10 text-rose-100" + }; + return /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("div", { className: cn("inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em]", toneClasses2[tone] || toneClasses2.sky) }, label), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-3xl font-semibold tracking-[-0.05em] text-white" }, value), detail ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm leading-6 text-slate-400" }, detail) : null); +} +function MetricTile({ label, value, hint }) { + return /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.18em] text-slate-400" }, label), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-base font-semibold text-white" }, value), hint ? /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs leading-5 text-slate-400" }, hint) : null); +} +function DockButton({ label, detail, tone = "sky", onClick }) { + const toneClasses2 = { + sky: "border-sky-300/25 bg-sky-400/12 text-sky-50", + amber: "border-amber-300/25 bg-amber-400/12 text-amber-50", + slate: "border-white/10 bg-white/[0.05] text-slate-100" + }; + return /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick, + className: cn("flex min-w-0 flex-1 flex-col rounded-[20px] border px-4 py-3 text-left shadow-[0_14px_30px_rgba(2,6,23,0.24)] backdrop-blur-sm", toneClasses2[tone] || toneClasses2.sky) + }, + /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] opacity-80" }, label), + /* @__PURE__ */ React.createElement("span", { className: "mt-1 truncate text-sm font-semibold" }, detail) + ); +} +function FilterChip({ label, value, active, onClick, count }) { + return /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => onClick(value), + className: cn( + "inline-flex items-center gap-2 rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] transition", + active ? "border-sky-300/40 bg-sky-400/15 text-sky-50 shadow-[0_10px_30px_rgba(56,189,248,0.16)]" : "border-white/10 bg-white/[0.03] text-slate-300 hover:border-white/20 hover:bg-white/[0.06]" + ) + }, + /* @__PURE__ */ React.createElement("span", null, label), + /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-black/20 px-2 py-0.5 text-[10px] text-slate-200" }, count) + ); +} +function emptyForm() { + return { + artwork_id: "", + priority: 100, + featured_at: isoToLocalInput$3((/* @__PURE__ */ new Date()).toISOString()), + expires_at: "", + is_active: true + }; +} +function mapEntryToCandidate(entry) { + if (!entry) return null; + return { + ...entry.artwork, + medals: entry.medals, + eligibility: entry.eligibility, + existing_feature_count: entry.duplicate_count, + already_featured: entry.duplicate_count > 0 + }; } 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 || {}; const [entries, setEntries] = React.useState(Array.isArray(props.entries) ? props.entries : []); const [winner, setWinner] = React.useState(props.winner || null); const [stats, setStats] = React.useState(props.stats || {}); - const [notice, setNotice] = React.useState(""); + const [notice, setNotice] = React.useState(null); const [busy, setBusy] = React.useState(""); const [filter2, setFilter] = React.useState("all"); const [sortKey, setSortKey] = React.useState("priority"); const [sortDirection, setSortDirection] = React.useState("desc"); const [listQuery, setListQuery] = React.useState(""); + const deferredListQuery = React.useDeferredValue(listQuery); const [searchQuery, setSearchQuery] = React.useState(""); const [searchResults, setSearchResults] = React.useState([]); 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); 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); + }, [deferredListQuery, filter2, sortDirection, sortKey]); + function scrollToSection(ref2) { + ref2.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + } + function scrollToTop() { + if (typeof window !== "undefined") { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + } function syncPayload(payload) { setEntries(Array.isArray(payload.entries) ? payload.entries : []); setWinner(payload.winner || null); setStats(payload.stats || {}); if (payload.message) { - setNotice(payload.message); + setNotice({ tone: "success", message: payload.message }); } } function resetEditor() { @@ -24910,16 +25696,17 @@ function FeaturedArtworksAdmin() { return; } setBusy("search"); - setNotice(""); + setNotice(null); try { const url = `${endpoints.search}?q=${encodeURIComponent(searchQuery.trim())}`; const payload = await requestJson$p(url, { method: "GET" }); - setSearchResults(Array.isArray(payload.results) ? payload.results : []); - if ((payload.results || []).length === 0) { - setNotice("No artworks matched that search."); + const results = Array.isArray(payload.results) ? payload.results : []; + setSearchResults(results); + if (results.length === 0) { + setNotice({ tone: "info", message: "No artworks matched that search." }); } } catch (error) { - setNotice(error.message || "Artwork search failed."); + setNotice({ tone: "error", message: error.message || "Artwork search failed." }); } finally { setBusy(""); } @@ -24943,18 +25730,16 @@ function FeaturedArtworksAdmin() { expires_at: isoToLocalInput$3(entry.expires_at), is_active: Boolean(entry.is_active) }); - if (typeof window !== "undefined") { - window.scrollTo({ top: 0, behavior: "smooth" }); - } + scrollToSection(composerRef); } async function handleSubmit(event) { event.preventDefault(); if (!editingId && !form.artwork_id) { - setNotice("Select an artwork first."); + setNotice({ tone: "error", message: "Select an artwork first." }); return; } setBusy("submit"); - setNotice(""); + setNotice(null); try { const payload = await requestJson$p( editingId ? endpoints.updatePattern.replace("__FEATURE__", String(editingId)) : endpoints.store, @@ -24972,21 +25757,21 @@ function FeaturedArtworksAdmin() { syncPayload(payload); resetEditor(); } catch (error) { - setNotice(error.message || "Failed to save this featured entry."); + setNotice({ tone: "error", message: error.message || "Failed to save this featured entry." }); } finally { setBusy(""); } } async function handleToggle(entry) { setBusy(`toggle-${entry.id}`); - setNotice(""); + setNotice(null); try { const payload = await requestJson$p(endpoints.togglePattern.replace("__FEATURE__", String(entry.id)), { method: "PATCH" }); syncPayload(payload); } catch (error) { - setNotice(error.message || "Failed to change active state."); + setNotice({ tone: "error", message: error.message || "Failed to change active state." }); } finally { setBusy(""); } @@ -24996,7 +25781,7 @@ function FeaturedArtworksAdmin() { return; } setBusy(`delete-${entry.id}`); - setNotice(""); + setNotice(null); try { const payload = await requestJson$p(endpoints.destroyPattern.replace("__FEATURE__", String(entry.id)), { method: "DELETE" @@ -25006,34 +25791,89 @@ function FeaturedArtworksAdmin() { resetEditor(); } } catch (error) { - setNotice(error.message || "Failed to delete this featured entry."); + setNotice({ tone: "error", message: error.message || "Failed to delete this featured entry." }); } finally { setBusy(""); } } async function handleForceHero(entry) { setBusy(`force-${entry.id}`); - setNotice(""); + setNotice(null); try { const payload = await requestJson$p(endpoints.forceHeroPattern.replace("__FEATURE__", String(entry.id)), { method: "PATCH" }); syncPayload(payload); } catch (error) { - setNotice(error.message || "Failed to change force hero state."); + setNotice({ tone: "error", message: error.message || "Failed to change force hero state." }); } finally { setBusy(""); } } - const filteredEntries = React.useMemo(() => { - const query = listQuery.trim().toLowerCase(); + const duplicateSelection = !editingId && selectedArtwork?.already_featured; + const suggestedPriority = winner ? Number(winner.priority || 0) + 20 : 120; + const filterCounts = React.useMemo(() => ({ + all: entries.length, + winner: entries.filter((entry) => Boolean(entry.is_winner)).length, + active: entries.filter((entry) => Boolean(entry.is_active)).length, + eligible: entries.filter((entry) => Boolean(entry.eligibility?.is_eligible)).length, + attention: entries.filter((entry) => Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible)).length, + ineligible: entries.filter((entry) => !entry.eligibility?.is_eligible).length, + expired: entries.filter((entry) => Boolean(entry.is_expired)).length, + inactive: entries.filter((entry) => !entry.is_active).length + }), [entries]); + const activeForceHeroEntry = React.useMemo(() => entries.find((entry) => Boolean(entry.is_force_hero)) || null, [entries]); + const soonExpiringEntries = React.useMemo(() => { + const now = Date.now(); + const cutoff = now + 7 * 864e5; return entries.filter((entry) => { + if (!entry.expires_at || entry.is_expired) return false; + const expiresAt = new Date(entry.expires_at).getTime(); + return !Number.isNaN(expiresAt) && expiresAt >= now && expiresAt <= cutoff; + }).sort((left, right) => (new Date(left.expires_at).getTime() || 0) - (new Date(right.expires_at).getTime() || 0)); + }, [entries]); + const naturalFallback = React.useMemo(() => { + return [...entries].filter((entry) => Boolean(entry.is_active) && !Boolean(entry.is_expired) && Boolean(entry.eligibility?.is_eligible) && !Boolean(entry.is_winner)).sort(compareWinnerOrder)[0] || null; + }, [entries]); + const attentionEntries = React.useMemo(() => { + return [...entries].filter((entry) => Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible)).sort(compareWinnerOrder).slice(0, 3); + }, [entries]); + const selectedArtworkSignals = React.useMemo(() => { + if (!selectedArtwork) return []; + const signals = []; + const eligibilityReasons = Array.isArray(selectedArtwork.eligibility?.reasons) ? selectedArtwork.eligibility.reasons : []; + signals.push({ + label: selectedArtwork.eligibility?.is_eligible ? "Ready for rotation now" : "Needs editorial attention", + tone: selectedArtwork.eligibility?.is_eligible ? "emerald" : "rose", + detail: selectedArtwork.eligibility?.is_eligible ? "Current eligibility checks pass." : eligibilityReasons.join(", ") || "Eligibility checks are failing." + }); + if (winner) { + const priorityGap = Number(form.priority || 0) - Number(winner.priority || 0); + signals.push({ + label: priorityGap >= 0 ? "Priority matches or exceeds current winner" : "Lower priority than current winner", + tone: priorityGap >= 0 ? "sky" : "amber", + detail: `Current winner priority: ${winner.priority}. Proposed priority: ${Number(form.priority || 0)}.` + }); + } + if (selectedArtwork.already_featured) { + signals.push({ + label: "Existing featured row found", + tone: "amber", + detail: `This artwork already appears in ${selectedArtwork.existing_feature_count || 1} featured row${selectedArtwork.existing_feature_count === 1 ? "" : "s"}.` + }); + } + return signals; + }, [form.priority, selectedArtwork, winner]); + const filteredEntries = React.useMemo(() => { + const query = deferredListQuery.trim().toLowerCase(); + return [...entries].filter((entry) => { if (filter2 === "active") return Boolean(entry.is_active); if (filter2 === "inactive") return !entry.is_active; if (filter2 === "expired") return Boolean(entry.is_expired); if (filter2 === "winner") return Boolean(entry.is_winner); if (filter2 === "eligible") return Boolean(entry.eligibility?.is_eligible); if (filter2 === "ineligible") return !entry.eligibility?.is_eligible; + if (filter2 === "attention") return Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible); return true; }).filter((entry) => { if (!query) return true; @@ -25041,20 +25881,91 @@ function FeaturedArtworksAdmin() { entry.artwork?.title, entry.artwork?.owner?.display_name, entry.artwork?.owner?.username, - entry.artwork?.id + entry.artwork?.id, + entry.id ].join(" ").toLowerCase(); return haystack.includes(query); }).sort((left, right) => compareEntries(left, right, sortKey, sortDirection)); - }, [entries, filter2, listQuery, sortDirection, sortKey]); - const duplicateSelection = !editingId && selectedArtwork?.already_featured; - 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), /* @__PURE__ */ React.createElement("div", { className: "min-h-screen bg-[#07111c] text-white" }, /* @__PURE__ */ React.createElement("div", { className: "mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8" }, /* @__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-6 lg:flex-row lg:items-start lg: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.05em] text-white sm:text-5xl" }, "Homepage hero control, with the real winner logic exposed."), /* @__PURE__ */ React.createElement("p", { className: "mt-4 max-w-2xl text-sm leading-7 text-slate-300 sm:text-base" }, "Editors can create, update, activate, expire, and remove featured entries here. The winner summary below mirrors the public homepage selection order: priority, recent medal score, featured date, then published date.")), /* @__PURE__ */ React.createElement("div", { className: "grid w-full max-w-xl grid-cols-2 gap-4 md:grid-cols-3" }, /* @__PURE__ */ React.createElement(StatCard$8, { label: "Entries", value: stats.total || 0, tone: "sky" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Eligible", value: stats.eligible || 0, tone: "emerald" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Expired", value: stats.expired || 0, tone: "amber" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Active", value: stats.active || 0, tone: "sky" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Inactive", value: stats.inactive || 0, tone: "rose" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Not Eligible", value: stats.ineligible || 0, tone: "rose" })))), notice ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-sky-300/15 bg-sky-400/10 px-4 py-3 text-sm text-sky-50" }, notice) : null, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[1.1fr_0.9fr]" }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__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" }, "Current Homepage Hero"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, winner ? winner.artwork?.title : "No eligible featured artwork"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-2xl text-sm leading-7 text-slate-300" }, winner?.selection_reason || "There is no active, non-expired, eligible featured artwork right now."), winner?.is_force_hero ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 max-w-2xl rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm leading-6 text-amber-50" }, "Forced by editor. This artwork bypasses the normal hero winner order until Force Hero is disabled on its featured row.") : null), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, winner ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Winner", tone: "amber" }) : /* @__PURE__ */ React.createElement(Badge$2, { label: "No Winner", tone: "rose" }), winner?.is_force_hero ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Force Hero", tone: "amber" }) : null)), winner ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 lg:grid-cols-[220px_1fr]" }, /* @__PURE__ */ React.createElement("a", { href: winner.artwork?.canonical_url || "#", className: "overflow-hidden rounded-[24px] border border-white/10 bg-[#09121f]", target: "_blank", rel: "noreferrer" }, /* @__PURE__ */ React.createElement( + }, [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)); + } + 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", + { + type: "button", + onClick: () => { + React.startTransition(() => setFilter("attention")); + scrollToSection(rosterRef); + }, + className: "rounded-full border border-white/10 px-5 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/[0.06]" + }, + "Review attention items" + ), winner?.artwork?.canonical_url ? /* @__PURE__ */ React.createElement("a", { href: winner.artwork.canonical_url, target: "_blank", rel: "noreferrer", className: "rounded-full border border-amber-300/20 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-300/40 hover:bg-amber-400/10" }, "Open current winner") : null)), /* @__PURE__ */ React.createElement("div", { className: "grid w-full max-w-2xl gap-4 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement(StatCard$8, { label: "Active pool", value: stats.active || 0, detail: "Rows currently allowed to compete for the homepage slot.", tone: "sky" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Eligible now", value: stats.eligible || 0, detail: "Rows that can win without any manual override.", tone: "emerald" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Force Hero", value: activeForceHeroEntry ? "On" : "Off", detail: activeForceHeroEntry ? `${activeForceHeroEntry.artwork?.title || "Selected artwork"} is overriding the normal order.` : "Normal winner logic is in control.", tone: "amber" }), /* @__PURE__ */ React.createElement(StatCard$8, { label: "Expiring soon", value: soonExpiringEntries.length, detail: soonExpiringEntries[0] ? `${soonExpiringEntries[0].artwork?.title || "Next row"} is the nearest expiry.` : "No featured rows expire in the next 7 days.", tone: soonExpiringEntries.length > 0 ? "rose" : "sky" })))), /* @__PURE__ */ React.createElement(NoticeBanner, { notice }), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[1.3fr_0.7fr]" }, /* @__PURE__ */ React.createElement("section", { 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" }, /* @__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("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Current Homepage Hero"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-3xl font-semibold tracking-[-0.05em] text-white" }, winner ? winner.artwork?.title : "No eligible featured artwork"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-2xl text-sm leading-7 text-slate-300" }, winner?.selection_reason || "There is no active, non-expired, eligible featured artwork right now.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, winner ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Winner", tone: "amber" }) : /* @__PURE__ */ React.createElement(Badge$2, { label: "No winner", tone: "rose" }), winner?.is_force_hero ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Force Hero", tone: "amber" }) : /* @__PURE__ */ React.createElement(Badge$2, { label: "Normal logic", tone: "sky" }))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-6 lg:grid-cols-[240px_1fr]" }, /* @__PURE__ */ React.createElement("a", { href: winner?.artwork?.canonical_url || "#", className: "overflow-hidden rounded-[26px] border border-white/10 bg-[#09121f]", target: "_blank", rel: "noreferrer" }, /* @__PURE__ */ React.createElement( "img", { - src: winner.artwork?.thumbnail?.url, - alt: winner.artwork?.title || "Winner preview", - className: "h-full min-h-[180px] w-full object-cover" + src: winner?.artwork?.thumbnail?.url, + alt: winner?.artwork?.title || "Winner preview", + className: "h-full min-h-[220px] w-full object-cover" } - )), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Artist"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, winner.artwork?.owner?.display_name || "Unknown"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, winner.artwork?.owner?.type === "group" ? "Group publisher" : `@${winner.artwork?.owner?.username || ""}`)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Medal Score (30d)"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, winner.medals?.score_30d || 0)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Priority"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, winner.priority)), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Featured Since"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, formatDateTime$3(winner.featured_at))), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-white/10 bg-black/20 p-4 sm:col-span-2" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Published At"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, formatDateTime$3(winner.artwork?.published_at))))) : null), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__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" }, editingId ? "Edit Entry" : "Create Entry"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, editingId ? `Featured entry #${editingId}` : "Add an artwork to the featured pool")), 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-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement(Field$4, { label: "Artwork selector", help: "Search by artwork ID, title, slug, artist, or group. Pick a result to lock it into the form." }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-3" }, /* @__PURE__ */ React.createElement( + )), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(MetricTile, { label: "Artist", value: winner?.artwork?.owner?.display_name || "Unknown", hint: winner?.artwork?.owner?.type === "group" ? "Group publisher" : `@${winner?.artwork?.owner?.username || ""}` }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Priority", value: winner?.priority ?? "—", hint: "Primary tie-breaker in hero selection." }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Medal score", value: winner?.medals?.score_30d || 0, hint: "Last 30 days." }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Featured Since", value: formatDateTime$3(winner?.featured_at), hint: "Used after priority and medal score." }))), winner?.is_force_hero ? /* @__PURE__ */ React.createElement("div", { className: "mt-5 rounded-[22px] border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm leading-6 text-amber-50" }, "Force Hero is active. This row bypasses the normal ranking until editors disable the override from the roster below.") : null), /* @__PURE__ */ React.createElement("section", { 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" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Editorial Radar"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Know what happens next before you touch a row."), /* @__PURE__ */ React.createElement("div", { className: "mt-6 space-y-4" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.18em] text-slate-400" }, "Natural fallback"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, naturalFallback?.artwork?.title || "No fallback candidate"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm leading-6 text-slate-400" }, naturalFallback ? `${formatOwner(naturalFallback)} · Priority ${naturalFallback.priority} · Medal ${naturalFallback.medals?.score_30d || 0}` : "All remaining rows are either inactive, expired, or ineligible.")), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.18em] text-slate-400" }, "Needs attention"), attentionEntries.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm leading-6 text-slate-300" }, "No active rows are currently blocked by expiry or eligibility rules.") : /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3" }, attentionEntries.map((entry) => /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + key: entry.id, + onClick: () => { + React.startTransition(() => { + setFilter("attention"); + setListQuery(String(entry.artwork?.id || entry.artwork_id || entry.id)); + }); + scrollToSection(rosterRef); + }, + className: "block w-full rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-left transition hover:border-white/20 hover:bg-white/[0.05]" + }, + /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, entry.artwork?.title || `Featured row #${entry.id}`), + /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs leading-5 text-slate-400" }, buildEntrySummary(entry)) + )))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.18em] text-slate-400" }, "Next expiry checkpoint"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, soonExpiringEntries[0]?.artwork?.title || "Nothing expiring soon"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm leading-6 text-slate-400" }, soonExpiringEntries[0] ? `${formatRelativeExpiry(soonExpiringEntries[0].expires_at)} · ${formatDateTime$3(soonExpiringEntries[0].expires_at)}` : "No featured rows expire in the next 7 days."))))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 xl:grid-cols-[1.25fr_0.75fr]" }, /* @__PURE__ */ React.createElement("section", { ref: rosterRef, 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" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4" }, /* @__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("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Featured Pool"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Every featured row, with enough context to act quickly."), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-400" }, "Showing ", visibleEntries.length, " of ", filteredEntries.length, " matching rows. ", entries.length, " rows in the full pool.")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-[minmax(0,1fr)_220px_124px] lg:w-[720px]" }, /* @__PURE__ */ React.createElement( + "input", + { + type: "text", + value: listQuery, + onChange: (event) => setListQuery(event.target.value), + placeholder: "Filter by title, artist, row ID, or artwork ID", + className: "rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" + } + ), /* @__PURE__ */ React.createElement(NovaSelect, { value: sortKey, onChange: (value) => setSortKey(value), searchable: false, options: SORT_OPTIONS$2 }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSortDirection((current) => current === "desc" ? "asc" : "desc"), className: "rounded-2xl border border-white/10 px-4 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, sortDirection === "desc" ? "Desc" : "Asc"))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, FILTER_OPTIONS$1.map((option) => /* @__PURE__ */ React.createElement( + FilterChip, + { + key: option.value, + label: option.label, + value: option.value, + active: filter2 === option.value, + 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$3(entry.featured_at) }), /* @__PURE__ */ React.createElement(MetricTile, { label: "Expires", value: formatRelativeExpiry(entry.expires_at), hint: formatDateTime$3(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( + "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" + }, + "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$4, { 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", @@ -25063,27 +25974,55 @@ function FeaturedArtworksAdmin() { className: "w-full rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40", placeholder: "Try an artwork ID, title, or creator" } - ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy === "search", className: "rounded-2xl bg-sky-400 px-4 py-3 text-sm font-semibold text-slate-950 transition hover:bg-sky-300 disabled:cursor-not-allowed disabled:opacity-60" }, busy === "search" ? "Searching…" : "Search"))), searchResults.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, searchResults.map((artwork) => /* @__PURE__ */ React.createElement( + ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy === "search", className: "rounded-2xl bg-sky-400 px-4 py-3 text-sm font-semibold text-slate-950 transition hover:bg-sky-300 disabled:cursor-not-allowed disabled:opacity-60" }, busy === "search" ? "Searching…" : "Search"))), searchResults.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, "Search results"), searchResults.map((artwork) => /* @__PURE__ */ React.createElement( "button", { type: "button", key: artwork.id, onClick: () => chooseArtwork(artwork), - className: `grid gap-4 rounded-2xl border p-3 text-left transition sm:grid-cols-[88px_1fr] ${selectedArtwork?.id === artwork.id ? "border-sky-300/40 bg-sky-400/10" : "border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]"}` + className: cn("grid gap-4 rounded-[22px] border p-3 text-left transition sm:grid-cols-[88px_1fr]", selectedArtwork?.id === artwork.id ? "border-sky-300/40 bg-sky-400/10 shadow-[0_12px_30px_rgba(56,189,248,0.15)]" : "border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]") }, /* @__PURE__ */ React.createElement("img", { src: artwork.thumbnail?.url, alt: artwork.title, className: "h-24 w-full rounded-2xl object-cover" }), - /* @__PURE__ */ React.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, artwork.title), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-400" }, "#", artwork.id), artwork.already_featured ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Already Featured", tone: "amber" }) : null), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-400" }, artwork.owner?.display_name || "Unknown", " • Medal Score (30d): ", artwork.medals?.score_30d || 0), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, (artwork.eligibility?.is_eligible ? [{ label: "Eligible", tone: "emerald" }] : [{ label: "Not eligible", tone: "rose" }]).concat( + /* @__PURE__ */ React.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, artwork.title), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-400" }, "#", artwork.id), artwork.already_featured ? /* @__PURE__ */ React.createElement(Badge$2, { label: "Already Featured", tone: "amber" }) : null), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-slate-400" }, artwork.owner?.display_name || "Unknown", " · Medal Score (30d): ", artwork.medals?.score_30d || 0), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, (artwork.eligibility?.is_eligible ? [{ label: "Eligible", tone: "emerald" }] : [{ label: "Not eligible", tone: "rose" }]).concat( (artwork.eligibility?.reasons || []).map((reason) => ({ label: reason, tone: reason === "Missing preview" ? "rose" : "slate" })) ).slice(0, 4).map((badge) => /* @__PURE__ */ React.createElement(Badge$2, { key: `${artwork.id}-${badge.label}`, label: badge.label, tone: badge.tone })))) - ))) : null) : null, selectedArtwork ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 grid gap-4 rounded-[24px] border border-white/10 bg-black/20 p-4 sm:grid-cols-[108px_1fr]" }, /* @__PURE__ */ React.createElement("img", { src: selectedArtwork.thumbnail?.url, alt: selectedArtwork.title || "Artwork preview", className: "h-28 w-full rounded-2xl object-cover" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Selected Artwork"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, selectedArtwork.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, "#", selectedArtwork.id, " • ", selectedArtwork.owner?.display_name || "Unknown", " • Medal Score (30d): ", selectedArtwork.medals?.score_30d || 0)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, (selectedArtwork.eligibility?.is_eligible ? [{ label: "Currently eligible", tone: "emerald" }] : [{ label: "Currently ineligible", tone: "rose" }]).concat( + ))) : null) : null, selectedArtwork ? /* @__PURE__ */ React.createElement("div", { className: "mt-6 rounded-[26px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-[108px_1fr]" }, /* @__PURE__ */ React.createElement("img", { src: selectedArtwork.thumbnail?.url, alt: selectedArtwork.title || "Artwork preview", className: "h-28 w-full rounded-2xl object-cover" }), /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-xs uppercase tracking-[0.18em] text-slate-400" }, "Selected Artwork"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-lg font-semibold text-white" }, selectedArtwork.title), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm leading-6 text-slate-400" }, "#", selectedArtwork.id, " · ", selectedArtwork.owner?.display_name || "Unknown", " · Medal Score (30d): ", selectedArtwork.medals?.score_30d || 0)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, (selectedArtwork.eligibility?.is_eligible ? [{ label: "Currently eligible", tone: "emerald" }] : [{ label: "Currently ineligible", tone: "rose" }]).concat( (selectedArtwork.eligibility?.reasons || []).map((reason) => ({ label: reason, tone: reason === "Missing preview" ? "rose" : "slate" })) - ).map((badge) => /* @__PURE__ */ React.createElement(Badge$2, { key: `selected-${badge.label}`, label: badge.label, tone: badge.tone }))))) : null, duplicateSelection ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm text-amber-100" }, "This artwork already has a featured entry. Edit the existing row instead of creating a duplicate.") : null, /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "mt-6 grid gap-4 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$4, { label: "Priority", help: "Higher priority always wins before medal score is considered." }, /* @__PURE__ */ React.createElement( + ).map((badge) => /* @__PURE__ */ React.createElement(Badge$2, { key: `selected-${badge.label}`, label: badge.label, tone: badge.tone }))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, selectedArtwork.canonical_url ? /* @__PURE__ */ React.createElement("a", { href: selectedArtwork.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") : null, selectedArtwork.already_featured ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => { + React.startTransition(() => { + setFilter("all"); + setListQuery(String(selectedArtwork?.id || "")); + }); + scrollToSection(rosterRef); + }, + className: "rounded-full border border-amber-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-amber-50 transition hover:border-amber-300/40 hover:bg-amber-400/10" + }, + "Find existing row" + ) : null))), selectedArtworkSignals.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 grid gap-3" }, selectedArtworkSignals.map((signal) => /* @__PURE__ */ React.createElement("div", { key: signal.label, className: "rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement(Badge$2, { label: signal.label, tone: signal.tone })), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm leading-6 text-slate-300" }, signal.detail)))) : null) : null, duplicateSelection ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[22px] border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm leading-6 text-amber-100" }, "This artwork already has a featured entry. Edit the existing row instead of creating a duplicate.", /* @__PURE__ */ React.createElement("div", { className: "mt-3" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => { + React.startTransition(() => { + setFilter("all"); + setListQuery(String(selectedArtwork?.id || "")); + }); + scrollToSection(rosterRef); + }, + className: "rounded-full border border-amber-300/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-amber-50 transition hover:border-amber-300/40 hover:bg-amber-400/10" + }, + "Find existing row" + ))) : null, /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "mt-6 space-y-5" }, /* @__PURE__ */ React.createElement(Field$4, { label: "Priority", help: "Higher priority always wins before medal score is considered." }, /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, /* @__PURE__ */ React.createElement( "input", { type: "number", @@ -25092,13 +26031,38 @@ function FeaturedArtworksAdmin() { onChange: (event) => setForm((current) => ({ ...current, priority: event.target.value })), className: "w-full rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" } - )), /* @__PURE__ */ React.createElement(Field$4, { label: "Active", help: "Inactive rows stay visible in admin but cannot win the homepage hero." }, /* @__PURE__ */ React.createElement("label", { className: "flex h-[52px] items-center gap-3 rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-slate-100" }, /* @__PURE__ */ React.createElement( + ), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-2" }, PRIORITY_PRESETS.map((preset) => /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + key: preset, + onClick: () => setForm((current) => ({ ...current, priority: preset })), + className: cn("rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] transition", Number(form.priority) === preset ? "border-sky-300/40 bg-sky-400/10 text-sky-100" : "border-white/10 text-slate-300 hover:border-white/20 hover:bg-white/[0.05]") + }, + preset + )), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setForm((current) => ({ ...current, priority: suggestedPriority })), + className: "rounded-full border border-emerald-300/20 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-100 transition hover:border-emerald-300/40 hover:bg-emerald-400/10" + }, + "Winner + 20" + ), winner ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setForm((current) => ({ ...current, priority: Number(winner.priority || 0) })), + className: "rounded-full border border-white/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-slate-300 transition hover:border-white/20 hover:bg-white/[0.05]" + }, + "Match winner" + ) : null))), /* @__PURE__ */ React.createElement(Field$4, { label: "Active", help: "Inactive rows stay visible in admin but cannot win the homepage hero." }, /* @__PURE__ */ React.createElement("label", { className: "flex h-[52px] items-center gap-3 rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-slate-100" }, /* @__PURE__ */ React.createElement( Checkbox, { checked: Boolean(form.is_active), onChange: (event) => setForm((current) => ({ ...current, is_active: event.target.checked })) } - ), /* @__PURE__ */ React.createElement("span", null, form.is_active ? "Active on save" : "Inactive on save"))), /* @__PURE__ */ React.createElement(Field$4, { label: "Featured Since" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.featured_at, onChange: (nextValue) => setForm((current) => ({ ...current, featured_at: nextValue })), placeholder: "Featured since", clearable: true, className: "bg-[#08111d]" })), /* @__PURE__ */ React.createElement(Field$4, { label: "Expires" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.expires_at, onChange: (nextValue) => setForm((current) => ({ ...current, expires_at: nextValue })), placeholder: "Expiry date", clearable: true, className: "bg-[#08111d]" })), /* @__PURE__ */ React.createElement("div", { className: "sm:col-span-2 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement( + ), /* @__PURE__ */ React.createElement("span", null, form.is_active ? "Active on save" : "Inactive on save"))), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-2" }, /* @__PURE__ */ React.createElement(Field$4, { label: "Featured Since" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.featured_at, onChange: (nextValue) => setForm((current) => ({ ...current, featured_at: nextValue })), placeholder: "Featured since", clearable: true, className: "bg-[#08111d]" })), /* @__PURE__ */ React.createElement(Field$4, { label: "Expires" }, /* @__PURE__ */ React.createElement(DateTimePicker, { value: form.expires_at, onChange: (nextValue) => setForm((current) => ({ ...current, expires_at: nextValue })), placeholder: "Expiry date", clearable: true, className: "bg-[#08111d]" }))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement( "button", { type: "submit", @@ -25106,16 +26070,34 @@ function FeaturedArtworksAdmin() { className: "rounded-2xl bg-white px-5 py-3 text-sm font-semibold text-slate-950 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-60" }, busy === "submit" ? "Saving…" : editingId ? "Save Changes" : "Create Featured Entry" - ), editingId ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetEditor, className: "rounded-2xl border border-white/10 px-5 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Reset") : null)))), /* @__PURE__ */ React.createElement("section", { className: "rounded-[28px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm" }, /* @__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("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-slate-400" }, "Featured Pool"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, "Every featured row, with eligibility and winner state visible.")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-3 sm:grid-cols-3 lg:w-[720px]" }, /* @__PURE__ */ React.createElement( - "input", + ), editingId ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: resetEditor, className: "rounded-2xl border border-white/10 px-5 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, "Reset composer") : null)))))), /* @__PURE__ */ React.createElement("div", { className: "fixed inset-x-4 bottom-4 z-30 xl:hidden" }, /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-3 gap-3 rounded-[26px] border border-white/10 bg-[#08111d]/95 p-3 shadow-[0_24px_70px_rgba(2,6,23,0.42)] backdrop-blur-xl" }, /* @__PURE__ */ React.createElement( + DockButton, { - type: "text", - value: listQuery, - onChange: (event) => setListQuery(event.target.value), - placeholder: "Filter by title, artist, or artwork ID", - className: "rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" + label: "Winner", + detail: winner?.artwork?.title || "No live winner", + tone: "sky", + onClick: scrollToTop } - ), /* @__PURE__ */ React.createElement(NovaSelect, { value: filter2, onChange: (val) => setFilter(val), searchable: false, options: [{ value: "all", label: "All rows" }, { value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }, { value: "expired", label: "Expired" }, { value: "winner", label: "Winner" }, { value: "eligible", label: "Eligible" }, { value: "ineligible", label: "Not eligible" }] }), /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-[1fr_auto] gap-3" }, /* @__PURE__ */ React.createElement(NovaSelect, { value: sortKey, onChange: (val) => setSortKey(val), searchable: false, options: [{ value: "priority", label: "Priority" }, { value: "featured_at", label: "Featured Since" }, { value: "expires_at", label: "Expires" }, { value: "score_30d", label: "Medal Score (30d)" }] }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setSortDirection((current) => current === "desc" ? "asc" : "desc"), className: "rounded-2xl border border-white/10 px-4 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/5" }, sortDirection === "desc" ? "Desc" : "Asc")))), /* @__PURE__ */ React.createElement("div", { className: "mt-6 overflow-hidden rounded-[24px] border border-white/10" }, /* @__PURE__ */ React.createElement("div", { className: "hidden grid-cols-[1.2fr_1fr_0.5fr_0.9fr_0.9fr_0.7fr_1.5fr_0.9fr] gap-4 border-b border-white/10 bg-black/20 px-5 py-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400 lg:grid" }, /* @__PURE__ */ React.createElement("div", null, "Artwork"), /* @__PURE__ */ React.createElement("div", null, "Artist / Owner"), /* @__PURE__ */ React.createElement("div", null, "Priority"), /* @__PURE__ */ React.createElement("div", null, "Featured Since"), /* @__PURE__ */ React.createElement("div", null, "Expires"), /* @__PURE__ */ React.createElement("div", null, "Score (30d)"), /* @__PURE__ */ React.createElement("div", null, "Status"), /* @__PURE__ */ React.createElement("div", null, "Actions")), /* @__PURE__ */ React.createElement("div", { className: "divide-y divide-white/10" }, filteredEntries.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "px-5 py-10 text-center text-sm text-slate-400" }, "No featured entries match the current filter.") : filteredEntries.map((entry) => /* @__PURE__ */ React.createElement("div", { key: entry.id, className: "grid gap-5 bg-white/[0.02] px-5 py-5 lg:grid-cols-[1.2fr_1fr_0.5fr_0.9fr_0.9fr_0.7fr_1.5fr_0.9fr] lg:items-center" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-[92px_1fr]" }, /* @__PURE__ */ React.createElement("a", { href: entry.artwork?.canonical_url || "#", target: "_blank", rel: "noreferrer", className: "overflow-hidden rounded-2xl border border-white/10 bg-[#08111d]" }, /* @__PURE__ */ React.createElement("img", { src: entry.artwork?.thumbnail?.url, alt: entry.artwork?.title || "Artwork preview", className: "h-24 w-full object-cover" })), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-semibold text-white" }, entry.artwork?.title || "Missing artwork"), /* @__PURE__ */ React.createElement("span", { className: "text-xs text-slate-400" }, "#", entry.artwork?.id || entry.artwork_id)), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs leading-6 text-slate-400" }, "Visibility: ", entry.artwork?.visibility || "—", " • Published: ", entry.artwork?.published_at ? "Yes" : "No"), entry.is_winner && entry.winner_reason ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs leading-6 text-amber-100" }, entry.winner_reason) : null)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, entry.artwork?.owner?.display_name || "Unknown"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs text-slate-400" }, entry.artwork?.owner?.type === "group" ? "Group publisher" : `@${entry.artwork?.owner?.username || ""}`)), /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, entry.priority), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-200" }, formatDateTime$3(entry.featured_at)), /* @__PURE__ */ React.createElement("div", { className: "text-sm text-slate-200" }, formatDateTime$3(entry.expires_at)), /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold text-white" }, entry.medals?.score_30d || 0), /* @__PURE__ */ React.createElement("div", { className: "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: "flex flex-wrap gap-2 lg:justify-end" }, /* @__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"), capabilities.forceHeroEnabled ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => handleForceHero(entry), disabled: busy === `force-${entry.id}`, className: `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")))))))))); + ), /* @__PURE__ */ React.createElement( + DockButton, + { + label: "Attention", + detail: `${filterCounts.attention || 0} row${(filterCounts.attention || 0) === 1 ? "" : "s"}`, + tone: "amber", + onClick: () => { + React.startTransition(() => setFilter("attention")); + scrollToSection(rosterRef); + } + } + ), /* @__PURE__ */ React.createElement( + DockButton, + { + label: "Composer", + detail: editingId ? `Editing #${editingId}` : "Add featured row", + tone: "slate", + onClick: () => scrollToSection(composerRef) + } + ))))); } const __vite_glob_0_49 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -25285,7 +26267,7 @@ function HomepageAnnouncement({ announcement, mode = "live" }) { isPreviewMode ? "mt-auto flex w-full flex-col gap-4 border-t border-white/10 pt-5" : "flex flex-col items-start gap-3 lg:items-end" ) }, /* @__PURE__ */ React.createElement("div", { className: cx$9("flex flex-wrap gap-3", isPreviewMode ? "w-full" : "lg:justify-end") }, announcement.primary_link ? /* @__PURE__ */ React.createElement("a", { href: announcement.primary_link.url, className: cx$9("inline-flex items-center justify-center rounded-full border px-5 py-3 text-sm font-semibold transition", isPreviewMode ? "min-w-[11rem]" : "", preset.primary) }, announcement.primary_link.label) : null, announcement.secondary_link ? /* @__PURE__ */ React.createElement("a", { href: announcement.secondary_link.url, className: cx$9("inline-flex items-center justify-center rounded-full border px-5 py-3 text-sm font-semibold transition", isPreviewMode ? "min-w-[11rem]" : "", preset.secondary) }, announcement.secondary_link.label) : null)))))); } -function ToolbarButton({ onClick, active, disabled, title, children }) { +function ToolbarButton$1({ onClick, active, disabled, title, children }) { return /* @__PURE__ */ React.createElement( "button", { @@ -25318,7 +26300,7 @@ function Toolbar({ editor }) { } editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); }, [editor]); - return /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-0.5 border-b border-white/[0.06] px-2.5 py-2" }, /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleBold().run(), active: editor.isActive("bold"), title: "Bold" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6zm0 8h9a4 4 0 014 4 4 4 0 01-4 4H6z" }))), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleItalic().run(), active: editor.isActive("italic"), title: "Italic" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "19", y1: "4", x2: "10", y2: "4" }), /* @__PURE__ */ React.createElement("line", { x1: "14", y1: "20", x2: "5", y2: "20" }), /* @__PURE__ */ React.createElement("line", { x1: "15", y1: "4", x2: "9", y2: "20" }))), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), active: editor.isActive("heading", { level: 2 }), title: "Heading 2" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs font-bold" }, "H2")), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), active: editor.isActive("heading", { level: 3 }), title: "Heading 3" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs font-bold" }, "H3")), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), active: editor.isActive("bulletList"), title: "Bullet list" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "6", x2: "20", y2: "6" }), /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "12", x2: "20", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "18", x2: "20", y2: "18" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "6", r: "1", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "12", r: "1", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "18", r: "1", fill: "currentColor" }))), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), active: editor.isActive("orderedList"), title: "Numbered list" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "6", x2: "21", y2: "6" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "12", x2: "21", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "18", x2: "21", y2: "18" }), /* @__PURE__ */ React.createElement("text", { x: "3", y: "8", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "1"), /* @__PURE__ */ React.createElement("text", { x: "3", y: "14", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "2"), /* @__PURE__ */ React.createElement("text", { x: "3", y: "20", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "3"))), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().toggleBlockquote().run(), active: editor.isActive("blockquote"), title: "Quote" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M4.583 17.321C3.553 16.227 3 15 3 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311C9.591 11.68 11 13.24 11 15.14c0 .94-.36 1.84-1.001 2.503A3.34 3.34 0 017.559 18.6a3.77 3.77 0 01-2.976-.879zm10.4 0C13.953 16.227 13.4 15 13.4 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311 1.986.169 3.395 1.729 3.395 3.629 0 .94-.36 1.84-1.001 2.503a3.34 3.34 0 01-2.44.957 3.77 3.77 0 01-2.976-.879z" }))), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: addLink, active: editor.isActive("link"), title: "Link" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ React.createElement("path", { d: "M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" }))), /* @__PURE__ */ React.createElement("div", { className: "ml-auto flex items-center gap-0.5" }, /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), title: "Undo" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "1 4 1 10 7 10" }), /* @__PURE__ */ React.createElement("path", { d: "M3.51 15a9 9 0 102.13-9.36L1 10" }))), /* @__PURE__ */ React.createElement(ToolbarButton, { onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), title: "Redo" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "23 4 23 10 17 10" }), /* @__PURE__ */ React.createElement("path", { d: "M20.49 15a9 9 0 11-2.13-9.36L23 10" }))))); + return /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-0.5 border-b border-white/[0.06] px-2.5 py-2" }, /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleBold().run(), active: editor.isActive("bold"), title: "Bold" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6zm0 8h9a4 4 0 014 4 4 4 0 01-4 4H6z" }))), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleItalic().run(), active: editor.isActive("italic"), title: "Italic" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "19", y1: "4", x2: "10", y2: "4" }), /* @__PURE__ */ React.createElement("line", { x1: "14", y1: "20", x2: "5", y2: "20" }), /* @__PURE__ */ React.createElement("line", { x1: "15", y1: "4", x2: "9", y2: "20" }))), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), active: editor.isActive("heading", { level: 2 }), title: "Heading 2" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs font-bold" }, "H2")), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), active: editor.isActive("heading", { level: 3 }), title: "Heading 3" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs font-bold" }, "H3")), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleBulletList().run(), active: editor.isActive("bulletList"), title: "Bullet list" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "6", x2: "20", y2: "6" }), /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "12", x2: "20", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "18", x2: "20", y2: "18" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "6", r: "1", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "12", r: "1", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "4.5", cy: "18", r: "1", fill: "currentColor" }))), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleOrderedList().run(), active: editor.isActive("orderedList"), title: "Numbered list" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "6", x2: "21", y2: "6" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "12", x2: "21", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "18", x2: "21", y2: "18" }), /* @__PURE__ */ React.createElement("text", { x: "3", y: "8", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "1"), /* @__PURE__ */ React.createElement("text", { x: "3", y: "14", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "2"), /* @__PURE__ */ React.createElement("text", { x: "3", y: "20", fontSize: "7", fill: "currentColor", stroke: "none", fontFamily: "sans-serif" }, "3"))), /* @__PURE__ */ React.createElement(Divider, null), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().toggleBlockquote().run(), active: editor.isActive("blockquote"), title: "Quote" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M4.583 17.321C3.553 16.227 3 15 3 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311C9.591 11.68 11 13.24 11 15.14c0 .94-.36 1.84-1.001 2.503A3.34 3.34 0 017.559 18.6a3.77 3.77 0 01-2.976-.879zm10.4 0C13.953 16.227 13.4 15 13.4 13.011c0-3.5 2.457-6.637 6.03-8.188l.893 1.378c-3.335 1.804-3.987 4.145-4.247 5.621.537-.278 1.24-.375 1.929-.311 1.986.169 3.395 1.729 3.395 3.629 0 .94-.36 1.84-1.001 2.503a3.34 3.34 0 01-2.44.957 3.77 3.77 0 01-2.976-.879z" }))), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: addLink, active: editor.isActive("link"), title: "Link" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ React.createElement("path", { d: "M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" }))), /* @__PURE__ */ React.createElement("div", { className: "ml-auto flex items-center gap-0.5" }, /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), title: "Undo" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "1 4 1 10 7 10" }), /* @__PURE__ */ React.createElement("path", { d: "M3.51 15a9 9 0 102.13-9.36L1 10" }))), /* @__PURE__ */ React.createElement(ToolbarButton$1, { onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), title: "Redo" }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "23 4 23 10 17 10" }), /* @__PURE__ */ React.createElement("path", { d: "M20.49 15a9 9 0 11-2.13-9.36L23 10" }))))); } function HomepageAnnouncementEditor({ content: content2 = "", @@ -39096,7 +40078,7 @@ function ReactionBar({ entityType, entityId, initialTotals = {}, isLoggedIn = fa ) ); } -const TONES$3 = { +const TONES$4 = { 1: "border-slate-500/30 bg-slate-500/10 text-slate-200", 2: "border-sky-400/35 bg-sky-500/10 text-sky-200", 3: "border-emerald-400/35 bg-emerald-500/10 text-emerald-200", @@ -39110,7 +40092,7 @@ function cx$8(...parts) { } function LevelBadge({ level = 1, rank = "Newbie", compact = false, className = "" }) { const numericLevel = Number(level || 1); - const tone = TONES$3[numericLevel] || TONES$3[1]; + const tone = TONES$4[numericLevel] || TONES$4[1]; return /* @__PURE__ */ React.createElement( "span", { @@ -39612,6 +40594,9 @@ function DownloadArrowIcon() { function ChartIcon() { return /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.8, stroke: "currentColor", className: "h-4 w-4" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3v18h18" }), /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M7 15.5 10.5 12l3 2.5 4.5-6" })); } +function EnhanceIcon() { + return /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.8, stroke: "currentColor", className: "h-4 w-4" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3l1.9 4.6L18.5 9l-4.6 1.4L12 15l-1.9-4.6L5.5 9l4.6-1.4L12 3Z" }), /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M18 15l.95 2.05L21 18l-2.05.95L18 21l-.95-2.05L15 18l2.05-.95L18 15Z" })); +} function FlagIcon() { return /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "h-5 w-5" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3v1.5M3 21v-6m0 0 2.77-.693a9 9 0 0 1 6.208.682l.108.054a9 9 0 0 0 6.086.71l3.114-.732a48.524 48.524 0 0 1-.005-10.499l-3.11.732a9 9 0 0 1-6.085-.711l-.108-.054a9 9 0 0 0-6.208-.682L3 4.5M3 15V4.5" })); } @@ -39730,6 +40715,7 @@ function ArtworkActionBar({ artwork, stats, canonicalUrl, onStatsChange }) { }, [artwork?.id, artwork?.stats?.bookmarks, stats?.bookmarks]); const shareUrl = canonicalUrl || artwork?.canonical_url || (typeof window !== "undefined" ? window.location.href : "#"); const analyticsUrl = artwork?.management?.analytics_url || (artwork?.viewer?.is_owner ? `/studio/artworks/${artwork.id}/analytics` : null); + const enhanceUrl = artwork?.viewer?.is_owner && artwork?.id ? `/enhance/create?artwork=${encodeURIComponent(artwork.id)}` : null; const csrfToken2 = typeof document !== "undefined" ? document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") : null; reactExports.useEffect(() => { if (!artwork?.id) return; @@ -39862,6 +40848,14 @@ function ArtworkActionBar({ artwork, stats, canonicalUrl, onStatsChange }) { }, /* @__PURE__ */ React.createElement(ChartIcon, null), "Statistics" + ) : null, enhanceUrl ? /* @__PURE__ */ React.createElement( + "a", + { + href: enhanceUrl, + className: "inline-flex items-center gap-2 rounded-full border border-violet-300/25 bg-violet-400/12 px-5 py-2.5 text-sm font-medium text-violet-50 transition-all duration-200 hover:border-violet-200/40 hover:bg-violet-400/18 hover:text-white" + }, + /* @__PURE__ */ React.createElement(EnhanceIcon, null), + "Enhance image" ) : null, /* @__PURE__ */ React.createElement( "button", { @@ -39922,6 +40916,16 @@ function ArtworkActionBar({ artwork, stats, canonicalUrl, onStatsChange }) { className: "inline-flex items-center gap-1.5 rounded-full border border-sky-400/30 bg-sky-400/12 px-3.5 py-2 text-xs font-medium text-sky-100 transition-all hover:border-sky-300/45 hover:bg-sky-400/18 hover:text-white" }, /* @__PURE__ */ React.createElement(ChartIcon, null) + ) : null, enhanceUrl ? /* @__PURE__ */ React.createElement( + "a", + { + href: enhanceUrl, + "aria-label": "Enhance artwork image", + title: "Enhance image", + className: "inline-flex items-center gap-1.5 rounded-full border border-violet-300/25 bg-violet-400/12 px-3.5 py-2 text-xs font-medium text-violet-50 transition-all hover:border-violet-200/40 hover:bg-violet-400/18 hover:text-white" + }, + /* @__PURE__ */ React.createElement(EnhanceIcon, null), + "Enhance" ) : null, /* @__PURE__ */ React.createElement( "button", { @@ -40145,7 +41149,7 @@ function formatCount$1(value) { if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`; return NUMBER_FORMATTER$2.format(n); } -function formatDate$e(value, useRelative = true) { +function formatDate$i(value, useRelative = true) { if (!value) return "—"; try { const d2 = new Date(value); @@ -40189,7 +41193,7 @@ function ArtworkDetailsPanel({ artwork, stats }) { label: "Downloads", value: formatCount$1(stats?.downloads) } - )), /* @__PURE__ */ React.createElement("div", { className: "mt-4 divide-y divide-white/[0.05]" }, resolution ? /* @__PURE__ */ React.createElement("div", { className: "py-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-wider text-white/35" }, "Resolution"), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium text-white/80" }, resolution)), /* @__PURE__ */ React.createElement(ArtworkFormatBadges, { width, height, className: "mt-2" })) : null, /* @__PURE__ */ React.createElement(InfoRow$1, { label: "Uploaded", value: formatDate$e(artwork?.published_at, hydrated) }))); + )), /* @__PURE__ */ React.createElement("div", { className: "mt-4 divide-y divide-white/[0.05]" }, resolution ? /* @__PURE__ */ React.createElement("div", { className: "py-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React.createElement("span", { className: "text-xs uppercase tracking-wider text-white/35" }, "Resolution"), /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium text-white/80" }, resolution)), /* @__PURE__ */ React.createElement(ArtworkFormatBadges, { width, height, className: "mt-2" })) : null, /* @__PURE__ */ React.createElement(InfoRow$1, { label: "Uploaded", value: formatDate$i(artwork?.published_at, hydrated) }))); } function galleryUrlFor(author) { if (!author?.username) return null; @@ -73504,7 +74508,7 @@ const SORT_OPTIONS$1 = [ { value: "artworks", label: "Most artworks" } ]; const PAGE_SIZE = 24; -const numberFormatter = new Intl.NumberFormat(); +const numberFormatter$1 = new Intl.NumberFormat(); function normalizeInitialData(initialData) { if (!initialData || typeof initialData !== "object") { return { @@ -73732,7 +74736,7 @@ function CategoriesPage({ apiUrl: apiUrl2 = "/api/categories", pageTitle = "Cate const showingStart = loadedCount > 0 ? 1 : 0; const showingEnd = loadedCount; const hasMorePages = meta.current_page < meta.last_page; - return /* @__PURE__ */ React.createElement("div", { className: "pb-24 text-white" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 top-0 h-[28rem] bg-[radial-gradient(circle_at_top_left,rgba(34,211,238,0.12),transparent_38%),radial-gradient(circle_at_top_right,rgba(249,115,22,0.14),transparent_34%)]" }), /* @__PURE__ */ React.createElement("div", { className: "relative w-full px-6 pb-8 pt-14 sm:px-8 sm:pt-20 xl:px-10 2xl:px-14 lg:pt-24" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[minmax(0,1.2fr)_20rem] lg:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.24em] text-white/50 backdrop-blur-sm" }, "Category directory"), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 max-w-4xl text-4xl font-semibold tracking-[-0.05em] text-white sm:text-5xl lg:text-6xl" }, pageTitle), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-2xl text-base leading-8 text-white/62 sm:text-lg" }, pageDescription || "Browse all wallpapers, skins, themes and digital art categories")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-3 lg:grid-cols-1" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "Categories"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter.format(summary.total_categories))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "Artworks indexed"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter.format(summary.total_artworks))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "View"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Grid")))), /* @__PURE__ */ React.createElement("div", { className: "mt-10 rounded-[30px] border border-white/10 bg-black/25 p-4 shadow-[0_30px_80px_rgba(0,0,0,0.25)] backdrop-blur-xl sm:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_16rem] lg:items-center" }, /* @__PURE__ */ React.createElement("label", { className: "relative block" }, /* @__PURE__ */ React.createElement("span", { className: "pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-white/35" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: "h-5 w-5" }, /* @__PURE__ */ React.createElement("path", { fillRule: "evenodd", d: "M8.5 3a5.5 5.5 0 1 0 3.473 9.765l3.63 3.63a.75.75 0 1 0 1.06-1.06l-3.63-3.63A5.5 5.5 0 0 0 8.5 3Zm-4 5.5a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z", clipRule: "evenodd" }))), /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement("div", { className: "pb-24 text-white" }, /* @__PURE__ */ React.createElement("section", { className: "relative overflow-hidden" }, /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 top-0 h-[28rem] bg-[radial-gradient(circle_at_top_left,rgba(34,211,238,0.12),transparent_38%),radial-gradient(circle_at_top_right,rgba(249,115,22,0.14),transparent_34%)]" }), /* @__PURE__ */ React.createElement("div", { className: "relative w-full px-6 pb-8 pt-14 sm:px-8 sm:pt-20 xl:px-10 2xl:px-14 lg:pt-24" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-8 lg:grid-cols-[minmax(0,1.2fr)_20rem] lg:items-end" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.24em] text-white/50 backdrop-blur-sm" }, "Category directory"), /* @__PURE__ */ React.createElement("h1", { className: "mt-5 max-w-4xl text-4xl font-semibold tracking-[-0.05em] text-white sm:text-5xl lg:text-6xl" }, pageTitle), /* @__PURE__ */ React.createElement("p", { className: "mt-5 max-w-2xl text-base leading-8 text-white/62 sm:text-lg" }, pageDescription || "Browse all wallpapers, skins, themes and digital art categories")), /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 sm:grid-cols-3 lg:grid-cols-1" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "Categories"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter$1.format(summary.total_categories))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "Artworks indexed"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter$1.format(summary.total_artworks))), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-white/40" }, "View"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Grid")))), /* @__PURE__ */ React.createElement("div", { className: "mt-10 rounded-[30px] border border-white/10 bg-black/25 p-4 shadow-[0_30px_80px_rgba(0,0,0,0.25)] backdrop-blur-xl sm:p-5" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_16rem] lg:items-center" }, /* @__PURE__ */ React.createElement("label", { className: "relative block" }, /* @__PURE__ */ React.createElement("span", { className: "pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-white/35" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: "h-5 w-5" }, /* @__PURE__ */ React.createElement("path", { fillRule: "evenodd", d: "M8.5 3a5.5 5.5 0 1 0 3.473 9.765l3.63 3.63a.75.75 0 1 0 1.06-1.06l-3.63-3.63A5.5 5.5 0 0 0 8.5 3Zm-4 5.5a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z", clipRule: "evenodd" }))), /* @__PURE__ */ React.createElement( "input", { type: "search", @@ -73768,8 +74772,8 @@ function CategoriesPage({ apiUrl: apiUrl2 = "/api/categories", pageTitle = "Cate className: "inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-white/72 transition hover:border-white/20 hover:bg-white/[0.05] hover:text-white" }, /* @__PURE__ */ React.createElement("span", null, category.name), - /* @__PURE__ */ React.createElement("span", { className: "text-white/38" }, numberFormatter.format(category.artwork_count)) - ))))), /* @__PURE__ */ React.createElement("div", { className: "mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.22em] text-white/38" }, "Directory results"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter.format(meta.total), " categories visible")), !loading && !error && meta.total > 0 ? /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/52" }, "Showing ", numberFormatter.format(showingStart), " to ", numberFormatter.format(showingEnd), " of ", numberFormatter.format(meta.total), " categories.") : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/52" }, "Browse all wallpapers, skins, themes and digital art categories.")), loading && /* @__PURE__ */ React.createElement(LoadingGrid, null), !loading && error && /* @__PURE__ */ React.createElement(ErrorState, { onRetry: handleRetry }), !loading && !error && meta.total === 0 && /* @__PURE__ */ React.createElement(EmptyState$5, { query: deferredQuery }), !loading && !error && meta.total > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5" }, categories.map((category, index2) => /* @__PURE__ */ React.createElement(CategoryCard$1, { key: category.id, category, index: index2 }))), /* @__PURE__ */ React.createElement("div", { ref: sentinelRef, className: "h-6 w-full", "aria-hidden": "true" }), loadingMore && /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex items-center justify-center gap-3 rounded-2xl border border-white/8 bg-black/18 px-4 py-4 text-sm text-white/56 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("span", { className: "h-2.5 w-2.5 animate-pulse rounded-full bg-cyan-300" }), "Loading more categories"), /* @__PURE__ */ React.createElement("div", { className: "mt-10 flex flex-col items-center justify-center gap-3 rounded-[24px] border border-white/8 bg-black/18 px-4 py-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/46" }, "Loaded through page ", numberFormatter.format(meta.current_page), " of ", numberFormatter.format(meta.last_page)), /* @__PURE__ */ React.createElement(Pagination$1, { meta, onPageChange: handlePageChange }), hasMorePages && /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.2em] text-white/28" }, "Scroll to load the next page automatically"))))); + /* @__PURE__ */ React.createElement("span", { className: "text-white/38" }, numberFormatter$1.format(category.artwork_count)) + ))))), /* @__PURE__ */ React.createElement("div", { className: "mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("p", { className: "text-xs font-semibold uppercase tracking-[0.22em] text-white/38" }, "Directory results"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold tracking-[-0.04em] text-white" }, numberFormatter$1.format(meta.total), " categories visible")), !loading && !error && meta.total > 0 ? /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/52" }, "Showing ", numberFormatter$1.format(showingStart), " to ", numberFormatter$1.format(showingEnd), " of ", numberFormatter$1.format(meta.total), " categories.") : /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/52" }, "Browse all wallpapers, skins, themes and digital art categories.")), loading && /* @__PURE__ */ React.createElement(LoadingGrid, null), !loading && error && /* @__PURE__ */ React.createElement(ErrorState, { onRetry: handleRetry }), !loading && !error && meta.total === 0 && /* @__PURE__ */ React.createElement(EmptyState$5, { query: deferredQuery }), !loading && !error && meta.total > 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5" }, categories.map((category, index2) => /* @__PURE__ */ React.createElement(CategoryCard$1, { key: category.id, category, index: index2 }))), /* @__PURE__ */ React.createElement("div", { ref: sentinelRef, className: "h-6 w-full", "aria-hidden": "true" }), loadingMore && /* @__PURE__ */ React.createElement("div", { className: "mt-6 flex items-center justify-center gap-3 rounded-2xl border border-white/8 bg-black/18 px-4 py-4 text-sm text-white/56 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("span", { className: "h-2.5 w-2.5 animate-pulse rounded-full bg-cyan-300" }), "Loading more categories"), /* @__PURE__ */ React.createElement("div", { className: "mt-10 flex flex-col items-center justify-center gap-3 rounded-[24px] border border-white/8 bg-black/18 px-4 py-5 backdrop-blur-sm" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-white/46" }, "Loaded through page ", numberFormatter$1.format(meta.current_page), " of ", numberFormatter$1.format(meta.last_page)), /* @__PURE__ */ React.createElement(Pagination$1, { meta, onPageChange: handlePageChange }), hasMorePages && /* @__PURE__ */ React.createElement("p", { className: "text-xs uppercase tracking-[0.2em] text-white/28" }, "Scroll to load the next page automatically"))))); } if (typeof document !== "undefined") { const mountElement = document.getElementById("categories-page-root"); @@ -73815,14 +74819,14 @@ const STYLES = { unlisted: "border-amber-300/25 bg-amber-300/10 text-amber-100", private: "border-white/15 bg-white/6 text-slate-200" }; -const LABELS = { +const LABELS$1 = { public: "Public", unlisted: "Unlisted", private: "Private" }; function CollectionVisibilityBadge({ visibility, className = "" }) { const value = String(visibility || "public").toLowerCase(); - const label = LABELS[value] || "Public"; + const label = LABELS$1[value] || "Public"; const style = STYLES[value] || STYLES.public; return /* @__PURE__ */ React.createElement("span", { className: `inline-flex items-center rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] ${style} ${className}`.trim() }, label); } @@ -80787,6 +81791,207 @@ const __vite_glob_0_57 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de __proto__: null, default: LatestCommentsPage }, Symbol.toStringTag, { value: "Module" })); +function EnhanceStubWarning({ config, moderation = false, className = "" }) { + if (!config?.showStubWarning) { + return null; + } + return /* @__PURE__ */ React.createElement("div", { className: `rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm text-amber-50 ${className}`.trim() }, /* @__PURE__ */ React.createElement("div", null, "Skinbase Enhance is currently running in preview mode. The generated result is a workflow placeholder until the real upscaling worker is enabled."), moderation ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-xs uppercase tracking-[0.14em] text-amber-100/80" }, "Engine: ", config.engine, ". This is not a real AI upscale result.") : null); +} +function EnhanceCreate() { + const { props } = X$1(); + const form = G$1({ image: null, scale: props.options?.scales?.[0]?.value || 2, mode: props.options?.modes?.[0]?.value || "standard" }); + const [previewUrl, setPreviewUrl] = React.useState(null); + const [sourceType, setSourceType] = React.useState(props.selectedArtwork ? "artwork" : "upload"); + React.useEffect(() => () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }, [previewUrl]); + function handleFileChange(event) { + const file = event.target.files?.[0] || null; + form.setData("image", file); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + setPreviewUrl(file ? URL.createObjectURL(file) : null); + } + function submit(event) { + event.preventDefault(); + const action = sourceType === "artwork" && props.selectedArtwork?.store_url ? props.selectedArtwork.store_url : props.storeUrl; + form.post(action, { + forceFormData: sourceType !== "artwork", + preserveScroll: true + }); + } + 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(14,165,233,0.18),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" }, "Create an upscaled image"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, "Large images may take longer to process. The original image will stay unchanged.")), /* @__PURE__ */ React.createElement(xe, { href: props.indexUrl, 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]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left text-[10px]" }), "Back to jobs"))), /* @__PURE__ */ React.createElement(EnhanceStubWarning, { config: props.enhanceConfig, className: "mt-6" }), /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "mt-8 grid gap-6 xl:grid-cols-[minmax(0,1.05fr)_380px]" }, /* @__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: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400" }, "Enhance source"), props.selectedArtwork ? /* @__PURE__ */ React.createElement("div", { className: "inline-flex rounded-full border border-white/10 bg-white/[0.04] p-1 text-xs font-semibold text-slate-300" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setSourceType("artwork"), + className: `rounded-full px-4 py-2 transition ${sourceType === "artwork" ? "bg-sky-400/15 text-sky-50" : "hover:bg-white/[0.06]"}` + }, + "Existing artwork" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setSourceType("upload"), + className: `rounded-full px-4 py-2 transition ${sourceType === "upload" ? "bg-sky-400/15 text-sky-50" : "hover:bg-white/[0.06]"}` + }, + "Upload image" + )) : null), sourceType === "artwork" && props.selectedArtwork ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-[28px] border border-sky-300/20 bg-[linear-gradient(180deg,rgba(14,165,233,0.1),rgba(8,17,29,0.9))] p-6 text-left" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80" }, "Existing artwork source"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-2xl font-semibold tracking-[-0.03em] text-white" }, props.selectedArtwork.title), /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-2xl text-sm leading-6 text-slate-300" }, "Use the current artwork source without re-uploading a file. The original artwork remains untouched and the enhanced result will be stored as a separate job output."), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("a", { href: props.selectedArtwork.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.08]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-image text-[10px]" }), "View artwork"), /* @__PURE__ */ React.createElement("span", { 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 uppercase tracking-[0.14em] text-emerald-100" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-lock text-[10px]" }), "Original stays unchanged")), form.errors.source ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, form.errors.source) : null) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", { className: "mt-4 flex min-h-[420px] cursor-pointer flex-col items-center justify-center rounded-[28px] border border-dashed border-white/15 bg-black/20 px-6 py-8 text-center transition hover:border-sky-300/30 hover:bg-sky-400/[0.03]" }, previewUrl ? /* @__PURE__ */ React.createElement("img", { src: previewUrl, alt: "Selected for enhance", className: "max-h-[420px] w-full rounded-[20px] object-contain" }) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-white/10 bg-white/[0.05] text-white/70" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-cloud-arrow-up text-2xl" })), /* @__PURE__ */ React.createElement("div", { className: "mt-4 text-lg font-semibold text-white" }, "Choose a JPEG, PNG, or WebP image"), /* @__PURE__ */ React.createElement("div", { className: "mt-2 max-w-md text-sm text-slate-400" }, "Upload an image up to ", props.maxUploadMb, " MB. SVG, GIF, and unsupported file types are rejected.")), /* @__PURE__ */ React.createElement("input", { type: "file", accept: "image/jpeg,image/png,image/webp", onChange: handleFileChange, className: "hidden" })), form.errors.image ? /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-sm text-rose-300" }, form.errors.image) : null)), /* @__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" }, "Enhance settings"), /* @__PURE__ */ React.createElement("div", { className: "mt-5 space-y-5" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "text-sm font-semibold text-white" }, "Scale"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 grid grid-cols-2 gap-3" }, (props.options?.scales || []).map((option) => /* @__PURE__ */ React.createElement( + "button", + { + key: option.value, + type: "button", + onClick: () => form.setData("scale", option.value), + className: `rounded-2xl border px-4 py-4 text-left transition ${Number(form.data.scale) === Number(option.value) ? "border-sky-300/30 bg-sky-400/12 text-sky-50" : "border-white/10 bg-white/[0.04] text-slate-300 hover:bg-white/[0.08]"}` + }, + /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold" }, option.label), + /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.16em] text-current/70" }, "Upscale size") + ))), form.errors.scale ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-rose-300" }, form.errors.scale) : null), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { className: "text-sm font-semibold text-white" }, "Mode"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3" }, (props.options?.modes || []).map((option) => /* @__PURE__ */ React.createElement( + "button", + { + key: option.value, + type: "button", + onClick: () => form.setData("mode", option.value), + className: `w-full rounded-2xl border px-4 py-4 text-left transition ${String(form.data.mode) === String(option.value) ? "border-sky-300/30 bg-sky-400/12 text-sky-50" : "border-white/10 bg-white/[0.04] text-slate-300 hover:bg-white/[0.08]"}` + }, + /* @__PURE__ */ React.createElement("div", { className: "text-sm font-semibold" }, option.label), + /* @__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_58 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: EnhanceCreate +}, Symbol.toStringTag, { value: "Module" })); +const TONES$3 = { + pending: "border-white/10 bg-white/[0.05] text-slate-200", + queued: "border-sky-300/20 bg-sky-400/12 text-sky-100", + processing: "border-violet-300/20 bg-violet-400/12 text-violet-100", + completed: "border-emerald-300/20 bg-emerald-400/12 text-emerald-100", + failed: "border-rose-300/20 bg-rose-400/12 text-rose-100", + cancelled: "border-amber-300/20 bg-amber-400/12 text-amber-100", + expired: "border-white/10 bg-white/[0.05] text-slate-300", + unknown: "border-white/10 bg-white/[0.04] text-slate-300" +}; +const LABELS = { + pending: "Pending", + queued: "Queued", + processing: "Processing", + completed: "Completed", + failed: "Failed", + cancelled: "Cancelled", + expired: "Expired" +}; +function EnhanceStatusBadge({ status: status2, className = "" }) { + const key = String(status2 || "").toLowerCase(); + const tone = TONES$3[key] || TONES$3.unknown; + const label = LABELS[key] || "Unknown"; + return /* @__PURE__ */ React.createElement("span", { className: `inline-flex items-center rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${tone} ${className}`.trim() }, label); +} +const dateFormatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + timeZone: "UTC" +}); +const numberFormatter = new Intl.NumberFormat("en-US"); +function formatEnhanceDate(value) { + if (!value) return "—"; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return "—"; + } + return `${dateFormatter.format(parsed)} UTC`; +} +function formatEnhanceInteger(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return "0"; + } + return numberFormatter.format(parsed); +} +function formatDate$h(value) { + return formatEnhanceDate(value); +} +function JobCard({ job }) { + return /* @__PURE__ */ React.createElement("article", { 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-[220px_1fr]" }, /* @__PURE__ */ React.createElement("div", { className: "aspect-square bg-black/30" }, job.preview_url || job.source_url ? /* @__PURE__ */ React.createElement("img", { src: job.preview_url || job.source_url, alt: "Enhance preview", 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-image text-4xl" }))), /* @__PURE__ */ React.createElement("div", { className: "p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement(EnhanceStatusBadge, { status: job.status }), /* @__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.16em] text-slate-200" }, job.scale, "x"), /* @__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.16em] text-slate-200" }, job.mode), /* @__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.16em] text-slate-200" }, job.engine)), /* @__PURE__ */ React.createElement("h2", { className: "mt-4 text-2xl font-semibold tracking-[-0.03em] text-white" }, job.artwork?.title ? `Artwork enhance: ${job.artwork.title}` : `Enhance job #${job.id}`), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-slate-300" }, "Created ", formatDate$h(job.created_at), " ", job.processing_seconds ? `• ${job.processing_seconds}s processing` : ""), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-400" }, job.input_width, " × ", job.input_height, job.output_width && job.output_height ? ` → ${job.output_width} × ${job.output_height}` : ""), job.error_message ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100" }, job.error_message) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement(xe, { href: job.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]" }, "Open job"), job.artwork?.url ? /* @__PURE__ */ React.createElement("a", { href: job.artwork.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 artwork") : null)))); +} +function EnhanceIndex() { + const { props } = X$1(); + const jobs = props.jobs?.data || []; + const latestCompleted = props.latestCompleted || []; + const flash = props.flash || {}; + 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_59 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: EnhanceIndex +}, Symbol.toStringTag, { value: "Module" })); +function BeforeAfterSlider({ beforeUrl, afterUrl, beforeAlt = "Original image", afterAlt = "Enhanced image" }) { + const [position2, setPosition] = React.useState(50); + if (!beforeUrl || !afterUrl) { + return null; + } + return /* @__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: "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" }, "Before / after"), /* @__PURE__ */ React.createElement("h3", { className: "mt-2 text-xl font-semibold tracking-[-0.03em] text-white" }, "Compare the original with the enhanced result")), /* @__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.16em] text-slate-200" }, position2, "%")), /* @__PURE__ */ React.createElement("div", { className: "relative mt-5 overflow-hidden rounded-[24px] border border-white/10 bg-black/40" }, /* @__PURE__ */ React.createElement("img", { src: beforeUrl, alt: beforeAlt, className: "block w-full object-cover" }), /* @__PURE__ */ React.createElement("div", { className: "pointer-events-none absolute inset-y-0 left-0 overflow-hidden border-r border-white/80", style: { width: `${position2}%` } }, /* @__PURE__ */ React.createElement("img", { src: afterUrl, alt: afterAlt, className: "block h-full w-full object-cover" })), /* @__PURE__ */ React.createElement("div", { className: "pointer-events-none absolute inset-y-0 left-0", style: { left: `calc(${position2}% - 1px)` } }, /* @__PURE__ */ React.createElement("div", { className: "flex h-full items-center" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-full border border-white/80 bg-black/60 text-white shadow-[0_0_30px_rgba(15,23,42,0.5)]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-left-right text-xs" })))), /* @__PURE__ */ React.createElement("div", { className: "pointer-events-none absolute left-3 top-3 rounded-full border border-white/10 bg-[#08111dd8] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-100" }, "Original"), /* @__PURE__ */ React.createElement("div", { className: "pointer-events-none absolute right-3 top-3 rounded-full border border-emerald-300/20 bg-emerald-400/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-emerald-100" }, "Enhanced")), /* @__PURE__ */ React.createElement("label", { className: "mt-5 block" }, /* @__PURE__ */ React.createElement("span", { className: "sr-only" }, "Adjust before and after comparison slider"), /* @__PURE__ */ React.createElement( + "input", + { + type: "range", + min: "0", + max: "100", + step: "1", + value: position2, + onChange: (event) => setPosition(Number(event.target.value || 50)), + className: "h-2 w-full cursor-pointer appearance-none rounded-full bg-white/10 accent-sky-300", + "aria-label": "Adjust before and after comparison slider" + } + ))); +} +function formatDate$g(value) { + return formatEnhanceDate(value); +} +function DetailRow$1({ label, value }) { + return /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4 border-b border-white/[0.06] py-3 last:border-b-0 last:pb-0" }, /* @__PURE__ */ React.createElement("dt", { className: "text-sm text-slate-400" }, label), /* @__PURE__ */ React.createElement("dd", { className: "text-right text-sm text-white" }, value)); +} +function EnhanceShow() { + const { props } = X$1(); + const job = props.job || {}; + const flash = props.flash || {}; + const errors = props.errors || {}; + const statusKey = String(job.status || "").toLowerCase(); + const statusCopy = { + pending: "Waiting to be queued.", + queued: "Waiting for processor.", + processing: "Enhancing image.", + completed: "Enhanced image ready.", + failed: "Enhancement failed.", + cancelled: "Cancelled.", + expired: "Enhanced output expired and cleaned files were removed." + }[statusKey] || "Unknown status."; + React.useEffect(() => { + if (!["pending", "queued", "processing"].includes(statusKey)) { + return void 0; + } + const timer = window.setTimeout(() => { + At.reload({ only: ["job", "flash"], preserveScroll: true }); + }, 8e3); + return () => window.clearTimeout(timer); + }, [statusKey]); + const canCompare = Boolean(job.source_url && job.output_url && job.status === "completed"); + return /* @__PURE__ */ React.createElement("div", { className: "w-full pb-16 pt-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: `Enhance Job #${job.id || ""}` }), /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(14,165,233,0.18),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 xl:flex-row xl:items-end xl:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement(EnhanceStatusBadge, { status: job.status }), /* @__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.16em] text-slate-200" }, job.scale, "x"), /* @__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.16em] text-slate-200" }, job.mode), /* @__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.16em] text-slate-200" }, job.engine)), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Enhance job #", job.id), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, statusCopy)), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: props.indexUrl, 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]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left text-[10px]" }), "Back to jobs"), /* @__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-plus text-[10px]" }), "New enhance"), job.download_url ? /* @__PURE__ */ React.createElement("a", { href: job.download_url, className: "inline-flex items-center gap-2 rounded-full border border-emerald-300/20 bg-emerald-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-50 transition hover:bg-emerald-400/20" }, "Download enhanced") : null, job.can_retry ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(job.retry_url, {}, { preserveScroll: true }), className: "inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-amber-50 transition hover:bg-amber-400/20" }, "Retry") : null, job.can_delete ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => { + if (!window.confirm("Delete this enhance job and its generated files?")) return; + 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_60 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: EnhanceShow +}, Symbol.toStringTag, { value: "Module" })); function PostActions({ post: post2, isLoggedIn, @@ -81454,7 +82659,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_58 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_61 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: FollowingFeed }, Symbol.toStringTag, { value: "Module" })); @@ -81514,7 +82719,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_59 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_62 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: HashtagFeed }, Symbol.toStringTag, { value: "Module" })); @@ -81572,7 +82777,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_60 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_63 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SavedFeed }, Symbol.toStringTag, { value: "Module" })); @@ -81697,7 +82902,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_61 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_64 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: SearchFeed }, Symbol.toStringTag, { value: "Module" })); @@ -81759,7 +82964,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_62 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_65 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: TrendingFeed }, Symbol.toStringTag, { value: "Module" })); @@ -81786,7 +82991,7 @@ function ThreadRow({ thread, isFirst = false }) { ].filter(Boolean).join(" ") }, /* @__PURE__ */ React.createElement("div", { className: "mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-sky-500/10 text-sky-400 group-hover:bg-sky-500/15 transition-colors" }, isPinned ? /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React.createElement("path", { d: "M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2z" })) : /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("h3", { className: "m-0 truncate text-sm font-semibold leading-tight text-white transition-colors group-hover:text-sky-300" }, title), isPinned && /* @__PURE__ */ React.createElement("span", { className: "shrink-0 rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-300" }, "Pinned")), excerpt && /* @__PURE__ */ React.createElement("p", { className: "mt-0.5 truncate text-xs text-white/40" }, stripHtml$3(excerpt).slice(0, 180)), /* @__PURE__ */ React.createElement("div", { className: "mt-1.5 flex flex-wrap items-center gap-3 text-xs text-white/35" }, /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "7", r: "4" })), author), /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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" })), posts, " ", posts === 1 ? "reply" : "replies"), lastUpdate && /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ React.createElement("polyline", { points: "12 6 12 12 16 14" })), formatDate$d(lastUpdate)))), + /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("h3", { className: "m-0 truncate text-sm font-semibold leading-tight text-white transition-colors group-hover:text-sky-300" }, title), isPinned && /* @__PURE__ */ React.createElement("span", { className: "shrink-0 rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-300" }, "Pinned")), excerpt && /* @__PURE__ */ React.createElement("p", { className: "mt-0.5 truncate text-xs text-white/40" }, stripHtml$3(excerpt).slice(0, 180)), /* @__PURE__ */ React.createElement("div", { className: "mt-1.5 flex flex-wrap items-center gap-3 text-xs text-white/35" }, /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "7", r: "4" })), author), /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", 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" })), posts, " ", posts === 1 ? "reply" : "replies"), lastUpdate && /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ React.createElement("polyline", { points: "12 6 12 12 16 14" })), formatDate$f(lastUpdate)))), /* @__PURE__ */ React.createElement("div", { className: "mt-1 shrink-0" }, /* @__PURE__ */ React.createElement("span", { className: "inline-flex min-w-[2rem] items-center justify-center rounded-full bg-white/[0.06] px-2.5 py-1 text-xs font-medium text-white/60" }, posts)) ); } @@ -81817,7 +83022,7 @@ function stripHtml$3(html2) { } return decodeEntities(html2).replace(/<[^>]*>/g, ""); } -function formatDate$d(dateStr) { +function formatDate$f(dateStr) { try { const d2 = new Date(dateStr); return d2.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }) + " " + d2.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); @@ -81844,7 +83049,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_63 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_66 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumCategory }, Symbol.toStringTag, { value: "Module" })); @@ -82108,7 +83313,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_64 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_67 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumEditPost }, Symbol.toStringTag, { value: "Module" })); @@ -82199,7 +83404,7 @@ function formatLastActivity(value) { } return `Updated ${date.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}`; } -const __vite_glob_0_65 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_68 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumIndex }, Symbol.toStringTag, { value: "Module" })); @@ -82320,7 +83525,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_66 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_69 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumNewThread }, Symbol.toStringTag, { value: "Module" })); @@ -82335,7 +83540,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_67 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_70 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumSection }, Symbol.toStringTag, { value: "Module" })); @@ -82414,7 +83619,7 @@ function PostCard({ post: post2, thread, isOp = false, isAuthenticated = false, id: `post-${postId}`, className: "overflow-hidden rounded-2xl border border-white/[0.06] bg-nova-800/50 backdrop-blur transition-all hover:border-white/10" }, - /* @__PURE__ */ React.createElement("header", { className: "flex flex-col gap-3 border-b border-white/[0.06] px-5 py-4 sm:flex-row sm:items-center sm:justify-between" }, /* @__PURE__ */ React.createElement(AuthorBadge, { user: author }), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 text-xs text-zinc-500" }, postedAt && /* @__PURE__ */ React.createElement("time", { dateTime: postedAt }, formatDate$c(postedAt)), isOp && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-cyan-500/15 px-2.5 py-0.5 text-[11px] font-medium text-cyan-300" }, "OP"))), + /* @__PURE__ */ React.createElement("header", { className: "flex flex-col gap-3 border-b border-white/[0.06] px-5 py-4 sm:flex-row sm:items-center sm:justify-between" }, /* @__PURE__ */ React.createElement(AuthorBadge, { user: author }), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 text-xs text-zinc-500" }, postedAt && /* @__PURE__ */ React.createElement("time", { dateTime: postedAt }, formatDate$e(postedAt)), isOp && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-cyan-500/15 px-2.5 py-0.5 text-[11px] font-medium text-cyan-300" }, "OP"))), /* @__PURE__ */ React.createElement("div", { className: "px-5 py-5" }, /* @__PURE__ */ React.createElement( "div", { @@ -82479,7 +83684,7 @@ function AttachmentItem({ attachment }) { function getCsrf$1() { return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ?? ""; } -function formatDate$c(dateStr) { +function formatDate$e(dateStr) { try { const d2 = new Date(dateStr); return d2.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }) + " " + d2.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); @@ -82496,7 +83701,7 @@ function formatTimeAgo(dateStr) { if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; - return formatDate$c(dateStr); + return formatDate$e(dateStr); } catch { return ""; } @@ -82621,7 +83826,7 @@ function ForumThread({ url.searchParams.set("sort", newSort); window.location.href = url.toString(); }, [currentSort]); - return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo }), /* @__PURE__ */ React.createElement("div", { className: "px-4 pt-10 pb-20 sm:px-6 lg:px-8 max-w-5xl mx-auto space-y-5" }, /* @__PURE__ */ React.createElement(Breadcrumbs, { items: breadcrumbs }), status2 && /* @__PURE__ */ React.createElement("div", { className: "rounded-xl border border-emerald-500/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-300" }, status2), /* @__PURE__ */ React.createElement("section", { className: "rounded-2xl border border-white/[0.06] bg-nova-800/50 p-5 backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("h1", { className: "text-2xl font-bold text-white leading-snug" }, thread?.title), thread?.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-6 text-zinc-300 sm:text-[15px]" }, thread.description) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap items-center gap-2 text-xs text-zinc-500" }, /* @__PURE__ */ React.createElement("span", null, "By ", author?.name ?? "Unknown"), /* @__PURE__ */ React.createElement("span", { className: "text-zinc-700" }, "•"), thread?.created_at && /* @__PURE__ */ React.createElement("time", { dateTime: thread.created_at }, formatDate$b(thread.created_at)))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-xs" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-sky-500/15 px-2.5 py-1 text-sky-300" }, number(thread?.views ?? 0), " views"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-cyan-500/15 px-2.5 py-1 text-cyan-300" }, number(replyCount), " replies"), thread?.is_pinned && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-amber-500/15 px-2.5 py-1 text-amber-300" }, "Pinned"), thread?.is_locked && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-red-500/15 px-2.5 py-1 text-red-300" }, "Locked"))), canModerate && /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center gap-2 border-t border-white/[0.06] pt-3" }, thread?.is_locked ? /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/unlock`, csrf: csrfToken2, label: "Unlock", variant: "danger" }) : /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/lock`, csrf: csrfToken2, label: "Lock", variant: "danger" }), thread?.is_pinned ? /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/unpin`, csrf: csrfToken2, label: "Unpin", variant: "warning" }) : /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/pin`, csrf: csrfToken2, label: "Pin", variant: "warning" }))), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-zinc-500" }, number(replyCount), " ", replyCount === 1 ? "reply" : "replies"), /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(SeoHead, { seo }), /* @__PURE__ */ React.createElement("div", { className: "px-4 pt-10 pb-20 sm:px-6 lg:px-8 max-w-5xl mx-auto space-y-5" }, /* @__PURE__ */ React.createElement(Breadcrumbs, { items: breadcrumbs }), status2 && /* @__PURE__ */ React.createElement("div", { className: "rounded-xl border border-emerald-500/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-300" }, status2), /* @__PURE__ */ React.createElement("section", { className: "rounded-2xl border border-white/[0.06] bg-nova-800/50 p-5 backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("h1", { className: "text-2xl font-bold text-white leading-snug" }, thread?.title), thread?.description ? /* @__PURE__ */ React.createElement("p", { className: "mt-3 max-w-3xl text-sm leading-6 text-zinc-300 sm:text-[15px]" }, thread.description) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex flex-wrap items-center gap-2 text-xs text-zinc-500" }, /* @__PURE__ */ React.createElement("span", null, "By ", author?.name ?? "Unknown"), /* @__PURE__ */ React.createElement("span", { className: "text-zinc-700" }, "•"), thread?.created_at && /* @__PURE__ */ React.createElement("time", { dateTime: thread.created_at }, formatDate$d(thread.created_at)))), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-xs" }, /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-sky-500/15 px-2.5 py-1 text-sky-300" }, number(thread?.views ?? 0), " views"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-cyan-500/15 px-2.5 py-1 text-cyan-300" }, number(replyCount), " replies"), thread?.is_pinned && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-amber-500/15 px-2.5 py-1 text-amber-300" }, "Pinned"), thread?.is_locked && /* @__PURE__ */ React.createElement("span", { className: "rounded-full bg-red-500/15 px-2.5 py-1 text-red-300" }, "Locked"))), canModerate && /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap items-center gap-2 border-t border-white/[0.06] pt-3" }, thread?.is_locked ? /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/unlock`, csrf: csrfToken2, label: "Unlock", variant: "danger" }) : /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/lock`, csrf: csrfToken2, label: "Lock", variant: "danger" }), thread?.is_pinned ? /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/unpin`, csrf: csrfToken2, label: "Unpin", variant: "warning" }) : /* @__PURE__ */ React.createElement(ModForm, { action: `/forum/topic/${thread.slug}/pin`, csrf: csrfToken2, label: "Pin", variant: "warning" }))), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("p", { className: "text-xs text-zinc-500" }, number(replyCount), " ", replyCount === 1 ? "reply" : "replies"), /* @__PURE__ */ React.createElement( "button", { onClick: handleSortToggle, @@ -82665,7 +83870,7 @@ function ModForm({ action, csrf, label, variant }) { function number(n) { return (n ?? 0).toLocaleString(); } -function formatDate$b(dateStr) { +function formatDate$d(dateStr) { try { const d2 = new Date(dateStr); return d2.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric" }) + " " + d2.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); @@ -82673,7 +83878,7 @@ function formatDate$b(dateStr) { return ""; } } -const __vite_glob_0_68 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_71 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ForumThread }, Symbol.toStringTag, { value: "Module" })); @@ -82917,7 +84122,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_69 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_72 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupChallengeShow }, Symbol.toStringTag, { value: "Module" })); @@ -82927,7 +84132,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_70 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_73 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupEventShow }, Symbol.toStringTag, { value: "Module" })); @@ -83617,7 +84822,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_71 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_74 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupFaqPage }, Symbol.toStringTag, { value: "Module" })); @@ -84165,7 +85370,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_72 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_75 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -84240,7 +85445,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_73 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_76 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupIndex }, Symbol.toStringTag, { value: "Module" })); @@ -84270,7 +85475,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_74 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_77 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupPostShow }, Symbol.toStringTag, { value: "Module" })); @@ -84286,7 +85491,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_75 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_78 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupProjectShow }, Symbol.toStringTag, { value: "Module" })); @@ -84585,7 +85790,7 @@ function GroupQuickstartPage() { /* @__PURE__ */ React.createElement(QuickstartNextSteps, { items: nextSteps }) ))))); } -const __vite_glob_0_76 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_79 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupQuickstartPage }, Symbol.toStringTag, { value: "Module" })); @@ -84603,7 +85808,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_77 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_80 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupReleaseShow }, Symbol.toStringTag, { value: "Module" })); @@ -85031,7 +86236,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_78 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_81 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: GroupShow }, Symbol.toStringTag, { value: "Module" })); @@ -85289,7 +86494,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_79 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_82 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AccountHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -85645,7 +86850,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_80 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_83 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: AuthHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -86071,7 +87276,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_81 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_84 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: CardsHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -86980,7 +88185,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_82 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_85 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: HelpCenterPage }, Symbol.toStringTag, { value: "Module" })); @@ -87369,7 +88574,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_83 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_86 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -87824,7 +89029,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_84 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_87 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -88066,7 +89271,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_85 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_88 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: TroubleshootingHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -88484,7 +89689,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_86 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_89 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UploadHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -88947,7 +90152,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_87 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_90 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldsHelpPage }, Symbol.toStringTag, { value: "Module" })); @@ -89058,7 +90263,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: "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: "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_88 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_91 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: LeaderboardPage }, Symbol.toStringTag, { value: "Module" })); @@ -90996,7 +92201,7 @@ if (typeof document !== "undefined") { ); } } -const __vite_glob_0_89 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_92 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: MessagesPage }, Symbol.toStringTag, { value: "Module" })); @@ -91163,10 +92368,51 @@ function ArtworkMaturityQueue() { } )); } -const __vite_glob_0_91 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_94 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ArtworkMaturityQueue }, Symbol.toStringTag, { value: "Module" })); +function formatDate$c(value) { + return formatEnhanceDate(value); +} +function ModerationEnhanceIndex() { + const { props } = X$1(); + const [filters, setFilters] = React.useState(props.filters || {}); + const jobs = props.jobs?.data || []; + const flash = props.flash || {}; + React.useEffect(() => { + setFilters(props.filters || {}); + }, [props.filters]); + function applyFilters(event) { + event.preventDefault(); + At.get(props.indexUrl, filters, { preserveScroll: true, preserveState: true, replace: true }); + } + 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_95 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: ModerationEnhanceIndex +}, Symbol.toStringTag, { value: "Module" })); +function formatDate$b(value) { + return formatEnhanceDate(value); +} +function DetailRow({ label, value }) { + return /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-4 border-b border-white/[0.06] py-3 last:border-b-0 last:pb-0" }, /* @__PURE__ */ React.createElement("dt", { className: "text-sm text-slate-400" }, label), /* @__PURE__ */ React.createElement("dd", { className: "text-right text-sm text-white" }, value)); +} +function ModerationEnhanceShow() { + const { props } = X$1(); + const job = props.job || {}; + const flash = props.flash || {}; + const errors = props.errors || {}; + return /* @__PURE__ */ React.createElement("div", { className: "w-full px-6 pb-16 pt-8" }, /* @__PURE__ */ React.createElement(Se$1, { title: `Enhance Job #${job.id || ""}` }), /* @__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 xl:flex-row xl:items-end xl:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement(EnhanceStatusBadge, { status: job.status }), /* @__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.16em] text-slate-200" }, job.scale, "x"), /* @__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.16em] text-slate-200" }, job.mode), /* @__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.16em] text-slate-200" }, job.engine)), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Enhance job #", job.id), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-relaxed text-slate-300" }, "Created by ", job.user?.name || "Unknown user", " ", job.user?.username ? `(@${job.user.username})` : "", ".")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement(xe, { href: props.indexUrl, 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]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-left text-[10px]" }), "Back to list"), job.download_url ? /* @__PURE__ */ React.createElement("a", { href: job.download_url, className: "inline-flex items-center gap-2 rounded-full border border-emerald-300/20 bg-emerald-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-50 transition hover:bg-emerald-400/20" }, "Download output") : null, job.can_retry ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(job.retry_url), className: "inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/12 px-5 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-amber-50 transition hover:bg-amber-400/20" }, "Retry") : null, job.can_mark_failed ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(job.mark_failed_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" }, "Mark failed") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => { + if (!window.confirm("Delete this enhance job and any owned enhance files?")) return; + 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_96 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: ModerationEnhanceShow +}, Symbol.toStringTag, { value: "Module" })); function getCsrfToken$5() { if (typeof document === "undefined") return ""; return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || ""; @@ -91298,7 +92544,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_92 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_97 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldWebStoriesIndex }, Symbol.toStringTag, { value: "Module" })); @@ -91520,7 +92766,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_93 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_98 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldWebStoryEditor }, Symbol.toStringTag, { value: "Module" })); @@ -91941,7 +93187,7 @@ if (typeof document !== "undefined") { clientExports.createRoot(mountEl).render(/* @__PURE__ */ React.createElement(NewsComments, { ...props })); } } -const __vite_glob_0_94 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_99 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NewsComments }, Symbol.toStringTag, { value: "Module" })); @@ -91954,17 +93200,17 @@ function ensurePreviewOverlay() { return previewOverlay; } previewOverlay = document.createElement("div"); - previewOverlay.className = "fixed inset-0 z-[130] hidden items-center justify-center bg-[#020611e8] p-4 backdrop-blur-md"; + previewOverlay.className = "fixed inset-0 z-[130] hidden items-center justify-center bg-[#020611bf] p-6 backdrop-blur-xl"; previewOverlay.setAttribute("role", "dialog"); previewOverlay.setAttribute("aria-modal", "true"); previewOverlay.setAttribute("aria-label", "Image preview"); const frame = document.createElement("div"); - frame.className = "relative max-h-[92vh] max-w-6xl"; + frame.className = "relative flex max-h-[92vh] w-full max-w-6xl flex-col items-center gap-4"; previewImage = document.createElement("img"); - previewImage.className = "max-h-[92vh] max-w-full rounded-[28px] border border-white/10 shadow-[0_28px_90px_rgba(2,6,23,0.6)]"; + previewImage.className = "max-h-[78vh] max-w-full rounded-[28px] border border-white/10 shadow-[0_28px_90px_rgba(2,6,23,0.45)]"; previewImage.alt = "Image preview"; previewCaption = document.createElement("div"); - previewCaption.className = "absolute inset-x-0 bottom-0 rounded-b-[28px] bg-gradient-to-t from-black/80 to-transparent px-5 py-4 text-sm font-medium text-white/90"; + previewCaption.className = "w-full max-w-4xl rounded-[24px] border border-white/10 bg-black/30 px-6 py-4 text-center text-sm font-medium leading-6 text-white/90 backdrop-blur-md"; const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.setAttribute("aria-label", "Close image preview"); @@ -92028,7 +93274,7 @@ if (typeof document !== "undefined") { document.addEventListener("keydown", handleKeyDown); } const NewsImagePreview = null; -const __vite_glob_0_95 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_100 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: NewsImagePreview }, Symbol.toStringTag, { value: "Module" })); @@ -92992,7 +94238,7 @@ function ProfileGallery() { } )))); } -const __vite_glob_0_96 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_101 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileGallery }, Symbol.toStringTag, { value: "Module" })); @@ -95842,7 +97088,7 @@ function ProfileShow() { } )))); } -const __vite_glob_0_97 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_102 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileShow }, Symbol.toStringTag, { value: "Module" })); @@ -97270,7 +98516,7 @@ function ProfileEdit() { ) ); } -const __vite_glob_0_98 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_103 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: ProfileEdit }, Symbol.toStringTag, { value: "Module" })); @@ -97783,7 +99029,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_99 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_104 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioActivity }, Symbol.toStringTag, { value: "Module" })); @@ -97874,7 +99120,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_100 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_105 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -98101,6 +99347,8 @@ function StudioContentBrowser({ quickCreate = [], hideModuleFilter = false, hideBucketFilter = false, + defaultSort = "updated_desc", + sortStorageKey = null, emptyTitle = "Nothing here yet", emptyBody = "Try adjusting filters or create something new." }) { @@ -98112,7 +99360,7 @@ function StudioContentBrowser({ const [pendingFilters, setPendingFilters] = reactExports.useState({ q: "", bucket: "all", - sort: "updated_desc", + sort: defaultSort, content_type: "all", category: "all", tag: "" @@ -98163,12 +99411,36 @@ function StudioContentBrowser({ setPendingFilters({ q: filters.q || "", bucket: filters.bucket || "all", - sort: filters.sort || "updated_desc", + sort: filters.sort || defaultSort, content_type: filters.content_type || "all", category: filters.category || "all", tag: filters.tag || "" }); - }, [filters.q, filters.bucket, filters.sort, filters.content_type, filters.category, filters.tag]); + }, [filters.q, filters.bucket, filters.sort, filters.content_type, filters.category, filters.tag, defaultSort]); + reactExports.useEffect(() => { + if (!sortStorageKey) { + return; + } + const params = new URLSearchParams(window.location.search); + if (params.has("sort")) { + return; + } + const storedSort = window.localStorage.getItem(sortStorageKey); + const sortOptions = new Set((listing?.sort_options || []).map((option) => option.value)); + const activeSort = filters.sort || defaultSort; + if (!storedSort || !sortOptions.has(storedSort) || storedSort === activeSort) { + return; + } + At.get(window.location.pathname, { + ...filters, + sort: storedSort, + page: 1 + }, { + preserveScroll: true, + preserveState: true, + replace: true + }); + }, [sortStorageKey, listing?.sort_options, filters, defaultSort]); const updateQuery = (patch2) => { const next = { ...filters, @@ -98184,6 +99456,9 @@ function StudioContentBrowser({ patch: patch2 } }); + if (sortStorageKey && typeof next.sort === "string" && next.sort !== "") { + window.localStorage.setItem(sortStorageKey, next.sort); + } At.get(window.location.pathname, next, { preserveScroll: true, preserveState: true, @@ -98513,7 +99788,7 @@ function StudioContentBrowser({ id: "studio-filter-sort", options: selectOptions(listing?.sort_options || []), value: pendingFilters.sort, - onChange: (nextValue) => setPendingFilter("sort", nextValue ?? "updated_desc"), + onChange: (nextValue) => setPendingFilter("sort", nextValue ?? defaultSort), placeholder: "Recently updated", searchable: false } @@ -98690,7 +99965,7 @@ function StudioArchived() { } )); } -const __vite_glob_0_101 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_106 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArchived }, Symbol.toStringTag, { value: "Module" })); @@ -98726,10 +100001,178 @@ 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_102 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_107 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworkAnalytics }, Symbol.toStringTag, { value: "Module" })); +function ToolbarButton({ title, onClick, children, className = "" }) { + return /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + title, + onMouseDown: (event) => { + event.preventDefault(); + onClick?.(); + }, + className: [ + "inline-flex h-8 min-w-8 items-center justify-center rounded-md px-2 text-xs font-semibold text-white/60 transition", + "hover:bg-white/10 hover:text-white", + className + ].join(" ") + }, + children + ); +} +function UploadDescriptionEditor({ id, value, onChange, placeholder, error, rows = 8 }) { + const [tab2, setTab] = reactExports.useState("write"); + const textareaRef = reactExports.useRef(null); + const focusTextarea = reactExports.useCallback(() => { + requestAnimationFrame(() => { + textareaRef.current?.focus(); + }); + }, []); + const wrapSelection = reactExports.useCallback((before, after, placeholderText = "text") => { + const textarea = textareaRef.current; + if (!textarea) return; + const current = String(value || ""); + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selected = current.slice(start, end); + const replacement = `${before}${selected || placeholderText}${after}`; + const next = current.slice(0, start) + replacement + current.slice(end); + onChange?.(next); + requestAnimationFrame(() => { + textarea.focus(); + if (selected) { + textarea.selectionStart = start + replacement.length; + textarea.selectionEnd = start + replacement.length; + } else { + textarea.selectionStart = start + before.length; + textarea.selectionEnd = start + before.length + placeholderText.length; + } + }); + }, [onChange, value]); + const prefixLines = reactExports.useCallback((prefix) => { + const textarea = textareaRef.current; + if (!textarea) return; + const current = String(value || ""); + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selected = current.slice(start, end); + const fallback = prefix.endsWith(". ") ? `${prefix}item` : `${prefix}item`; + const source = selected || fallback; + const nextBlock = source.split("\n").map((line) => `${prefix}${line}`).join("\n"); + const next = current.slice(0, start) + nextBlock + current.slice(end); + onChange?.(next); + requestAnimationFrame(() => { + textarea.focus(); + textarea.selectionStart = start; + textarea.selectionEnd = start + nextBlock.length; + }); + }, [onChange, value]); + const insertLink = reactExports.useCallback(() => { + const textarea = textareaRef.current; + if (!textarea) return; + const current = String(value || ""); + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selected = current.slice(start, end); + const replacement = selected && /^https?:\/\//i.test(selected) ? `[link](${selected})` : `[link](https://)`; + const next = current.slice(0, start) + replacement + current.slice(end); + onChange?.(next); + requestAnimationFrame(() => { + textarea.focus(); + if (selected && /^https?:\/\//i.test(selected)) { + textarea.selectionStart = start + 1; + textarea.selectionEnd = start + 5; + } else { + const urlStart = start + replacement.indexOf("https://"); + textarea.selectionStart = urlStart; + textarea.selectionEnd = urlStart + "https://".length; + } + }); + }, [onChange, value]); + const insertAtCursor = reactExports.useCallback((text2) => { + const textarea = textareaRef.current; + if (!textarea) { + onChange?.(`${String(value || "")}${text2}`); + return; + } + const current = String(value || ""); + const start = textarea.selectionStart ?? current.length; + const end = textarea.selectionEnd ?? current.length; + const next = current.slice(0, start) + text2 + current.slice(end); + onChange?.(next); + requestAnimationFrame(() => { + textarea.focus(); + textarea.selectionStart = start + text2.length; + textarea.selectionEnd = start + text2.length; + }); + }, [onChange, value]); + const handleKeyDown2 = reactExports.useCallback((event) => { + const withModifier = event.ctrlKey || event.metaKey; + if (!withModifier) return; + switch (event.key.toLowerCase()) { + case "b": + event.preventDefault(); + wrapSelection("**", "**"); + break; + case "i": + event.preventDefault(); + wrapSelection("*", "*"); + break; + case "k": + event.preventDefault(); + insertLink(); + break; + case "e": + event.preventDefault(); + wrapSelection("`", "`"); + break; + } + }, [insertLink, wrapSelection]); + const previewValue = String(value || "").trim(); + return /* @__PURE__ */ React.createElement("div", { className: `overflow-hidden rounded-xl border bg-white/10 ${error ? "border-red-300/60" : "border-white/15"}` }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between border-b border-white/10 px-2 py-1.5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setTab("write"), + className: `rounded-md px-2.5 py-1 text-xs font-medium transition ${tab2 === "write" ? "bg-white/12 text-white" : "text-white/55 hover:text-white/80"}` + }, + "Write" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => setTab("preview"), + className: `rounded-md px-2.5 py-1 text-xs font-medium transition ${tab2 === "preview" ? "bg-white/12 text-white" : "text-white/55 hover:text-white/80"}` + }, + "Preview" + )), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-white/40" }, "Safe formatting only")), tab2 === "write" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-1 border-b border-white/10 px-2 py-1" }, /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Bold (Ctrl+B)", onClick: () => wrapSelection("**", "**") }, "B"), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Italic (Ctrl+I)", onClick: () => wrapSelection("*", "*") }, "I"), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Inline code (Ctrl+E)", onClick: () => wrapSelection("`", "`") }, ""), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Link (Ctrl+K)", onClick: insertLink }, "Link"), /* @__PURE__ */ React.createElement("span", { className: "mx-1 h-4 w-px bg-white/10", "aria-hidden": "true" }), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Bulleted list", onClick: () => prefixLines("- ") }, "List"), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Numbered list", onClick: () => prefixLines("1. ") }, "1."), /* @__PURE__ */ React.createElement(ToolbarButton, { title: "Quote", onClick: () => prefixLines("> ") }, "Quote"), /* @__PURE__ */ React.createElement("span", { className: "mx-1 h-4 w-px bg-white/10", "aria-hidden": "true" }), /* @__PURE__ */ React.createElement(EmojiPickerButton, { onEmojiSelect: insertAtCursor, className: "h-8 w-8 rounded-md text-white/60 hover:bg-white/10 hover:text-white" })), /* @__PURE__ */ React.createElement( + "textarea", + { + id, + ref: textareaRef, + value, + onChange: (event) => onChange?.(event.target.value), + onKeyDown: handleKeyDown2, + rows, + className: "w-full resize-y bg-transparent px-3 py-3 text-sm text-white placeholder-white/45 focus:outline-none", + placeholder + } + ), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center justify-between gap-2 px-3 pb-2 text-[11px] text-white/45" }, /* @__PURE__ */ React.createElement("span", null, "Supports bold, italic, code, links, lists, quotes, and emoji."), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: focusTextarea, className: "text-white/50 transition hover:text-white/80" }, "Continue editing"))), tab2 === "preview" && /* @__PURE__ */ React.createElement("div", { className: "min-h-[188px] px-3 py-3" }, previewValue ? /* @__PURE__ */ React.createElement("div", { className: "prose prose-invert prose-sm max-w-none text-white/85 [&_a]:text-sky-300 [&_a]:no-underline hover:[&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-white/20 [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-white/10 [&_code]:px-1 [&_code]:py-0.5 [&_ul]:pl-4 [&_ol]:pl-4" }, /* @__PURE__ */ React.createElement( + Markdown, + { + allowedElements: ["p", "strong", "em", "a", "code", "pre", "ul", "ol", "li", "blockquote", "br"], + unwrapDisallowed: true, + components: { + a: ({ href, children }) => /* @__PURE__ */ React.createElement("a", { href, target: "_blank", rel: "noopener noreferrer nofollow" }, children) + } + }, + previewValue + )) : /* @__PURE__ */ React.createElement("p", { className: "text-sm italic text-white/35" }, "Nothing to preview yet."))); +} function FormField({ label, required, error, hint, htmlFor, children, className = "" }) { return /* @__PURE__ */ React.createElement("div", { className: `flex flex-col gap-1.5 ${className}` }, label && /* @__PURE__ */ React.createElement( "label", @@ -99694,6 +101137,58 @@ function WorldSubmissionSelector({ ); }))); } +const HTML_TAG_RE = /<[a-z][^>]*>/i; +const MAX_CONTENT_LENGTH = 1e4; +function decodeHtmlEntities(value) { + const decoded = String(value || ""); + if (typeof document === "undefined") { + return decoded.replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'"); + } + const textarea = document.createElement("textarea"); + textarea.innerHTML = decoded; + return textarea.value; +} +function stripResidualTags(value) { + return String(value || "").replace(/<[^>]+>/g, ""); +} +function normalizeMarkdownLiteContent(value) { + const raw = String(value || ""); + const trimmed = raw.trim(); + if (!trimmed || !HTML_TAG_RE.test(trimmed)) { + return raw; + } + const normalized = raw.replace(/<\s*a[^>]*href=(['"])(.*?)\1[^>]*>([\s\S]*?)<\s*\/a\s*>/gi, (_2, __, href, label) => { + const text2 = stripResidualTags(label).trim() || href; + return `[${text2}](${href})`; + }).replace(/<\s*(strong|b)(?:\s+[^>]*)?>([\s\S]*?)<\s*\/\s*\1\s*>/gi, (_2, __, text2) => `**${stripResidualTags(text2)}**`).replace(/<\s*(em|i)(?:\s+[^>]*)?>([\s\S]*?)<\s*\/\s*\1\s*>/gi, (_2, __, text2) => `*${stripResidualTags(text2)}*`).replace(/<\s*code(?:\s+[^>]*)?>([\s\S]*?)<\s*\/code\s*>/gi, (_2, text2) => `\`${stripResidualTags(text2)}\``).replace(/<\s*br\s*\/?>/gi, "\n").replace(/<\s*\/p\s*>/gi, "\n\n").replace(/<\s*p(?:\s+[^>]*)?>/gi, "").replace(/<\s*li(?:\s+[^>]*)?>([\s\S]*?)<\s*\/li\s*>/gi, (_2, text2) => `- ${stripResidualTags(text2).trim()} +`).replace(/<\s*\/ul\s*>|<\s*\/ol\s*>/gi, "\n").replace(/<\s*(ul|ol)(?:\s+[^>]*)?>/gi, "").replace(/<\s*blockquote(?:\s+[^>]*)?>([\s\S]*?)<\s*\/blockquote\s*>/gi, (_2, text2) => { + const lines = stripResidualTags(text2).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => `> ${line}`); + return `${lines.join("\n")} + +`; + }).replace(/<[^>]+>/g, ""); + return decodeHtmlEntities(normalized).replace(/\r\n?/g, "\n").replace(/[\t ]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim(); +} +function validateMarkdownLiteContent(value) { + const raw = String(value || ""); + const trimmed = raw.trim(); + if (!trimmed) return []; + const errors = []; + if (trimmed.length > MAX_CONTENT_LENGTH) { + errors.push("Content exceeds maximum length of 10,000 characters."); + } + if (HTML_TAG_RE.test(trimmed)) { + errors.push("HTML tags are not allowed. Use Markdown formatting instead."); + } + const emojiCount = countEmoji(trimmed); + if (emojiCount > FLOOD_COUNT_THRESHOLD) { + errors.push("Too many emoji. Please limit emoji usage."); + } + if (emojiCount > 5 && trimmed.length > 0 && emojiCount / trimmed.length > FLOOD_DENSITY_THRESHOLD) { + errors.push("Content is mostly emoji. Please add some text."); + } + return errors; +} const TABS = [ { id: "details", label: "Details", icon: "fa-solid fa-pen-fancy" }, { id: "media", label: "Media", icon: "fa-solid fa-photo-film" }, @@ -99893,7 +101388,7 @@ function StudioArtworkEdit() { const [categoryId, setCategoryId] = reactExports.useState(artwork?.parent_category_id || null); const [subCategoryId, setSubCategoryId] = reactExports.useState(artwork?.sub_category_id || null); const [title, setTitle] = reactExports.useState(artwork?.title || ""); - const [description, setDescription] = reactExports.useState(artwork?.description || ""); + const [description, setDescription] = reactExports.useState(() => normalizeMarkdownLiteContent(artwork?.description || "")); const [tagSlugs, setTagSlugs] = reactExports.useState(() => (artwork?.tags || []).map((t) => t.slug || t.name)); const [visibility, setVisibility] = reactExports.useState(artwork?.visibility || (artwork?.is_public ? "public" : "private")); const [publishMode, setPublishMode] = reactExports.useState(artwork?.publish_mode || (artwork?.artwork_status === "scheduled" ? "schedule" : "now")); @@ -100079,7 +101574,7 @@ function StudioArtworkEdit() { const syncCurrentPayload = reactExports.useCallback((current) => { if (!current) return; setTitle(current.title || ""); - setDescription(current.description || ""); + setDescription(normalizeMarkdownLiteContent(current.description || "")); setTagSlugs(Array.isArray(current.tags) ? current.tags : []); setContentTypeId(current.content_type_id || null); setCategoryId(current.category_id || null); @@ -100674,14 +102169,14 @@ function StudioArtworkEdit() { error: errors.title?.[0] } ), /* @__PURE__ */ React.createElement(FormField, { label: /* @__PURE__ */ React.createElement(FieldLabel, { label: "Description", actionLabel: "Description", onAction: () => requestAiIntent("description"), disabled: aiAction !== "", loading: aiAction === "analyze" || aiAction === "regenerate" }), htmlFor: "artwork-description" }, /* @__PURE__ */ React.createElement( - RichTextEditor, + UploadDescriptionEditor, { - content: description, + id: "artwork-description", + value: description, onChange: handleDescriptionChange, placeholder: "Describe your artwork, tools, inspiration…", error: errors.description?.[0], - minHeight: 12, - autofocus: false + rows: 10 } )), /* @__PURE__ */ React.createElement(Section, { className: "space-y-5 border-white/8 bg-white/[0.02]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(SectionTitle, { icon: "fa-solid fa-users" }, "Attribution"), /* @__PURE__ */ React.createElement("p", { className: "-mt-2 text-sm text-slate-400" }, "Switch between personal and group context, then maintain primary author and contributor credits without leaving the edit screen.")), /* @__PURE__ */ React.createElement("span", { className: `rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${groupSlug ? "border-sky-300/20 bg-sky-300/10 text-sky-100" : "border-white/10 bg-white/[0.04] text-slate-300"}` }, selectedGroupOption ? `Group: ${selectedGroupOption.name}` : "Personal publish")), /* @__PURE__ */ React.createElement( NovaSelect, @@ -101283,7 +102778,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_103 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_108 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworkEdit }, Symbol.toStringTag, { value: "Module" })); @@ -101293,9 +102788,18 @@ function SummaryCard$3({ label, value, icon }) { function StudioArtworks() { const { props } = X$1(); const summary = props.summary || {}; - return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 grid gap-4 md:grid-cols-4" }, /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Artworks", value: summary.count, icon: "fa-solid fa-images" }), /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Drafts", value: summary.draft_count, icon: "fa-solid fa-file-pen" }), /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Published", value: summary.published_count, icon: "fa-solid fa-rocket" }), /* @__PURE__ */ React.createElement("a", { href: "/upload", className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Upload artwork"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Start a new visual upload flow without leaving Creator Studio."))), /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: props.quickCreate, hideModuleFilter: true })); + return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 grid gap-4 md:grid-cols-4" }, /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Artworks", value: summary.count, icon: "fa-solid fa-images" }), /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Drafts", value: summary.draft_count, icon: "fa-solid fa-file-pen" }), /* @__PURE__ */ React.createElement(SummaryCard$3, { label: "Published", value: summary.published_count, icon: "fa-solid fa-rocket" }), /* @__PURE__ */ React.createElement("a", { href: "/upload", className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Upload artwork"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Start a new visual upload flow without leaving Creator Studio."))), /* @__PURE__ */ React.createElement( + StudioContentBrowser, + { + listing: props.listing, + quickCreate: props.quickCreate, + hideModuleFilter: true, + defaultSort: "published_desc", + sortStorageKey: "studio-artworks-sort" + } + )); } -const __vite_glob_0_104 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_109 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioArtworks }, Symbol.toStringTag, { value: "Module" })); @@ -101421,7 +102925,7 @@ function StudioAssets() { /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }) )))); } -const __vite_glob_0_105 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioAssets }, Symbol.toStringTag, { value: "Module" })); @@ -101593,7 +103097,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_106 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_111 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCalendar }, Symbol.toStringTag, { value: "Module" })); @@ -101615,7 +103119,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_107 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_112 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardAnalytics }, Symbol.toStringTag, { value: "Module" })); @@ -102759,7 +104263,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_108 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_113 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardEditor }, Symbol.toStringTag, { value: "Module" })); @@ -102769,9 +104273,19 @@ function StatCard$1({ label, value, icon }) { function StudioCardsIndex() { const { props } = X$1(); const summary = props.summary || {}; - return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.15),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-5 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75" }, "Creation surface"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Build quote cards, mood cards, and visual text art."), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Cards now live inside the same shared Creator Studio queue as artworks, collections, and stories, while keeping the dedicated editor and analytics flow.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("a", { href: "/studio/cards/create", 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" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-plus" }), "New card"), /* @__PURE__ */ React.createElement("a", { href: props.publicBrowseUrl, className: "inline-flex items-center gap-2 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]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-compass" }), "Browse public cards")))), /* @__PURE__ */ React.createElement("section", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$1, { label: "All cards", value: summary.count || 0, icon: "fa-layer-group" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Drafts", value: summary.draft_count || 0, icon: "fa-file-lines" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Archived", value: summary.archived_count || 0, icon: "fa-box-archive" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Published", value: summary.published_count || 0, icon: "fa-earth-americas" })), /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: props.quickCreate, hideModuleFilter: true, emptyTitle: "No cards yet", emptyBody: "Create your first Nova card and it will appear here alongside your other Creator Studio content." }))); + return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("section", { className: "rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.15),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-5 lg:flex-row lg:items-end lg:justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "max-w-3xl" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75" }, "Creation surface"), /* @__PURE__ */ React.createElement("h2", { className: "mt-3 text-3xl font-semibold tracking-[-0.04em] text-white" }, "Build quote cards, mood cards, and visual text art."), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-7 text-slate-300" }, "Cards now live inside the same shared Creator Studio queue as artworks, collections, and stories, while keeping the dedicated editor and analytics flow.")), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-3" }, /* @__PURE__ */ React.createElement("a", { href: "/studio/cards/create", 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" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-plus" }), "New card"), /* @__PURE__ */ React.createElement("a", { href: props.publicBrowseUrl, className: "inline-flex items-center gap-2 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]" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-compass" }), "Browse public cards")))), /* @__PURE__ */ React.createElement("section", { className: "mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4" }, /* @__PURE__ */ React.createElement(StatCard$1, { label: "All cards", value: summary.count || 0, icon: "fa-layer-group" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Drafts", value: summary.draft_count || 0, icon: "fa-file-lines" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Archived", value: summary.archived_count || 0, icon: "fa-box-archive" }), /* @__PURE__ */ React.createElement(StatCard$1, { label: "Published", value: summary.published_count || 0, icon: "fa-earth-americas" })), /* @__PURE__ */ React.createElement("section", { className: "mt-8" }, /* @__PURE__ */ React.createElement( + StudioContentBrowser, + { + listing: props.listing, + quickCreate: props.quickCreate, + hideModuleFilter: true, + sortStorageKey: "studio-cards-sort", + emptyTitle: "No cards yet", + emptyBody: "Create your first Nova card and it will appear here alongside your other Creator Studio content." + } + ))); } -const __vite_glob_0_109 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_114 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCardsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -102844,7 +104358,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_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_115 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioChallenges }, Symbol.toStringTag, { value: "Module" })); @@ -102854,9 +104368,17 @@ function SummaryCard$2({ label, value, icon }) { function StudioCollections() { const { props } = X$1(); const summary = props.summary || {}; - return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 grid gap-4 md:grid-cols-4" }, /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Collections", value: summary.count, icon: "fa-solid fa-layer-group" }), /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Drafts", value: summary.draft_count, icon: "fa-solid fa-file-pen" }), /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Published", value: summary.published_count, icon: "fa-solid fa-rocket" }), /* @__PURE__ */ React.createElement("a", { href: props.dashboardUrl, className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Collection dashboard"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Open the full collection workflow surface for rules, history, and collaboration."))), /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: props.quickCreate, hideModuleFilter: true })); + return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement("div", { className: "mb-6 grid gap-4 md:grid-cols-4" }, /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Collections", value: summary.count, icon: "fa-solid fa-layer-group" }), /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Drafts", value: summary.draft_count, icon: "fa-solid fa-file-pen" }), /* @__PURE__ */ React.createElement(SummaryCard$2, { label: "Published", value: summary.published_count, icon: "fa-solid fa-rocket" }), /* @__PURE__ */ React.createElement("a", { href: props.dashboardUrl, className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Collection dashboard"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Open the full collection workflow surface for rules, history, and collaboration."))), /* @__PURE__ */ React.createElement( + StudioContentBrowser, + { + listing: props.listing, + quickCreate: props.quickCreate, + hideModuleFilter: true, + sortStorageKey: "studio-collections-sort" + } + )); } -const __vite_glob_0_111 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_116 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioCollections }, Symbol.toStringTag, { value: "Module" })); @@ -103143,7 +104665,7 @@ function StudioComments() { /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-arrow-right" }) ))))); } -const __vite_glob_0_112 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_117 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioComments }, Symbol.toStringTag, { value: "Module" })); @@ -103159,7 +104681,7 @@ function StudioContentIndex() { } )); } -const __vite_glob_0_113 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_118 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioContentIndex }, Symbol.toStringTag, { value: "Module" })); @@ -103285,7 +104807,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_114 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_119 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioDashboard }, Symbol.toStringTag, { value: "Module" })); @@ -103302,7 +104824,7 @@ function StudioDrafts() { } )); } -const __vite_glob_0_115 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_120 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioDrafts }, Symbol.toStringTag, { value: "Module" })); @@ -103385,7 +104907,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_116 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_121 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioFeatured }, Symbol.toStringTag, { value: "Module" })); @@ -103411,7 +104933,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_117 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_122 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioFollowers }, Symbol.toStringTag, { value: "Module" })); @@ -103420,7 +104942,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_118 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_123 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupActivity }, Symbol.toStringTag, { value: "Module" })); @@ -103428,7 +104950,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_119 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_124 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupArtworks }, Symbol.toStringTag, { value: "Module" })); @@ -103469,7 +104991,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_120 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_125 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupAssets }, Symbol.toStringTag, { value: "Module" })); @@ -103541,7 +105063,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_121 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_126 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupChallengeEditor }, Symbol.toStringTag, { value: "Module" })); @@ -103550,7 +105072,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_122 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_127 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupChallenges }, Symbol.toStringTag, { value: "Module" })); @@ -103558,7 +105080,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_123 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_128 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupCollections }, Symbol.toStringTag, { value: "Module" })); @@ -103672,7 +105194,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_124 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_129 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupCreate }, Symbol.toStringTag, { value: "Module" })); @@ -103737,7 +105259,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_125 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_130 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupDashboard }, Symbol.toStringTag, { value: "Module" })); @@ -103776,7 +105298,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_126 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_131 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupEventEditor }, Symbol.toStringTag, { value: "Module" })); @@ -103785,7 +105307,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_127 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_132 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupEvents }, Symbol.toStringTag, { value: "Module" })); @@ -103812,7 +105334,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_128 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_133 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupInvitations }, Symbol.toStringTag, { value: "Module" })); @@ -103841,7 +105363,7 @@ function routeUrl(baseUrl, id, action) { if (!baseUrl) return ""; return `${String(baseUrl).replace(/\/$/, "")}/${id}/${action}`; } -const __vite_glob_0_129 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_134 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupJoinRequests }, Symbol.toStringTag, { value: "Module" })); @@ -103908,7 +105430,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_130 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_135 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupMembers }, Symbol.toStringTag, { value: "Module" })); @@ -103932,7 +105454,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_131 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_136 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupPostEditor }, Symbol.toStringTag, { value: "Module" })); @@ -103941,7 +105463,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_132 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_137 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupPosts }, Symbol.toStringTag, { value: "Module" })); @@ -103990,7 +105512,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_133 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_138 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupProjectEditor }, Symbol.toStringTag, { value: "Module" })); @@ -104000,7 +105522,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_134 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_139 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupProjects }, Symbol.toStringTag, { value: "Module" })); @@ -104031,7 +105553,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_135 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_140 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupRecruitment }, Symbol.toStringTag, { value: "Module" })); @@ -104084,7 +105606,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_136 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_141 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReleaseEditor }, Symbol.toStringTag, { value: "Module" })); @@ -104096,7 +105618,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_137 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_142 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReleases }, Symbol.toStringTag, { value: "Module" })); @@ -104113,7 +105635,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_138 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_143 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReputation }, Symbol.toStringTag, { value: "Module" })); @@ -104130,7 +105652,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_139 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_144 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupReviewQueue }, Symbol.toStringTag, { value: "Module" })); @@ -104223,7 +105745,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_140 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_145 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupSettings }, Symbol.toStringTag, { value: "Module" })); @@ -104251,7 +105773,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_141 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_146 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGroupsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -104352,7 +105874,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_142 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_147 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioGrowth }, Symbol.toStringTag, { value: "Module" })); @@ -104412,7 +105934,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_143 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_148 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioInbox }, Symbol.toStringTag, { value: "Module" })); @@ -104766,11 +106288,32 @@ function NewsTagInput({ options, selectedIds, newTagNames, onSelectedIdsChange, )); } function RelationCard({ relation, index: index2, onChange, onRemove, onSearch, results, relationTypeOptions }) { - return /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[180px_minmax(0,1fr)_auto] lg:items-end" }, /* @__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: relation.entity_type, onChange: (val) => onChange(index2, { ...relation, entity_type: val, entity_id: "", preview: null, query: "" }), options: relationTypeOptions, 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" }, "Search entity"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement("input", { value: relation.query || "", onChange: (event) => onChange(index2, { ...relation, query: event.target.value }), placeholder: "Search by name, slug, or title", className: "min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => onSearch(index2), className: "rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white" }, "Search"))), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => onRemove(index2), className: "rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100" }, "Remove")), relation.preview ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 p-4 text-sm text-emerald-50" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, "Linked: ", relation.preview.title), relation.preview.subtitle ? /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.14em] text-emerald-100/70" }, relation.preview.subtitle) : null) : null, /* @__PURE__ */ React.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React.createElement(SearchResultList$3, { items: results, onSelect: (item) => onChange(index2, { ...relation, entity_id: item.id, preview: item, query: item.title }), emptyLabel: "Search to attach a related entity." })), /* @__PURE__ */ React.createElement("label", { className: "mt-4 grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Context label"), /* @__PURE__ */ React.createElement("input", { value: relation.context_label || "", onChange: (event) => onChange(index2, { ...relation, context_label: event.target.value }), placeholder: "Featured release, Meet the creator, Join this challenge…", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }))); + const isSourceRelation = String(relation.entity_type || "").trim().toLowerCase() === "source"; + return /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 lg:grid-cols-[180px_minmax(0,1fr)_auto] lg:items-end" }, /* @__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: relation.entity_type, onChange: (val) => onChange(index2, { ...relation, entity_type: val, entity_id: "", external_url: "", preview: null, query: "" }), options: relationTypeOptions, searchable: false })), isSourceRelation ? /* @__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" }, "Source URL"), /* @__PURE__ */ React.createElement( + "input", + { + value: relation.external_url || "", + onChange: (event) => onChange(index2, { ...relation, external_url: event.target.value, query: event.target.value, entity_id: "" }), + placeholder: "https://example.com/original-article", + className: "min-w-0 flex-1 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" }, "Search entity"), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement("input", { value: relation.query || "", onChange: (event) => onChange(index2, { ...relation, query: event.target.value }), placeholder: "Search by name, slug, or title", className: "min-w-0 flex-1 rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => onSearch(index2), className: "rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white" }, "Search"))), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => onRemove(index2), className: "rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100" }, "Remove")), !isSourceRelation && relation.preview ? /* @__PURE__ */ React.createElement("div", { className: "mt-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 p-4 text-sm text-emerald-50" }, /* @__PURE__ */ React.createElement("div", { className: "font-semibold" }, "Linked: ", relation.preview.title), relation.preview.subtitle ? /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-xs uppercase tracking-[0.14em] text-emerald-100/70" }, relation.preview.subtitle) : null) : null, isSourceRelation ? /* @__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-100/90" }, "Source relations store a direct external URL instead of an internal Nova entity ID.") : /* @__PURE__ */ React.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React.createElement(SearchResultList$3, { items: results, onSelect: (item) => onChange(index2, { ...relation, entity_id: item.id, preview: item, query: item.title }), emptyLabel: "Search to attach a related entity." })), /* @__PURE__ */ React.createElement("label", { className: "mt-4 grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Context label"), /* @__PURE__ */ React.createElement("input", { value: relation.context_label || "", onChange: (event) => onChange(index2, { ...relation, context_label: event.target.value }), placeholder: "Featured release, Meet the creator, Join this challenge…", className: "rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" }))); } function stripHtml$1(value) { return String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); } +function unwrapMarkdownLinkUrl(value) { + const raw = String(value || "").trim(); + if (!raw) return ""; + const markdownMatch = raw.match(/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i); + if (markdownMatch) { + return String(markdownMatch[1] || "").trim(); + } + return raw; +} +function isSourceRelationType(entityType) { + return String(entityType || "").trim().toLowerCase() === "source"; +} const NEWS_NEW_TAG_LIMIT = 30; function slugifyNewsTitle(value) { return String(value || "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 180); @@ -104807,7 +106350,8 @@ function buildSubmitPayload(data) { og_image: String(data.og_image || "").trim(), relations: Array.isArray(data.relations) ? data.relations.map((relation) => ({ entity_type: String(relation.entity_type || "").trim(), - entity_id: relation.entity_id === "" || relation.entity_id == null ? "" : Number(relation.entity_id), + entity_id: isSourceRelationType(relation.entity_type) || relation.entity_id === "" || relation.entity_id == null ? "" : Number(relation.entity_id), + external_url: isSourceRelationType(relation.entity_type) ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || "") : "", context_label: String(relation.context_label || "").trim() })) : [] }; @@ -104850,10 +106394,11 @@ function buildInitialFormData(article, defaultAuthor, typeOptions, oldInput = {} og_image: String(getDraftValue(oldInput, "og_image", article.og_image || "")), relations: Array.isArray(getDraftValue(oldInput, "relations", article.relations)) ? getDraftValue(oldInput, "relations", article.relations).map((relation) => ({ entity_type: relation.entity_type || "group", - entity_id: relation.entity_id || "", + entity_id: isSourceRelationType(relation.entity_type) ? "" : relation.entity_id || "", + external_url: isSourceRelationType(relation.entity_type) ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || "") : "", context_label: relation.context_label || "", preview: relation.preview || null, - query: relation.preview?.title || "" + query: isSourceRelationType(relation.entity_type) ? unwrapMarkdownLinkUrl(relation.external_url || relation.entity_id || relation.query || "") : relation.preview?.title || relation.query || "" })) : [] }; } @@ -104953,7 +106498,6 @@ function parseStructuredNewsImport(rawValue, context) { applyString("meta_keywords"); applyString("og_title"); applyString("og_description"); - applyString("og_image"); if (parsed.type != null) { const requested = String(parsed.type).trim().toLowerCase(); const match = typeOptions.find((option) => String(option.value ?? option.id ?? "").trim().toLowerCase() === requested || String(option.label ?? option.name ?? "").trim().toLowerCase() === requested); @@ -105000,13 +106544,18 @@ function parseStructuredNewsImport(rawValue, context) { applied.push("tag_ids", "new_tag_names"); } if (Array.isArray(parsed.relations)) { - next.relations = parsed.relations.map((relation) => ({ - entity_type: String(relation?.entity_type || relation?.type || "group").trim(), - entity_id: relation?.entity_id == null || relation?.entity_id === "" ? "" : Number(relation.entity_id), - context_label: String(relation?.context_label || relation?.label || "").trim(), - preview: null, - query: String(relation?.query || relation?.title || "").trim() - })).filter((relation) => relation.entity_type); + next.relations = parsed.relations.map((relation) => { + const entityType = String(relation?.entity_type || relation?.type || "group").trim(); + const externalUrl = isSourceRelationType(entityType) ? unwrapMarkdownLinkUrl(relation?.external_url || relation?.url || relation?.entity_id || relation?.query || relation?.title || "") : ""; + return { + entity_type: entityType, + entity_id: isSourceRelationType(entityType) || relation?.entity_id == null || relation?.entity_id === "" ? "" : Number(relation.entity_id), + external_url: externalUrl, + context_label: String(relation?.context_label || relation?.label || "").trim(), + preview: null, + query: isSourceRelationType(entityType) ? externalUrl : String(relation?.query || relation?.title || "").trim() + }; + }).filter((relation) => relation.entity_type); applied.push("relations"); } return { @@ -105091,6 +106640,209 @@ function buildNewsMarkdownExport(data) { } return lines.join("\n\n").trim(); } +const NEWS_PROMPT_TYPE_MOODS = { + announcement: "Futuristic", + release: "Software Release", + editorial: "Editorial", + opinion: "Editorial", + tutorial: "Clean Instructional", + platform_update: "Modern Tech", + event: "Futuristic", + challenge: "Futuristic", + interview: "Editorial", + spotlight: "Editorial", + archive: "Retro Tech", + industry_news: "Modern Tech", + review: "Modern Tech", + roundup: "Modern Tech" +}; +const NEWS_PROMPT_TYPE_ADDONS = { + release: "Use a glossy software-release poster style with product UI panels, feature highlights, and a polished launch atmosphere.", + announcement: "Use a clean announcement-poster style with a strong headline, clear hero image, and supporting modules that communicate the main update quickly.", + editorial: "Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.", + opinion: "Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.", + event: "Use a conference or event-poster style with keynote energy, glowing screens, stage-like lighting, and a premium event atmosphere.", + tutorial: "Use a clear structured instructional poster style with organized UI panels, workflow callouts, and helpful visual hierarchy.", + platform_update: "Use a modern platform-update style with system UI visuals, feature modules, and a polished ecosystem presentation.", + archive: "Use a retro-tech editorial style inspired by early 2000s computer magazines, with classic hardware, vintage UI influences, and modern polished lighting." +}; +const NEWS_PROMPT_KEYWORD_PATTERNS = [ + { + keywords: ["apple", "wwdc", "ios", "macos", "iphone", "ipad", "swift"], + addon: "Use a sleek developer-conference atmosphere with modern device screens, app ecosystem visuals, and a premium keynote mood." + }, + { + keywords: ["google", "gemini", "google i/o", "android", "pixel", "tensorflow"], + addon: "Use a colorful futuristic creative AI studio style with glowing panels, image and video creation tools, search elements, and generative media visuals." + }, + { + keywords: ["intel", "amd", "processor", "cpu", "gpu", "nvidia", "radeon", "chip"], + addon: "Use a retro computing hardware feature style with processor chips, technical callouts, old-school PC references, and magazine-cover energy." + }, + { + keywords: ["skin", "theme", "desktop", "customize", "customization", "rainmeter", "widget"], + addon: "Use a desktop customization promo style with theme previews, icon panels, widget windows, and a glossy desktop software aesthetic." + }, + { + keywords: ["ai", "artificial intelligence", "llm", "chatgpt", "openai", "midjourney", "stable diffusion", "generative"], + addon: "Use a colorful futuristic creative AI studio style with glowing panels, generative media outputs, neural network visuals, and advanced AI tool interfaces." + } +]; +function resolveNewsPromptHeadline(data) { + return String(data.title || data.meta_title || "").trim() || "Skinbase News"; +} +function resolveNewsPromptSubheadline(data) { + const raw = String(data.excerpt || data.meta_description || "").replace(/<[^>]*>/g, "").trim(); + if (raw) { + const words = raw.split(/\s+/); + return words.slice(0, 18).join(" ") + (words.length > 18 ? "…" : ""); + } + const plain = String(data.content || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); + if (plain) { + const sentence = plain.split(/[.!?]/)[0].trim(); + if (sentence.length > 10) { + const words = sentence.split(/\s+/); + return words.slice(0, 18).join(" "); + } + } + return "Latest technology and creative industry update"; +} +function resolveNewsPromptTopic(data) { + const parts = []; + const cat = String(data.category || "").trim(); + if (cat) parts.push(cat); + const tagList = (Array.isArray(data.tag_names) ? data.tag_names : []).slice(0, 5).filter(Boolean); + if (tagList.length) parts.push(tagList.join(", ")); + if (!parts.length) { + const words = String(data.title || "").split(/\s+/).filter((w2) => w2.length > 3).slice(0, 4); + if (words.length) parts.push(words.join(" ")); + } + return parts.join(" · ") || "Technology and digital culture news"; +} +function resolveNewsPromptType(data) { + const raw = String(data.type || "").trim(); + if (!raw) return "News"; + return raw.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} +function resolveNewsPromptHeroSubject(data) { + const title = String(data.title || "").toLowerCase(); + const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(" ").toLowerCase(); + const combined = `${title} ${tags}`; + const type2 = String(data.type || "").toLowerCase(); + if (/apple|wwdc|ios|macos/.test(combined)) return "sleek developer conference scene with modern Apple devices, app ecosystem screens, and a keynote stage atmosphere"; + if (/google|gemini|google i\/o/.test(combined)) return "futuristic creative AI workspace with Google AI tools, image and video generation screens, and colorful generative panels"; + if (/intel|amd|cpu|processor|gpu|nvidia|radeon/.test(combined)) return "high-detail processor chip and PC hardware setup with technical callouts and magazine-style editorial framing"; + if (/\bai\b|artificial intelligence|llm|chatgpt|openai|midjourney|stable diffusion/.test(combined)) return "futuristic AI creative studio with generative media outputs, neural network interfaces, and glowing AI panels"; + if (/skin|theme|desktop|customiz|rainmeter|widget/.test(combined)) return "polished desktop customization interface with theme previews, icon panels, and widget windows on a dark desktop"; + if (/game|gaming/.test(combined)) return "immersive gaming setup or game UI with dynamic lighting, modern peripherals, and a premium game atmosphere"; + if (/microsoft|windows/.test(combined)) return "modern Windows interface with system UI panels, taskbar, settings, and a polished OS environment"; + if (type2 === "tutorial") return "organized instructional workflow panel with step-by-step UI callouts and visual hierarchy"; + if (type2 === "event") return "keynote conference stage with large screens, glowing hall, and event atmosphere"; + if (type2 === "archive") return "retro computing hardware from the early 2000s with classic monitors and vintage PC aesthetic"; + return "professional editorial tech workspace with software screens, feature panels, and a polished digital newsroom atmosphere"; +} +function resolveNewsPromptSupportingModules(data) { + const type2 = String(data.type || "").toLowerCase(); + const title = String(data.title || "").toLowerCase(); + const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(" ").toLowerCase(); + const combined = `${title} ${tags}`; + if (type2 === "release" || /release|launch|version/.test(combined)) return "version badge, feature highlight cards, changelog strip, UI screenshots, product icon panels"; + if (type2 === "tutorial") return "step-by-step panels, UI callouts, workflow arrows, numbered feature blocks"; + if (type2 === "event") return "schedule panels, speaker cards, keynote countdown, location badge, feature preview cards"; + if (type2 === "archive") return "retro spec badges, vintage hardware panels, timeline strip, era-appropriate UI screenshots"; + if (/\bai\b|artificial intelligence|generative/.test(combined)) return "AI feature cards, generative output previews, glowing interface panels, model capability badges"; + if (/hardware|chip|cpu|gpu/.test(combined)) return "performance charts, spec comparison cards, hardware close-ups, benchmark badges"; + return "feature cards, interface panels, product highlights, mini screenshots, icon blocks"; +} +function resolveNewsPromptMood(data) { + const type2 = String(data.type || "").toLowerCase().replace(/\s+/g, "_"); + return NEWS_PROMPT_TYPE_MOODS[type2] || "Modern Tech"; +} +function resolveNewsPromptTypeAddon(data) { + const type2 = String(data.type || "").toLowerCase().replace(/\s+/g, "_"); + return NEWS_PROMPT_TYPE_ADDONS[type2] || ""; +} +function resolveNewsPromptKeywordAddon(data) { + const title = String(data.title || "").toLowerCase(); + const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(" ").toLowerCase(); + const category = String(data.category || "").toLowerCase(); + const combined = `${title} ${tags} ${category}`; + const addons = []; + for (const pattern of NEWS_PROMPT_KEYWORD_PATTERNS) { + if (pattern.keywords.some((kw) => combined.includes(kw))) { + addons.push(pattern.addon); + } + } + return [...new Set(addons)].join("\n"); +} +function buildNewsImagePrompt(data) { + const headline2 = resolveNewsPromptHeadline(data); + const subheadline = resolveNewsPromptSubheadline(data); + const topic = resolveNewsPromptTopic(data); + const newsType = resolveNewsPromptType(data); + const heroSubject = resolveNewsPromptHeroSubject(data); + const supportingModules = resolveNewsPromptSupportingModules(data); + const mood = resolveNewsPromptMood(data); + const typeAddon = resolveNewsPromptTypeAddon(data); + const keywordAddon = resolveNewsPromptKeywordAddon(data); + const lines = [ + "Create a premium Skinbase news cover image in 16:9 aspect ratio.", + "", + "Design it as a professional editorial tech poster for a digital culture, software, hardware, AI, creative tools, desktop customization, or retro computing news article.", + "", + "ARTICLE DETAILS:", + `Headline: "${headline2}"`, + `Subheadline: "${subheadline}"`, + `Topic: ${topic}`, + `News type: ${newsType}`, + `Hero subject: ${heroSubject}`, + `Supporting modules: ${supportingModules}`, + `Mood: ${mood}`, + "", + "LAYOUT:", + "Use a structured 16:9 news hero composition with:", + "- Large bold headline in the upper-left or top-center", + "- Smaller subtitle directly below the headline", + "- One strong central hero visual", + "- Supporting side panels, feature cards, icons, UI windows, diagrams, or mini screenshots", + "- A bottom strip with 3 to 6 small highlight blocks or visual details", + "- Clean spacing and a strong visual hierarchy", + "", + "VISUAL STYLE:", + "Use a dark premium background with blue, cyan, violet, neon, or topic-matching accent colors. Add glossy highlights, subtle glow, cinematic depth, crisp lighting, and a polished high-tech editorial look.", + "", + "The image should feel like a professional magazine cover, software release poster, tech conference banner, or retro computing feature graphic. It should be visually rich, but still clean, readable, and organized.", + "", + "TEXT STYLE:", + "Use bold clean sans-serif typography. Keep all visible text short and readable. Avoid long paragraphs inside the image. Use only short labels, feature names, or headline-style phrases.", + "", + "CONTENT DIRECTION:", + "Represent the topic clearly through the central visual. Use relevant objects such as:", + "- software windows", + "- futuristic workstations", + "- creative AI panels", + "- computer chips", + "- retro hardware", + "- desktop customization elements", + "- conference screens", + "- app interface mockups", + "- glowing diagrams", + "- feature cards", + "- product-style panels", + "", + "QUALITY RULES:", + "Make it sharp, premium, polished, high detail, thumbnail-friendly, and suitable as a Skinbase news article cover image.", + "", + "Avoid clutter, random filler objects, unreadable microtext, messy typography, distorted UI, weak composition, watermarks, fake signatures, low-quality stock-photo style, and irrelevant logos." + ]; + if (typeAddon) { + lines.push("", typeAddon); + } + if (keywordAddon) { + lines.push("", keywordAddon); + } + return lines.join("\n"); +} function buildNewsExportPayloads(data, context = {}) { const normalized = buildSubmitPayload(data || {}); const category = findNewsOptionById(context.categoryOptions, normalized.category_id); @@ -105153,18 +106905,21 @@ function buildNewsExportPayloads(data, context = {}) { markdownInput: markdown }; } -function JsonImportDialog({ open, value, error, onChange, onClose, onApply, exportPayloads, newTagLimit = NEWS_NEW_TAG_LIMIT }) { +function JsonImportDialog({ open, value, error, onChange, onClose, onApply, exportPayloads, articleData = {}, newTagLimit = NEWS_NEW_TAG_LIMIT }) { const backdropRef = reactExports.useRef(null); const [activeImportTab, setActiveImportTab] = reactExports.useState("input"); const [copyFeedback, setCopyFeedback] = reactExports.useState(""); const [exportMode, setExportMode] = reactExports.useState("full"); const [markdownExportText, setMarkdownExportText] = reactExports.useState(String(exportPayloads?.markdown || "")); + const [promptText, setPromptText] = reactExports.useState(""); + const [promptIsManual, setPromptIsManual] = reactExports.useState(false); const importTabs = [ { id: "input", label: "Input", description: "Paste JSON and apply it to the editor." }, { id: "structure", label: "Structure example", description: "A working example of the expected payload." }, { id: "docs", label: "Documentation", description: "Field notes and mapping rules." }, { id: "prompts", label: "AI prompts", description: "Prompt examples for generating structured news." }, - { id: "export", label: "Export", description: "Copy the current article out as JSON, text, or Markdown." } + { id: "export", label: "Export", description: "Copy the current article out as JSON, text, or Markdown." }, + { id: "image_prompt", label: "Image Prompt", description: "Auto-generate a cover image prompt from article data." } ]; const structureExample = { title: "Sample News Title", @@ -105199,8 +106954,7 @@ function JsonImportDialog({ open, value, error, onChange, onClose, onApply, expo meta_description: "This is a sample news meta description for the structured import example.", meta_keywords: "sample news, structured import, editorial example", og_title: "Sample News Title", - og_description: "This is a sample news OG description for the structured import example.", - og_image: "sample-news-cover.webp" + og_description: "This is a sample news OG description for the structured import example." }; const newsJsonSchemaSummary = `You are generating a Skinbase news article JSON object. @@ -105223,7 +106977,7 @@ Recommended fields: - is_featured: boolean - is_pinned: boolean - meta_title, meta_description, meta_keywords -- og_title, og_description, og_image +- og_title, og_description - tags: array of strings or objects with name/title/label/slug - tag_names: array of strings - tag_ids: array of ids if you already know them @@ -105247,7 +107001,7 @@ Transform the following article into a news payload for the editor. - Write content as HTML paragraphs. - Include 8 to 14 highly relevant tags. - Include category_id when possible, otherwise use category_slug or category to help matching. -- Fill meta_title, meta_description, og_title, og_description, and og_image when available. +- Fill meta_title, meta_description, og_title, and og_description when available. - Make comments_enabled true unless the source clearly says otherwise. Input article text: @@ -105327,6 +107081,19 @@ Source article: cancelled = true; }; }, [activeImportTab, exportMode, exportPayloads, open]); + reactExports.useEffect(() => { + if (!open || activeImportTab !== "image_prompt") return; + if (promptIsManual) return; + setPromptText(buildNewsImagePrompt(articleData)); + }, [open, activeImportTab, articleData, promptIsManual]); + const handleRegeneratePrompt = reactExports.useCallback(() => { + setPromptIsManual(false); + setPromptText(buildNewsImagePrompt(articleData)); + }, [articleData]); + const handleResetPrompt = reactExports.useCallback(() => { + setPromptIsManual(false); + setPromptText(buildNewsImagePrompt(articleData)); + }, [articleData]); if (!open) return null; return reactDomExports.createPortal( /* @__PURE__ */ React.createElement( @@ -105350,7 +107117,7 @@ Source article: className: "flex h-[min(90vh,780px)] w-full max-w-5xl flex-col overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(180deg,rgba(16,22,34,0.98),rgba(8,12,19,0.98))] shadow-[0_30px_80px_rgba(0,0,0,0.55)]" }, /* @__PURE__ */ React.createElement("div", { className: "border-b border-white/[0.06] bg-white/[0.02] px-6 py-5" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-white/35" }, "Structured import"), /* @__PURE__ */ React.createElement("h3", { id: "news-json-import-title", className: "mt-2 text-lg font-semibold text-white" }, "Import or export article JSON"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-white/65" }, "Use this for migrations, AI-assisted drafting, bulk handoff from another editorial system, or copying the current article into reusable JSON.")), - /* @__PURE__ */ React.createElement("div", { className: "border-b border-white/[0.06] px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 md:grid-cols-5" }, importTabs.map((tab2) => /* @__PURE__ */ React.createElement( + /* @__PURE__ */ React.createElement("div", { className: "border-b border-white/[0.06] px-4 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 grid-cols-2 md:grid-cols-3 lg:grid-cols-6" }, importTabs.map((tab2) => /* @__PURE__ */ React.createElement( "button", { key: tab2.id, @@ -105370,7 +107137,7 @@ Source article: placeholder: '{\n "title": "My news title",\n "slug": "my-news-title",\n "excerpt": "Short summary",\n "tags": ["release", "community"]\n}', className: "nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm text-white outline-none placeholder:text-white/30" } - ), error ? /* @__PURE__ */ React.createElement("div", { className: "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: "rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Recognized keys"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2 leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("p", null, "`title`, `slug`, `excerpt`, `content`, `cover_image`"), /* @__PURE__ */ React.createElement("p", null, "`type`, `category_id`, `category`, `category_slug`"), /* @__PURE__ */ React.createElement("p", null, "`editorial_status`, `published_at`, `author_id`, `author_name`"), /* @__PURE__ */ React.createElement("p", null, "`is_featured`, `is_pinned`, `comments_enabled`"), /* @__PURE__ */ React.createElement("p", null, "`tags`, `tag_names`, `tag_ids`, `relations`"), /* @__PURE__ */ React.createElement("p", null, "`new_tag_names` is capped at ", newTagLimit, " items per article."), /* @__PURE__ */ React.createElement("p", null, "`meta_title`, `meta_description`, `meta_keywords`, `og_title`, `og_description`, `og_image`")))) : null, activeImportTab === "structure" ? /* @__PURE__ */ React.createElement("div", { className: "grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "mb-3 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Structure example"), /* @__PURE__ */ React.createElement( + ), error ? /* @__PURE__ */ React.createElement("div", { className: "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: "rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Recognized keys"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-2 leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("p", null, "`title`, `slug`, `excerpt`, `content`, `cover_image`"), /* @__PURE__ */ React.createElement("p", null, "`type`, `category_id`, `category`, `category_slug`"), /* @__PURE__ */ React.createElement("p", null, "`editorial_status`, `published_at`, `author_id`, `author_name`"), /* @__PURE__ */ React.createElement("p", null, "`is_featured`, `is_pinned`, `comments_enabled`"), /* @__PURE__ */ React.createElement("p", null, "`tags`, `tag_names`, `tag_ids`, `relations`"), /* @__PURE__ */ React.createElement("p", null, "`new_tag_names` is capped at ", newTagLimit, " items per article."), /* @__PURE__ */ React.createElement("p", null, "`meta_title`, `meta_description`, `meta_keywords`, `og_title`, `og_description`")))) : null, activeImportTab === "structure" ? /* @__PURE__ */ React.createElement("div", { className: "grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_360px]" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "mb-3 flex items-center justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Structure example"), /* @__PURE__ */ React.createElement( "button", { type: "button", @@ -105421,7 +107188,44 @@ Source article: rows: 18, className: "nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm text-white outline-none" } - )), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Export options"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3 leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Full news JSON"), " includes the current editable article state: slug, status, tags, metadata, and relations."), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Structured JSON"), " keeps the reduced handoff shape: title, excerpt, date, body, and category."), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Markdown"), " converts the current article body into Markdown and includes the title plus summary fields for external reuse."), /* @__PURE__ */ React.createElement("p", null, "The export uses the live editor state, so unsaved changes are included immediately.")))) : null), + )), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Export options"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3 leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Full news JSON"), " includes the current editable article state: slug, status, tags, metadata, and relations."), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Structured JSON"), " keeps the reduced handoff shape: title, excerpt, date, body, and category."), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Markdown"), " converts the current article body into Markdown and includes the title plus summary fields for external reuse."), /* @__PURE__ */ React.createElement("p", null, "The export uses the live editor state, so unsaved changes are included immediately.")))) : null, activeImportTab === "image_prompt" ? /* @__PURE__ */ React.createElement("div", { className: "grid h-full min-h-0 gap-5 xl:grid-cols-[minmax(0,1.2fr)_320px]" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "flex-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Generated cover image prompt", promptIsManual ? /* @__PURE__ */ React.createElement("span", { className: "ml-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-2 py-0.5 text-[10px] text-amber-100" }, "Manually edited") : null), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: handleRegeneratePrompt, + className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-white/[0.08]" + }, + "Regenerate" + ), promptIsManual ? /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: handleResetPrompt, + className: "rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1.5 text-xs font-semibold text-amber-100 transition hover:bg-amber-400/20" + }, + "Reset to auto" + ) : null, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + onClick: () => copyText(promptText, "Image prompt"), + className: "rounded-full border border-sky-300/25 bg-sky-400/90 px-3 py-1.5 text-xs font-semibold text-slate-950 transition hover:brightness-110" + }, + "Copy prompt" + )), /* @__PURE__ */ React.createElement( + "textarea", + { + value: promptText, + onChange: (event) => { + setPromptText(event.target.value); + setPromptIsManual(true); + }, + rows: 22, + spellCheck: false, + className: "nova-scrollbar w-full rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm leading-6 text-white outline-none placeholder:text-white/30", + placeholder: "Opening the tab will generate a prompt automatically…" + } + )), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-4 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "How it works"), /* @__PURE__ */ React.createElement("div", { className: "mt-3 space-y-3 leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("p", null, "The prompt is built automatically from the current article fields: title, excerpt, type, category, and tags."), /* @__PURE__ */ React.createElement("p", null, "You can edit the prompt freely. It will be marked as ", /* @__PURE__ */ React.createElement("span", { className: "rounded border border-amber-300/20 bg-amber-400/10 px-1 text-amber-100" }, "Manually edited"), " once you change it."), /* @__PURE__ */ React.createElement("p", null, "Click ", /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Regenerate"), " or ", /* @__PURE__ */ React.createElement("strong", { className: "text-slate-200" }, "Reset to auto"), " to rebuild from the current article state."), /* @__PURE__ */ React.createElement("p", null, "Copy the prompt and paste it into an AI image generator such as Midjourney, DALL-E, Stable Diffusion, or Flux.")), /* @__PURE__ */ React.createElement("div", { className: "mt-5 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500" }, "Auto-filled from"), /* @__PURE__ */ React.createElement("ul", { className: "mt-2 space-y-1 text-xs leading-6 text-slate-400" }, /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Headline"), " — title, meta_title"), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Subheadline"), " — excerpt, meta_description, content"), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Topic"), " — category, tags"), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Type"), " — article type"), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Hero / Mood"), " — inferred from title, tags, type"), /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: "text-slate-300" }, "Addons"), " — type-based and keyword-based style blocks")))) : null), copyFeedback ? /* @__PURE__ */ React.createElement("div", { className: "px-6 pb-2 text-right text-xs font-medium text-sky-200/80" }, copyFeedback) : null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-end gap-3 border-t border-white/[0.06] px-6 py-4" }, /* @__PURE__ */ React.createElement( "button", @@ -105447,7 +107251,7 @@ Source article: className: "inline-flex items-center justify-center rounded-full border border-sky-300/25 bg-sky-400/90 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:brightness-110" }, "Copy export" - )) : /* @__PURE__ */ React.createElement( + )) : activeImportTab === "image_prompt" ? null : /* @__PURE__ */ React.createElement( "button", { type: "button", @@ -105483,6 +107287,7 @@ function StudioNewsEditor() { const normalizedInitialPayload = reactExports.useMemo(() => JSON.stringify(buildSubmitPayload(initialFormData)), [initialFormData]); const normalizedCurrentPayload = reactExports.useMemo(() => JSON.stringify(buildSubmitPayload(form.data)), [form.data]); const hasUnsavedChanges = normalizedCurrentPayload !== normalizedInitialPayload; + const frontendArticleUrl = String(article.canonical_url || "").trim(); reactExports.useEffect(() => { if (lastSyncedArticleKeyRef.current === articleSyncKey) { return; @@ -105581,6 +107386,24 @@ function StudioNewsEditor() { tagOptions: props.tagOptions, author: selectedAuthor }), [form.data, props.categoryOptions, props.tagOptions, selectedAuthor]); + const imagePromptArticleData = reactExports.useMemo(() => { + const category = findNewsOptionById(props.categoryOptions, form.data.category_id); + const existingTags = findNewsTagsByIds(props.tagOptions, form.data.tag_ids); + return { + title: form.data.title, + excerpt: form.data.excerpt, + content: form.data.content, + type: form.data.type, + category: category?.name ?? category?.label ?? "", + category_slug: category?.slug ?? "", + tag_names: [ + ...existingTags.map((t) => t.name), + ...Array.isArray(form.data.new_tag_names) ? form.data.new_tag_names : [] + ], + meta_title: form.data.meta_title, + meta_description: form.data.meta_description + }; + }, [form.data, props.categoryOptions, props.tagOptions]); reactExports.useEffect(() => { const firstErrorTab = NEWS_EDITOR_TABS.find((tab2) => tabErrorCounts[tab2.id] > 0); if (firstErrorTab) { @@ -105613,6 +107436,7 @@ function StudioNewsEditor() { { entity_type: props.relationTypeOptions?.[0]?.value || "group", entity_id: "", + external_url: "", context_label: "", preview: null, query: "" @@ -105723,7 +107547,7 @@ function StudioNewsEditor() { setJsonImportError(error instanceof Error ? error.message : "Could not parse JSON."); } }; - return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement(ToastStack, { toasts, onDismiss: dismissToast }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6 pb-24" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[28px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_34%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.94))] shadow-[0_24px_70px_rgba(2,6,23,0.34)] backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-start justify-between gap-4 border-b border-white/10 px-5 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, props.indexUrl ? /* @__PURE__ */ React.createElement("a", { href: props.indexUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-white transition hover:bg-white/[0.08]" }, "Back to news list") : null, /* @__PURE__ */ React.createElement("span", null, article.id ? `Article #${article.id}` : "New article"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 px-3 py-1.5 text-sky-100/80" }, currentTab.label)), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 truncate text-2xl font-semibold tracking-[-0.03em] text-white" }, String(form.data.title || "").trim() || (article.id ? "Untitled article" : "Create article")), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "Keep the draft flow simple: write the story in one place, handle publishing in one place, and keep promotion metadata nearby instead of buried below the fold."))), /* @__PURE__ */ React.createElement("div", { className: "sticky top-16 z-30 border-y border-white/10 bg-[linear-gradient(180deg,rgba(9,14,24,0.98),rgba(6,10,18,0.98))] px-4 py-3 backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex justify-end gap-2 overflow-x-auto" }, props.previewUrl ? /* @__PURE__ */ React.createElement("a", { href: props.previewUrl, target: "_blank", rel: "noreferrer", className: "rounded-2xl border border-indigo-300/20 bg-indigo-400/10 px-4 py-2.5 text-sm font-semibold text-indigo-100 transition hover:bg-indigo-400/15" }, "Preview") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setJsonImportOpen(true), 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]" }, "Import JSON"), /* @__PURE__ */ React.createElement("button", { type: "submit", form: "studio-news-editor-form", disabled: form.processing, 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-60" }, form.processing ? "Saving…" : "Save article"), props.publishUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.publishUrl), className: "rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-2.5 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/15" }, "Publish now") : null)), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2 overflow-x-auto px-4 py-3" }, NEWS_EDITOR_TABS.map((tab2) => { + return /* @__PURE__ */ React.createElement(StudioLayout, { title: props.title, subtitle: props.description }, /* @__PURE__ */ React.createElement(ToastStack, { toasts, onDismiss: dismissToast }), /* @__PURE__ */ React.createElement("div", { className: "space-y-6 pb-24" }, /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[28px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_34%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.94))] shadow-[0_24px_70px_rgba(2,6,23,0.34)] backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-5 border-b border-white/10 px-5 py-4 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start" }, /* @__PURE__ */ React.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400" }, props.indexUrl ? /* @__PURE__ */ React.createElement("a", { href: props.indexUrl, className: "rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-white transition hover:bg-white/[0.08]" }, "Back to news list") : null, /* @__PURE__ */ React.createElement("span", null, article.id ? `Article #${article.id}` : "New article"), /* @__PURE__ */ React.createElement("span", { className: "rounded-full border border-white/10 px-3 py-1.5 text-sky-100/80" }, currentTab.label)), /* @__PURE__ */ React.createElement("h1", { className: "mt-3 truncate text-2xl font-semibold tracking-[-0.03em] text-white" }, String(form.data.title || "").trim() || (article.id ? "Untitled article" : "Create article")), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "Keep the draft flow simple: write the story in one place, handle publishing in one place, and keep promotion metadata nearby instead of buried below the fold.")), coverPreviewUrl ? /* @__PURE__ */ React.createElement("div", { className: "overflow-hidden rounded-[24px] border border-white/10 bg-black/20 shadow-[0_18px_40px_rgba(2,6,23,0.35)]" }, /* @__PURE__ */ React.createElement("div", { className: "relative aspect-[16/9] bg-black/30" }, /* @__PURE__ */ React.createElement("img", { src: coverPreviewUrl, alt: String(form.data.title || "").trim() || "News cover preview", className: "h-full w-full object-cover" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 bg-gradient-to-t from-[#020611d9] via-[#02061144] to-transparent" }), /* @__PURE__ */ React.createElement("div", { className: "absolute inset-x-0 bottom-0 p-4" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100/80" }, "Header cover preview"), /* @__PURE__ */ React.createElement("div", { className: "mt-1 line-clamp-2 text-sm font-semibold text-white" }, String(form.data.title || "").trim() || "Cover image preview")))) : null), /* @__PURE__ */ React.createElement("div", { className: "sticky top-16 z-30 border-y border-white/10 bg-[linear-gradient(180deg,rgba(9,14,24,0.98),rgba(6,10,18,0.98))] px-4 py-3 backdrop-blur" }, /* @__PURE__ */ React.createElement("div", { className: "flex justify-end gap-2 overflow-x-auto" }, frontendArticleUrl ? /* @__PURE__ */ React.createElement("a", { href: frontendArticleUrl, target: "_blank", rel: "noreferrer", className: "rounded-2xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-2.5 text-sm font-semibold text-cyan-100 transition hover:bg-cyan-400/15" }, "Frontend link") : null, props.previewUrl ? /* @__PURE__ */ React.createElement("a", { href: props.previewUrl, target: "_blank", rel: "noreferrer", className: "rounded-2xl border border-indigo-300/20 bg-indigo-400/10 px-4 py-2.5 text-sm font-semibold text-indigo-100 transition hover:bg-indigo-400/15" }, "Preview") : null, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setJsonImportOpen(true), 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]" }, "Import JSON"), /* @__PURE__ */ React.createElement("button", { type: "submit", form: "studio-news-editor-form", disabled: form.processing, className: "inline-flex items-center gap-2 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-60" }, hasUnsavedChanges && !form.processing ? /* @__PURE__ */ React.createElement("span", { className: "h-2.5 w-2.5 rounded-full bg-rose-400 shadow-[0_0_10px_rgba(251,113,133,0.9)] animate-pulse", "aria-hidden": "true" }) : null, /* @__PURE__ */ React.createElement("span", null, form.processing ? "Saving…" : "Save article")), props.publishUrl ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => At.post(props.publishUrl), className: "rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-2.5 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/15" }, "Publish now") : null)), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2 overflow-x-auto px-4 py-3" }, NEWS_EDITOR_TABS.map((tab2) => { const active = tab2.id === activeTab; const errorCount = tabErrorCounts[tab2.id] || 0; return /* @__PURE__ */ React.createElement( @@ -105797,7 +107621,12 @@ function StudioNewsEditor() { minHeight: 24, autofocus: false, advancedNews: true, - searchEntities + searchEntities, + mediaSupport: { + uploadUrl: props.coverUploadUrl, + deleteUrl: props.coverDeleteUrl, + slot: "body" + } } ), /* @__PURE__ */ React.createElement("div", { className: "rounded-[24px] border border-white/10 bg-black/20 px-4 py-3 text-xs leading-6 text-slate-400" }, "Story workflow suggestion: lead with the change, explain why it matters, add supporting detail, then end with a clear call to action or next step.")))) : null, activeTab === "publishing" ? /* @__PURE__ */ React.createElement(SectionCard, { eyebrow: "Editorial controls", title: "Publishing", description: "Set ownership, placement, timing, and surface behavior before the article leaves draft." }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-4 md:grid-cols-2" }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement(NovaSelect, { label: "Type", value: form.data.type || null, onChange: (nextValue) => form.setData("type", String(nextValue || "")), options: typeOptions, searchable: false, className: "bg-black/20", error: form.errors.type })), /* @__PURE__ */ React.createElement("div", { className: "grid gap-2 text-sm text-slate-300" }, /* @__PURE__ */ React.createElement(NovaSelect, { label: "Category", value: form.data.category_id || "", onChange: (nextValue) => { form.setData("category_id", String(nextValue || "")); @@ -105847,6 +107676,7 @@ function StudioNewsEditor() { value: jsonImportValue, error: jsonImportError, exportPayloads: jsonExportPayloads, + articleData: imagePromptArticleData, newTagLimit: props.newsTagLimit || NEWS_NEW_TAG_LIMIT, onChange: (nextValue) => { setJsonImportValue(nextValue); @@ -105862,7 +107692,7 @@ function StudioNewsEditor() { } )); } -const __vite_glob_0_144 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_149 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsEditor }, Symbol.toStringTag, { value: "Module" })); @@ -106048,7 +107878,7 @@ function StudioNewsIndex() { "Next" ))) : null); } -const __vite_glob_0_145 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_150 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -106081,7 +107911,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_146 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_151 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioNewsTaxonomies }, Symbol.toStringTag, { value: "Module" })); @@ -106231,7 +108061,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_147 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_152 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioPreferences }, Symbol.toStringTag, { value: "Module" })); @@ -106411,7 +108241,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_148 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_153 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioProfile }, Symbol.toStringTag, { value: "Module" })); @@ -106488,7 +108318,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_149 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_154 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioScheduled }, Symbol.toStringTag, { value: "Module" })); @@ -106506,7 +108336,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_150 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_155 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioSearch }, Symbol.toStringTag, { value: "Module" })); @@ -106514,7 +108344,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_151 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_156 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioSettings }, Symbol.toStringTag, { value: "Module" })); @@ -106525,9 +108355,17 @@ function StudioStories() { ["Stories", summary.count, "fa-solid fa-feather-pointed"], ["Drafts", summary.draft_count, "fa-solid fa-file-pen"], ["Published", summary.published_count, "fa-solid fa-sparkles"] - ].map(([label, value, icon]) => /* @__PURE__ */ React.createElement("div", { key: label, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-slate-300" }, /* @__PURE__ */ React.createElement("i", { className: icon }), /* @__PURE__ */ React.createElement("span", { className: "text-sm" }, label)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-3xl font-semibold text-white" }, Number(value || 0).toLocaleString()))), /* @__PURE__ */ React.createElement("a", { href: props.dashboardUrl, className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Story dashboard"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Jump into the existing story workspace when you need the full editor and publishing controls."))), /* @__PURE__ */ React.createElement(StudioContentBrowser, { listing: props.listing, quickCreate: props.quickCreate, hideModuleFilter: true })); + ].map(([label, value, icon]) => /* @__PURE__ */ React.createElement("div", { key: label, className: "rounded-[24px] border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3 text-slate-300" }, /* @__PURE__ */ React.createElement("i", { className: icon }), /* @__PURE__ */ React.createElement("span", { className: "text-sm" }, label)), /* @__PURE__ */ React.createElement("div", { className: "mt-3 text-3xl font-semibold text-white" }, Number(value || 0).toLocaleString()))), /* @__PURE__ */ React.createElement("a", { href: props.dashboardUrl, className: "rounded-[24px] border border-sky-300/20 bg-sky-300/10 p-5 text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, /* @__PURE__ */ React.createElement("p", { className: "text-[11px] font-semibold uppercase tracking-[0.2em]" }, "Story dashboard"), /* @__PURE__ */ React.createElement("p", { className: "mt-3 text-sm leading-6" }, "Jump into the existing story workspace when you need the full editor and publishing controls."))), /* @__PURE__ */ React.createElement( + StudioContentBrowser, + { + listing: props.listing, + quickCreate: props.quickCreate, + hideModuleFilter: true, + sortStorageKey: "studio-stories-sort" + } + )); } -const __vite_glob_0_152 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_157 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioStories }, Symbol.toStringTag, { value: "Module" })); @@ -107190,7 +109028,7 @@ function StudioUploadQueue() { ))))); }))))); } -const __vite_glob_0_153 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_158 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioUploadQueue }, Symbol.toStringTag, { value: "Module" })); @@ -108922,7 +110760,7 @@ function StudioWorldEditor() { )) : null )); } -const __vite_glob_0_154 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_159 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioWorldEditor }, Symbol.toStringTag, { value: "Module" })); @@ -109008,7 +110846,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_155 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_160 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: StudioWorldsIndex }, Symbol.toStringTag, { value: "Module" })); @@ -109586,6 +111424,14 @@ function normalizeType(value, fallback = "error") { if (normalized === "success" || normalized === "warning" || normalized === "error") return normalized; return fallback; } +function firstValidationError(errors) { + if (!errors || typeof errors !== "object") return ""; + for (const value of Object.values(errors)) { + if (Array.isArray(value) && value[0]) return String(value[0]).trim(); + if (typeof value === "string" && value.trim()) return value.trim(); + } + return ""; +} function mapUploadErrorNotice(error, fallback = "Upload failed.") { const status2 = Number(error?.response?.status || 0); const payload = error?.response?.data || {}; @@ -109593,10 +111439,11 @@ function mapUploadErrorNotice(error, fallback = "Upload failed.") { const mapped = REASON_MAP[reason]; const errorCode = String(error?.code || "").toUpperCase(); const rawMessage = typeof error?.message === "string" ? error.message.trim() : ""; + const validationMessage = firstValidationError(payload?.errors); const timedOut = errorCode === "ECONNABORTED" || /timeout/i.test(rawMessage); const requestTooLarge = status2 === 413; const type2 = mapped?.type ? mapped.type : normalizeType(payload?.type || payload?.level, requestTooLarge ? "warning" : status2 >= 500 ? "error" : "warning"); - const message = (requestTooLarge ? "Server rejected this upload chunk as too large. Retrying with smaller chunks may help, or increase Nginx/PHP upload limits." : "") || (timedOut ? "Upload request timed out before the server responded. Check Nginx/PHP-FPM body handling and try again." : "") || mapped?.message || typeof payload?.message === "string" && payload.message.trim() || rawMessage || fallback; + const message = (requestTooLarge ? "Server rejected this upload chunk as too large. Retrying with smaller chunks may help, or increase Nginx/PHP upload limits." : "") || (timedOut ? "Upload request timed out before the server responded. Check Nginx/PHP-FPM body handling and try again." : "") || mapped?.message || validationMessage || typeof payload?.message === "string" && payload.message.trim() || rawMessage || fallback; return { type: type2, message, @@ -111564,16 +113411,16 @@ function UploadSidebar({ placeholder: "Give your artwork a clear title" } ), errors.title && /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-red-200" }, errors.title)), /* @__PURE__ */ React.createElement("label", { className: "block" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium text-white/90" }, "Description ", /* @__PURE__ */ React.createElement("span", { className: "text-red-300" }, "*")), /* @__PURE__ */ React.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React.createElement( - RichTextEditor, + UploadDescriptionEditor, { - content: metadata.description, + id: "upload-sidebar-description", + value: metadata.description, onChange: onChangeDescription, - placeholder: "Describe your artwork, tools, inspiration…", + placeholder: "Describe your artwork, tools, inspiration...", error: Array.isArray(errors.description) ? errors.description[0] : errors.description, - minHeight: 12, - autofocus: false + rows: 9 } - ))))), /* @__PURE__ */ React.createElement("section", { className: "rounded-2xl border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white" }, "Tags"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-white/60" }, "Use keywords people would search for. Press Enter, comma, or Tab to add a tag.")), /* @__PURE__ */ React.createElement( + )), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-white/50" }, "This upload editor only allows safe formatting and emoji. Images, embeds, and raw HTML are blocked."), errors.description && /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-red-200" }, Array.isArray(errors.description) ? errors.description[0] : errors.description)))), /* @__PURE__ */ React.createElement("section", { className: "rounded-2xl border border-white/10 bg-white/[0.03] p-5" }, /* @__PURE__ */ React.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React.createElement("h4", { className: "text-sm font-semibold text-white" }, "Tags"), /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-xs text-white/60" }, "Use keywords people would search for. Press Enter, comma, or Tab to add a tag.")), /* @__PURE__ */ React.createElement( TagPicker, { value: metadata.tags, @@ -112344,6 +114191,15 @@ function UploadWizard({ if (metadata.rootCategoryId && requiresSubCategory && !metadata.subCategoryId) { errors.category = "Subcategory is required for the selected category."; } + if (!Array.isArray(metadata.tags) || metadata.tags.length === 0) errors.tags = "Add at least one tag."; + if (!String(metadata.description || "").trim()) { + errors.description = "Description is required."; + } else { + const descriptionErrors = validateMarkdownLiteContent(metadata.description); + if (descriptionErrors.length > 0) { + errors.description = descriptionErrors[0]; + } + } if (!metadata.rightsAccepted) errors.rights = "Rights confirmation is required."; return errors; }, [metadata, requiresSubCategory]); @@ -112377,7 +114233,7 @@ function UploadWizard({ ); const hasTag = Array.isArray(metadata.tags) && metadata.tags.length > 0; const hasRequiredScreenshot = !isArchive || screenshots.length > 0; - const canPublish = reactExports.useMemo(() => uploadReady && hasTitle && hasCompleteCategory && hasTag && hasRequiredScreenshot && metadata.rightsAccepted && machine.state !== machineStates.publishing, [uploadReady, hasTitle, hasCompleteCategory, hasTag, hasRequiredScreenshot, metadata.rightsAccepted, machine.state]); + const canPublish = reactExports.useMemo(() => uploadReady && hasTitle && hasCompleteCategory && hasTag && hasRequiredScreenshot && String(metadata.description || "").trim() && !metadataErrors.description && metadata.rightsAccepted && machine.state !== machineStates.publishing, [uploadReady, hasTitle, hasCompleteCategory, hasTag, hasRequiredScreenshot, metadata.description, metadata.rightsAccepted, metadataErrors.description, machine.state]); const canScheduleSubmit = reactExports.useMemo(() => { if (!canPublish) return false; if (reviewSubmissionMode) return true; @@ -112923,7 +114779,7 @@ function getTypeKey(ct) { } return String(ct.name || "").toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, ""); } -function useUploadMachine({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs, userId }) { +function useUploadMachine({ draftId = null, filesCdnUrl = "", chunkSize, chunkRequestTimeoutMs, userId = null } = {}) { const [state, dispatch] = reactExports.useReducer(reducer, { ...initialState, draftId }); const pollRef = reactExports.useRef(null); const adaptiveChunkSizeRef = reactExports.useRef(Math.max(1, Number(chunkSize || 0))); @@ -113249,6 +115105,13 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeout pushNotice("error", message); return; } + const descriptionErrors = validateMarkdownLiteContent(state.metadata.description); + if (descriptionErrors.length > 0) { + const message = descriptionErrors[0]; + dispatch({ type: "UPLOAD_ERROR", error: message }); + pushNotice("error", message); + return; + } if (!state.metadata.licenseAccepted) { const message = "You must confirm ownership of the artwork."; dispatch({ type: "UPLOAD_ERROR", error: message }); @@ -113306,7 +115169,7 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeout pushNotice }; } -function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) { +function UploadPage({ draftId = null, filesCdnUrl = "", chunkSize, chunkRequestTimeoutMs } = {}) { const { props } = X$1(); const pageTitle = "Upload Artwork — Creator Studio"; const pageDescription = "Submit a new artwork, complete the required metadata, and publish it from Skinbase Creator Studio."; @@ -113378,6 +115241,7 @@ function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) ); const categoryOptions = reactExports.useMemo(() => selectedType?.categories || [], [selectedType]); const hasAtLeastOneTag = reactExports.useMemo(() => parseUiTags(state.metadata.tags).length > 0, [state.metadata.tags]); + const descriptionErrors = reactExports.useMemo(() => validateMarkdownLiteContent(state.metadata.description), [state.metadata.description]); reactExports.useEffect(() => { if (props?.content_types && Array.isArray(props.content_types)) { setContentTypes(props.content_types); @@ -113545,16 +115409,17 @@ function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) popularEndpoint: "/api/tags/popular", placeholder: "Type tags (e.g. cyberpunk, city)" } - ))), /* @__PURE__ */ React.createElement("label", { className: "mt-4 block text-sm" }, /* @__PURE__ */ React.createElement("span", { className: "text-white/80" }, "Description"), /* @__PURE__ */ React.createElement( - "textarea", + ))), /* @__PURE__ */ React.createElement("label", { className: "mt-4 block text-sm" }, /* @__PURE__ */ React.createElement("span", { className: "text-white/80" }, "Description"), /* @__PURE__ */ React.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React.createElement( + UploadDescriptionEditor, { + id: "legacy-upload-description", value: state.metadata.description, - onChange: (e) => dispatch({ type: "SET_METADATA", payload: { description: e.target.value } }), - className: "mt-2 w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 text-white focus:border-sky-400 focus:outline-none", - rows: 4, - placeholder: "Tell the story behind this artwork." + onChange: (value) => dispatch({ type: "SET_METADATA", payload: { description: value } }), + placeholder: "Tell the story behind this artwork.", + error: descriptionErrors[0] || "", + rows: 8 } - )), /* @__PURE__ */ React.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React.createElement( + )), descriptionErrors.length > 0 && /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-xs text-red-200" }, descriptionErrors[0])), /* @__PURE__ */ React.createElement("div", { className: "mt-4" }, /* @__PURE__ */ React.createElement( Checkbox, { checked: state.metadata.isMature, @@ -113577,8 +115442,8 @@ function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) { type: "button", onClick: startUpload, - disabled: !state.file || !state.metadata.title.trim() || !state.metadata.type || !state.metadata.category || !hasAtLeastOneTag || !state.metadata.description.trim() || !state.metadata.licenseAccepted, - className: `inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-semibold text-white ${!state.file || !state.metadata.title.trim() || !state.metadata.type || !state.metadata.category || !hasAtLeastOneTag || !state.metadata.description.trim() || !state.metadata.licenseAccepted ? "bg-white/10 cursor-not-allowed" : "bg-emerald-500 shadow-lg shadow-emerald-500/30"}` + disabled: !state.file || !state.metadata.title.trim() || !state.metadata.type || !state.metadata.category || !hasAtLeastOneTag || !state.metadata.description.trim() || descriptionErrors.length > 0 || !state.metadata.licenseAccepted, + className: `inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-semibold text-white ${!state.file || !state.metadata.title.trim() || !state.metadata.type || !state.metadata.category || !hasAtLeastOneTag || !state.metadata.description.trim() || descriptionErrors.length > 0 || !state.metadata.licenseAccepted ? "bg-white/10 cursor-not-allowed" : "bg-emerald-500 shadow-lg shadow-emerald-500/30"}` }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-rocket", "aria-hidden": "true" }), "Start upload" @@ -113615,7 +115480,7 @@ function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) "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_156 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_161 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: UploadPage }, Symbol.toStringTag, { value: "Module" })); @@ -113859,7 +115724,7 @@ function WorldIndex() { } ))); } -const __vite_glob_0_157 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_162 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldIndex }, Symbol.toStringTag, { value: "Module" })); @@ -114160,7 +116025,7 @@ function WorldShow() { } ))); } -const __vite_glob_0_158 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const __vite_glob_0_163 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: WorldShow }, Symbol.toStringTag, { value: "Module" })); @@ -139809,107 +141674,112 @@ const pages = /* @__PURE__ */ Object.assign({ "./Pages/Collection/SavedCollections.jsx": __vite_glob_0_55, "./Pages/Community/CommunityActivityPage.jsx": __vite_glob_0_56, "./Pages/Community/LatestCommentsPage.jsx": __vite_glob_0_57, - "./Pages/Feed/FollowingFeed.jsx": __vite_glob_0_58, - "./Pages/Feed/HashtagFeed.jsx": __vite_glob_0_59, - "./Pages/Feed/SavedFeed.jsx": __vite_glob_0_60, - "./Pages/Feed/SearchFeed.jsx": __vite_glob_0_61, - "./Pages/Feed/TrendingFeed.jsx": __vite_glob_0_62, - "./Pages/Forum/ForumCategory.jsx": __vite_glob_0_63, - "./Pages/Forum/ForumEditPost.jsx": __vite_glob_0_64, - "./Pages/Forum/ForumIndex.jsx": __vite_glob_0_65, - "./Pages/Forum/ForumNewThread.jsx": __vite_glob_0_66, - "./Pages/Forum/ForumSection.jsx": __vite_glob_0_67, - "./Pages/Forum/ForumThread.jsx": __vite_glob_0_68, - "./Pages/Group/GroupChallengeShow.jsx": __vite_glob_0_69, - "./Pages/Group/GroupEventShow.jsx": __vite_glob_0_70, - "./Pages/Group/GroupFaqPage.jsx": __vite_glob_0_71, - "./Pages/Group/GroupHelpPage.jsx": __vite_glob_0_72, - "./Pages/Group/GroupIndex.jsx": __vite_glob_0_73, - "./Pages/Group/GroupPostShow.jsx": __vite_glob_0_74, - "./Pages/Group/GroupProjectShow.jsx": __vite_glob_0_75, - "./Pages/Group/GroupQuickstartPage.jsx": __vite_glob_0_76, - "./Pages/Group/GroupReleaseShow.jsx": __vite_glob_0_77, - "./Pages/Group/GroupShow.jsx": __vite_glob_0_78, - "./Pages/Help/AccountHelpPage.jsx": __vite_glob_0_79, - "./Pages/Help/AuthHelpPage.jsx": __vite_glob_0_80, - "./Pages/Help/CardsHelpPage.jsx": __vite_glob_0_81, - "./Pages/Help/HelpCenterPage.jsx": __vite_glob_0_82, - "./Pages/Help/ProfileHelpPage.jsx": __vite_glob_0_83, - "./Pages/Help/StudioHelpPage.jsx": __vite_glob_0_84, - "./Pages/Help/TroubleshootingHelpPage.jsx": __vite_glob_0_85, - "./Pages/Help/UploadHelpPage.jsx": __vite_glob_0_86, - "./Pages/Help/WorldsHelpPage.jsx": __vite_glob_0_87, - "./Pages/Leaderboard/LeaderboardPage.jsx": __vite_glob_0_88, - "./Pages/Messages/Index.jsx": __vite_glob_0_89, - "./Pages/Moderation/AiBiographyAdmin.jsx": __vite_glob_0_90, - "./Pages/Moderation/ArtworkMaturityQueue.jsx": __vite_glob_0_91, - "./Pages/Moderation/WorldWebStoriesIndex.jsx": __vite_glob_0_92, - "./Pages/Moderation/WorldWebStoryEditor.jsx": __vite_glob_0_93, - "./Pages/News/NewsComments.jsx": __vite_glob_0_94, - "./Pages/News/NewsImagePreview.jsx": __vite_glob_0_95, - "./Pages/Profile/ProfileGallery.jsx": __vite_glob_0_96, - "./Pages/Profile/ProfileShow.jsx": __vite_glob_0_97, - "./Pages/Settings/ProfileEdit.jsx": __vite_glob_0_98, - "./Pages/Studio/StudioActivity.jsx": __vite_glob_0_99, - "./Pages/Studio/StudioAnalytics.jsx": __vite_glob_0_100, - "./Pages/Studio/StudioArchived.jsx": __vite_glob_0_101, - "./Pages/Studio/StudioArtworkAnalytics.jsx": __vite_glob_0_102, - "./Pages/Studio/StudioArtworkEdit.jsx": __vite_glob_0_103, - "./Pages/Studio/StudioArtworks.jsx": __vite_glob_0_104, - "./Pages/Studio/StudioAssets.jsx": __vite_glob_0_105, - "./Pages/Studio/StudioCalendar.jsx": __vite_glob_0_106, - "./Pages/Studio/StudioCardAnalytics.jsx": __vite_glob_0_107, - "./Pages/Studio/StudioCardEditor.jsx": __vite_glob_0_108, - "./Pages/Studio/StudioCardsIndex.jsx": __vite_glob_0_109, - "./Pages/Studio/StudioChallenges.jsx": __vite_glob_0_110, - "./Pages/Studio/StudioCollections.jsx": __vite_glob_0_111, - "./Pages/Studio/StudioComments.jsx": __vite_glob_0_112, - "./Pages/Studio/StudioContentIndex.jsx": __vite_glob_0_113, - "./Pages/Studio/StudioDashboard.jsx": __vite_glob_0_114, - "./Pages/Studio/StudioDrafts.jsx": __vite_glob_0_115, - "./Pages/Studio/StudioFeatured.jsx": __vite_glob_0_116, - "./Pages/Studio/StudioFollowers.jsx": __vite_glob_0_117, - "./Pages/Studio/StudioGroupActivity.jsx": __vite_glob_0_118, - "./Pages/Studio/StudioGroupArtworks.jsx": __vite_glob_0_119, - "./Pages/Studio/StudioGroupAssets.jsx": __vite_glob_0_120, - "./Pages/Studio/StudioGroupChallengeEditor.jsx": __vite_glob_0_121, - "./Pages/Studio/StudioGroupChallenges.jsx": __vite_glob_0_122, - "./Pages/Studio/StudioGroupCollections.jsx": __vite_glob_0_123, - "./Pages/Studio/StudioGroupCreate.jsx": __vite_glob_0_124, - "./Pages/Studio/StudioGroupDashboard.jsx": __vite_glob_0_125, - "./Pages/Studio/StudioGroupEventEditor.jsx": __vite_glob_0_126, - "./Pages/Studio/StudioGroupEvents.jsx": __vite_glob_0_127, - "./Pages/Studio/StudioGroupInvitations.jsx": __vite_glob_0_128, - "./Pages/Studio/StudioGroupJoinRequests.jsx": __vite_glob_0_129, - "./Pages/Studio/StudioGroupMembers.jsx": __vite_glob_0_130, - "./Pages/Studio/StudioGroupPostEditor.jsx": __vite_glob_0_131, - "./Pages/Studio/StudioGroupPosts.jsx": __vite_glob_0_132, - "./Pages/Studio/StudioGroupProjectEditor.jsx": __vite_glob_0_133, - "./Pages/Studio/StudioGroupProjects.jsx": __vite_glob_0_134, - "./Pages/Studio/StudioGroupRecruitment.jsx": __vite_glob_0_135, - "./Pages/Studio/StudioGroupReleaseEditor.jsx": __vite_glob_0_136, - "./Pages/Studio/StudioGroupReleases.jsx": __vite_glob_0_137, - "./Pages/Studio/StudioGroupReputation.jsx": __vite_glob_0_138, - "./Pages/Studio/StudioGroupReviewQueue.jsx": __vite_glob_0_139, - "./Pages/Studio/StudioGroupSettings.jsx": __vite_glob_0_140, - "./Pages/Studio/StudioGroupsIndex.jsx": __vite_glob_0_141, - "./Pages/Studio/StudioGrowth.jsx": __vite_glob_0_142, - "./Pages/Studio/StudioInbox.jsx": __vite_glob_0_143, - "./Pages/Studio/StudioNewsEditor.jsx": __vite_glob_0_144, - "./Pages/Studio/StudioNewsIndex.jsx": __vite_glob_0_145, - "./Pages/Studio/StudioNewsTaxonomies.jsx": __vite_glob_0_146, - "./Pages/Studio/StudioPreferences.jsx": __vite_glob_0_147, - "./Pages/Studio/StudioProfile.jsx": __vite_glob_0_148, - "./Pages/Studio/StudioScheduled.jsx": __vite_glob_0_149, - "./Pages/Studio/StudioSearch.jsx": __vite_glob_0_150, - "./Pages/Studio/StudioSettings.jsx": __vite_glob_0_151, - "./Pages/Studio/StudioStories.jsx": __vite_glob_0_152, - "./Pages/Studio/StudioUploadQueue.jsx": __vite_glob_0_153, - "./Pages/Studio/StudioWorldEditor.jsx": __vite_glob_0_154, - "./Pages/Studio/StudioWorldsIndex.jsx": __vite_glob_0_155, - "./Pages/Upload/Index.jsx": __vite_glob_0_156, - "./Pages/World/WorldIndex.jsx": __vite_glob_0_157, - "./Pages/World/WorldShow.jsx": __vite_glob_0_158 + "./Pages/Enhance/Create.jsx": __vite_glob_0_58, + "./Pages/Enhance/Index.jsx": __vite_glob_0_59, + "./Pages/Enhance/Show.jsx": __vite_glob_0_60, + "./Pages/Feed/FollowingFeed.jsx": __vite_glob_0_61, + "./Pages/Feed/HashtagFeed.jsx": __vite_glob_0_62, + "./Pages/Feed/SavedFeed.jsx": __vite_glob_0_63, + "./Pages/Feed/SearchFeed.jsx": __vite_glob_0_64, + "./Pages/Feed/TrendingFeed.jsx": __vite_glob_0_65, + "./Pages/Forum/ForumCategory.jsx": __vite_glob_0_66, + "./Pages/Forum/ForumEditPost.jsx": __vite_glob_0_67, + "./Pages/Forum/ForumIndex.jsx": __vite_glob_0_68, + "./Pages/Forum/ForumNewThread.jsx": __vite_glob_0_69, + "./Pages/Forum/ForumSection.jsx": __vite_glob_0_70, + "./Pages/Forum/ForumThread.jsx": __vite_glob_0_71, + "./Pages/Group/GroupChallengeShow.jsx": __vite_glob_0_72, + "./Pages/Group/GroupEventShow.jsx": __vite_glob_0_73, + "./Pages/Group/GroupFaqPage.jsx": __vite_glob_0_74, + "./Pages/Group/GroupHelpPage.jsx": __vite_glob_0_75, + "./Pages/Group/GroupIndex.jsx": __vite_glob_0_76, + "./Pages/Group/GroupPostShow.jsx": __vite_glob_0_77, + "./Pages/Group/GroupProjectShow.jsx": __vite_glob_0_78, + "./Pages/Group/GroupQuickstartPage.jsx": __vite_glob_0_79, + "./Pages/Group/GroupReleaseShow.jsx": __vite_glob_0_80, + "./Pages/Group/GroupShow.jsx": __vite_glob_0_81, + "./Pages/Help/AccountHelpPage.jsx": __vite_glob_0_82, + "./Pages/Help/AuthHelpPage.jsx": __vite_glob_0_83, + "./Pages/Help/CardsHelpPage.jsx": __vite_glob_0_84, + "./Pages/Help/HelpCenterPage.jsx": __vite_glob_0_85, + "./Pages/Help/ProfileHelpPage.jsx": __vite_glob_0_86, + "./Pages/Help/StudioHelpPage.jsx": __vite_glob_0_87, + "./Pages/Help/TroubleshootingHelpPage.jsx": __vite_glob_0_88, + "./Pages/Help/UploadHelpPage.jsx": __vite_glob_0_89, + "./Pages/Help/WorldsHelpPage.jsx": __vite_glob_0_90, + "./Pages/Leaderboard/LeaderboardPage.jsx": __vite_glob_0_91, + "./Pages/Messages/Index.jsx": __vite_glob_0_92, + "./Pages/Moderation/AiBiographyAdmin.jsx": __vite_glob_0_93, + "./Pages/Moderation/ArtworkMaturityQueue.jsx": __vite_glob_0_94, + "./Pages/Moderation/Enhance/Index.jsx": __vite_glob_0_95, + "./Pages/Moderation/Enhance/Show.jsx": __vite_glob_0_96, + "./Pages/Moderation/WorldWebStoriesIndex.jsx": __vite_glob_0_97, + "./Pages/Moderation/WorldWebStoryEditor.jsx": __vite_glob_0_98, + "./Pages/News/NewsComments.jsx": __vite_glob_0_99, + "./Pages/News/NewsImagePreview.jsx": __vite_glob_0_100, + "./Pages/Profile/ProfileGallery.jsx": __vite_glob_0_101, + "./Pages/Profile/ProfileShow.jsx": __vite_glob_0_102, + "./Pages/Settings/ProfileEdit.jsx": __vite_glob_0_103, + "./Pages/Studio/StudioActivity.jsx": __vite_glob_0_104, + "./Pages/Studio/StudioAnalytics.jsx": __vite_glob_0_105, + "./Pages/Studio/StudioArchived.jsx": __vite_glob_0_106, + "./Pages/Studio/StudioArtworkAnalytics.jsx": __vite_glob_0_107, + "./Pages/Studio/StudioArtworkEdit.jsx": __vite_glob_0_108, + "./Pages/Studio/StudioArtworks.jsx": __vite_glob_0_109, + "./Pages/Studio/StudioAssets.jsx": __vite_glob_0_110, + "./Pages/Studio/StudioCalendar.jsx": __vite_glob_0_111, + "./Pages/Studio/StudioCardAnalytics.jsx": __vite_glob_0_112, + "./Pages/Studio/StudioCardEditor.jsx": __vite_glob_0_113, + "./Pages/Studio/StudioCardsIndex.jsx": __vite_glob_0_114, + "./Pages/Studio/StudioChallenges.jsx": __vite_glob_0_115, + "./Pages/Studio/StudioCollections.jsx": __vite_glob_0_116, + "./Pages/Studio/StudioComments.jsx": __vite_glob_0_117, + "./Pages/Studio/StudioContentIndex.jsx": __vite_glob_0_118, + "./Pages/Studio/StudioDashboard.jsx": __vite_glob_0_119, + "./Pages/Studio/StudioDrafts.jsx": __vite_glob_0_120, + "./Pages/Studio/StudioFeatured.jsx": __vite_glob_0_121, + "./Pages/Studio/StudioFollowers.jsx": __vite_glob_0_122, + "./Pages/Studio/StudioGroupActivity.jsx": __vite_glob_0_123, + "./Pages/Studio/StudioGroupArtworks.jsx": __vite_glob_0_124, + "./Pages/Studio/StudioGroupAssets.jsx": __vite_glob_0_125, + "./Pages/Studio/StudioGroupChallengeEditor.jsx": __vite_glob_0_126, + "./Pages/Studio/StudioGroupChallenges.jsx": __vite_glob_0_127, + "./Pages/Studio/StudioGroupCollections.jsx": __vite_glob_0_128, + "./Pages/Studio/StudioGroupCreate.jsx": __vite_glob_0_129, + "./Pages/Studio/StudioGroupDashboard.jsx": __vite_glob_0_130, + "./Pages/Studio/StudioGroupEventEditor.jsx": __vite_glob_0_131, + "./Pages/Studio/StudioGroupEvents.jsx": __vite_glob_0_132, + "./Pages/Studio/StudioGroupInvitations.jsx": __vite_glob_0_133, + "./Pages/Studio/StudioGroupJoinRequests.jsx": __vite_glob_0_134, + "./Pages/Studio/StudioGroupMembers.jsx": __vite_glob_0_135, + "./Pages/Studio/StudioGroupPostEditor.jsx": __vite_glob_0_136, + "./Pages/Studio/StudioGroupPosts.jsx": __vite_glob_0_137, + "./Pages/Studio/StudioGroupProjectEditor.jsx": __vite_glob_0_138, + "./Pages/Studio/StudioGroupProjects.jsx": __vite_glob_0_139, + "./Pages/Studio/StudioGroupRecruitment.jsx": __vite_glob_0_140, + "./Pages/Studio/StudioGroupReleaseEditor.jsx": __vite_glob_0_141, + "./Pages/Studio/StudioGroupReleases.jsx": __vite_glob_0_142, + "./Pages/Studio/StudioGroupReputation.jsx": __vite_glob_0_143, + "./Pages/Studio/StudioGroupReviewQueue.jsx": __vite_glob_0_144, + "./Pages/Studio/StudioGroupSettings.jsx": __vite_glob_0_145, + "./Pages/Studio/StudioGroupsIndex.jsx": __vite_glob_0_146, + "./Pages/Studio/StudioGrowth.jsx": __vite_glob_0_147, + "./Pages/Studio/StudioInbox.jsx": __vite_glob_0_148, + "./Pages/Studio/StudioNewsEditor.jsx": __vite_glob_0_149, + "./Pages/Studio/StudioNewsIndex.jsx": __vite_glob_0_150, + "./Pages/Studio/StudioNewsTaxonomies.jsx": __vite_glob_0_151, + "./Pages/Studio/StudioPreferences.jsx": __vite_glob_0_152, + "./Pages/Studio/StudioProfile.jsx": __vite_glob_0_153, + "./Pages/Studio/StudioScheduled.jsx": __vite_glob_0_154, + "./Pages/Studio/StudioSearch.jsx": __vite_glob_0_155, + "./Pages/Studio/StudioSettings.jsx": __vite_glob_0_156, + "./Pages/Studio/StudioStories.jsx": __vite_glob_0_157, + "./Pages/Studio/StudioUploadQueue.jsx": __vite_glob_0_158, + "./Pages/Studio/StudioWorldEditor.jsx": __vite_glob_0_159, + "./Pages/Studio/StudioWorldsIndex.jsx": __vite_glob_0_160, + "./Pages/Upload/Index.jsx": __vite_glob_0_161, + "./Pages/World/WorldIndex.jsx": __vite_glob_0_162, + "./Pages/World/WorldShow.jsx": __vite_glob_0_163 }); const ClientOnlyPlaceholder = () => null; d( diff --git a/composer.json b/composer.json index ddbef3b0..3854709e 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,6 @@ "license": "MIT", "require": { "php": "^8.2", - "alexusmai/laravel-file-manager": "*", "composer/installers": "^2.3", "gumlet/php-image-resize": "*", "inertiajs/inertia-laravel": "^1.0", diff --git a/composer.lock b/composer.lock index 730ea70a..06cee776 100644 --- a/composer.lock +++ b/composer.lock @@ -4,67 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ae4cbbbd3390e2a18df6cb08a6caf6aa", + "content-hash": "541533f2d5a6c0c966730bac8f9c2b37", "packages": [ - { - "name": "alexusmai/laravel-file-manager", - "version": "3.3.3", - "source": { - "type": "git", - "url": "https://github.com/alexusmai/laravel-file-manager.git", - "reference": "74bebe32d821d19c1c026545af7e4043fe074aba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/alexusmai/laravel-file-manager/zipball/74bebe32d821d19c1c026545af7e4043fe074aba", - "reference": "74bebe32d821d19c1c026545af7e4043fe074aba", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-zip": "*", - "intervention/image-laravel": "^1.2.0", - "laravel/framework": "^9.0|^10.0|^11.0|^12.0|^13.0", - "league/flysystem": "^3.0", - "php": "^8.1" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Alexusmai\\LaravelFileManager\\FileManagerServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Alexusmai\\LaravelFileManager\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aleksandr Manekin", - "email": "alexusmai@gmail.com", - "role": "Developer" - } - ], - "description": "File manager for Laravel", - "homepage": "https://github.com/alexusami/laravel-file-manager", - "keywords": [ - "file", - "laravel", - "manager" - ], - "support": { - "issues": "https://github.com/alexusmai/laravel-file-manager/issues", - "source": "https://github.com/alexusmai/laravel-file-manager/tree/3.3.3" - }, - "time": "2026-05-12T10:06:23+00:00" - }, { "name": "aws/aws-crt-php", "version": "v1.2.7", @@ -1921,90 +1862,6 @@ ], "time": "2026-05-01T08:20:10+00:00" }, - { - "name": "intervention/image-laravel", - "version": "1.5.9", - "source": { - "type": "git", - "url": "https://github.com/Intervention/image-laravel.git", - "reference": "a760b041e5133fd81509414f4955c93ffefb4a7b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Intervention/image-laravel/zipball/a760b041e5133fd81509414f4955c93ffefb4a7b", - "reference": "a760b041e5133fd81509414f4955c93ffefb4a7b", - "shasum": "" - }, - "require": { - "illuminate/http": "^8|^9|^10|^11|^12|^13", - "illuminate/routing": "^8|^9|^10|^11|^12|^13", - "illuminate/support": "^8|^9|^10|^11|^12|^13", - "intervention/image": "^3.11", - "php": "^8.1" - }, - "require-dev": { - "ext-fileinfo": "*", - "orchestra/testbench": "^8.18 || ^9.9 || ^10.6", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Image": "Intervention\\Image\\Laravel\\Facades\\Image" - }, - "providers": [ - "Intervention\\Image\\Laravel\\ServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Intervention\\Image\\Laravel\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Oliver Vogel", - "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" - } - ], - "description": "Laravel Integration of Intervention Image", - "homepage": "https://image.intervention.io/", - "keywords": [ - "gd", - "image", - "imagick", - "laravel", - "resize", - "thumbnail", - "watermark" - ], - "support": { - "issues": "https://github.com/Intervention/image-laravel/issues", - "source": "https://github.com/Intervention/image-laravel/tree/1.5.9" - }, - "funding": [ - { - "url": "https://paypal.me/interventionio", - "type": "custom" - }, - { - "url": "https://github.com/Intervention", - "type": "github" - }, - { - "url": "https://ko-fi.com/interventionphp", - "type": "ko_fi" - } - ], - "time": "2026-03-24T15:10:30+00:00" - }, { "name": "jaybizzle/crawler-detect", "version": "v1.3.11", diff --git a/config/enhance.php b/config/enhance.php new file mode 100644 index 00000000..a750b4e8 --- /dev/null +++ b/config/enhance.php @@ -0,0 +1,60 @@ + env('ENHANCE_DISK', env('FILESYSTEM_DISK', 'public')), + + 'source_prefix' => env('ENHANCE_SOURCE_PREFIX', 'enhance/sources'), + 'output_prefix' => env('ENHANCE_OUTPUT_PREFIX', 'enhance/outputs'), + 'preview_prefix' => env('ENHANCE_PREVIEW_PREFIX', 'enhance/previews'), + + 'default_engine' => env('ENHANCE_ENGINE', 'stub'), + + 'max_upload_mb' => (int) env('ENHANCE_MAX_UPLOAD_MB', 20), + 'max_input_width' => (int) env('ENHANCE_MAX_INPUT_WIDTH', 4096), + 'max_input_height' => (int) env('ENHANCE_MAX_INPUT_HEIGHT', 4096), + 'max_output_width' => (int) env('ENHANCE_MAX_OUTPUT_WIDTH', 8192), + 'max_output_height' => (int) env('ENHANCE_MAX_OUTPUT_HEIGHT', 8192), + + 'allowed_mimes' => [ + 'image/jpeg', + 'image/png', + 'image/webp', + ], + + 'allowed_modes' => [ + 'standard', + 'artwork', + 'photo', + 'illustration', + ], + + 'allowed_scales' => [2, 4], + + 'daily_limit' => (int) env('ENHANCE_DAILY_LIMIT', 10), + 'queue' => env('ENHANCE_QUEUE', 'default'), + + 'lifecycle' => [ + 'completed_expires_after_days' => (int) env('ENHANCE_COMPLETED_EXPIRES_AFTER_DAYS', 30), + 'failed_expires_after_days' => (int) env('ENHANCE_FAILED_EXPIRES_AFTER_DAYS', 7), + 'deleted_file_grace_days' => (int) env('ENHANCE_DELETED_FILE_GRACE_DAYS', 1), + 'cleanup_chunk_size' => (int) env('ENHANCE_CLEANUP_CHUNK_SIZE', 100), + ], + + 'health' => [ + 'stuck_processing_after_minutes' => (int) env('ENHANCE_STUCK_PROCESSING_AFTER_MINUTES', 30), + 'stuck_queued_after_minutes' => (int) env('ENHANCE_STUCK_QUEUED_AFTER_MINUTES', 60), + ], + + 'stub' => [ + 'show_warning' => filter_var(env('ENHANCE_STUB_SHOW_WARNING', true), FILTER_VALIDATE_BOOL), + ], + + 'external_worker' => [ + 'url' => env('ENHANCE_WORKER_URL'), + 'timeout' => (int) env('ENHANCE_WORKER_TIMEOUT', 300), + 'token' => env('ENHANCE_WORKER_TOKEN'), + 'max_download_mb' => (int) env('ENHANCE_WORKER_MAX_DOWNLOAD_MB', 60), + ], +]; \ No newline at end of file diff --git a/config/file-manager.php b/config/file-manager.php deleted file mode 100644 index 63fd749d..00000000 --- a/config/file-manager.php +++ /dev/null @@ -1,166 +0,0 @@ - DefaultConfigRepository::class, - - /** - * ACL rules repository - * - * Default - ConfigACLRepository (see rules in - aclRules) - */ - 'aclRepository' => ConfigACLRepository::class, - - //********* Default configuration for DefaultConfigRepository ************** - - /** - * LFM Route prefix - * !!! WARNING - if you change it, you should compile frontend with new prefix(baseUrl) !!! - */ - 'routePrefix' => 'file-manager', - - /** - * List of disk names that you want to use - * (from config/filesystems) - */ - 'diskList' => ['public'], - - /** - * Default disk for left manager - * - * null - auto select the first disk in the disk list - */ - 'leftDisk' => null, - - /** - * Default disk for right manager - * - * null - auto select the first disk in the disk list - */ - 'rightDisk' => null, - - /** - * Default path for left manager - * - * null - root directory - */ - 'leftPath' => null, - - /** - * Default path for right manager - * - * null - root directory - */ - 'rightPath' => null, - - /** - * File manager modules configuration - * - * 1 - only one file manager window - * 2 - one file manager window with directories tree module - * 3 - two file manager windows - */ - 'windowsConfig' => 2, - - /** - * File upload - Max file size in KB - * - * null - no restrictions - */ - 'maxUploadFileSize' => null, - - /** - * File upload - Allow these file types - * - * [] - no restrictions - */ - 'allowFileTypes' => [], - - /** - * Show / Hide system files and folders - */ - 'hiddenFiles' => true, - - /*************************************************************************** - * Middleware - * - * Add your middleware name to array -> ['web', 'auth', 'admin'] - * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!! - */ - 'middleware' => ['web'], - - /*************************************************************************** - * ACL mechanism ON/OFF - * - * default - false(OFF) - */ - 'acl' => false, - - /** - * Hide files and folders from file-manager if user doesn't have access - * - * ACL access level = 0 - */ - 'aclHideFromFM' => true, - - /** - * ACL strategy - * - * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list - * - * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list - */ - 'aclStrategy' => 'blacklist', - - /** - * ACL Rules cache - * - * null or value in minutes - */ - 'aclRulesCache' => null, - - //********* Default configuration for DefaultConfigRepository END ********** - - - /*************************************************************************** - * ACL rules list - used for default ACL repository (ConfigACLRepository) - * - * 1 it's user ID - * null - for not authenticated user - * - * 'disk' => 'disk-name' - * - * 'path' => 'folder-name' - * 'path' => 'folder1*' - select folder1, folder12, folder1/sub-folder, ... - * 'path' => 'folder2/*' - select folder2/sub-folder,... but not select folder2 !!! - * 'path' => 'folder-name/file-name.jpg' - * 'path' => 'folder-name/*.jpg' - * - * * - wildcard - * - * access: 0 - deny, 1 - read, 2 - read/write - */ - 'aclRules' => [ - null => [ - //['disk' => 'public', 'path' => '/', 'access' => 2], - ], - 1 => [ - //['disk' => 'public', 'path' => 'images/arch*.jpg', 'access' => 2], - //['disk' => 'public', 'path' => 'files/*', 'access' => 1], - ], - ], - - /** - * Enable slugification of filenames of uploaded files. - * - */ - 'slugifyNames' => false, -]; diff --git a/database/migrations/2026_05_26_120000_create_enhance_jobs_table.php b/database/migrations/2026_05_26_120000_create_enhance_jobs_table.php new file mode 100644 index 00000000..a8da6269 --- /dev/null +++ b/database/migrations/2026_05_26_120000_create_enhance_jobs_table.php @@ -0,0 +1,66 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('artwork_id')->nullable()->constrained('artworks')->nullOnDelete(); + + $table->string('status', 32)->default('pending'); + $table->string('engine', 64)->default('stub'); + $table->string('mode', 32)->default('standard'); + $table->unsignedTinyInteger('scale')->default(2); + + $table->string('source_disk', 64)->nullable(); + $table->string('source_path')->nullable(); + $table->string('source_hash', 128)->nullable(); + + $table->unsignedInteger('input_width')->nullable(); + $table->unsignedInteger('input_height')->nullable(); + $table->unsignedBigInteger('input_filesize')->nullable(); + $table->string('input_mime', 128)->nullable(); + + $table->string('output_disk', 64)->nullable(); + $table->string('output_path')->nullable(); + $table->string('output_hash', 128)->nullable(); + + $table->unsignedInteger('output_width')->nullable(); + $table->unsignedInteger('output_height')->nullable(); + $table->unsignedBigInteger('output_filesize')->nullable(); + $table->string('output_mime', 128)->nullable(); + + $table->string('preview_disk', 64)->nullable(); + $table->string('preview_path')->nullable(); + + $table->unsignedInteger('processing_seconds')->nullable(); + $table->text('error_message')->nullable(); + $table->json('metadata')->nullable(); + + $table->timestamp('queued_at')->nullable(); + $table->timestamp('started_at')->nullable(); + $table->timestamp('finished_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->index(['user_id', 'status']); + $table->index(['status', 'created_at']); + $table->index(['artwork_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('enhance_jobs'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_30_000001_add_unique_index_to_news_views_table.php b/database/migrations/2026_05_30_000001_add_unique_index_to_news_views_table.php new file mode 100644 index 00000000..e92fe670 --- /dev/null +++ b/database/migrations/2026_05_30_000001_add_unique_index_to_news_views_table.php @@ -0,0 +1,120 @@ +indexExists('news_views', 'news_views_article_ip_unique')) { + $table->unique(['article_id', 'ip'], 'news_views_article_ip_unique'); + } + + if (! $this->indexExists('news_views', 'news_views_article_user_unique')) { + $table->unique(['article_id', 'user_id'], 'news_views_article_user_unique'); + } + }); + } + + public function down(): void + { + if (! Schema::hasTable('news_views')) { + return; + } + + Schema::table('news_views', function (Blueprint $table): void { + if ($this->indexExists('news_views', 'news_views_article_ip_unique')) { + $table->dropUnique('news_views_article_ip_unique'); + } + + if ($this->indexExists('news_views', 'news_views_article_user_unique')) { + $table->dropUnique('news_views_article_user_unique'); + } + }); + } + + private function indexExists(string $table, string $indexName): bool + { + if (DB::getDriverName() === 'sqlite') { + return collect(DB::select("PRAGMA index_list('{$table}')")) + ->contains(static fn (object $row): bool => ($row->name ?? null) === $indexName); + } + + $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); + + return count($indexes) > 0; + } +}; diff --git a/database/migrations/2026_05_30_000002_add_external_url_to_news_article_relations_table.php b/database/migrations/2026_05_30_000002_add_external_url_to_news_article_relations_table.php new file mode 100644 index 00000000..1571850f --- /dev/null +++ b/database/migrations/2026_05_30_000002_add_external_url_to_news_article_relations_table.php @@ -0,0 +1,49 @@ +string('external_url', 2048)->nullable()->after('entity_id'); + } + }); + + Schema::table('news_article_relations', function (Blueprint $table): void { + $table->unsignedBigInteger('entity_id')->nullable()->change(); + }); + } + + public function down(): void + { + if (! Schema::hasTable('news_article_relations')) { + return; + } + + DB::table('news_article_relations') + ->whereNotNull('external_url') + ->delete(); + + Schema::table('news_article_relations', function (Blueprint $table): void { + if (Schema::hasColumn('news_article_relations', 'external_url')) { + $table->dropColumn('external_url'); + } + }); + + Schema::table('news_article_relations', function (Blueprint $table): void { + $table->unsignedBigInteger('entity_id')->nullable(false)->change(); + }); + } +}; \ No newline at end of file diff --git a/docs/enhance-setup.md b/docs/enhance-setup.md new file mode 100644 index 00000000..79663897 --- /dev/null +++ b/docs/enhance-setup.md @@ -0,0 +1,580 @@ +Skinbase Enhance Setup Guide +=========================== + +This guide explains how to set up, enable, verify, and operate the Skinbase Enhance module end to end. + +Use this document when you need to: + +- enable Enhance in a local environment +- switch from stub mode to the external worker +- run the worker in Pillow mode or Real-ESRGAN mode +- configure queues, cleanup, and health checks +- understand what the module stores and how it behaves in production + +What the module does +-------------------- + +Enhance accepts an uploaded or selected source image, creates an Enhance job, processes that job through the configured engine, stores the generated output on the Enhance storage disk, and keeps the original source file untouched. + +Current supported engines on the Laravel side: + +- `ENHANCE_ENGINE=stub` +- `ENHANCE_ENGINE=external_worker` + +Current supported worker engines: + +- `WORKER_ENGINE=pillow` +- `WORKER_ENGINE=realesrgan-ncnn` +- `WORKER_ENGINE=realesrgan` + +`WORKER_ENGINE=realesrgan` currently aliases to `realesrgan-ncnn`. + +Architecture +------------ + +The Enhance module is split into two layers. + +Laravel application: + +- accepts the Enhance request +- validates allowed file types, dimensions, scales, and modes +- stores job records +- dispatches the job to the queue +- owns permanent storage for Enhance sources and outputs +- exposes moderation, cleanup, and health commands + +Optional external worker: + +- downloads the copied Enhance source image +- upscales it with Pillow or Real-ESRGAN +- exposes a temporary internal result URL +- deletes its temporary result after Laravel confirms download + +Laravel remains the source of truth. The worker is temporary processing only. + +Default behavior and limits +--------------------------- + +Default config values from [config/enhance.php](config/enhance.php): + +- disk: `ENHANCE_DISK` or the app filesystem default +- source prefix: `enhance/sources` +- output prefix: `enhance/outputs` +- preview prefix: `enhance/previews` +- default engine: `stub` +- allowed MIME types: `image/jpeg`, `image/png`, `image/webp` +- allowed modes: `standard`, `artwork`, `photo`, `illustration` +- allowed scales: `2`, `4` +- daily limit: `10` +- default queue: `default` + +Lifecycle defaults: + +- completed jobs expire after `30` days +- failed jobs expire after `7` days +- deleted job files get a `1` day grace period + +Health defaults: + +- processing jobs are considered stuck after `30` minutes +- queued jobs are considered stale after `60` minutes + +Prerequisites +------------- + +Laravel requirements: + +- the application boots normally +- database migrations are current +- the configured filesystem disk is writable +- a queue worker or Horizon is running for the queue you assign to Enhance +- Laravel scheduler is enabled if you want automatic cleanup + +Worker requirements for `external_worker` mode: + +- Docker or a Python runtime for `services/enhance-worker` +- network access from Laravel to the worker URL +- a shared bearer token between Laravel and the worker +- for Real-ESRGAN mode, the `realesrgan-ncnn-vulkan` binary and model files + +Setup paths +----------- + +There are three practical ways to run Enhance. + +1. Stub mode + +- safest local starting point +- exercises the Laravel flow without a real AI runtime +- no worker needed + +2. External worker with Pillow + +- good for local integration testing and CI-like validation +- real HTTP worker contract +- deterministic fallback upscale path +- no Real-ESRGAN runtime files required + +3. External worker with Real-ESRGAN + +- production-oriented path +- uses the `realesrgan-ncnn-vulkan` CLI runtime +- requires runtime files and a verified worker host + +Laravel setup +------------- + +Minimum env for stub mode: + +```env +ENHANCE_ENGINE=stub +ENHANCE_QUEUE=default +``` + +Recommended env for external worker mode: + +```env +ENHANCE_ENGINE=external_worker +ENHANCE_QUEUE=enhance +ENHANCE_WORKER_URL=http://127.0.0.1:8095 +ENHANCE_WORKER_TIMEOUT=900 +ENHANCE_WORKER_TOKEN=change-this-token +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 +``` + +Optional Laravel env keys you may tune: + +```env +ENHANCE_DISK=public +ENHANCE_SOURCE_PREFIX=enhance/sources +ENHANCE_OUTPUT_PREFIX=enhance/outputs +ENHANCE_PREVIEW_PREFIX=enhance/previews + +ENHANCE_MAX_UPLOAD_MB=20 +ENHANCE_MAX_INPUT_WIDTH=4096 +ENHANCE_MAX_INPUT_HEIGHT=4096 +ENHANCE_MAX_OUTPUT_WIDTH=8192 +ENHANCE_MAX_OUTPUT_HEIGHT=8192 + +ENHANCE_DAILY_LIMIT=10 + +ENHANCE_COMPLETED_EXPIRES_AFTER_DAYS=30 +ENHANCE_FAILED_EXPIRES_AFTER_DAYS=7 +ENHANCE_DELETED_FILE_GRACE_DAYS=1 +ENHANCE_CLEANUP_CHUNK_SIZE=100 + +ENHANCE_STUCK_PROCESSING_AFTER_MINUTES=30 +ENHANCE_STUCK_QUEUED_AFTER_MINUTES=60 + +ENHANCE_STUB_SHOW_WARNING=true +``` + +After changing env: + +```bash +php artisan config:clear +``` + +Queue setup +----------- + +Enhance jobs run on `ENHANCE_QUEUE`. + +If you keep the default queue: + +```bash +php artisan queue:work --queue=default +``` + +If you use a dedicated Enhance queue: + +```bash +php artisan queue:work --queue=enhance,default +``` + +If Horizon or workers do not consume the configured queue, Enhance jobs will stay queued. + +Scheduler and cleanup +--------------------- + +Enhance cleanup is scheduled from [routes/console.php](routes/console.php) and runs: + +```bash +php artisan enhance:cleanup --force +``` + +Useful cleanup and health commands: + +```bash +php artisan enhance:health +php artisan enhance:health --json +php artisan enhance:cleanup --dry-run +php artisan enhance:cleanup --force +``` + +Cleanup only removes files under the configured Enhance prefixes. It does not delete artwork originals or unrelated storage paths. + +Enable stub mode +---------------- + +Use stub mode first if you want to validate the Laravel module without introducing worker runtime variables. + +1. Set: + +```env +ENHANCE_ENGINE=stub +ENHANCE_QUEUE=default +``` + +2. Clear config: + +```bash +php artisan config:clear +``` + +3. Start a queue worker: + +```bash +php artisan queue:work --queue=default +``` + +4. Check health: + +```bash +php artisan enhance:health +``` + +5. Open `/enhance/create`, submit a small image, and verify the job completes. + +Enable external worker mode with Pillow +--------------------------------------- + +This is the safest real integration path because it exercises the HTTP worker contract without requiring Real-ESRGAN files. + +1. Start the worker: + +```bash +cd services/enhance-worker +docker compose -f docker-compose.example.yml up --build +``` + +2. Confirm worker health: + +```bash +curl http://127.0.0.1:8095/health +``` + +Expected result: + +- `status: ok` +- `engine: pillow` + +3. Set Laravel env: + +```env +ENHANCE_ENGINE=external_worker +ENHANCE_QUEUE=enhance +ENHANCE_WORKER_URL=http://127.0.0.1:8095 +ENHANCE_WORKER_TIMEOUT=600 +ENHANCE_WORKER_TOKEN=change-this-token +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 +``` + +4. Clear config and start queue workers: + +```bash +php artisan config:clear +php artisan queue:work --queue=enhance,default +``` + +5. Verify the Laravel side: + +```bash +php artisan enhance:health +php artisan test --filter=EnhanceExternalWorker +``` + +Enable external worker mode with Real-ESRGAN +-------------------------------------------- + +This is the production-oriented setup. + +1. Install or mount the runtime files inside [services/enhance-worker](services/enhance-worker): + +```bash +cd services/enhance-worker +bash scripts/download-realesrgan-ncnn.sh +bash scripts/verify-realesrgan.sh +``` + +Required file locations: + +- binary: `bin/realesrgan-ncnn-vulkan` +- models: `models/*.param` and `models/*.bin` + +2. Start the Real-ESRGAN worker: + +```bash +docker compose -f docker-compose.realesrgan.example.yml up --build +``` + +3. Confirm worker health: + +```bash +curl http://127.0.0.1:8095/health +``` + +Expected result when ready: + +- `status: ok` +- `engine: realesrgan-ncnn` + +Expected result when runtime files are missing or invalid: + +- `status: degraded` + +4. Set Laravel env: + +```env +ENHANCE_ENGINE=external_worker +ENHANCE_QUEUE=enhance +ENHANCE_WORKER_URL=http://127.0.0.1:8095 +ENHANCE_WORKER_TIMEOUT=900 +ENHANCE_WORKER_TOKEN=change-this-token +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 +``` + +5. Clear config and start the queue worker: + +```bash +php artisan config:clear +php artisan queue:work --queue=enhance,default +``` + +6. Verify through the application by submitting a small image from `/enhance/create`. + +Worker configuration +-------------------- + +Main worker env values: + +```env +WORKER_HOST=0.0.0.0 +WORKER_PORT=8095 +WORKER_TOKEN=change-this-token + +WORKER_ENGINE=pillow +WORKER_DEVICE=cpu + +WORKER_MAX_UPLOAD_MB=20 +WORKER_MAX_INPUT_WIDTH=4096 +WORKER_MAX_INPUT_HEIGHT=4096 +WORKER_MAX_OUTPUT_WIDTH=8192 +WORKER_MAX_OUTPUT_HEIGHT=8192 + +WORKER_TMP_DIR=/app/storage/tmp +WORKER_OUTPUT_DIR=/app/storage/output +WORKER_RESULT_TTL_MINUTES=60 +``` + +Real-ESRGAN-specific worker env values: + +```env +WORKER_REALESRGAN_BIN=/app/bin/realesrgan-ncnn-vulkan +WORKER_REALESRGAN_MODEL_DIR=/app/models +WORKER_REALESRGAN_DEFAULT_MODEL=realesrgan-x4plus +WORKER_REALESRGAN_ANIME_MODEL=realesrgan-x4plus-anime +WORKER_REALESRGAN_TILE=0 +WORKER_REALESRGAN_TTA=false +WORKER_REALESRGAN_VERBOSE=false +WORKER_REALESRGAN_TIMEOUT_SECONDS=900 +WORKER_REALESRGAN_PREPROCESS_MAX_PIXELS=16777216 +WORKER_REALESRGAN_OUTPUT_EXT=webp +WORKER_REALESRGAN_ALLOW_MODEL_FALLBACK=true +``` + +Compatibility values kept by the worker: + +```env +WORKER_MODEL_DIR=/app/app/models +WORKER_DEFAULT_MODEL=realesrgan-x4plus +``` + +Worker behavior +--------------- + +Worker request contract: + +- endpoint: `POST /v1/upscale` +- bearer auth required +- accepted output formats: `webp`, `png`, `jpg` +- allowed scales: `2`, `4` +- allowed modes: `standard`, `artwork`, `photo`, `illustration` + +Health and temp file endpoints: + +- `GET /health` +- `GET /v1/results/{filename}` +- `DELETE /v1/results/{filename}` + +Mode and scale behavior +----------------------- + +Real-ESRGAN mode mapping: + +- `standard` -> default model +- `artwork` -> default model +- `photo` -> default model +- `illustration` -> anime model when available + +Fallback behavior: + +- if the requested model exists, it is used +- if it is missing and fallback is enabled, the default model is used +- if it is missing and fallback is disabled, processing fails safely + +Scale behavior: + +- `4x` returns native 4x output +- `2x` currently runs the 4x model and then downsamples to 2x + +Storage and data flow +--------------------- + +Laravel stores Enhance files under the configured prefixes: + +- sources under `enhance/sources` +- outputs under `enhance/outputs` +- previews under `enhance/previews` + +The worker does not permanently store Enhance results. + +When `ENHANCE_ENGINE=external_worker`: + +1. Laravel prepares a copied Enhance source file. +2. Laravel sends the worker a temporary URL or a temporary signed internal route. +3. The worker downloads the source file. +4. The worker processes the image. +5. The worker exposes a temporary result URL. +6. Laravel downloads, validates, and stores the final output. +7. Laravel instructs the worker to delete the temporary result. + +Verification checklist +---------------------- + +Laravel verification: + +```bash +php artisan config:clear +php artisan enhance:health +php artisan enhance:health --json +``` + +Worker verification: + +```bash +curl http://127.0.0.1:8095/health +``` + +Queue verification: + +```bash +php artisan queue:work --queue=enhance,default +``` + +Application verification: + +1. Open `/enhance/create`. +2. Upload or choose a small source image. +3. Select `2x` or `4x` and a valid mode. +4. Submit the job. +5. Confirm the job transitions from queued to completed. +6. Confirm output exists on the Enhance disk. +7. Confirm the source file and original artwork remain untouched. + +Health states +------------- + +Laravel health command helps identify: + +- configured engine +- queue usage +- stuck jobs +- lifecycle status + +Worker `/health` helps identify: + +- current worker engine +- maximum input and output limits +- Real-ESRGAN binary availability +- Real-ESRGAN model directory readiness +- available Real-ESRGAN models + +If the worker is in Real-ESRGAN mode and health returns `degraded`, do not treat the runtime as production-ready yet. + +Troubleshooting +--------------- + +`Worker URL is missing.` + +- set `ENHANCE_WORKER_URL` +- clear config + +`Worker token is missing.` + +- set `ENHANCE_WORKER_TOKEN` +- make sure the worker uses the same `WORKER_TOKEN` + +`Worker is unavailable.` + +- confirm the worker is reachable +- confirm the worker container is running +- confirm the URL points to the worker base URL + +`Upscale engine is not available. Check model files and worker installation.` + +- confirm `WORKER_ENGINE=realesrgan-ncnn` +- confirm `bin/realesrgan-ncnn-vulkan` exists and is executable +- confirm the required `.param` and `.bin` model files exist +- run `bash scripts/verify-realesrgan.sh` + +Jobs stay queued + +- confirm queue workers consume `ENHANCE_QUEUE` +- if using `enhance`, run workers with `--queue=enhance,default` + +`status: degraded` from worker health + +- verify binary and model directory paths +- verify runtime files are mounted inside the container +- fall back to `WORKER_ENGINE=pillow` until the runtime is fixed + +Operations and safety notes +--------------------------- + +- Keep the worker bound to `127.0.0.1` or a private container network. +- Do not expose the worker publicly. +- Use a strong shared token. +- Keep Enhance on a dedicated queue when load increases. +- Keep cleanup enabled so stale outputs and failed files do not accumulate. +- Do not commit Real-ESRGAN binary or model weight files unless explicitly approved. +- The worker only serves generated files from its own output directory. +- The worker rejects unsupported source URLs and unsafe output paths. +- Original artwork files are never replaced by the Enhance flow. + +Production rollout recommendation +--------------------------------- + +Recommended rollout sequence: + +1. Enable `ENHANCE_ENGINE=stub` and verify the Laravel workflow. +2. Move to `ENHANCE_ENGINE=external_worker` with `WORKER_ENGINE=pillow`. +3. Verify queue, storage, cleanup, and health behavior. +4. Install Real-ESRGAN runtime files and switch the worker to `WORKER_ENGINE=realesrgan-ncnn`. +5. Confirm worker health is `ok` before calling the runtime production-ready. + +Related docs +------------ + +- operational notes: [docs/enhance.md](docs/enhance.md) +- worker runtime docs: [services/enhance-worker/README.md](services/enhance-worker/README.md) \ No newline at end of file diff --git a/docs/enhance.md b/docs/enhance.md new file mode 100644 index 00000000..8c4d2d35 --- /dev/null +++ b/docs/enhance.md @@ -0,0 +1,105 @@ +Skinbase Enhance +================ + +Operational notes for the Enhance v1/v1.1 module. + +For full setup, enablement, worker configuration, verification, and production rollout guidance, see [docs/enhance-setup.md](docs/enhance-setup.md). + +Configuration +------------- + +- `ENHANCE_ENGINE=stub` keeps Enhance in preview mode for local and workflow testing. +- `ENHANCE_ENGINE=external_worker` uses the prepared external worker adapter boundary. +- `ENHANCE_QUEUE=default` is the current safe default. +- `ENHANCE_QUEUE=enhance` is supported when you want a dedicated queue later. +- If you switch to `ENHANCE_QUEUE=enhance`, Horizon or any worker process must explicitly consume the `enhance` queue in production. + +Helpful commands +---------------- + +```bash +php artisan enhance:health +php artisan enhance:health --json +php artisan enhance:cleanup --dry-run +php artisan enhance:cleanup --force +php artisan queue:work --queue=enhance,default +``` + +Cleanup behavior +---------------- + +- Enhance cleanup only removes files under the configured Enhance prefixes: + - `enhance/sources` + - `enhance/outputs` + - `enhance/previews` +- Cleanup never deletes artwork originals, thumbnails, avatars, or other non-Enhance paths. +- Completed jobs can expire automatically via `ENHANCE_COMPLETED_EXPIRES_AFTER_DAYS`. +- Failed jobs can have stale files pruned via `ENHANCE_FAILED_EXPIRES_AFTER_DAYS`. +- Soft-deleted jobs respect `ENHANCE_DELETED_FILE_GRACE_DAYS` before file cleanup. + +Scheduler +--------- + +- The app schedules `php artisan enhance:cleanup --force` daily from `routes/console.php`. +- If you disable Laravel's scheduler in an environment, run cleanup manually or through external cron. + +Queue and Horizon reminder +-------------------------- + +- Stub mode still dispatches queued Enhance jobs and exercises the same lifecycle. +- If workers only consume `default` and you later move Enhance to a dedicated queue, completed jobs will stall in `queued` until `enhance` is added to the worker queue list. + +External Worker v1 +------------------ + +- Set `ENHANCE_ENGINE=external_worker` to switch Laravel from the stub processor to the HTTP worker integration. +- Recommended local Laravel env: + +```env +ENHANCE_ENGINE=external_worker +ENHANCE_WORKER_URL=http://127.0.0.1:8095 +ENHANCE_WORKER_TIMEOUT=600 +ENHANCE_WORKER_TOKEN=change-this-token +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 +ENHANCE_QUEUE=enhance +``` + +- Keep the worker bound to `127.0.0.1` or a private container network. Do not expose it publicly. +- Laravel sends the worker a short-lived source URL. If the storage disk cannot issue temporary URLs, Laravel falls back to a temporary signed internal route that serves only the copied Enhance source file. +- Laravel remains the source of truth: the worker only returns a temporary result, Laravel downloads it, validates it, stores it on the Enhance disk, and then asks the worker to delete the temporary file. +- Useful commands after enabling the worker: + +```bash +php artisan config:clear +php artisan enhance:health +php artisan queue:work --queue=enhance,default +php artisan test --filter=EnhanceExternalWorker +``` + +Real-ESRGAN Runtime +------------------- + +- Laravel still uses `ENHANCE_ENGINE=external_worker`. It does not need to know whether the worker uses Pillow or Real-ESRGAN internally. +- Use `WORKER_ENGINE=pillow` for local development, CI, and fallback operation. +- Use `WORKER_ENGINE=realesrgan-ncnn` for the real ncnn-vulkan runtime path. +- If worker health reports `status: degraded`, keep Laravel on the stub processor or a Pillow worker until the Real-ESRGAN runtime is verified. +- Do not expose the worker port publicly. +- Move to the `enhance` Horizon queue only after the worker is healthy and verified in your environment. +- Real-ESRGAN runtime files are not committed. Install them locally or in deployment with the worker scripts: + +```bash +cd services/enhance-worker +bash scripts/download-realesrgan-ncnn.sh +bash scripts/verify-realesrgan.sh +``` + +- Recommended Laravel env when using the real worker: + +```env +ENHANCE_ENGINE=external_worker +ENHANCE_QUEUE=enhance +ENHANCE_WORKER_URL=http://127.0.0.1:8095 +ENHANCE_WORKER_TIMEOUT=900 +ENHANCE_WORKER_TOKEN=change-this-token +ENHANCE_WORKER_MAX_DOWNLOAD_MB=60 +``` \ No newline at end of file diff --git a/public/opcache-reset.php b/public/opcache-reset.php deleted file mode 100644 index 1a12d373..00000000 --- a/public/opcache-reset.php +++ /dev/null @@ -1,8 +0,0 @@ - code { + display: inline-block; + padding: 0.14em 0.46em 0.16em; + border: 1px solid rgba(125, 211, 252, 0.18); + border-radius: 0.38rem; + background: rgba(56, 189, 248, 0.08); + color: rgb(186 230 253); + font-family: ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Monaco, Consolas, monospace; + font-size: 0.875em; + font-weight: 500; + line-height: 1.5; + vertical-align: baseline; + white-space: normal; + word-break: break-word; +} + .academy-lesson-prose pre::after { inset: 3rem 0 auto 0; background: linear-gradient(90deg, rgba(56, 189, 248, 0), rgba(56, 189, 248, 0.26), rgba(56, 189, 248, 0)); diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index 137d395a..c845d237 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -23,6 +23,7 @@ const buildAdminNavGroups = (isAdmin) => [ items: [ { label: 'Stories', href: '/moderation/stories', icon: 'fa-solid fa-feather-pointed' }, { label: 'Artworks', href: '/moderation/artworks', icon: 'fa-solid fa-images' }, + { label: 'Enhance Jobs', href: '/moderation/enhance', icon: 'fa-solid fa-up-right-and-down-left-from-center' }, { label: 'Featured Artworks', href: '/moderation/artworks/featured', icon: 'fa-solid fa-star' }, { label: 'Web Stories', href: '/moderation/web-stories', icon: 'fa-solid fa-book-open-reader' }, { label: 'Homepage Announcements', href: '/moderation/homepage/announcements', icon: 'fa-solid fa-bullhorn' }, diff --git a/resources/js/Pages/Academy/CoursesIndex.jsx b/resources/js/Pages/Academy/CoursesIndex.jsx index 42df2250..88e68e51 100644 --- a/resources/js/Pages/Academy/CoursesIndex.jsx +++ b/resources/js/Pages/Academy/CoursesIndex.jsx @@ -4,8 +4,90 @@ import SeoHead from '../../components/seo/SeoHead' import NovaSelect from '../../components/ui/NovaSelect' import { trackAcademySearchResultClick, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics' -function CourseCard({ course, variant = 'default', analytics = null, searchContext = null, position = null }) { - const isFeatured = variant === 'featured' +function Breadcrumbs({ items = [] }) { + if (!items.length) { + return null + } + + return ( + + ) +} + +function formatAccessDate(value) { + if (!value) { + return null + } + + const parsed = new Date(value) + + if (Number.isNaN(parsed.getTime())) { + return null + } + + return new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }).format(parsed) +} + +function academyAccessHeading(access) { + switch (access?.status) { + case 'staff_access': + return 'You currently have full staff access to the Academy.' + case 'grace_period': + return `${access.tierLabel} access is still active.` + case 'trialing': + return `${access.tierLabel} trial is active right now.` + case 'active': + return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : 'Your Academy access is active.' + case 'free': + return 'You currently have Free access to the Academy.' + default: + return null + } +} + +function academyAccessMeta(access) { + if (!access?.signedIn) { + return [] + } + + const items = [ + { label: 'Current tier', value: access?.tierLabel || 'Free' }, + { label: 'Status', value: access?.statusLabel || 'Free access' }, + ] + + const formattedDate = formatAccessDate(access?.expiresAt) + + if (formattedDate && access?.dateLabel) { + items.push({ label: access.dateLabel, value: formattedDate }) + } else if (access?.renewsAutomatically) { + items.push({ label: 'Billing', value: 'Renews automatically' }) + } else if (!access?.hasPaidAccess) { + items.push({ label: 'Upgrade', value: 'Creator and Pro unlock premium lessons and courses' }) + } + + return items +} + +function CourseCard({ course, analytics = null, searchContext = null, position = null }) { const progress = course?.progress || null const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || '' const trackSearchClick = () => { @@ -29,48 +111,56 @@ function CourseCard({ course, variant = 'default', analytics = null, searchConte data-academy-search-query={searchContext?.query || undefined} data-academy-search-results-count={searchContext?.resultsCount || undefined} data-academy-search-position={position || undefined} - className={[ - 'group overflow-hidden rounded-[30px] border border-white/10 transition hover:border-sky-300/25 hover:bg-white/[0.06]', - isFeatured ? 'bg-[linear-gradient(135deg,rgba(14,165,233,0.14),rgba(15,23,42,0.92))]' : 'bg-white/[0.04]', - ].join(' ')} + className="group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-amber-200/24 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]" > -
- {cover ? :
} -
-
- {course.difficulty} - {course.access_level} +
+ {cover ? : null} +
+
+ {course?.difficulty ? {course.difficulty} : null} + {course?.access_level ? {course.access_level} : null} {course.is_featured ? Featured : null}
+
+
+ {course?.lessons_count || 0} lessons + {course?.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible pace'} +
+
-
-

{course.title}

- {course.subtitle ?

{course.subtitle}

: null} -

{course.excerpt || course.description || 'Structured Academy course.'}

- -
-
-

Lessons

-

{course.lessons_count || 0}

-
-
-

Duration

-

{course.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible'}

-
-
-

Progress

-

{progress ? `${progress.percent}%` : 'Start fresh'}

-
+
+
+

Academy course

+ {course?.subtitle ? {course.subtitle} : null}
+

{course.title}

+

{course.excerpt || course.description || 'Structured Academy course.'}

+

{progress ? `${progress.percent}% complete` : 'Start fresh'}{course?.access_level ? ` · ${course.access_level}` : ''}

) } -export default function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, analytics }) { +export default function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, lessonsUrl, promptLibraryUrl, academyAccess = null, analytics }) { const flash = usePage().props.flash || {} useAcademyPageAnalytics(analytics) + const breadcrumbs = [ + { label: 'Academy', href: '/academy' }, + { label: 'Courses', href: '/academy/courses' }, + ] + const visibleItems = Array.isArray(items?.data) ? items.data : [] + const totalCourses = Number(items?.total || items?.data?.length || 0) + const featuredCount = featuredCourses.length + const featuredCourse = featuredCourses.find((course) => course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image) || featuredCourses[0] || visibleItems[0] || null + const featuredCover = featuredCourse?.cover_image_url || featuredCourse?.teaser_image_url || featuredCourse?.cover_image || featuredCourse?.teaser_image || '' + const showSignedInAccess = Boolean(academyAccess?.signedIn) + const accessHeading = academyAccessHeading(academyAccess) + const accessMeta = academyAccessMeta(academyAccess) + const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl + const primaryActionLabel = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'Renew now' : 'Manage billing') : 'See plans' + const primaryActionIcon = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'fa-solid fa-rotate-right' : 'fa-solid fa-sliders') : 'fa-solid fa-arrow-up-right-from-square' + const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl const searchContext = analytics?.search ? { query: analytics.search.query, normalizedQuery: analytics.search.normalizedQuery, @@ -90,34 +180,137 @@ export default function AcademyCoursesIndex({ seo, title, description, items, fe { value: 'mixed', label: 'Mixed' }, ] + const handlePrimaryAction = () => { + if (!useBillingAction) { + trackUpgradeClick(analytics, { source: 'academy_courses_index_hero_primary' }) + } + } + return (
-
-
-
-

Skinbase AI Academy

-

{title}

-

{description}

+
+
+
+
+ +
+
+ + +
+ Skinbase AI Academy + Courses + {totalCourses} guided paths +
+ +
+
+

{title}

+

{description}

+
+ + + +
+ +
+
+

Library

+

{totalCourses} guided courses

+
+
+

Focus

+

Sequenced learning + tracked completion

+
+
+ +
+ Structured progression + Tracked completion + Reusable lesson paths +
+ +
+ + + Browse lessons + + + + Prompt library + + + + {primaryActionLabel} + +
+
+ +
+
+
+

Featured course

+ {featuredCourse?.difficulty ? {featuredCourse.difficulty} : null} +
+ +
+ {featuredCover ? : null} +
+
+ {featuredCourse?.access_level ? {featuredCourse.access_level} : null} + {featuredCourse?.is_featured ? Spotlight : null} +
+
+
+

{featuredCourse?.subtitle || 'Guided learning path'}

+

{featuredCourse?.title || 'Explore courses'}

+

{featuredCourse?.excerpt || featuredCourse?.description || 'Open a structured Academy course built from reusable lessons.'}

+
+ +
+
+ +
+
+
+ +
+

{showSignedInAccess ? 'Your Academy access' : 'Upgrade for full access'}

+

{showSignedInAccess ? accessHeading : 'Unlock the full course library, premium lesson paths, and the broader Academy learning track.'}

+
+
+ {showSignedInAccess ? ( +
+ {accessMeta.map((item) => ( +
+

{item.label}

+

{item.value}

+
+ ))} +
+ ) : null} +
+ + + {primaryActionLabel} + + + + Browse lessons + +
+ {academyAccess?.status === 'grace_period' ?

Opens billing account to restore renewal before access ends.

: null} +
- trackUpgradeClick(analytics, { source: 'academy_courses_index_hero' })} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See Academy plans
{flash.success ?
{flash.success}
: null} {flash.error ?
{flash.error}
: null} - {featuredCourses.length ? ( -
- -
- {featuredCourses.slice(1, 3).map((course, index) => )} -
-
- ) : null} -
- {(items?.data || []).length === 0 ? ( + {visibleItems.length === 0 ? (
No published Academy courses matched these filters.
) : ( -
- {items.data.map((course, index) => )} +
+ {visibleItems.map((course, index) => )}
)}
diff --git a/resources/js/Pages/Academy/CoursesShow.jsx b/resources/js/Pages/Academy/CoursesShow.jsx index 3323300f..9e8a9a8e 100644 --- a/resources/js/Pages/Academy/CoursesShow.jsx +++ b/resources/js/Pages/Academy/CoursesShow.jsx @@ -64,35 +64,35 @@ function LessonChip({ lesson }) { const isCompleted = Boolean(lesson?.completed) const readingMinutes = Number(lesson?.reading_minutes || 0) const ctaLabel = isCompleted ? 'Review lesson' : 'Open lesson' + const difficultyLabel = lesson?.difficulty || 'lesson' + const accessLabel = lesson?.access_level || 'free' + const lessonTypeLabel = lesson?.lesson_type || 'article' + const statusLabel = isCompleted ? 'Completed' : lesson?.is_required ? 'Required next' : 'Optional read' + const supportCopy = isCompleted ? 'You already finished this lesson.' : lesson?.is_required ? 'Recommended as the next required step in this course.' : 'Optional depth you can take at your own pace.' return ( -
+
+
-
-
+
+
{thumbnail ? ( - + ) : ( -
+
)} -
-
- {lesson.is_required ? ( - - Required - - ) : ( - - Optional - - )} +
+
+ + {lesson.is_required ? 'Required' : 'Optional'} + {isCompleted ? ( ) : null}
-
-
+
+
{stepLabel ?

{stepLabel}

: null} {stepNumber > 0 ?

{String(stepNumber).padStart(2, '0')}

: null} + {!stepNumber && lesson.formatted_lesson_number ?

{lesson.formatted_lesson_number}

: null}
-
+
{stepLabel ?

{stepLabel}

: null} {lesson.formatted_lesson_number ? {lesson.formatted_lesson_number} : null} - {lesson.difficulty || 'lesson'} - {lesson.access_level || 'free'} + {difficultyLabel} + {accessLabel} {readingMinutes > 0 ? {readingMinutes} min : null}

{lesson.title}

-

{isCompleted ? 'You already finished this lesson.' : 'Follow this step next in the course path.'}

+

{supportCopy}

-
+

{lesson.excerpt || lesson.content_preview || 'Open this lesson inside the course.'}

-
- {lesson.lesson_type || 'article'} +
+ {lessonTypeLabel} {lesson.category_name ? {lesson.category_name} : null} + Course flow
-
-
-
-

Status

-

{isCompleted ? 'Completed' : 'Up next'}

-
-
-

Access

-

{lesson.access_level || 'Free'}

-
-
-

Read time

-

{readingMinutes > 0 ? `${readingMinutes} min` : 'Quick read'}

+
+
+

Lesson path

+
+
+ Status + {statusLabel} +
+
+ Access + {accessLabel} +
+
+ Read time + {readingMinutes > 0 ? `${readingMinutes} min` : 'Quick read'} +
@@ -170,26 +175,32 @@ function LessonChip({ lesson }) { function SectionBlock({ section, isActive = false }) { if (!section?.is_visible) return null + const lessonCount = section.lessons?.length || 0 + const requiredCount = (section.lessons || []).filter((lesson) => lesson?.is_required).length + return ( -
-
-
-
+
+
+
+
+

Course section

{section.order_num + 1} + {requiredCount > 0 ? {requiredCount} required : null}
-

{section.title}

+

{section.title}

{section.description ?

{section.description}

: null} +

{lessonCount} lessons mapped in this section

- {section.lessons?.length || 0} lessons + {lessonCount} lessons {isActive ? Reading now : null}
-
+
{(section.lessons || []).map((lesson) => ( ))} @@ -202,20 +213,24 @@ export default function AcademyCoursesShow({ seo, course, sections = [], unsecti const flash = usePage().props.flash || {} useAcademyPageAnalytics(analytics) const cover = course?.cover_image_url || course?.cover_image || course?.teaser_image_url || course?.teaser_image || '' + const heroBackground = course?.teaser_image_url || course?.teaser_image || course?.cover_image_url || course?.cover_image || '' const progress = course?.progress || null const [liked, setLiked] = useState(Boolean(interaction?.liked)) const [saved, setSaved] = useState(Boolean(interaction?.saved)) const [likesCount, setLikesCount] = useState(Number(interaction?.likes_count || 0)) const [savesCount, setSavesCount] = useState(Number(interaction?.saves_count || 0)) + const visibleSections = sections.filter((section) => section?.is_visible) + const totalLessons = Number(course?.lessons_count || (unsectionedLessons.length + visibleSections.reduce((sum, section) => sum + (section.lessons || []).length, 0))) + const totalSections = visibleSections.length + (unsectionedLessons.length ? 1 : 0) + const estimatedMinutes = course?.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible pace' const sectionJumpItems = useMemo( () => [ ...(unsectionedLessons.length ? [{ id: 'course-outline-core', label: 'Core lessons', count: unsectionedLessons.length }] : []), - ...sections - .filter((section) => section?.is_visible) + ...visibleSections .map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })), ], - [sections, unsectionedLessons], + [unsectionedLessons, visibleSections], ) const [activeJumpId, setActiveJumpId] = useState(sectionJumpItems[0]?.id || null) @@ -316,69 +331,95 @@ export default function AcademyCoursesShow({ seo, course, sections = [], unsecti {flash.success ?
{flash.success}
: null} {flash.error ?
{flash.error}
: null} -
-
-
- {cover ? : null} -
+
+
+
+
+ +
+
+ {heroBackground ? : null}
- Academy course + Skinbase AI Academy + Course path {course?.difficulty} {course?.access_level} {progress?.percent ? {progress.percent}% complete : null}
-
-

{course?.title}

- {course?.subtitle ?

{course.subtitle}

: null} -

{course?.excerpt || course?.description}

- -
- - - - trackUpgradeClick(analytics, { source: 'academy_course_header' })} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See plans +
+
+ {course?.subtitle ?

{course.subtitle}

: null} +

{course?.title}

+

{course?.excerpt || course?.description}

+ + + +
-
- {cover ? ( - - ) : ( -
- No course cover image yet -
- )} +
+ + + + trackUpgradeClick(analytics, { source: 'academy_course_header' })} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See plans +
+ +
+
+

Library

+

{totalLessons} lessons

+
+
+

Structure

+

{totalSections} sections

+
+
+

Pace

+

{estimatedMinutes}

+
+
+

Status

+

{progress?.percent ? `${progress.percent}% complete` : 'Ready to start'}

-