Wire admin studio SSR and search infrastructure
This commit is contained in:
@@ -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"> {{ $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;">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'">
|
||||
|
||||
12
resources/views/admin.blade.php
Normal file
12
resources/views/admin.blade.php
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
27
resources/views/errors/429.blade.php
Normal file
27
resources/views/errors/429.blade.php
Normal 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
|
||||
29
resources/views/errors/http.blade.php
Normal file
29
resources/views/errors/http.blade.php
Normal 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
|
||||
9
resources/views/forum.blade.php
Normal file
9
resources/views/forum.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@push('head')
|
||||
@vite(['resources/js/forum.jsx'])
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@inertia
|
||||
@endsection
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
51
resources/views/layouts/story-editor.blade.php
Normal file
51
resources/views/layouts/story-editor.blade.php
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 "{{ $query }}"</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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="' . e(now()->toDateString()) . '">' . 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="' . e($displayDateIso) . '">' . 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user