Wire admin studio SSR and search infrastructure

This commit is contained in:
2026-05-01 11:46:06 +02:00
parent 257b0dbef6
commit 18cea8b0f0
329 changed files with 197465 additions and 2741 deletions

View File

@@ -1,6 +1,9 @@
@extends('layouts.nova')
@section('content')
@php
$sanitizeHtml = fn (?string $value) => \App\Services\ContentSanitizer::sanitizeRenderedHtml($value ?? '');
@endphp
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
@@ -17,7 +20,7 @@
<div class="panel panel-default">
<div class="panel-body">
<h1 class="skinTitle">{{ $ar->headline }}</h1>
<div>{!! $ar->tekst !!}</div>
<div>{!! $sanitizeHtml($ar->tekst) !!}</div>
</div>
</div>
@@ -41,7 +44,7 @@
<td valign="top" height="10" style="background:#eee">&nbsp;{{ $local_date }}</td>
</tr>
<tr>
<td height="50" style="background:#fff;padding-left:13px; padding-right:3px;">{!! $comment->tekst !!}</td>
<td height="50" style="background:#fff;padding-left:13px; padding-right:3px;">{!! $sanitizeHtml($comment->tekst) !!}</td>
</tr>
<tr>
<td valign="bottom" align="center" height="10" style="background:#fff;">

View File

@@ -1,6 +1,9 @@
@extends('layouts.nova')
@section('content')
@php
$sanitizeHtml = fn (?string $value) => \App\Services\ContentSanitizer::sanitizeRenderedHtml($value ?? '');
@endphp
<div class="container-fluid legacy-page">
<div class="page-heading">
<h1 class="page-header">{{ $news->headline ?? 'News' }}</h1>
@@ -18,7 +21,7 @@
<div class="panel-body">
<h1 class="panel-title" style="font-size:26px;color:#000;">{!! nl2br(e($news->headline ?? '')) !!}</h1>
<br>
<p>{!! html_entity_decode(stripslashes($news->content ?? '')) !!}</p>
<div>{!! $sanitizeHtml(html_entity_decode(stripslashes($news->content ?? ''))) !!}</div>
</div>
</div>

View File

@@ -192,7 +192,7 @@
@if($countryName)
<p class="text-[--sb-muted] text-sm mt-1 flex items-center justify-center sm:justify-start gap-1.5">
@if($profile?->country_code)
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
<img src="{{ rtrim((string) config('cdn.files_url', ''), '/') }}/images/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
alt="{{ e($countryName) }}"
class="w-5 h-auto rounded-sm inline-block"
onerror="this.style.display='none'">
@@ -434,7 +434,7 @@
<td>Country</td>
<td class="flex items-center justify-end gap-1.5">
@if($profile?->country_code)
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
<img src="{{ rtrim((string) config('cdn.files_url', ''), '/') }}/images/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
alt="{{ e($countryName) }}"
class="w-4 h-auto rounded-sm"
onerror="this.style.display='none'">

View File

@@ -0,0 +1,12 @@
@extends('layouts.nova')
@push('head')
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
<meta name="csrf-token" content="{{ csrf_token() }}" />
@endif
@vite(['resources/js/admin.jsx'])
@endpush
@section('content')
@inertia
@endsection

View File

@@ -1,57 +1,10 @@
@extends('layouts.nova')
@php
$presentMd = $presentMd ?? \App\Services\ThumbnailPresenter::present($artwork, 'md');
$presentLg = $presentLg ?? \App\Services\ThumbnailPresenter::present($artwork, 'lg');
$presentXl = $presentXl ?? \App\Services\ThumbnailPresenter::present($artwork, 'xl');
$presentSq = $presentSq ?? \App\Services\ThumbnailPresenter::present($artwork, 'sq');
$canonicalUrl = route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]);
$meta = $meta ?? [
'title' => trim((string) ($artwork->title ?? 'Artwork') . ' by ' . (string) ($artwork->user?->name ?? $artwork->user?->username ?? 'Unknown Author') . ' | Skinbase'),
'description' => (string) ($artwork->description ?? ''),
'canonical' => $canonicalUrl,
'og_image' => $presentXl['url'] ?? $presentLg['url'] ?? $presentMd['url'] ?? null,
'og_width' => $presentXl['width'] ?? $presentLg['width'] ?? null,
'og_height' => $presentXl['height'] ?? $presentLg['height'] ?? null,
];
$artworkData = $artworkData ?? [];
$relatedItems = $relatedItems ?? [];
$comments = $comments ?? [];
$groupSummary = $groupSummary ?? null;
$useUnifiedSeo = true;
$canReadSessionAuth = request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped');
@endphp
@push('head')
@php
$preloadSrcset = ($presentMd['url'] ?? '') . ' 640w, ' . ($presentLg['url'] ?? '') . ' 1280w, ' . ($presentXl['url'] ?? '') . ' 1920w';
@endphp
@if(!empty($presentLg['url']))
<link rel="preload" as="image"
href="{{ $presentLg['url'] }}"
imagesrcset="{{ trim($preloadSrcset) }}"
imagesizes="(min-width: 1280px) 1200px, (min-width: 768px) 90vw, 100vw">
@endif
@vite(['resources/js/artwork.jsx'])
@endpush
@section('content')
<div id="artwork-page"
data-artwork='@json($artworkData)'
data-related='@json($relatedItems)'
data-present-md='@json($presentMd)'
data-present-lg='@json($presentLg)'
data-present-xl='@json($presentXl)'
data-present-sq='@json($presentSq)'
data-cdn='@json(rtrim((string) config("cdn.files_url", "https://files.skinbase.org"), "/"))'
data-canonical='@json($meta["canonical"])'
data-comments='@json($comments)'
data-group-summary='@json($groupSummary)'
data-is-authenticated='@json($canReadSessionAuth && auth()->check())'>
</div>
@vite(['resources/js/Pages/ArtworkPage.jsx'])
@inertia
@endsection

View File

@@ -64,11 +64,24 @@
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
@endif
@php
$rememberMeProps = [
'initialChecked' => (bool) old('remember'),
'label' => 'Remember me',
'name' => 'remember',
];
@endphp
<div class="flex items-center justify-between text-sm text-white/60">
<label class="flex items-center gap-2">
<input type="checkbox" name="remember" class="rounded bg-slate-800 border-white/20" />
Remember me
</label>
<div
data-remember-me-checkbox-root
data-props='@json($rememberMeProps)'
>
<label class="flex items-center gap-2">
<input type="checkbox" name="remember" value="1" class="rounded bg-slate-800 border-white/20" @checked(old('remember')) />
Remember me
</label>
</div>
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="text-purple-400 hover:underline">Forgot password?</a>

View File

@@ -0,0 +1,27 @@
{{--
429 Too Many Requests / rate limited
--}}
@extends('errors._layout', [
'error_code' => 429,
'error_title' => 'You Are Moving Too Fast',
'error_message' => 'Skinbase has rate-limited this action for a moment. Please wait a bit and try again.',
])
@section('badge', 'Rate Limited')
@section('primary-cta')
<button onclick="window.location.reload()"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors cursor-pointer">
<i class="fas fa-clock" aria-hidden="true"></i>
Try Again Shortly
</button>
@endsection
@section('secondary-ctas')
<a href="/discover/trending" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Browse Discover
</a>
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection

View File

@@ -0,0 +1,29 @@
{{--
Generic HTTP error fallback.
Used when Laravel throws an HttpException with a status code that does not
have its own dedicated errors/<status>.blade.php template yet.
--}}
@extends('errors._layout', [
'error_code' => $error_code ?? 500,
'error_title' => $error_title ?? 'Unexpected Error',
'error_message' => $error_message ?? 'Something went wrong while loading this page.',
])
@section('badge', 'Request Error')
@section('primary-cta')
<button onclick="window.location.reload()"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors cursor-pointer">
<i class="fas fa-redo" aria-hidden="true"></i>
Try Again
</button>
@endsection
@section('secondary-ctas')
<a href="/discover/trending" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Browse Discover
</a>
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection

View File

@@ -0,0 +1,9 @@
@extends('layouts.nova')
@push('head')
@vite(['resources/js/forum.jsx'])
@endpush
@section('content')
@inertia
@endsection

View File

@@ -207,6 +207,7 @@
{{-- ═══════════════════════════════════════════════════════════════ --}}
{{-- RANKING TABS --}}
{{-- ═══════════════════════════════════════════════════════════════ --}}
@if(!($hide_rank_tabs ?? false))
@php
$rankingTabs = [
['value' => 'trending', 'label' => 'Trending', 'icon' => '🔥'],
@@ -257,11 +258,13 @@
</div>
</div>
</div>
@endif
{{-- ═══════════════════════════════════════════════════════════════ --}}
{{-- HORIZONTAL CATEGORY FILTER ROW --}}
{{-- ═══════════════════════════════════════════════════════════════ --}}
@php
$activeTab = $activeTab ?? ($current_sort ?? 'trending');
$filterItems = $subcategories ?? collect();
$activeFilterId = isset($category) ? ($category->id ?? null) : null;
$categoryAllHref = isset($subcategory_parent) && $subcategory_parent && ($subcategory_parent->url ?? null)

View File

@@ -26,6 +26,9 @@
@if($skinbaseCanUseSession)
<meta name="csrf-token" content="{{ csrf_token() }}">
@endif
<meta name="msvalidate.01" content="E81C84AA9CE4A9CDF1B0039010228C41">
<meta name="verify-v1" content="HNZJnSy5ZbqcrmXUXUwUMtPZzXsKQ+esjxPgXIXDQdk=">
<meta name="google-site-verification" content="D5L-4F-ZP1HFLzLsau6ge7LNGEGb9Sfio4RINkleQto">
@if($shouldRenderBladeSeo)
@include('partials.seo.head', ['seo' => $seo ?? null])
@endif

View File

@@ -25,7 +25,7 @@
<!-- Logo -->
<a href="/" class="flex items-center gap-2 pr-2 shrink-0">
<img src="/gfx/sb_logo.webp" alt="" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
<img src="https://cdn.skinbase.org/images/sb_logo.webp" alt="" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
<span class="sr-only">Skinbase.org</span>
</a>
@@ -45,6 +45,7 @@
'digital-art' => 'fa-palette',
'other' => 'fa-folder-open',
];
$toolbarActiveCampaign = $toolbarActiveCampaign ?? null;
$navSection = match(true) {
request()->is('discover', 'discover/*') => 'discover',
request()->is('worlds', 'worlds/*') => 'discover',
@@ -90,6 +91,15 @@
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('worlds.index') }}">
<i class="fa-solid fa-globe w-4 text-center text-sb-muted"></i>Worlds
</a>
@if($toolbarActiveCampaign)
<a class="flex items-center gap-3 px-4 py-2.5 text-sm border-t border-white/5 bg-white/[0.02] hover:bg-white/5" href="{{ $toolbarActiveCampaign['url'] }}">
<i class="fa-solid fa-bolt w-4 text-center text-emerald-300"></i>
<span>
<span class="block text-[11px] uppercase tracking-[0.16em] text-emerald-200/75">{{ $toolbarActiveCampaign['campaign_label'] }}</span>
<span class="block text-white">{{ \Illuminate\Support\Str::limit($toolbarActiveCampaign['title'], 24) }}</span>
</span>
</a>
@endif
@if($skinbaseToolbarCanAuth)
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('discover.for-you') }}">
<i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You
@@ -494,6 +504,9 @@
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('downloads.today') }}"><i class="fa-solid fa-arrow-down-short-wide w-4 text-center text-sb-muted"></i>Today Downloads</a>
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/discover/on-this-day"><i class="fa-solid fa-calendar-day w-4 text-center text-sb-muted"></i>On This Day</a>
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg {{ request()->is('worlds', 'worlds/*') ? 'bg-white/10 text-white' : 'hover:bg-white/5' }}" href="{{ route('worlds.index') }}"><i class="fa-solid fa-stars w-4 text-center text-sb-muted"></i>Worlds</a>
@if($toolbarActiveCampaign)
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg border border-emerald-300/10 bg-emerald-400/10 hover:bg-emerald-400/15" href="{{ $toolbarActiveCampaign['url'] }}"><i class="fa-solid fa-bolt w-4 text-center text-emerald-200"></i>{{ \Illuminate\Support\Str::limit($toolbarActiveCampaign['title'], 28) }}</a>
@endif
</div>
</div>

View File

@@ -0,0 +1,51 @@
{{--
Story Editor Layout full-viewport, no site chrome.
Loads the same Nova assets (CSS + nova.js) but strips the topbar,
toolbar, and footer so the React editor owns the entire viewport.
--}}
@php
$skinbaseSessionSkipped = request()->attributes->get('skinbase.session_skipped') === true;
$skinbaseCanUseSession = request()->hasSession() && ! $skinbaseSessionSkipped;
@endphp
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@if($skinbaseCanUseSession)
<meta name="csrf-token" content="{{ csrf_token() }}">
@endif
<title>{{ $page_title ?? 'Story Editor' }} Skinbase</title>
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" crossorigin>
@vite([
'resources/css/app.css',
'resources/css/nova-grid.css',
'resources/scss/nova.scss',
'resources/js/nova.js',
])
<style>
html, body { margin: 0; padding: 0; }
#story-editor-wrap { min-height: 100vh; }
</style>
@stack('head')
</head>
<body class="bg-nova-900 text-white" style="background-color: rgb(14,18,27);">
<div id="story-editor-wrap">
@yield('content')
</div>
@stack('scripts')
</body>
</html>

View File

@@ -20,6 +20,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="'Published News stories from ' . $archiveDate->format('F Y') . '.'"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
/>
<div class="mx-auto max-w-7xl px-6 pt-8 pb-16 md:px-10">

View File

@@ -21,6 +21,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="'Editorial stories and updates by ' . $authorLabel . '.'"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
/>
<div class="mx-auto max-w-7xl px-6 pt-8 pb-16 md:px-10">

View File

@@ -20,6 +20,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="$category->description ?: ('Announcements filed under ' . $category->name . '.')"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
/>
<div class="mx-auto max-w-7xl px-6 pt-8 pb-16 md:px-10">

View File

@@ -19,6 +19,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.')"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-2 text-sm">

View File

@@ -20,6 +20,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="'Stories and announcements tagged with #' . $tag->name . '.'"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
/>
<div class="mx-auto max-w-7xl px-6 pt-8 pb-16 md:px-10">

View File

@@ -1,24 +1,243 @@
@extends('layouts.nova')
@php($useUnifiedSeo = true)
<?php $useUnifiedSeo = true; ?>
@section('main-class', '')
@section('content')
<?php
$initialPayload = $initialPayload ?? [
'data' => [],
'meta' => ['current_page' => 1, 'last_page' => 1, 'per_page' => 24, 'total' => 0],
'summary' => ['total_categories' => 0, 'total_artworks' => 0],
'popular_categories' => [],
'request' => ['query' => '', 'sort' => 'popular', 'page' => 1, 'per_page' => 24],
];
$categories = $initialPayload['data'] ?? [];
$meta = $initialPayload['meta'] ?? ['current_page' => 1, 'last_page' => 1, 'per_page' => 24, 'total' => 0];
$summary = $initialPayload['summary'] ?? ['total_categories' => 0, 'total_artworks' => 0];
$popularCategories = $initialPayload['popular_categories'] ?? [];
$requestState = $initialPayload['request'] ?? ['query' => '', 'sort' => 'popular', 'page' => 1, 'per_page' => 24];
$query = (string) ($requestState['query'] ?? '');
$sort = (string) ($requestState['sort'] ?? 'popular');
$currentPage = (int) ($meta['current_page'] ?? 1);
$lastPage = (int) ($meta['last_page'] ?? 1);
$buildCategoriesUrl = function (int $page) use ($query, $sort): string {
$params = [];
if ($page > 1) {
$params['page'] = $page;
}
if ($sort !== 'popular') {
$params['sort'] = $sort;
}
if ($query !== '') {
$params['q'] = $query;
}
$queryString = http_build_query($params);
return $queryString === ''
? url('/categories')
: url('/categories') . '?' . $queryString;
};
$categoryStyles = [
'wallpapers' => ['badge' => 'from-cyan-400/90 to-sky-500/90', 'overlay' => 'from-sky-950/10 via-slate-950/12 to-slate-950/92'],
'skins' => ['badge' => 'from-orange-400/90 to-amber-500/90', 'overlay' => 'from-orange-950/10 via-slate-950/12 to-slate-950/92'],
'photography' => ['badge' => 'from-emerald-400/90 to-teal-500/90', 'overlay' => 'from-emerald-950/10 via-slate-950/12 to-slate-950/92'],
'other' => ['badge' => 'from-fuchsia-400/90 to-rose-500/90', 'overlay' => 'from-rose-950/10 via-slate-950/12 to-slate-950/92'],
'default' => ['badge' => 'from-cyan-400/90 to-orange-400/90', 'overlay' => 'from-slate-900/10 via-slate-950/12 to-slate-950/92'],
];
?>
<script id="categories-page-props" type="application/json">
{!! json_encode([
'apiUrl' => route('api.categories.index'),
'pageTitle' => $page_title ?? 'Categories',
'pageDescription' => $page_meta_description ?? null,
'initialData' => $initialPayload,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
</script>
<div id="categories-page-root" class="min-h-screen bg-[radial-gradient(circle_at_top,rgba(34,211,238,0.14),transparent_28%),radial-gradient(circle_at_80%_20%,rgba(249,115,22,0.16),transparent_30%),linear-gradient(180deg,#050b13_0%,#09111c_42%,#050913_100%)]">
<div class="mx-auto flex min-h-[60vh] max-w-7xl items-center justify-center px-6 py-20">
<div class="flex items-center gap-3 rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm text-white/70 shadow-[0_18px_60px_rgba(0,0,0,0.28)] backdrop-blur">
<span class="h-2.5 w-2.5 animate-pulse rounded-full bg-cyan-300"></span>
Loading categories
</div>
<div class="pb-24 text-white">
<section class="relative overflow-hidden">
<div class="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%)]"></div>
<div class="relative w-full px-6 pb-8 pt-14 sm:px-8 sm:pt-20 xl:px-10 2xl:px-14 lg:pt-24">
<div class="grid gap-8 lg:grid-cols-[minmax(0,1.2fr)_20rem] lg:items-end">
<div>
<div class="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
</div>
<h1 class="mt-5 max-w-4xl text-4xl font-semibold tracking-[-0.05em] text-white sm:text-5xl lg:text-6xl">
{{ $page_title ?? 'Categories' }}
</h1>
<p class="mt-5 max-w-2xl text-base leading-8 text-white/62 sm:text-lg">
{{ $page_meta_description ?? 'Browse all wallpapers, skins, themes and digital art categories' }}
</p>
</div>
<div class="grid gap-4 sm:grid-cols-3 lg:grid-cols-1">
<div class="rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-white/40">Categories</p>
<p class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">{{ number_format((int) ($summary['total_categories'] ?? 0)) }}</p>
</div>
<div class="rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-white/40">Artworks indexed</p>
<p class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">{{ number_format((int) ($summary['total_artworks'] ?? 0)) }}</p>
</div>
<div class="rounded-[24px] border border-white/10 bg-black/22 p-5 backdrop-blur-md shadow-[0_24px_60px_rgba(0,0,0,0.24)]">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-white/40">View</p>
<p class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">Grid</p>
</div>
</div>
</div>
<form method="GET" action="{{ url('/categories') }}" class="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">
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_16rem] lg:items-center">
<label class="relative block">
<span class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-white/35">
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-5 w-5">
<path fill-rule="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" clip-rule="evenodd" />
</svg>
</span>
<input
type="search"
name="q"
value="{{ $query }}"
placeholder="Search categories"
aria-label="Search categories"
class="h-14 w-full rounded-2xl border border-white/10 bg-white/[0.04] pl-12 pr-4 text-sm text-white placeholder:text-white/28 focus:border-cyan-300/45 focus:outline-none focus:ring-2 focus:ring-cyan-300/15"
/>
</label>
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-[0.2em] text-white/38">Sort by</span>
<select name="sort" aria-label="Sort categories" class="h-14 w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 text-sm text-white focus:border-orange-300/45 focus:outline-none focus:ring-2 focus:ring-orange-300/12">
<option value="popular" @selected($sort === 'popular') class="bg-slate-950 text-white">Popular</option>
<option value="az" @selected($sort === 'az') class="bg-slate-950 text-white">A-Z</option>
<option value="artworks" @selected($sort === 'artworks') class="bg-slate-950 text-white">Most artworks</option>
</select>
</label>
</div>
</form>
</div>
</section>
<section class="w-full px-6 sm:px-8 xl:px-10 2xl:px-14">
@if ($query === '' && count($popularCategories) > 0)
<div class="mb-10 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_24px_60px_rgba(0,0,0,0.18)] backdrop-blur-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-white/38">Popular categories</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">Start with the busiest destinations</h2>
</div>
<div class="flex flex-wrap gap-3">
@foreach ($popularCategories as $category)
<a href="{{ $category['url'] }}" class="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">
<span>{{ $category['name'] }}</span>
<span class="text-white/38">{{ number_format((int) ($category['artwork_count'] ?? 0)) }}</span>
</a>
@endforeach
</div>
</div>
</div>
@endif
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-white/38">Directory results</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{{ number_format((int) ($meta['total'] ?? 0)) }} categories visible</h2>
</div>
<p class="text-sm text-white/52">
@if ((int) ($meta['total'] ?? 0) > 0)
Showing {{ number_format($categories === [] ? 0 : 1) }} to {{ number_format(count($categories)) }} of {{ number_format((int) ($meta['total'] ?? 0)) }} categories.
@else
Browse all wallpapers, skins, themes and digital art categories.
@endif
</p>
</div>
@if ((int) ($meta['total'] ?? 0) === 0)
<div class="rounded-[28px] border border-dashed border-white/14 bg-black/20 px-6 py-14 text-center backdrop-blur-sm">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/35">No matching categories</p>
<h2 class="mt-3 text-2xl font-semibold tracking-[-0.03em] text-white">Nothing matched &quot;{{ $query }}&quot;</h2>
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-white/58">
Try a shorter term or switch sorting to browse the full category directory again.
</p>
</div>
@else
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
@foreach ($categories as $index => $category)
@php
$contentTypeSlug = $category['content_type']['slug'] ?? 'default';
$contentTypeName = $category['content_type']['name'] ?? 'Category';
$style = $categoryStyles[$contentTypeSlug] ?? $categoryStyles['default'];
@endphp
<a
href="{{ $category['url'] }}"
aria-label="Browse {{ $category['name'] }} category"
class="group relative block cursor-pointer overflow-hidden rounded-2xl transition duration-300 ease-out hover:-translate-y-1 hover:scale-[1.01] hover:shadow-[0_0_28px_rgba(125,211,252,0.16)]"
>
<div class="relative aspect-[4/5] overflow-hidden rounded-2xl border border-white/10 bg-slate-950/80">
<img
src="{{ $category['cover_image'] }}"
alt="Cover artwork for {{ $category['name'] }}"
loading="lazy"
class="h-full w-full object-cover transition duration-500 group-hover:scale-110"
/>
<div class="absolute inset-0 bg-gradient-to-b {{ $style['overlay'] }} transition duration-500 group-hover:from-black/20 group-hover:to-black/90"></div>
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.22),transparent_42%)] opacity-0 transition duration-500 group-hover:opacity-100"></div>
<div class="absolute inset-x-0 top-0 flex items-center justify-between gap-3 p-4">
<span class="inline-flex rounded-full bg-gradient-to-r {{ $style['badge'] }} px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-950 shadow-[0_10px_24px_rgba(0,0,0,0.24)]">
{{ $contentTypeName }}
</span>
<span class="rounded-full border border-white/15 bg-black/25 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-white/78 backdrop-blur">
{{ number_format((int) ($category['artwork_count'] ?? 0)) }} artworks
</span>
</div>
<div class="absolute inset-x-0 bottom-0 p-4 sm:p-5">
<div class="rounded-[22px] border border-white/10 bg-black/30 p-4 backdrop-blur-md transition duration-300 group-hover:border-white/20 group-hover:bg-black/42">
<div class="mb-3 h-px w-14 bg-gradient-to-r from-white/70 to-transparent transition duration-300 group-hover:w-24"></div>
<h3 class="text-lg font-semibold tracking-[-0.02em] text-white sm:text-xl">{{ $category['name'] }}</h3>
<p class="mt-2 text-sm leading-6 text-white/65">
Explore {{ $category['name'] }} across wallpapers, skins, themes, and digital art collections.
</p>
</div>
</div>
</div>
</a>
@endforeach
</div>
<div class="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">
<p class="text-sm text-white/46">
Loaded through page {{ number_format($currentPage) }} of {{ number_format($lastPage) }}
</p>
<div class="flex flex-wrap items-center justify-center gap-2">
@if ($currentPage > 1)
<a href="{{ $buildCategoriesUrl($currentPage - 1) }}" class="inline-flex items-center justify-center rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm text-white/72 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">Previous</a>
@endif
<span class="inline-flex items-center justify-center rounded-full border border-white/10 bg-black/25 px-4 py-2 text-sm text-white/72">
Page {{ number_format($currentPage) }} / {{ number_format($lastPage) }}
</span>
@if ($currentPage < $lastPage)
<a href="{{ $buildCategoriesUrl($currentPage + 1) }}" class="inline-flex items-center justify-center rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm text-white/72 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">Next</a>
@endif
</div>
</div>
@endif
</section>
</div>
</div>

View File

@@ -23,6 +23,58 @@
'my' => 'My Activity',
default => 'All Activity',
};
$serverActivities = $props['initialActivities'] ?? [];
$serverMeta = $props['initialMeta'] ?? [];
$serverFilter = $props['initialFilter'] ?? 'all';
$serverUserId = $props['initialUserId'] ?? null;
$serverIsAuthenticated = (bool) ($props['isAuthenticated'] ?? false);
$serverResultsLabel = ((int) ($serverMeta['total'] ?? count($serverActivities))) > 0
? number_format((int) ($serverMeta['total'] ?? count($serverActivities))) . ' events'
: 'No recent activity';
$serverFilterTabs = [
['key' => 'all', 'label' => 'All Activity', 'auth_required' => false],
['key' => 'comments', 'label' => 'Comments', 'auth_required' => false],
['key' => 'replies', 'label' => 'Replies', 'auth_required' => false],
['key' => 'following', 'label' => 'Following', 'auth_required' => true],
['key' => 'my', 'label' => 'My Activity', 'auth_required' => true],
];
$buildFilterUrl = static function (string $filterKey, ?int $userId): string {
return route('community.activity', array_filter([
'filter' => $filterKey !== 'all' ? $filterKey : null,
'user_id' => $userId,
], static fn (mixed $value): bool => $value !== null && $value !== ''));
};
$describeActivity = static function (array $activity): array {
$type = (string) ($activity['type'] ?? 'activity');
$artworkTitle = (string) data_get($activity, 'artwork.title', 'an artwork');
$artworkUrl = data_get($activity, 'artwork.url');
$storyTitle = (string) data_get($activity, 'story.title', 'a story');
$storyUrl = data_get($activity, 'story.url');
$targetUsername = (string) (data_get($activity, 'target_user.username') ?: data_get($activity, 'target_user.name', 'another creator'));
$targetUrl = data_get($activity, 'target_user.profile_url');
$mentionedUsername = (string) (data_get($activity, 'mentioned_user.username') ?: data_get($activity, 'mentioned_user.name', 'someone'));
$mentionedUrl = data_get($activity, 'mentioned_user.profile_url');
$commentAuthor = (string) (data_get($activity, 'comment.author.name') ?: data_get($activity, 'comment.author.username', 'a creator'));
$commentAuthorUrl = data_get($activity, 'comment.author.profile_url');
$reactionLabel = trim((string) data_get($activity, 'reaction.emoji', '')) . ' ' . (string) data_get($activity, 'reaction.label', 'Like');
return match ($type) {
'upload' => $storyUrl || data_get($activity, 'story.title')
? ['verb' => 'published', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null]
: ['verb' => 'published', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'favorite' => ['verb' => 'favorited', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'follow' => ['verb' => 'followed', 'subject' => '@' . ltrim($targetUsername, '@'), 'subject_url' => $targetUrl, 'context' => null, 'context_url' => null],
'award' => ['verb' => 'awarded', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'story_like' => ['verb' => 'liked', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null],
'story_comment' => ['verb' => 'commented on', 'subject' => $storyTitle, 'subject_url' => $storyUrl, 'context' => null, 'context_url' => null],
'comment' => ['verb' => 'commented on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'reply' => ['verb' => 'replied on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
'reaction' => ['verb' => 'reacted ' . trim($reactionLabel), 'subject' => $commentAuthor, 'subject_url' => $commentAuthorUrl, 'context' => $artworkTitle, 'context_url' => $artworkUrl],
'mention' => ['verb' => 'mentioned', 'subject' => '@' . ltrim($mentionedUsername, '@'), 'subject_url' => $mentionedUrl, 'context' => $artworkTitle, 'context_url' => $artworkUrl],
default => ['verb' => 'shared new activity on', 'subject' => $artworkTitle, 'subject_url' => $artworkUrl, 'context' => null, 'context_url' => null],
};
};
@endphp
@section('content')
@@ -58,15 +110,136 @@
<div id="community-activity-root" class="min-h-[480px]">
<div class="mx-auto max-w-6xl px-6 pt-8 pb-20 md:px-10">
<div class="mb-6 rounded-[28px] border border-white/[0.06] bg-white/[0.025] p-5 shadow-[0_18px_45px_rgba(0,0,0,0.22)]">
<div class="h-3 w-40 animate-pulse rounded bg-white/[0.08]"></div>
<div class="mt-3 h-3 w-2/3 animate-pulse rounded bg-white/[0.06]"></div>
<div class="mt-5 flex gap-2">
<div class="h-10 w-28 animate-pulse rounded-full bg-white/[0.06]"></div>
<div class="h-10 w-24 animate-pulse rounded-full bg-white/[0.05]"></div>
<div class="h-10 w-24 animate-pulse rounded-full bg-white/[0.05]"></div>
<div class="mb-6 flex flex-col gap-4 rounded-[28px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(10,16,26,0.92),rgba(6,10,18,0.88))] p-5 shadow-[0_20px_60px_rgba(0,0,0,0.25)]">
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-white/35">Live community pulse</p>
<p class="mt-2 max-w-2xl text-sm leading-6 text-white/55">
Comments, replies, reactions, and mentions from across Skinbase in one scrolling Nova feed.
</p>
</div>
<div class="text-sm font-medium text-white/45">{{ $serverResultsLabel }}</div>
</div>
<div class="flex flex-wrap items-center gap-2">
@foreach ($serverFilterTabs as $tab)
@php
$isDisabled = $tab['auth_required'] && ! $serverIsAuthenticated;
$isActive = $serverFilter === $tab['key'];
@endphp
@if ($isDisabled)
<span class="cursor-not-allowed rounded-full border border-white/[0.06] bg-white/[0.03] px-4 py-2 text-sm font-medium text-white/35 opacity-60" aria-disabled="true">
{{ $tab['label'] }}
</span>
@else
<a
href="{{ $buildFilterUrl($tab['key'], $serverUserId) }}"
class="rounded-full border px-4 py-2 text-sm font-medium transition-all {{ $isActive ? 'border-sky-400/30 bg-sky-500/14 text-sky-200 shadow-[0_0_0_1px_rgba(56,189,248,0.08)]' : 'border-white/[0.06] bg-white/[0.03] text-white/55 hover:border-white/15 hover:bg-white/[0.05] hover:text-white/85' }}"
>
{{ $tab['label'] }}
</a>
@endif
@endforeach
</div>
</div>
@if (count($serverActivities) === 0)
<div class="rounded-[28px] border border-white/[0.06] bg-white/[0.025] px-6 py-16 text-center">
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/[0.06] bg-white/[0.03] text-white/35">
<i class="fa-solid fa-wave-square text-xl"></i>
</div>
<h3 class="text-lg font-semibold text-white/80">No activity yet</h3>
<p class="mx-auto mt-2 max-w-md text-sm leading-6 text-white/45">
When creators and members interact around artworks, their activity will appear here.
</p>
</div>
@else
<div class="space-y-4">
@foreach ($serverActivities as $activity)
@php
$activityUser = data_get($activity, 'user', []);
$activityArtwork = data_get($activity, 'artwork', []);
$activityStory = data_get($activity, 'story', []);
$activityCommentBody = trim((string) data_get($activity, 'comment.body', ''));
$activityHeadline = $describeActivity($activity);
$activityAvatarUrl = data_get($activityUser, 'avatar_url') ?: '/images/avatar_default.webp';
$activityProfileUrl = data_get($activityUser, 'profile_url');
$activityName = data_get($activityUser, 'name') ?: data_get($activityUser, 'username', 'Skinbase creator');
$activityUsername = data_get($activityUser, 'username');
$activityPreviewUrl = data_get($activityArtwork, 'thumb_url') ?: data_get($activityStory, 'cover_url');
$activityPreviewAlt = data_get($activityArtwork, 'title') ?: data_get($activityStory, 'title') ?: 'Activity preview';
$activityPreviewLink = data_get($activityArtwork, 'url') ?: data_get($activityStory, 'url');
@endphp
<article class="rounded-[28px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(11,16,26,0.96),rgba(7,11,19,0.92))] p-4 shadow-[0_18px_45px_rgba(0,0,0,0.28)] backdrop-blur-xl sm:p-5">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
<div class="sm:w-[220px] sm:shrink-0">
<div class="flex items-start gap-3">
<a href="{{ $activityProfileUrl ?: '#' }}" class="shrink-0 {{ $activityProfileUrl ? '' : 'pointer-events-none' }}">
<img src="{{ $activityAvatarUrl }}" alt="{{ $activityName }}" class="h-11 w-11 rounded-2xl object-cover" loading="lazy">
</a>
<div class="min-w-0">
<p class="text-sm font-semibold text-white">
@if ($activityProfileUrl)
<a href="{{ $activityProfileUrl }}" class="hover:text-sky-200">{{ $activityName }}</a>
@else
{{ $activityName }}
@endif
</p>
@if ($activityUsername)
<p class="text-xs uppercase tracking-[0.18em] text-white/35">@{{ $activityUsername }}</p>
@endif
<p class="mt-2 text-[11px] uppercase tracking-[0.18em] text-white/25">{{ data_get($activity, 'time_ago', '') }}</p>
</div>
</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm leading-6 text-white/70">
<span class="font-medium text-white">{{ $activityHeadline['verb'] }}</span>
@if (!empty($activityHeadline['subject']))
<span> </span>
@if (!empty($activityHeadline['subject_url']))
<a href="{{ $activityHeadline['subject_url'] }}" class="text-sky-300 hover:text-sky-200">{{ $activityHeadline['subject'] }}</a>
@else
<span class="text-white">{{ $activityHeadline['subject'] }}</span>
@endif
@endif
@if (!empty($activityHeadline['context']))
<span> on </span>
@if (!empty($activityHeadline['context_url']))
<a href="{{ $activityHeadline['context_url'] }}" class="text-sky-300 hover:text-sky-200">{{ $activityHeadline['context'] }}</a>
@else
<span class="text-white">{{ $activityHeadline['context'] }}</span>
@endif
@endif
</p>
@if ($activityCommentBody !== '')
<div class="mt-3 rounded-2xl border border-white/[0.06] bg-white/[0.025] px-4 py-3">
<p class="whitespace-pre-line break-words text-sm leading-6 text-white/80">{{ \Illuminate\Support\Str::limit($activityCommentBody, 240) }}</p>
</div>
@endif
@if (($activity['type'] ?? null) === 'mention' && data_get($activity, 'mentioned_user.username'))
<div class="mt-3 inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-xs text-sky-200">
<i class="fa-solid fa-at"></i>
Mentioned @{{ data_get($activity, 'mentioned_user.username') }}
</div>
@endif
</div>
@if ($activityPreviewUrl)
<div class="sm:ml-auto sm:w-[120px] sm:shrink-0">
<a href="{{ $activityPreviewLink ?: '#' }}" class="block overflow-hidden rounded-2xl border border-white/[0.06] bg-white/[0.03] {{ $activityPreviewLink ? '' : 'pointer-events-none' }}">
<img src="{{ $activityPreviewUrl }}" alt="{{ $activityPreviewAlt }}" class="h-[132px] w-full object-cover" loading="lazy">
</a>
</div>
@endif
</div>
</article>
@endforeach
</div>
@endif
</div>
</div>

View File

@@ -1,8 +1,14 @@
@extends('layouts.nova')
@php
$displayDate = \Carbon\Carbon::parse($display_date ?? now()->toDateString());
$isFallbackWindow = (bool) ($is_fallback_window ?? false);
$displayDateIso = $displayDate->toDateString();
$displayDateText = $displayDate->format('d F Y');
$page_title = $page_title ?? 'Most Downloaded Today';
$page_meta_description = 'Browse the artworks downloaded the most today on Skinbase.';
$page_meta_description = $isFallbackWindow
? 'Browse the artworks downloaded the most from the latest 1000 downloads on Skinbase.'
: 'Browse the artworks downloaded the most today on Skinbase.';
$page_canonical = route('downloads.today', request()->query());
$gallery_type = 'today-downloads';
$useUnifiedSeo = true;
@@ -43,13 +49,15 @@
:title="$page_title ?? 'Most Downloaded Today'"
icon="fa-download"
:breadcrumbs="$headerBreadcrumbs"
:description="'Artworks downloaded the most on <time datetime=&quot;' . e(now()->toDateString()) . '&quot;>' . e(now()->format('d F Y')) . '</time>.'"
:description="$isFallbackWindow
? 'Showing rankings calculated from the latest 1000 download events because no downloads have been recorded today yet.'
: 'Artworks downloaded the most on <time datetime=&quot;' . e($displayDateIso) . '&quot;>' . e($displayDateText) . '</time>.'"
headerClass="pb-6"
>
<x-slot name="actions">
<span class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium bg-emerald-500/10 text-emerald-300 ring-1 ring-emerald-500/25">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
Live today
{{ $isFallbackWindow ? 'Latest 1000 downloads' : 'Live today' }}
</span>
</x-slot>
</x-nova-page-header>
@@ -70,8 +78,8 @@
<svg class="mx-auto mb-3 w-10 h-10 text-white/20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
<p class="text-white/40 text-sm">No downloads recorded today yet.</p>
<p class="text-white/25 text-xs mt-1">Check back later as the day progresses.</p>
<p class="text-white/40 text-sm">No download activity is available yet.</p>
<p class="text-white/25 text-xs mt-1">Check back later after new downloads are recorded.</p>
</div>
@endif
</div>

View File

@@ -1,6 +1,6 @@
@extends('layouts.nova')
@php($useUnifiedSeo = true)
<?php $useUnifiedSeo = true; ?>
@push('head')
{{-- Preload hero image for faster LCP --}}
@@ -20,6 +20,49 @@
@section('main-class', '')
@section('content')
<?php
$isLoggedIn = ! empty($props['is_logged_in']);
$artFallback = 'https://files.skinbase.org/default/missing_md.webp';
$avatarFallback = 'https://files.skinbase.org/default/avatar_default.webp';
$guestArtworkSections = [
[
'title' => 'Rising Now',
'href' => '/discover/rising',
'link_label' => 'See all',
'items' => is_array($props['rising'] ?? null) ? $props['rising'] : [],
'badge' => 'Rising',
'badge_class' => 'bg-emerald-500/80 text-white',
],
[
'title' => 'Trending Now',
'href' => '/discover/trending',
'link_label' => 'See all',
'items' => is_array($props['trending'] ?? null) ? $props['trending'] : [],
'badge' => 'Trending',
'badge_class' => 'bg-sky-500/80 text-white',
],
[
'title' => 'Community Favorites',
'href' => '/explore?sort=top-rated',
'link_label' => 'See all',
'items' => is_array($props['community_favorites'] ?? null) ? $props['community_favorites'] : [],
'badge' => 'Favorites',
'badge_class' => 'bg-amber-500/85 text-slate-950',
],
[
'title' => 'Fresh Uploads',
'href' => '/discover/fresh',
'link_label' => 'See all',
'items' => is_array($props['fresh'] ?? null) ? $props['fresh'] : [],
'badge' => 'Fresh',
'badge_class' => 'bg-fuchsia-500/80 text-white',
],
];
$guestTags = is_array($props['tags'] ?? null) ? $props['tags'] : [];
$guestCreators = is_array($props['creators'] ?? null) ? $props['creators'] : [];
$guestNews = is_array($props['news'] ?? null) ? $props['news'] : [];
?>
@include('web.home.hero', ['artwork' => $props['hero'] ?? null])
{{-- Inline props for the React component (avoids data-attribute length limits) --}}
@@ -28,16 +71,236 @@
</script>
<div id="homepage-root">
@if(!empty($props['is_logged_in']))
@include('web.home.announcement', ['announcement' => $props['announcement'] ?? null])
@if($isLoggedIn)
@include('web.home.skeleton-sections', [
'showWelcomeSpacer' => true,
'variants' => ['gallery', 'gallery', 'gallery', 'gallery', 'gallery', 'gallery', 'gallery', 'gallery', 'collections', 'groups', 'categories', 'creators', 'tags', 'creators', 'news', 'cta'],
])
@else
@include('web.home.skeleton-sections', [
'showWelcomeSpacer' => false,
'variants' => ['gallery', 'gallery', 'gallery', 'gallery', 'gallery', 'collections', 'groups', 'categories', 'tags', 'creators', 'news', 'cta'],
])
<div class="pb-24">
@foreach ($guestArtworkSections as $section)
@if (count($section['items']) > 0)
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<div class="mb-5 flex items-center justify-between gap-4">
<h2 class="text-xl font-bold text-white">{{ $section['title'] }}</h2>
<a href="{{ $section['href'] }}" class="text-sm text-nova-300 transition hover:text-white">
{{ $section['link_label'] }}
</a>
</div>
<div class="flex snap-x snap-mandatory gap-4 overflow-x-auto pb-3 lg:grid lg:grid-cols-5 lg:overflow-visible">
@foreach (array_slice(array_values($section['items']), 0, 5) as $item)
@php
$itemTitle = (string) ($item['title'] ?? $item['name'] ?? 'Untitled');
$itemUrl = (string) ($item['url'] ?? '#');
$itemThumb = (string) ($item['thumb'] ?? $item['thumb_url'] ?? $artFallback);
$itemAuthor = (string) ($item['author'] ?? 'Artist');
$itemAuthorAvatar = (string) ($item['author_avatar'] ?? $item['avatar_url'] ?? $avatarFallback);
$itemAuthorUsername = (string) ($item['author_username'] ?? $item['username'] ?? '');
// Generate responsive srcset from md thumbnail (CDN has sm/md/lg variants)
$itemThumbSm = str_contains($itemThumb, '/artworks/md/')
? str_replace('/artworks/md/', '/artworks/sm/', $itemThumb)
: '';
$itemThumbLg = str_contains($itemThumb, '/artworks/md/')
? str_replace('/artworks/md/', '/artworks/lg/', $itemThumb)
: '';
@endphp
<article class="min-w-[72%] snap-start sm:min-w-[44%] lg:min-w-0">
<a
href="{{ $itemUrl }}"
class="group relative block overflow-hidden rounded-2xl bg-black/20 shadow-lg shadow-black/40 ring-1 ring-white/5 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70"
>
<div class="relative aspect-video overflow-hidden bg-neutral-900">
<div class="pointer-events-none absolute inset-0 z-10 bg-gradient-to-br from-white/10 via-white/5 to-transparent"></div>
<img
src="{{ $itemThumb }}"
@if($itemThumbSm !== '' && $itemThumbLg !== '')
srcset="{{ $itemThumbSm }} 300w, {{ $itemThumb }} 500w, {{ $itemThumbLg }} 900w"
sizes="(max-width: 640px) 72vw, (max-width: 1024px) 44vw, 240px"
@endif
alt="{{ $itemTitle }}"
class="h-full w-full object-cover transition-[transform,filter] duration-300 ease-out group-hover:scale-[1.04]"
loading="lazy"
decoding="async"
/>
<div class="absolute left-3 top-3 z-30">
<span class="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-bold ring-1 ring-white/10 backdrop-blur-sm {{ $section['badge_class'] }}">
{{ $section['badge'] }}
</span>
</div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100">
<div class="truncate text-sm font-semibold text-white">{{ $itemTitle }}</div>
<div class="mt-1 flex items-center gap-2 text-xs text-white/80">
<img
src="{{ $itemAuthorAvatar }}"
alt="{{ $itemAuthor }}"
class="h-6 w-6 shrink-0 rounded-full object-cover"
loading="lazy"
decoding="async"
/>
<span class="truncate">{{ $itemAuthor }}</span>
@if ($itemAuthorUsername !== '')
<span class="shrink-0 text-white/50">@{{ $itemAuthorUsername }}</span>
@endif
</div>
</div>
</div>
<span class="sr-only">{{ $itemTitle }} by {{ $itemAuthor }}</span>
</a>
</article>
@endforeach
</div>
</section>
@endif
@endforeach
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<div class="overflow-hidden rounded-2xl border border-white/10 bg-[linear-gradient(180deg,rgba(11,17,28,0.92),rgba(7,11,19,0.95))] px-6 py-8 ring-1 ring-white/5 sm:px-8">
<div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div class="max-w-2xl">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-white/35">Browse the archive</p>
<h2 class="mt-3 text-2xl font-bold text-white sm:text-3xl">Explore categories, wallpapers, skins, and creator collections.</h2>
<p class="mt-3 text-sm leading-7 text-white/60">
Dive into the full Skinbase directory to browse curated categories, trending artwork types, and classic collections.
</p>
</div>
<div class="flex flex-wrap gap-3">
<a href="/categories" class="rounded-full bg-sky-500 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-sky-400">Open categories</a>
<a href="/wallpapers" class="rounded-full border border-white/10 bg-white/[0.04] px-5 py-2.5 text-sm font-semibold text-white/80 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">Wallpapers</a>
<a href="/skins" class="rounded-full border border-white/10 bg-white/[0.04] px-5 py-2.5 text-sm font-semibold text-white/80 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">Skins</a>
<a href="/photography" class="rounded-full border border-white/10 bg-white/[0.04] px-5 py-2.5 text-sm font-semibold text-white/80 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">Photography</a>
</div>
</div>
</div>
</section>
@if (count($guestTags) > 0)
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<h2 class="mb-5 text-xl font-bold text-white">Popular Tags</h2>
<div class="flex flex-wrap gap-2">
@foreach ($guestTags as $tag)
<a
href="/tag/{{ $tag['slug'] ?? '' }}"
class="rounded-full bg-nova-800 px-4 py-1.5 text-sm font-medium text-nova-200 transition hover:bg-nova-700 hover:text-white"
>
{{ $tag['name'] ?? 'Tag' }}
@if ((int) ($tag['count'] ?? 0) > 0)
<span class="ml-1.5 text-xs text-soft">{{ number_format((int) ($tag['count'] ?? 0)) }}</span>
@endif
</a>
@endforeach
</div>
</section>
@endif
@if (count($guestCreators) > 0)
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<div class="mb-5 flex items-center justify-between gap-4">
<h2 class="text-xl font-bold text-white">Creator Spotlight</h2>
<a href="/members" class="text-sm text-nova-300 transition hover:text-white">All creators</a>
</div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
@foreach (array_slice(array_values($guestCreators), 0, 6) as $creator)
@php
$creatorName = (string) ($creator['name'] ?? 'Creator');
$creatorUrl = (string) ($creator['url'] ?? '#');
$creatorAvatar = (string) ($creator['avatar'] ?? $avatarFallback);
$creatorBgThumb = (string) ($creator['bg_thumb'] ?? '');
@endphp
<a
href="{{ $creatorUrl }}"
aria-label="View {{ $creatorName }} profile"
class="group relative flex min-h-[16rem] flex-col items-center overflow-hidden rounded-xl bg-panel p-5 text-center shadow-sm transition hover:ring-1 hover:ring-nova-500"
@if ($creatorBgThumb !== '')
style="background-image: linear-gradient(to top, rgba(13, 19, 28, 0.96), rgba(13, 19, 28, 0.7)), url('{{ $creatorBgThumb }}'); background-size: cover; background-position: center;"
@endif
>
<img
src="{{ $creatorAvatar }}"
alt="{{ $creatorName }}"
class="relative mx-auto h-16 w-16 rounded-full bg-nova-800/80 object-cover ring-4 ring-nova-800"
loading="lazy"
decoding="async"
/>
<h3 class="relative mt-2 text-sm font-semibold text-white">{{ $creatorName }}</h3>
<p class="relative mt-1 flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-soft">
<span>Uploads {{ number_format((int) ($creator['uploads'] ?? 0)) }}</span>
@if ((int) ($creator['weekly_uploads'] ?? 0) > 0)
<span class="font-semibold text-accent">{{ number_format((int) ($creator['weekly_uploads'] ?? 0)) }} this week</span>
@endif
<span>Views {{ number_format((int) ($creator['views'] ?? 0)) }}</span>
@if ((int) ($creator['awards'] ?? 0) > 0)
<span>Awards {{ number_format((int) ($creator['awards'] ?? 0)) }}</span>
@endif
</p>
<span class="relative mt-3 inline-flex items-center justify-center rounded-lg bg-nova-700 px-4 py-1.5 text-xs font-semibold text-white transition group-hover:bg-nova-600">
View profile
</span>
</a>
@endforeach
</div>
</section>
@endif
@if (count($guestNews) > 0)
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<div class="mb-5 flex items-center justify-between gap-4">
<h2 class="text-xl font-bold text-white">News and Updates</h2>
<a href="/news" class="text-sm text-nova-300 transition hover:text-white">All news</a>
</div>
<div class="overflow-hidden rounded-[24px] border border-white/10 bg-panel divide-y divide-nova-800">
@foreach (array_slice(array_values($guestNews), 0, 6) as $item)
<a
href="{{ $item['url'] ?? '#' }}"
class="grid gap-3 px-5 py-4 transition hover:bg-nova-800 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start"
>
<div class="min-w-0">
@if (!empty($item['eyebrow']))
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-nova-300">{{ $item['eyebrow'] }}</div>
@endif
<div class="mt-1 line-clamp-2 text-sm font-medium text-white">{{ $item['title'] ?? 'News item' }}</div>
@if (!empty($item['excerpt']))
<p class="mt-2 line-clamp-2 text-sm leading-6 text-soft">{{ $item['excerpt'] }}</p>
@endif
</div>
@if (!empty($item['date']))
<span class="shrink-0 text-xs text-soft">{{ $item['date'] }}</span>
@endif
</a>
@endforeach
</div>
</section>
@endif
<section class="mt-14 px-4 sm:px-6 lg:px-8">
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-accent/20 via-nova-800 to-nova-900 px-8 py-12 text-center ring-1 ring-white/5">
<div class="pointer-events-none absolute -right-12 -top-12 h-40 w-40 rounded-full bg-accent/10 blur-3xl"></div>
<div class="pointer-events-none absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-sky-500/10 blur-2xl"></div>
<div class="relative z-10">
<p class="text-xs font-semibold uppercase tracking-widest text-accent">Join the community</p>
<h2 class="mt-2 text-2xl font-bold text-white sm:text-3xl">Ready to share your creativity?</h2>
<p class="mx-auto mt-3 max-w-md text-sm text-nova-300">
Upload your artworks, wallpapers, and skins to reach thousands of enthusiasts around the world.
</p>
<div class="mt-6 flex flex-wrap justify-center gap-3">
<a href="/login?redirect=/upload" class="btn-accent-solid rounded-xl px-6 py-2.5 text-sm font-semibold">
Upload your artwork
</a>
<a href="/register" class="rounded-xl border border-white/10 bg-nova-700 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-nova-600">
Create account
</a>
</div>
</div>
</div>
</section>
</div>
@endif
</div>

View File

@@ -1,11 +1,10 @@
@extends('layouts.nova.content-layout')
@extends('layouts.story-editor')
@php
$hero_title = $mode === 'create' ? 'Write Story' : 'Edit Story';
$hero_description = 'A focused writing studio with autosave, embeds, live preview, and a cleaner publish workflow.';
$page_title = $mode === 'create' ? 'Write Story' : 'Edit Story';
@endphp
@section('page-content')
@section('content')
@php
$initialContent = $story->content;
if (is_string($initialContent)) {
@@ -49,7 +48,7 @@
];
@endphp
<div class="mx-auto max-w-7xl" id="story-editor-react-root"
<div id="story-editor-react-root"
data-mode="{{ $mode }}"
data-story='@json($storyPayload)'
data-story-types='@json($storyTypes)'

View File

@@ -88,7 +88,7 @@
<span class="inline-flex w-fit rounded-full border border-sky-400/30 bg-sky-500/10 px-3 py-1 text-xs font-semibold text-sky-300">Featured Story</span>
<h2 class="text-2xl font-bold text-white">{{ $featured->title }}</h2>
<p class="text-gray-300">{{ $featured->excerpt }}</p>
<p class="text-sm text-gray-400">by @{{ $featured->creator?->username ?? 'unknown' }} {{ $featured->reading_time }} min read {{ number_format((int) $featured->views) }} views</p>
<p class="text-sm text-gray-400">by {{ $featured->creator?->username ?? 'unknown' }} {{ $featured->reading_time }} min read {{ number_format((int) $featured->views) }} views</p>
</div>
</a>
</section>

View File

@@ -5,6 +5,59 @@
$hero_description = 'This preview mirrors the final published layout.';
@endphp
@section('page-hero')
<div class="mx-auto max-w-7xl">
@include('components.breadcrumbs', ['breadcrumbs' => collect([
['label' => 'Home', 'url' => route('index')],
['label' => 'Story Preview'],
])])
<div class="mt-4 grid gap-6 lg:grid-cols-12 lg:items-start">
<div class="lg:col-span-8">
<div class="rounded-3xl border border-white/10 bg-gradient-to-br from-slate-800/95 via-slate-800/88 to-sky-950/60 p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] ring-1 ring-white/6 sm:p-8">
<div class="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-sky-200/80">
<span class="rounded-full border border-sky-400/30 bg-sky-400/10 px-3 py-1.5 text-[0.65rem]">Preview mode</span>
<span class="text-white/40">Matches the published story canvas</span>
</div>
<div class="mt-5 max-w-3xl">
<h1 class="text-3xl font-semibold leading-tight tracking-tight text-white sm:text-4xl">{{ $hero_title }}</h1>
<p class="mt-3 max-w-2xl text-sm leading-7 text-slate-300 sm:text-base">
{{ $hero_description }} Review the story rhythm, media scale, and spacing here before you publish.
</p>
</div>
</div>
</div>
<div class="lg:col-span-4">
<div class="rounded-3xl border border-white/10 bg-white/[0.05] p-5 shadow-[0_18px_45px_rgba(2,6,23,0.2)] ring-1 ring-white/6 backdrop-blur-sm">
<div class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-200/75">Preview workspace</div>
<p class="mt-3 text-sm leading-7 text-slate-300">
Use this surface to check composition, scan the story in its published canvas, and jump back into editing without hunting for controls.
</p>
<div class="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="rounded-2xl border border-sky-500/35 bg-sky-500/10 px-4 py-3 text-sm font-medium text-sky-100 transition hover:-translate-y-0.5 hover:bg-sky-500/15">
Back to editor
</a>
<a href="{{ route('creator.stories.analytics', ['story' => $story->id]) }}" class="rounded-2xl border border-violet-500/35 bg-violet-500/10 px-4 py-3 text-sm font-medium text-violet-100 transition hover:-translate-y-0.5 hover:bg-violet-500/15">
Story analytics
</a>
</div>
<div class="mt-5 rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div class="text-[0.7rem] font-semibold uppercase tracking-[0.22em] text-slate-400">Current status</div>
<p class="mt-2 text-sm font-medium text-white">{{ str_replace('_', ' ', ucfirst($story->status)) }}</p>
@if($story->rejected_reason)
<p class="mt-3 rounded-xl border border-rose-500/30 bg-rose-500/10 p-3 text-xs leading-6 text-rose-100">{{ $story->rejected_reason }}</p>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@section('page-content')
<div class="mx-auto grid max-w-7xl gap-6 lg:grid-cols-12">
<article class="lg:col-span-8">
@@ -23,29 +76,31 @@
<p class="mt-3 text-gray-300">{{ $story->excerpt }}</p>
@endif
<div class="story-prose prose prose-invert mt-6 max-w-none prose-a:text-sky-300 prose-pre:overflow-x-auto prose-pre:rounded-2xl prose-pre:border prose-pre:border-slate-700 prose-pre:bg-slate-950 prose-pre:px-8 prose-pre:py-6 prose-pre:text-slate-100 prose-pre:shadow-[0_24px_70px_rgba(2,6,23,0.45)] prose-pre:ring-1 prose-pre:ring-sky-500/10 prose-pre:font-mono prose-pre:text-[0.95rem] prose-pre:leading-8 prose-code:text-amber-200 prose-code:bg-white/[0.08] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-blockquote:border-l-4 prose-blockquote:border-sky-400/55 prose-blockquote:bg-sky-400/[0.06] prose-blockquote:px-5 prose-blockquote:py-3 prose-blockquote:rounded-r-xl prose-blockquote:text-white/82 prose-blockquote:italic prose-pre:prose-code:bg-transparent prose-pre:prose-code:p-0 prose-pre:prose-code:text-slate-100 prose-pre:prose-code:rounded-none [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-2 [&_li]:text-white/85">
<div class="story-prose prose prose-invert prose-lg mt-6 max-w-none prose-a:text-sky-300 prose-p:leading-[1.9] prose-li:leading-[1.9] prose-pre:overflow-x-auto prose-pre:rounded-2xl prose-pre:border prose-pre:border-slate-700 prose-pre:bg-slate-950 prose-pre:px-8 prose-pre:py-6 prose-pre:text-slate-100 prose-pre:shadow-[0_24px_70px_rgba(2,6,23,0.45)] prose-pre:ring-1 prose-pre:ring-sky-500/10 prose-pre:font-mono prose-pre:text-[0.95rem] prose-pre:leading-8 prose-code:text-amber-200 prose-code:bg-white/[0.08] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-blockquote:border-l-4 prose-blockquote:border-sky-400/55 prose-blockquote:bg-sky-400/[0.06] prose-blockquote:px-5 prose-blockquote:py-3 prose-blockquote:rounded-r-xl prose-blockquote:text-white/82 prose-blockquote:italic prose-pre:prose-code:bg-transparent prose-pre:prose-code:p-0 prose-pre:prose-code:text-slate-100 prose-pre:prose-code:rounded-none [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-2 [&_li]:text-white/85">
{!! $safeContent !!}
</div>
</div>
</div>
</article>
<aside class="space-y-4 lg:col-span-4">
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Preview Actions</h3>
<div class="mt-3 flex flex-col gap-2 text-sm">
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="rounded-lg border border-sky-500/40 bg-sky-500/10 px-3 py-2 text-sky-200 transition hover:scale-[1.02]">Back to editor</a>
<a href="{{ route('creator.stories.analytics', ['story' => $story->id]) }}" class="rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 py-2 text-violet-200 transition hover:scale-[1.02]">Story analytics</a>
<aside class="space-y-4 lg:col-span-4 lg:sticky lg:top-24 lg:self-start">
<div class="rounded-2xl border border-gray-700/80 bg-gray-800/55 p-5 shadow-lg ring-1 ring-white/5">
<h3 class="text-sm font-semibold uppercase tracking-[0.2em] text-gray-300">Preview Checklist</h3>
<div class="mt-4 space-y-3 text-sm text-slate-300">
<div class="rounded-xl border border-white/8 bg-white/[0.03] px-4 py-3">
<div class="font-medium text-white">Check story rhythm</div>
<p class="mt-1 text-xs leading-6 text-slate-400">Scan headings, paragraph spacing, and separators to make sure the page breathes.</p>
</div>
<div class="rounded-xl border border-white/8 bg-white/[0.03] px-4 py-3">
<div class="font-medium text-white">Check media scale</div>
<p class="mt-1 text-xs leading-6 text-slate-400">Look for full-width images, embedded video sizing, and any block that feels too narrow.</p>
</div>
<div class="rounded-xl border border-white/8 bg-white/[0.03] px-4 py-3">
<div class="font-medium text-white">Check top fold</div>
<p class="mt-1 text-xs leading-6 text-slate-400">Confirm the cover, title, excerpt, and first block create a strong opening impression.</p>
</div>
</div>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Status</h3>
<p class="mt-2 text-sm text-gray-200">{{ str_replace('_', ' ', ucfirst($story->status)) }}</p>
@if($story->rejected_reason)
<p class="mt-2 rounded-lg border border-rose-500/30 bg-rose-500/10 p-2 text-xs text-rose-200">{{ $story->rejected_reason }}</p>
@endif
</div>
</aside>
</div>
@endsection

View File

@@ -64,7 +64,7 @@
<p class="mt-3 text-gray-300">{{ $story->excerpt }}</p>
@endif
<div class="story-prose mt-6 prose prose-invert max-w-none prose-a:text-sky-300 prose-pre:overflow-x-auto prose-pre:rounded-2xl prose-pre:border prose-pre:border-slate-700 prose-pre:bg-slate-950 prose-pre:px-8 prose-pre:py-6 prose-pre:text-slate-100 prose-pre:shadow-[0_24px_70px_rgba(2,6,23,0.45)] prose-pre:ring-1 prose-pre:ring-sky-500/10 prose-pre:font-mono prose-pre:text-[0.95rem] prose-pre:leading-8 prose-code:text-amber-200 prose-code:bg-white/[0.08] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-blockquote:border-l-4 prose-blockquote:border-sky-400/55 prose-blockquote:bg-sky-400/[0.06] prose-blockquote:px-5 prose-blockquote:py-3 prose-blockquote:rounded-r-xl prose-blockquote:text-white/82 prose-blockquote:italic prose-pre:prose-code:bg-transparent prose-pre:prose-code:p-0 prose-pre:prose-code:text-slate-100 prose-pre:prose-code:rounded-none [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-2 [&_li]:text-white/85">
<div class="story-prose mt-6 prose prose-invert prose-lg max-w-none prose-a:text-sky-300 prose-p:leading-[1.9] prose-li:leading-[1.9] prose-pre:overflow-x-auto prose-pre:rounded-2xl prose-pre:border prose-pre:border-slate-700 prose-pre:bg-slate-950 prose-pre:px-8 prose-pre:py-6 prose-pre:text-slate-100 prose-pre:shadow-[0_24px_70px_rgba(2,6,23,0.45)] prose-pre:ring-1 prose-pre:ring-sky-500/10 prose-pre:font-mono prose-pre:text-[0.95rem] prose-pre:leading-8 prose-code:text-amber-200 prose-code:bg-white/[0.08] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-blockquote:border-l-4 prose-blockquote:border-sky-400/55 prose-blockquote:bg-sky-400/[0.06] prose-blockquote:px-5 prose-blockquote:py-3 prose-blockquote:rounded-r-xl prose-blockquote:text-white/82 prose-blockquote:italic prose-pre:prose-code:bg-transparent prose-pre:prose-code:p-0 prose-pre:prose-code:text-slate-100 prose-pre:prose-code:rounded-none [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-2 [&_li]:text-white/85">
{!! $safeContent !!}
</div>
</div>