Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -0,0 +1,86 @@
@extends('layouts.nova.content-layout')
@php
$page_title = 'Apply to Join the Team';
$hero_description = "We're always grateful for volunteers who want to help.";
$center_content = true;
$center_max = '3xl';
@endphp
@section('page-content')
<div class="max-w-3xl">
@if(session('success'))
<div class="mb-4 rounded-lg bg-emerald-800/20 border border-emerald-700 p-4 text-emerald-200">
{{ session('success') }}
</div>
@endif
<form x-data='{ topic: @json(old('topic','apply')) }' method="POST" action="{{ route('contact.submit') }}" class="space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-neutral-200">Reason for contact</label>
<select name="topic" x-model="topic" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white">
<option value="apply" {{ old('topic') === 'apply' ? 'selected' : '' }}>Apply to join the team</option>
<option value="bug" {{ old('topic') === 'bug' ? 'selected' : '' }}>Report a bug / site issue</option>
<option value="contact" {{ old('topic') === 'contact' ? 'selected' : '' }}>General contact / question</option>
<option value="other" {{ old('topic') === 'other' ? 'selected' : '' }}>Other</option>
</select>
@error('topic') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-neutral-200">Full name</label>
<input name="name" value="{{ old('name') }}" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white" required />
@error('name') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-neutral-200">Email</label>
<input name="email" value="{{ old('email') }}" type="email" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white" required />
@error('email') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div x-show="topic === 'apply'" x-cloak>
<label class="block text-sm font-medium text-neutral-200">Role you're applying for</label>
<input name="role" value="{{ old('role') }}" placeholder="e.g. Moderator, Community Manager" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white" />
@error('role') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div x-show="topic === 'apply'" x-cloak>
<label class="block text-sm font-medium text-neutral-200">Portfolio / profile (optional)</label>
<input name="portfolio" value="{{ old('portfolio') }}" placeholder="https://" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white" />
@error('portfolio') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-neutral-200">Tell us about yourself</label>
<textarea name="message" rows="6" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white">{{ old('message') }}</textarea>
@error('message') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
{{-- Bug-specific fields --}}
<div x-show="topic === 'bug'" x-cloak>
<label class="block text-sm font-medium text-neutral-200">Affected URL (optional)</label>
<input name="affected_url" value="{{ old('affected_url') }}" placeholder="https://" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white" />
@error('affected_url') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
<label class="block text-sm font-medium text-neutral-200 mt-3">Steps to reproduce (optional)</label>
<textarea name="steps" rows="4" class="mt-1 block w-full rounded bg-white/2 border border-white/10 p-2 text-white">{{ old('steps') }}</textarea>
@error('steps') <p class="text-rose-400 text-sm mt-1">{{ $message }}</p> @enderror
</div>
{{-- Honeypot field (hidden from real users) --}}
<div style="display:none;" aria-hidden="true">
<label>Website</label>
<input type="text" name="website" value="" autocomplete="off" />
</div>
<div class="flex items-center justify-end">
<button type="submit" class="inline-flex items-center gap-2 rounded bg-sky-500 px-4 py-2 text-sm font-medium text-white hover:bg-sky-600">Submit</button>
</div>
</form>
<p class="mt-6 text-sm text-neutral-400">By submitting this form you consent to Skinbase storing your application details for review.</p>
</div>
@endsection

View File

@@ -0,0 +1,118 @@
@extends('layouts.nova')
@section('content')
<div class="legacy-artwork">
<div class="row">
<div class="col-md-8">
<div class="effect2" style="max-width:800px">
<a class="artwork-zoom" href="{{ $thumb_file ?? '#' }}" title="{{ $artwork->name }}">
<img src="{{ $thumb_file }}" alt="{{ $artwork->name ?? '' }}" class="img-thumbnail img-responsive">
</a>
</div>
<div style="clear:both;margin-top:10px;">
<img src="{{ \App\Support\AvatarUrl::forUser((int) ($artwork->user_id ?? 0), null, 50) }}" class="pull-left" style="padding-right:10px;max-height:50px;" alt="Avatar">
<h1 class="page-header">{{ $artwork->name }}</h1>
<p>By <i class="fa fa-user fa-fw"></i> <a href="/profile/{{ $artwork->user_id }}/{{ \Illuminate\Support\Str::slug($artwork->uname) }}" title="Profile of member {{ $artwork->uname }}">{{ $artwork->uname }}</a></p>
<hr>
</div>
@if(!empty($artwork->description))
<div class="panel panel-skinbase">
<div class="panel-body">{!! nl2br(e($artwork->description)) !!}</div>
</div>
@endif
{{-- Comments --}}
@if(!empty($comments) && $comments->count() > 0)
<h3 class="comment-title"><i class="fa fa-comments fa-fw"></i> Comments:</h3>
@foreach($comments as $comment)
<div class="comment_box effect3">
<div class="cb_image">
<a href="/profile/{{ $comment->user_id }}/{{ urlencode($comment->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $comment->user_id, null, 50) }}" width="50" height="50" class="comment_avatar" alt="{{ $comment->uname }}">
</a>
</div>
<div class="bubble_comment panel panel-skinbase">
<div class="panel-heading">
<div class="pull-right">{{ \Illuminate\Support\Str::limit($comment->date, 16) }}</div>
<h5 class="panel-title">Comment by: <a href="/profile/{{ $comment->user_id }}/{{ urlencode($comment->uname) }}">{{ $comment->uname }}</a></h5>
</div>
<div class="panel-body">
{!! nl2br(e($comment->description)) !!}
</div>
@if(!empty($comment->signature))
<div class="panel-footer comment-footer">{!! nl2br(e($comment->signature)) !!}</div>
@endif
</div>
</div>
@endforeach
@endif
@auth
<div class="comment_box effect3">
<div class="cb_image">
<a href="/profile/{{ auth()->id() }}/{{ urlencode(auth()->user()->name) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) auth()->id(), null, 50) }}" class="comment_avatar" width="50" height="50">
</a>
<br>
<a href="/profile/{{ auth()->id() }}/{{ urlencode(auth()->user()->name) }}">{{ auth()->user()->name }}</a>
</div>
<form action="/art/{{ $artwork->id }}" method="post">
@csrf
<div class="bubble_comment panel panel-skinbase">
<div class="panel-heading"><h4 class="panel-title">Write comment</h4></div>
<div class="panel-body">
<textarea name="comment_text" class="form-control" style="width:98%; height:120px;"></textarea><br>
</div>
<div class="panel-footer">
<button type="submit" class="btn btn-success">Post comment</button>
</div>
</div>
<input type="hidden" name="artwork_id" value="{{ $artwork->id }}">
<input type="hidden" name="action" value="store_comment">
</form>
</div>
@endauth
</div>
<div class="col-md-4">
<div class="panel panel-default effect3">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<a class="btn btn-info" style="width:100%;margin-bottom:5px" href="/download/{{ $artwork->id }}" title="Download Full Size Artwork to your computer">
<i class="fa fa-download fa-fw"></i> Download
</a>
</div>
<div class="col-md-6">
@if(auth()->check())
<span class="btn btn-warning addFavourites" style="width:100%" data-artwork_id="{{ $artwork->id }}"><i class="fa fa-heart fa-fw"></i> Add To Favourites</span>
@endif
</div>
</div>
<hr>
<h4 class="panel-title">Details</h4>
<table class="table table-condensed">
<tr><td>Category</td><td class="text-right">{{ $artwork->category_name ?? '' }}</td></tr>
<tr><td>Uptime</td><td class="text-right">{{ \Illuminate\Support\Str::limit($artwork->datum ?? '', 10) }}</td></tr>
<tr><td>Submitted</td><td class="text-right">{{ optional(\Carbon\Carbon::parse($artwork->datum))->format('d.m.Y') }}</td></tr>
<tr><td>Resolution</td><td class="text-right">{{ ($artwork->width ?? '') . 'x' . ($artwork->height ?? '') }}</td></tr>
</table>
<h4 class="panel-title">Statistics</h4>
<table class="table table-condensed">
<tr><td>Views</td><td class="text-right">{{ $artwork->views ?? 0 }}</td></tr>
<tr><td>Downloads</td><td class="text-right">{{ $artwork->dls ?? 0 }} @if(!empty($num_downloads)) <small>({{ $num_downloads }} today)</small>@endif</td></tr>
</table>
<h4 class="panel-title">Social</h4>
<div class="fb-like" data-href="{{ url()->current() }}" data-send="true" data-width="450" data-show-faces="true"></div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,179 @@
@extends('layouts.nova')
@section('content')
{{-- ── Hero header ── --}}
@php
$headerBreadcrumbs = collect([
(object) ['name' => 'Creators', 'url' => '/creators/top'],
(object) ['name' => 'Top Creators', 'url' => route('creators.top')],
]);
@endphp
<x-nova-page-header
section="Creators"
title="Top Creators"
icon="fa-star"
:breadcrumbs="$headerBreadcrumbs"
:description="'Most popular creators ranked by artwork ' . ($metric === 'downloads' ? 'downloads' : 'views') . '.'"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<a href="{{ route('creators.rising') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors">
<i class="fa-solid fa-arrow-trend-up text-xs"></i>
Rising Creators
</a>
<nav class="flex flex-wrap items-center gap-2" aria-label="Ranking metric">
<a href="{{ request()->fullUrlWithQuery(['metric' => 'views']) }}"
class="inline-flex items-center gap-1.5 rounded-full px-4 py-1.5 text-xs font-medium border transition-colors
{{ $metric === 'views' ? 'bg-sky-500/15 text-sky-300 border-sky-500/30' : 'border-white/[0.08] bg-white/[0.04] text-white/55 hover:text-white hover:bg-white/[0.08]' }}">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
Views
</a>
<a href="{{ request()->fullUrlWithQuery(['metric' => 'downloads']) }}"
class="inline-flex items-center gap-1.5 rounded-full px-4 py-1.5 text-xs font-medium border transition-colors
{{ $metric === 'downloads' ? 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30' : 'border-white/[0.08] bg-white/[0.04] text-white/55 hover:text-white hover:bg-white/[0.08]' }}">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<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>
Downloads
</a>
</nav>
</x-slot>
</x-nova-page-header>
{{-- ── Leaderboard ── --}}
<div class="px-6 pt-8 pb-16 md:px-10">
@php
$offset = ($authors->currentPage() - 1) * $authors->perPage();
$isFirstPage = $authors->currentPage() === 1;
$showcaseTop = $isFirstPage ? $authors->getCollection()->take(3)->values() : collect();
$tableAuthors = $isFirstPage ? $authors->getCollection()->slice(3)->values() : $authors->getCollection();
$rankBase = $isFirstPage ? 3 : 0;
@endphp
@if ($authors->isNotEmpty())
@if ($showcaseTop->isNotEmpty())
<div class="mb-6 grid gap-4 md:grid-cols-3">
@foreach ($showcaseTop as $i => $author)
@php
$rank = $i + 1;
$profileUrl = ($author->username ?? null)
? '/@' . $author->username
: '/profile/' . (int) $author->user_id;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) $author->user_id, $author->avatar_hash ?? null, 64);
$rankClasses = $rank === 1
? 'bg-amber-400/15 text-amber-300 ring-amber-400/30'
: ($rank === 2
? 'bg-slate-400/15 text-slate-300 ring-slate-400/30'
: 'bg-orange-700/20 text-orange-400 ring-orange-600/30');
@endphp
<a href="{{ $profileUrl }}"
class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-5 hover:bg-white/[0.05] transition-colors">
<div class="flex items-center justify-between mb-4">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold ring-1 {{ $rankClasses }}">
{{ $rank }}
</span>
<span class="text-xs font-semibold uppercase tracking-widest {{ $metric === 'downloads' ? 'text-emerald-300/80' : 'text-sky-300/80' }}">
{{ $metric === 'downloads' ? 'Downloads' : 'Views' }}
</span>
</div>
<div class="flex items-center justify-between gap-4">
<img src="{{ $avatarUrl }}" alt="{{ $author->uname }}"
class="w-14 h-14 rounded-full object-cover ring-1 ring-white/[0.12]">
<div class="min-w-0 flex-1">
<div class="truncate text-base font-semibold text-white">{{ $author->uname ?? 'Unknown' }}</div>
@if (!empty($author->username))
<div class="truncate text-xs text-white/40">{{ '@' . $author->username }}</div>
@endif
</div>
<div class="shrink-0 text-right">
<div class="text-lg font-bold {{ $metric === 'downloads' ? 'text-emerald-400' : 'text-sky-400' }}">
{{ number_format($author->total ?? 0) }}
</div>
</div>
</div>
</a>
@endforeach
</div>
@endif
<div class="rounded-xl border border-white/[0.06] overflow-hidden">
{{-- Table header --}}
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-3 bg-white/[0.03] border-b border-white/[0.06]">
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-center">#</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30">Author</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-right">
{{ $metric === 'downloads' ? 'Downloads' : 'Views' }}
</span>
</div>
{{-- Rows --}}
<div class="divide-y divide-white/[0.04]">
@foreach ($tableAuthors as $i => $author)
@php
$rank = $offset + $rankBase + $i + 1;
$profileUrl = ($author->username ?? null)
? '/@' . $author->username
: '/profile/' . (int) $author->user_id;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) $author->user_id, $author->avatar_hash ?? null, 64);
@endphp
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-4
{{ $rank <= 3 ? 'bg-white/[0.015]' : '' }} hover:bg-white/[0.03] transition-colors">
{{-- Rank badge --}}
<div class="text-center">
@if ($rank === 1)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-amber-400/15 text-amber-300 text-xs font-bold ring-1 ring-amber-400/30">1</span>
@elseif ($rank === 2)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-slate-400/15 text-slate-300 text-xs font-bold ring-1 ring-slate-400/30">2</span>
@elseif ($rank === 3)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-orange-700/20 text-orange-400 text-xs font-bold ring-1 ring-orange-600/30">3</span>
@else
<span class="text-sm text-white/30 font-medium">{{ $rank }}</span>
@endif
</div>
{{-- Author info --}}
<a href="{{ $profileUrl }}" class="flex items-center gap-3 min-w-0 hover:opacity-90 transition-opacity">
<img src="{{ $avatarUrl }}" alt="{{ $author->uname }}"
class="w-9 h-9 rounded-full object-cover flex-shrink-0 ring-1 ring-white/[0.08]">
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-white/90">{{ $author->uname ?? 'Unknown' }}</div>
@if (!empty($author->username))
<div class="truncate text-xs text-white/35">{{ '@' . $author->username }}</div>
@endif
</div>
</a>
{{-- Metric count --}}
<div class="text-right flex-shrink-0">
<span class="text-sm font-semibold {{ $metric === 'downloads' ? 'text-emerald-400' : 'text-sky-400' }}">
{{ number_format($author->total ?? 0) }}
</span>
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-8 flex justify-center">
{{ $authors->withQueryString()->links() }}
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No authors found.</p>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,54 @@
{{--
Blog index uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Blog';
$hero_description = 'News, tutorials and community stories from the Skinbase team.';
@endphp
@section('page-content')
@if($posts->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($posts as $post)
<a href="{{ $post->url }}"
class="group block rounded-xl border border-white/[0.06] bg-white/[0.02] overflow-hidden hover:bg-white/[0.05] transition-colors">
@if($post->featured_image)
<div class="aspect-video bg-nova-800 overflow-hidden">
<img src="{{ $post->featured_image }}" alt="{{ $post->title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />
</div>
@else
<div class="aspect-video bg-gradient-to-br from-sky-900/30 to-purple-900/30 flex items-center justify-center">
<i class="fa-solid fa-newspaper text-3xl text-white/20"></i>
</div>
@endif
<div class="p-5">
<h2 class="text-lg font-semibold text-white group-hover:text-sky-300 transition-colors line-clamp-2">
{{ $post->title }}
</h2>
@if($post->excerpt)
<p class="mt-2 text-sm text-white/50 line-clamp-3">{{ $post->excerpt }}</p>
@endif
@if($post->published_at)
<time class="mt-3 block text-xs text-white/30" datetime="{{ $post->published_at->toIso8601String() }}">
{{ $post->published_at->format('M j, Y') }}
</time>
@endif
</div>
</a>
@endforeach
</div>
<div class="mt-10 flex justify-center">
{{ $posts->withQueryString()->links() }}
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<i class="fa-solid fa-newspaper text-4xl text-white/20 mb-4"></i>
<p class="text-white/40 text-sm">No blog posts published yet. Check back soon!</p>
</div>
@endif
@endsection

View File

@@ -0,0 +1,66 @@
{{--
Blog post uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = $post->title;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->og(
type: 'article',
title: $post->meta_title ?: $post->title,
description: $post->meta_description ?: $post->excerpt ?: '',
url: $post->url,
image: $post->featured_image,
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $post->title,
'datePublished' => $post->published_at?->toIso8601String(),
'dateModified' => $post->updated_at?->toIso8601String(),
'author' => [
'@type' => 'Organization',
'name' => 'Skinbase',
],
'publisher' => [
'@type' => 'Organization',
'name' => 'Skinbase',
],
'description' => $post->meta_description ?: $post->excerpt ?: '',
'mainEntityOfPage' => $post->url,
'image' => $post->featured_image,
])
->build();
@endphp
@section('page-content')
<article class="max-w-3xl">
@if($post->featured_image)
<div class="rounded-xl overflow-hidden mb-8">
<img src="{{ $post->featured_image }}" alt="{{ $post->title }}" class="w-full" />
</div>
@endif
@if($post->published_at)
<time class="block text-sm text-white/40 mb-4" datetime="{{ $post->published_at->toIso8601String() }}">
{{ $post->published_at->format('F j, Y') }}
</time>
@endif
<div class="prose prose-invert prose-headings:text-white prose-a:text-sky-400 prose-p:text-white/70 max-w-none">
{!! $post->body !!}
</div>
<div class="mt-12 pt-8 border-t border-white/10">
<a href="/blog" class="inline-flex items-center gap-2 text-sm text-sky-400 hover:text-sky-300 transition-colors">
<i class="fa-solid fa-arrow-left text-xs"></i>
Back to Blog
</a>
</div>
</article>
@endsection

View File

@@ -0,0 +1,75 @@
@extends('layouts.nova.content-layout')
@section('page-content')
@if ($success)
<div class="mb-6 rounded-lg bg-green-500/10 border border-green-500/30 text-green-400 px-5 py-4 text-sm">
Your report was submitted successfully. Thank you we'll look into it as soon as possible.
</div>
@endif
<div class="max-w-2xl">
@guest
<div class="rounded-lg bg-nova-800 border border-neutral-700 px-6 py-8 text-center">
<svg class="mx-auto mb-3 h-10 w-10 text-neutral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/>
</svg>
<p class="text-neutral-400 text-sm mb-4">You need to be signed in to submit a bug report.</p>
<a href="{{ route('login') }}" class="inline-block rounded-md bg-accent px-5 py-2 text-sm font-medium text-white hover:opacity-90">
Sign In
</a>
</div>
@else
<p class="text-neutral-400 text-sm mb-6">
Found a bug or have a suggestion? Fill out the form below and our team will review it.
For security issues, please contact us directly via email.
</p>
<form action="{{ route('bug-report.submit') }}" method="POST" class="space-y-5">
@csrf
<div>
<label for="subject" class="block text-sm font-medium text-neutral-300 mb-1">Subject</label>
<input
type="text"
id="subject"
name="subject"
required
maxlength="255"
value="{{ old('subject') }}"
placeholder="Brief summary of the issue"
class="w-full rounded-md bg-nova-800 border border-neutral-700 px-4 py-2.5 text-sm text-white placeholder-neutral-500 focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent"
>
@error('subject')
<p class="mt-1 text-xs text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-neutral-300 mb-1">Description</label>
<textarea
id="description"
name="description"
required
rows="7"
maxlength="5000"
placeholder="Describe the bug in detail — steps to reproduce, what you expected, and what actually happened."
class="w-full rounded-md bg-nova-800 border border-neutral-700 px-4 py-2.5 text-sm text-white placeholder-neutral-500 focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent resize-y"
>{{ old('description') }}</textarea>
@error('description')
<p class="mt-1 text-xs text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<button type="submit"
class="rounded-md bg-accent px-6 py-2.5 text-sm font-medium text-white hover:opacity-90 transition-opacity">
Submit Report
</button>
</div>
</form>
@endguest
</div>
@endsection

View File

@@ -0,0 +1,26 @@
@extends('layouts.nova')
@php($useUnifiedSeo = true)
@section('main-class', '')
@section('content')
<script id="categories-page-props" type="application/json">
{!! json_encode([
'apiUrl' => route('api.categories.index'),
'pageTitle' => $page_title ?? 'Categories',
'pageDescription' => $page_meta_description ?? null,
], 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>
</div>
@vite(['resources/js/Pages/CategoriesPage.jsx'])
@endsection

View File

@@ -0,0 +1,81 @@
@extends('layouts.nova')
@php
use Illuminate\Support\Str;
use App\Banner;
$useUnifiedSeo = true;
@endphp
@section('content')
<div class="container-fluid legacy-page category-wrapper">
@php Banner::ShowResponsiveAd(); @endphp
<div id="category-artworks">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<div class="category-display"><i class="fa fa-bars fa-fw"></i></div>
@if (!empty($category->rootid) && $category->rootid > 0)
<div id="location_bar">
<a href="/{{ $category->category_name }}/{{ $category->category_id }}">{{ $category->category_name }}</a>
@if (!empty($category->rootid))
&raquo;
<a href="/{{ $group }}/{{ Str::slug($category->category_name) }}/{{ $category->category_id }}">{{ $category->category_name }}</a>
@endif
:
</div>
@endif
<h1 class="page-header">{{ $category->category_name }}</h1>
<p style="clear:both">{!! $category->description ?? ($group . ' artworks on Skinbase.') !!}</p>
</header>
</div>
@if ($artworks->count())
<div class="container_photo gallery_box">
@foreach ($artworks as $art)
@include('web.partials._artwork_card', ['art' => $art])
@endforeach
</div>
@else
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
<div class="panel-body">
<p>Once uploads arrive they will appear here. Check back soon.</p>
</div>
</div>
@endif
<div class="paginationMenu text-center">
{{ $artworks->withQueryString()->links('pagination::bootstrap-4') }}
</div>
</div>
<div id="category-list">
<div id="artwork_subcategories">
<div class="category-toggle"><i class="fa fa-bars fa-fw"></i></div>
<h5 class="browse_categories">Main Categories:</h5>
<ul>
<li><a href="/Photography/3" title="Stock Photography"><i class="fa fa-photo fa-fw"></i> Photography</a></li>
<li><a href="/Wallpapers/2" title="Desktop Wallpapers"><i class="fa fa-photo fa-fw"></i> Wallpapers</a></li>
<li><a href="/Skins/1" title="Skins for Applications"><i class="fa fa-photo fa-fw"></i> Skins</a></li>
<li><a href="/Other/4" title="Other Artworks"><i class="fa fa-photo fa-fw"></i> Other</a></li>
</ul>
<h5 class="browse_categories">Browse Subcategories:</h5>
<ul class="scrollContent" data-mcs-theme="dark">
@foreach ($subcategories as $sub)
@php $selected = $sub->category_id == $category->category_id ? 'selected_group' : ''; @endphp
<li class="subgroup {{ $selected }}">
<a href="/{{ $group }}/{{ Str::slug($sub->category_name) }}/{{ $sub->category_id }}">{{ $sub->category_name }}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
@endsection
@push('scripts')
@endpush

View File

@@ -0,0 +1,76 @@
@extends('layouts.nova')
@php
@php
$page_title = $page_title ?? 'Community Activity';
$page_meta_description = 'Track comments, replies, reactions, and mentions from across the Skinbase community in one live feed.';
$page_canonical = route('community.activity', array_filter([
'filter' => ($initialFilter ?? null) && ($initialFilter ?? 'all') !== 'all' ? $initialFilter : null,
'user_id' => $initialUserId ?? null,
], fn (mixed $value): bool => $value !== null && $value !== ''));
$useUnifiedSeo = true;
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $headerBreadcrumbs;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
$initialFilterLabel = match (($initialFilter ?? 'all')) {
'comments' => 'Comments',
'replies' => 'Replies',
'following' => 'Following',
'my' => 'My Activity',
default => 'All Activity',
};
@endphp
@section('content')
<x-nova-page-header
section="Community"
:title="$page_title ?? 'Community Activity'"
icon="fa-wave-square"
:breadcrumbs="$headerBreadcrumbs"
description="Track comments, replies, reactions, and mentions from across the Skinbase community in one live feed."
headerClass="pb-6"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
<span id="community-activity-filter-summary" class="inline-flex items-center gap-1.5 rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-sky-200">
<i class="fa-solid fa-filter"></i>
{{ $initialFilterLabel }}
</span>
@if (!empty($initialUserId))
<span id="community-activity-scope-summary" class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-white/65">
<i class="fa-solid fa-user"></i>
User #{{ $initialUserId }}
</span>
@else
<span id="community-activity-scope-summary" class="hidden"></span>
@endif
</div>
</x-slot>
</x-nova-page-header>
<script id="community-activity-props" type="application/json">
{!! json_encode($props, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
</script>
<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>
</div>
</div>
</div>
@vite(['resources/js/Pages/Community/CommunityActivityPage.jsx'])
@endsection

View File

@@ -0,0 +1,147 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Monthly Top Commentators';
$page_meta_description = 'Members who posted the most comments in the last 30 days.';
$page_canonical = route('comments.monthly', request()->query());
$useUnifiedSeo = true;
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $headerBreadcrumbs;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Community"
:title="$page_title ?? 'Monthly Top Commentators'"
icon="fa-ranking-star"
:breadcrumbs="$headerBreadcrumbs"
description="Members who posted the most comments in the last 30 days."
headerClass="pb-6"
>
<x-slot name="actions">
<span class="flex-shrink-0 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium bg-violet-500/10 text-violet-300 ring-1 ring-violet-500/25">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
Last 30 days
</span>
</x-slot>
</x-nova-page-header>
{{-- ── Leaderboard ── --}}
<div class="px-6 pt-8 pb-16 md:px-10">
@php
$offset = ($rows->currentPage() - 1) * $rows->perPage();
$isFirstPage = $rows->currentPage() === 1;
$showcaseTop = $isFirstPage ? $rows->getCollection()->take(3)->values() : collect();
$tableRows = $isFirstPage ? $rows->getCollection()->slice(3)->values() : $rows->getCollection();
$rankBase = $isFirstPage ? 3 : 0;
@endphp
@if ($rows->isNotEmpty())
@if ($showcaseTop->isNotEmpty())
<div class="mb-6 grid gap-4 md:grid-cols-3">
@foreach ($showcaseTop as $i => $row)
@php
$rank = $i + 1;
$profileUrl = ($row->user_username ?? null) ? '/@' . $row->user_username : '/profile/' . (int)($row->user_id ?? 0);
$avatarUrl = \App\Support\AvatarUrl::forUser((int)($row->user_id ?? 0), null, 64);
$rankClasses = $rank === 1
? 'bg-amber-400/15 text-amber-300 ring-amber-400/30'
: ($rank === 2
? 'bg-slate-400/15 text-slate-300 ring-slate-400/30'
: 'bg-orange-700/20 text-orange-400 ring-orange-600/30');
@endphp
<a href="{{ $profileUrl }}"
class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-5 hover:bg-white/[0.05] transition-colors">
<div class="flex items-center justify-between mb-4">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold ring-1 {{ $rankClasses }}">
{{ $rank }}
</span>
<span class="text-xs font-semibold uppercase tracking-widest text-violet-300/80">Comments</span>
</div>
<div class="flex items-center gap-3">
<img src="{{ $avatarUrl }}" alt="{{ $row->uname ?? 'User' }}"
class="w-14 h-14 rounded-full object-cover ring-1 ring-white/[0.12]">
<div class="min-w-0">
<div class="truncate text-base font-semibold text-white">{{ $row->uname ?? 'Unknown' }}</div>
<div class="mt-1 text-lg font-bold text-violet-400">{{ number_format((int)($row->num_comments ?? 0)) }}</div>
</div>
</div>
</a>
@endforeach
</div>
@endif
<div class="rounded-xl border border-white/[0.06] overflow-hidden">
{{-- Table header --}}
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-3 bg-white/[0.03] border-b border-white/[0.06]">
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-center">#</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30">Member</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Comments</span>
</div>
{{-- Rows --}}
<div class="divide-y divide-white/[0.04]">
@foreach ($tableRows as $i => $row)
@php
$rank = $offset + $rankBase + $i + 1;
$profileUrl = ($row->user_username ?? null) ? '/@' . $row->user_username : '/profile/' . (int)($row->user_id ?? 0);
$avatarUrl = \App\Support\AvatarUrl::forUser((int)($row->user_id ?? 0), null, 64);
@endphp
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-4
{{ $rank <= 3 ? 'bg-white/[0.015]' : '' }} hover:bg-white/[0.03] transition-colors">
{{-- Rank badge --}}
<div class="text-center">
@if ($rank === 1)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-amber-400/15 text-amber-300 text-xs font-bold ring-1 ring-amber-400/30">1</span>
@elseif ($rank === 2)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-slate-400/15 text-slate-300 text-xs font-bold ring-1 ring-slate-400/30">2</span>
@elseif ($rank === 3)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-orange-700/20 text-orange-400 text-xs font-bold ring-1 ring-orange-600/30">3</span>
@else
<span class="text-sm text-white/30 font-medium">{{ $rank }}</span>
@endif
</div>
{{-- Member info --}}
<a href="{{ $profileUrl }}" class="flex items-center gap-3 min-w-0 hover:opacity-90 transition-opacity">
<img src="{{ $avatarUrl }}" alt="{{ $row->uname ?? 'User' }}"
class="w-9 h-9 rounded-full object-cover flex-shrink-0 ring-1 ring-white/[0.08]">
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-white/90">{{ $row->uname ?? 'Unknown' }}</div>
</div>
</a>
{{-- Comment count --}}
<div class="text-right flex-shrink-0">
<span class="text-sm font-semibold text-violet-400">
{{ number_format((int)($row->num_comments ?? 0)) }}
</span>
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-8 flex justify-center">
{{ $rows->withQueryString()->links() }}
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No comment activity in the last 30 days.</p>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,87 @@
@extends('layouts.nova')
@section('title', $page_title . ' — Skinbase')
@section('content')
<div class="container-fluid legacy-page">
<div class="page-heading">
<h1 class="page-header"><i class="fa fa-stream"></i> {{ $page_title }}</h1>
</div>
{{-- Tab bar --}}
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link {{ $active_tab === 'global' ? 'active' : '' }}"
href="{{ route('community.activity', ['type' => 'global']) }}">
<i class="fa fa-globe"></i> Global
</a>
</li>
@auth
<li class="nav-item">
<a class="nav-link {{ $active_tab === 'following' ? 'active' : '' }}"
href="{{ route('community.activity', ['type' => 'following']) }}">
<i class="fa fa-user-group"></i> Following
</a>
</li>
@endauth
</ul>
<div class="activity-feed">
@forelse($enriched as $event)
<div class="activity-event media mb-3 p-2 rounded bg-dark-subtle">
<div class="media-body">
<span class="fw-semibold">
<a href="{{ $event['actor']['url'] ?? '#' }}">{{ $event['actor']['name'] ?? 'Someone' }}</a>
</span>
@switch($event['type'])
@case('upload')
uploaded
@break
@case('comment')
commented on
@break
@case('favorite')
favourited
@break
@case('award')
awarded
@break
@case('follow')
started following
@break
@default
interacted with
@endswitch
@if($event['target'])
@if($event['target_type'] === 'artwork')
<a href="{{ $event['target']['url'] }}">{{ $event['target']['title'] }}</a>
@if(!empty($event['target']['thumb']))
<img src="{{ $event['target']['thumb'] }}" alt="" class="ms-2 rounded" style="height:36px;width:auto;vertical-align:middle;">
@endif
@elseif($event['target_type'] === 'user')
<a href="{{ $event['target']['url'] ?? '#' }}">{{ $event['target']['name'] ?? $event['target']['username'] ?? '' }}</a>
@endif
@endif
<small class="text-muted ms-2">{{ \Carbon\Carbon::parse($event['created_at'])->diffForHumans() }}</small>
</div>
</div>
@empty
<div class="alert alert-info">
@if($active_tab === 'following')
Follow some creators to see their activity here.
@else
No activity yet. Be the first!
@endif
</div>
@endforelse
</div>
{{-- Pagination --}}
<div class="mt-3">
{{ $events->links() }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,144 @@
@extends('layouts.nova')
@section('content')
@php
$headerBreadcrumbs = collect([
(object) ['name' => 'Creators', 'url' => '/creators/top'],
(object) ['name' => 'Rising Creators', 'url' => route('creators.rising')],
]);
@endphp
<x-nova-page-header
section="Creators"
title="Rising Creators"
icon="fa-arrow-trend-up"
:breadcrumbs="$headerBreadcrumbs"
description="Creators gaining momentum with the most views over the last 90 days."
>
<x-slot name="actions">
<a href="{{ route('creators.top') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors">
<i class="fa-solid fa-star text-xs"></i>
Top Creators
</a>
</x-slot>
</x-nova-page-header>
<div class="px-6 pt-8 pb-16 md:px-10">
@php
$offset = ($creators->currentPage() - 1) * $creators->perPage();
$isFirstPage = $creators->currentPage() === 1;
$showcaseTop = $isFirstPage ? $creators->getCollection()->take(3)->values() : collect();
$tableCreators = $isFirstPage ? $creators->getCollection()->slice(3)->values() : $creators->getCollection();
$rankBase = $isFirstPage ? 3 : 0;
@endphp
@if ($creators->isNotEmpty())
@if ($showcaseTop->isNotEmpty())
<div class="mb-6 grid gap-4 md:grid-cols-3">
@foreach ($showcaseTop as $i => $creator)
@php
$rank = $i + 1;
$profileUrl = ($creator->username ?? null)
? '/@' . $creator->username
: '/profile/' . (int) $creator->user_id;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) $creator->user_id, $creator->avatar_hash ?? null, 64);
$rankClasses = $rank === 1
? 'bg-amber-400/15 text-amber-300 ring-amber-400/30'
: ($rank === 2
? 'bg-slate-400/15 text-slate-300 ring-slate-400/30'
: 'bg-orange-700/20 text-orange-400 ring-orange-600/30');
@endphp
<a href="{{ $profileUrl }}"
class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-5 hover:bg-white/[0.05] transition-colors">
<div class="flex items-center justify-between mb-4">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold ring-1 {{ $rankClasses }}">
{{ $rank }}
</span>
<span class="text-xs font-semibold uppercase tracking-widest text-sky-300/80">Recent Views</span>
</div>
<div class="flex items-center justify-between gap-4">
<img src="{{ $avatarUrl }}" alt="{{ $creator->uname }}"
class="w-14 h-14 rounded-full object-cover ring-1 ring-white/[0.12]"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'" />
<div class="min-w-0 flex-1">
<div class="truncate text-base font-semibold text-white">{{ $creator->uname ?? 'Unknown' }}</div>
@if($creator->username ?? null)
<div class="truncate text-xs text-white/40">{{ '@' . $creator->username }}</div>
@endif
</div>
<div class="shrink-0 text-right">
<div class="text-lg font-bold text-sky-400">{{ number_format($creator->total ?? 0) }}</div>
</div>
</div>
</a>
@endforeach
</div>
@endif
<div class="rounded-xl border border-white/[0.06] overflow-hidden">
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-3 bg-white/[0.03] border-b border-white/[0.06]">
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-center">#</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30">Creator</span>
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Recent Views</span>
</div>
<div class="divide-y divide-white/[0.04]">
@foreach ($tableCreators as $i => $creator)
@php
$rank = $offset + $rankBase + $i + 1;
$profileUrl = ($creator->username ?? null)
? '/@' . $creator->username
: '/profile/' . (int) $creator->user_id;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) $creator->user_id, $creator->avatar_hash ?? null, 64);
@endphp
<div class="grid grid-cols-[3rem_1fr_auto] items-center gap-4 px-5 py-4
{{ $rank <= 3 ? 'bg-white/[0.015]' : '' }} hover:bg-white/[0.03] transition-colors">
<div class="text-center">
@if ($rank === 1)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-amber-400/15 text-amber-300 text-xs font-bold ring-1 ring-amber-400/30">1</span>
@elseif ($rank === 2)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-slate-400/15 text-slate-300 text-xs font-bold ring-1 ring-slate-400/30">2</span>
@elseif ($rank === 3)
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-orange-700/20 text-orange-400 text-xs font-bold ring-1 ring-orange-600/30">3</span>
@else
<span class="text-sm text-white/30 font-medium">{{ $rank }}</span>
@endif
</div>
<a href="{{ $profileUrl }}" class="flex items-center gap-3 min-w-0 hover:opacity-90 transition-opacity">
<img src="{{ $avatarUrl }}" alt="{{ $creator->uname }}"
class="w-9 h-9 rounded-full object-cover ring-1 ring-white/10 shrink-0"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'" />
<div class="min-w-0">
<p class="text-sm font-medium text-white truncate">{{ $creator->uname }}</p>
@if($creator->username ?? null)
<p class="text-xs text-white/40 truncate">{{ '@' . $creator->username }}</p>
@endif
</div>
</a>
<div class="text-right">
<span class="text-sm font-semibold text-white/80">{{ number_format($creator->total) }}</span>
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-10 flex justify-center">
{{ $creators->withQueryString()->links() }}
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No rising creators found yet. Check back soon!</p>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,129 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Daily Uploads';
$page_meta_description = 'Browse public artworks grouped by upload date across the last two weeks on Skinbase.';
$page_canonical = route('uploads.daily', request()->query());
$useUnifiedSeo = true;
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $headerBreadcrumbs;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Uploads"
:title="$page_title ?? 'Daily Uploads'"
icon="fa-calendar-day"
:breadcrumbs="$headerBreadcrumbs"
description="Browse all artworks uploaded on a specific date."
headerClass="pb-6"
>
<x-slot name="actions">
<a
href="{{ route('uploads.latest') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Latest Uploads
</a>
</x-slot>
</x-nova-page-header>
{{-- ── Date strip ── --}}
<div class="px-6 pt-8 md:px-10 pb-5">
<div class="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none" id="dateStrip">
@foreach($dates as $i => $d)
<button type="button"
data-iso="{{ $d['iso'] }}"
id="tab-{{ $i+1 }}"
class="flex-shrink-0 rounded-lg px-3.5 py-1.5 text-xs font-medium border transition-colors
{{ $i === 0
? 'bg-sky-500/15 text-sky-300 border-sky-500/30 active-date-tab'
: 'border-white/[0.08] bg-white/[0.03] text-white/50 hover:text-white hover:bg-white/[0.07]' }}">
@if ($i === 0)
Today
@elseif ($i === 1)
Yesterday
@else
{{ \Carbon\Carbon::parse($d['iso'])->format('M j') }}
@endif
</button>
@endforeach
</div>
</div>
{{-- ── Active date label ── --}}
<div class="px-6 md:px-10 mb-4">
<p id="activeDateLabel" class="text-sm text-white/40">
Showing uploads from <strong class="text-white/70">{{ $dates[0]['label'] ?? 'today' }}</strong>
</p>
</div>
{{-- ── Grid container ── --}}
<div id="myContent" class="px-6 pb-16 md:px-10 min-h-48">
@include('web.partials.daily-uploads-grid', ['arts' => $recent])
</div>
{{-- ── Loading overlay (hidden) ── --}}
<template id="loadingTpl">
<div class="flex items-center justify-center py-20 text-white/30 text-sm gap-2">
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
Loading artworks…
</div>
</template>
@push('scripts')
<script>
(function () {
var endpoint = '/uploads/daily';
var strip = document.getElementById('dateStrip');
var content = document.getElementById('myContent');
var dateLabel = document.getElementById('activeDateLabel');
var loadingTpl = document.getElementById('loadingTpl');
function setActive(btn) {
strip.querySelectorAll('button').forEach(function (b) {
b.classList.remove('bg-sky-500/15', 'text-sky-300', 'border-sky-500/30', 'active-date-tab');
b.classList.add('border-white/[0.08]', 'bg-white/[0.03]', 'text-white/50');
});
btn.classList.add('bg-sky-500/15', 'text-sky-300', 'border-sky-500/30', 'active-date-tab');
btn.classList.remove('border-white/[0.08]', 'bg-white/[0.03]', 'text-white/50');
}
function loadDate(iso, label) {
content.innerHTML = loadingTpl.innerHTML;
dateLabel.innerHTML = 'Showing uploads from <strong class="text-white/70">' + label + '</strong>';
fetch(endpoint + '?ajax=1&datum=' + encodeURIComponent(iso), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.text(); })
.then(function (html) { content.innerHTML = html; })
.catch(function () {
content.innerHTML = '<p class="text-center text-white/30 py-16 text-sm">Failed to load artworks.</p>';
});
}
strip.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-iso]');
if (!btn || btn.classList.contains('active-date-tab')) return;
setActive(btn);
var label = btn.textContent.trim();
loadDate(btn.getAttribute('data-iso'), label);
});
})();
</script>
@endpush
@endsection

View File

@@ -0,0 +1,38 @@
{{--
Discover section-switcher pills.
Expected variable: $section (string) the active section slug, e.g. 'trending', 'for-you'
Expected variable: $isAuthenticated (bool, optional) whether the user is logged in
--}}
@php
$active = $section ?? '';
$isAuth = $isAuthenticated ?? auth()->check();
$sections = collect([
'for-you' => ['label' => 'For You', 'icon' => 'fa-wand-magic-sparkles', 'auth' => true, 'activeClass' => 'bg-yellow-500/20 text-yellow-300 border border-yellow-400/20'],
'following' => ['label' => 'Following', 'icon' => 'fa-user-group', 'auth' => true, 'activeClass' => 'bg-sky-600 text-white'],
'trending' => ['label' => 'Trending', 'icon' => 'fa-fire', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
'rising' => ['label' => 'Rising', 'icon' => 'fa-rocket', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
'fresh' => ['label' => 'Fresh', 'icon' => 'fa-bolt', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
'top-rated' => ['label' => 'Top Rated', 'icon' => 'fa-medal', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
'most-downloaded' => ['label' => 'Most Downloaded', 'icon' => 'fa-download', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
'on-this-day' => ['label' => 'On This Day', 'icon' => 'fa-calendar-day', 'auth' => false, 'activeClass' => 'bg-sky-600 text-white'],
]);
@endphp
<div class="flex flex-wrap items-center gap-2 text-sm">
@foreach($sections as $slug => $meta)
@if($meta['auth'] && !$isAuth)
@continue
@endif
<a href="{{ route('discover.' . $slug) }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
{{ $active === $slug
? $meta['activeClass']
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white' }}">
<i class="fa-solid {{ $meta['icon'] }} text-xs {{ $active === $slug && $slug === 'for-you' ? '' : '' }}"></i>
{{ $meta['label'] }}
</a>
@endforeach
</div>

View File

@@ -0,0 +1,156 @@
@extends('layouts.nova')
@php
$discoverBreadcrumbs = collect([
(object) ['name' => 'Discover', 'url' => '/discover/trending'],
(object) ['name' => 'For You', 'url' => '/discover/for-you'],
]);
@endphp
@section('content')
<x-nova-page-header
section="Discover"
title="For You"
icon="fa-wand-magic-sparkles"
:breadcrumbs="$discoverBreadcrumbs"
description="Artworks picked for you based on your taste."
headerClass="pb-6"
actionsClass="lg:pt-8"
iconClass="text-yellow-400"
>
<x-slot name="actions">
@include('web.discover._nav', ['section' => 'for-you'])
</x-slot>
</x-nova-page-header>
@php
$cacheTone = match ($feed_meta['cache_status'] ?? null) {
'hit' => 'text-emerald-200 ring-emerald-400/30 bg-emerald-500/12',
'stale' => 'text-amber-200 ring-amber-400/30 bg-amber-500/12',
default => 'text-sky-100 ring-sky-300/30 bg-sky-500/12',
};
$generatedAt = !empty($feed_meta['generated_at']) ? \Illuminate\Support\Carbon::parse($feed_meta['generated_at'])->diffForHumans() : null;
@endphp
<section class="px-6 md:px-10">
<div class="grid gap-3 rounded-[1.6rem] border border-white/8 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_42%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-5 shadow-[0_18px_60px_rgba(2,6,23,0.38)] md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
<div class="space-y-2">
<p class="text-[0.7rem] font-semibold uppercase tracking-[0.28em] text-sky-200/70">Personalized discovery</p>
<p class="max-w-3xl text-sm leading-6 text-slate-300">
This feed now runs on the same recommendation engine as the API, so your views and clicks on this page can refine what shows up next.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-slate-200/85">
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Model</span>
<span>{{ $feed_meta['algo_version'] ?? 'n/a' }}</span>
</span>
<span class="inline-flex items-center gap-2 rounded-full ring-1 px-3 py-1.5 {{ $cacheTone }}">
<span class="text-slate-300/70">Cache</span>
<span>{{ str_replace(['-', '_'], ' ', $feed_meta['cache_status'] ?? 'unknown') }}</span>
</span>
@if (!empty($feed_meta['total_candidates']))
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Candidates</span>
<span>{{ number_format((int) $feed_meta['total_candidates']) }}</span>
</span>
@endif
@if ($generatedAt)
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Refreshed</span>
<span>{{ $generatedAt }}</span>
</span>
@endif
</div>
</div>
</section>
{{-- ── Artwork grid (React MasonryGallery) ── --}}
@php
$galleryArtworks = $artworks->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'thumb' => $art->thumb_url ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'profile_url' => $art->profile_url ?? null,
'published_as_type' => $art->published_as_type ?? null,
'publisher' => $art->publisher ?? null,
'published_at' => $art->published_at ?? null,
'content_type_name' => $art->content_type_name ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'slug' => $art->slug ?? '',
'url' => $art->url ?? null,
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'recommendation_source' => $art->recommendation_source ?? 'mixed',
'recommendation_reason' => $art->recommendation_reason ?? 'Picked for you',
'recommendation_score' => $art->recommendation_score,
'recommendation_algo_version' => $art->recommendation_algo_version ?? ($feed_meta['algo_version'] ?? null),
])->values();
@endphp
<section class="px-6 pt-8 md:px-10">
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="for-you"
data-cursor-endpoint="{{ route('discover.for-you') }}"
data-discovery-endpoint="{{ route('api.discovery.events.store') }}"
data-algo-version="{{ $feed_meta['algo_version'] ?? '' }}"
@if (!empty($next_cursor)) data-next-cursor="{{ $next_cursor }}" @endif
data-limit="40"
class="min-h-32"
></div>
</section>
@endsection
@push('styles')
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-auto-rows: 8px;
gap: 1rem;
}
@media (min-width: 768px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(7, minmax(0, 1fr)) !important; }
}
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
.nova-skeleton-card {
border-radius: 1rem;
min-height: 180px;
background: linear-gradient(110deg, rgba(255,255,255,.06) 8%, rgba(255,255,255,.12) 18%, rgba(255,255,255,.06) 33%);
background-size: 200% 100%;
animation: novaShimmer 1.2s linear infinite;
}
@keyframes novaShimmer {
to { background-position-x: -200%; }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,259 @@
@extends('layouts.nova')
@php
$discoverBreadcrumbs = collect([
(object) ['name' => 'Discover', 'url' => '/discover/trending'],
(object) ['name' => $page_title ?? 'Discover', 'url' => request()->path()],
]);
@endphp
@php
$followingActivity = collect($following_activity ?? []);
$networkTrending = collect($network_trending ?? []);
$suggestedUsers = collect($suggested_users ?? $fallback_creators ?? []);
$fallbackTrending = collect($fallback_trending ?? []);
@endphp
@section('content')
<x-nova-page-header
section="Discover"
:title="$page_title ?? 'Discover'"
:icon="$icon ?? 'fa-compass'"
:breadcrumbs="$discoverBreadcrumbs"
:description="$description ?? null"
headerClass="pb-6"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
@include('web.discover._nav', ['section' => $section ?? ''])
</x-slot>
</x-nova-page-header>
@if (($section ?? null) === 'following')
<section class="px-6 pt-2 md:px-10">
@if (!empty($empty))
<div class="rounded-[32px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.08),rgba(255,255,255,0.03),rgba(249,115,22,0.06))] p-6 shadow-[0_20px_60px_rgba(2,6,23,0.24)]">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Personalized following feed</p>
<h2 class="mt-2 text-3xl font-semibold tracking-[-0.03em] text-white">Your network starts here</h2>
<p class="mt-3 max-w-2xl text-sm leading-relaxed text-slate-300">Follow a few creators to unlock a feed made of their newest art, social activity, and rising work from around your network.</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/discover/trending" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-medium text-slate-200 transition-colors hover:bg-white/[0.08]">
<i class="fa-solid fa-fire fa-fw"></i>
Explore trending
</a>
<a href="/feed/following" class="inline-flex items-center gap-2 rounded-2xl border border-sky-400/20 bg-sky-500/10 px-4 py-2.5 text-sm font-medium text-sky-200 transition-colors hover:bg-sky-500/15">
<i class="fa-solid fa-newspaper fa-fw"></i>
Open post feed
</a>
</div>
</div>
</div>
@endif
<div class="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Activity from people you follow</p>
<h3 class="mt-1 text-xl font-semibold text-white">Network activity</h3>
</div>
<a href="/community/activity?filter=following" class="text-sm text-sky-300/80 transition-colors hover:text-sky-200">View all</a>
</div>
<div class="space-y-3">
@forelse ($followingActivity as $activity)
<div class="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-4 py-3">
<div class="flex items-start gap-3">
<img src="{{ data_get($activity, 'user.avatar_url') ?: '/images/avatar_default.webp' }}" alt="{{ data_get($activity, 'user.username') }}" class="h-10 w-10 rounded-full object-cover ring-1 ring-white/10" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-white">{{ data_get($activity, 'user.username') ? '@' . data_get($activity, 'user.username') : data_get($activity, 'user.name', 'Creator') }}</p>
<p class="mt-1 text-sm text-slate-300">
@if (data_get($activity, 'type') === 'follow')
started following {{ data_get($activity, 'target_user.username') ? '@' . data_get($activity, 'target_user.username') : 'another creator' }}
@elseif (data_get($activity, 'type') === 'upload')
published <a href="{{ data_get($activity, 'artwork.url') }}" class="text-sky-300 hover:text-sky-200">{{ data_get($activity, 'artwork.title', 'a new artwork') }}</a>
@elseif (in_array(data_get($activity, 'type'), ['comment', 'reply'], true))
{{ data_get($activity, 'type') === 'reply' ? 'replied on' : 'commented on' }} <a href="{{ data_get($activity, 'artwork.url') }}" class="text-sky-300 hover:text-sky-200">{{ data_get($activity, 'artwork.title', 'an artwork') }}</a>
@else
{{ ucfirst(str_replace('_', ' ', (string) data_get($activity, 'type', 'activity'))) }}
@endif
</p>
<p class="mt-1 text-xs text-slate-500">{{ data_get($activity, 'time_ago') ?: data_get($activity, 'created_at') }}</p>
</div>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] px-4 py-8 text-center text-sm text-slate-400">Follow activity will appear here as your network starts moving.</div>
@endforelse
</div>
</div>
<div class="space-y-6">
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Trending in your network</p>
<h3 class="mt-1 text-xl font-semibold text-white">Network highlights</h3>
</div>
<div class="space-y-3">
@foreach (($empty ?? false) ? $fallbackTrending->take(4) : $networkTrending->take(4) as $item)
@php
$itemId = (int) data_get($item, 'id', 0);
$itemSlug = (string) data_get($item, 'slug', '');
$itemUrl = $itemSlug !== '' && $itemId > 0
? route('art.show', ['id' => $itemId, 'slug' => $itemSlug])
: data_get($item, 'url', '#');
$itemThumb = data_get($item, 'thumb_url') ?: data_get($item, 'thumb') ?: data_get($item, 'thumbnail_url') ?: '/images/placeholder.jpg';
$itemTitle = data_get($item, 'title') ?: data_get($item, 'name', 'Artwork');
@endphp
<a href="{{ $itemUrl }}" class="flex items-center gap-3 rounded-2xl border border-white/[0.06] bg-white/[0.02] p-3 transition-colors hover:bg-white/[0.04]">
<img src="{{ $itemThumb }}" alt="{{ $itemTitle }}" class="h-16 w-16 rounded-xl object-cover" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-white">{{ $itemTitle ?: 'Untitled artwork' }}</p>
<p class="truncate text-xs text-slate-400">{{ data_get($item, 'author.username') ? '@' . data_get($item, 'author.username') : data_get($item, 'username', data_get($item, 'uname')) }}</p>
@if (is_array($item) && isset($item['stats']))
<p class="mt-1 text-[11px] text-slate-500">{{ number_format((int) data_get($item, 'stats.favorites', 0)) }} favourites · {{ number_format((int) data_get($item, 'stats.views', 0)) }} views</p>
@endif
</div>
</a>
@endforeach
</div>
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Suggested creators</p>
<h3 class="mt-1 text-xl font-semibold text-white">Who to follow next</h3>
</div>
<div class="space-y-3">
@foreach ($suggestedUsers->take(4) as $userCard)
<a href="{{ $userCard['profile_url'] ?? ('/@' . strtolower((string) ($userCard['username'] ?? ''))) }}" class="flex items-start gap-3 rounded-2xl border border-white/[0.06] bg-white/[0.02] p-3 transition-colors hover:bg-white/[0.04]">
<img src="{{ $userCard['avatar_url'] ?? '/images/avatar_default.webp' }}" alt="{{ $userCard['username'] ?? 'creator' }}" class="h-10 w-10 rounded-full object-cover ring-1 ring-white/10" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-white">{{ $userCard['name'] ?? $userCard['username'] ?? 'Creator' }}</p>
<p class="truncate text-xs text-slate-500">@{{ $userCard['username'] ?? 'creator' }}</p>
<p class="mt-1 text-xs text-slate-400">{{ data_get($userCard, 'context.follower_overlap.label') ?: data_get($userCard, 'context.shared_following.label') ?: ($userCard['reason'] ?? 'Recommended for you') }}</p>
</div>
</a>
@endforeach
</div>
</div>
</div>
</div>
</section>
@endif
{{-- ── Artwork grid (React MasonryGallery) ── --}}
@php
$galleryItems = method_exists($artworks, 'items') ? $artworks->items() : (is_iterable($artworks) ? $artworks : []);
$galleryArtworks = collect($galleryItems)->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'thumb' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'profile_url' => $art->profile_url ?? null,
'published_as_type' => $art->published_as_type ?? null,
'publisher' => $art->publisher ?? null,
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'slug' => $art->slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
])->values();
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
@endphp
<section class="px-6 pt-8 md:px-10">
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="{{ $section ?? 'discover' }}"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="24"
class="min-h-32"
></div>
</section>
@endsection
@push('styles')
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-auto-rows: 8px;
gap: 1rem;
}
@media (min-width: 768px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(7, minmax(0, 1fr)) !important; }
}
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
[data-nova-gallery].is-enhanced [data-gallery-pagination] {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
}
[data-nova-gallery].is-enhanced [data-gallery-pagination] ul {
display: inline-flex;
gap: 0.25rem;
align-items: center;
padding: 0;
margin: 0;
list-style: none;
}
[data-nova-gallery].is-enhanced [data-gallery-pagination] li a,
[data-nova-gallery].is-enhanced [data-gallery-pagination] li span {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
padding: 0 0.5rem;
background: rgba(255,255,255,0.03);
color: #e6eef8;
border: 1px solid rgba(255,255,255,0.04);
text-decoration: none;
font-size: 0.875rem;
}
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
.nova-skeleton-card {
border-radius: 1rem;
min-height: 180px;
background: linear-gradient(110deg, rgba(255,255,255,.06) 8%, rgba(255,255,255,.12) 18%, rgba(255,255,255,.06) 33%);
background-size: 200% 100%;
animation: novaShimmer 1.2s linear infinite;
}
@keyframes novaShimmer {
to { background-position-x: -200%; }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,100 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Most Downloaded Today';
$page_meta_description = 'Browse the artworks downloaded the most today on Skinbase.';
$page_canonical = route('downloads.today', request()->query());
$gallery_type = 'today-downloads';
$useUnifiedSeo = true;
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $headerBreadcrumbs;
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'slug' => $art->slug ?? null,
'url' => isset($art->id) ? '/art/' . $art->id . '/' . ($art->slug ?: \Illuminate\Support\Str::slug($art->name ?? 'artwork')) : '#',
'thumb' => $art->thumb ?? $art->thumb_url ?? null,
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'metric_badge' => ((int) ($art->num_downloads ?? 0)) > 0 ? [
'label' => number_format((int) $art->num_downloads),
'iconClass' => 'fa-solid fa-download text-[10px]',
'className' => 'bg-emerald-500/14 text-emerald-200 ring-emerald-400/30',
] : null,
])->values();
$galleryNextPageUrl = $artworks->nextPageUrl();
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Downloads"
: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>.'"
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
</span>
</x-slot>
</x-nova-page-header>
{{-- ── Artwork grid ── --}}
<div class="px-6 pt-8 pb-16 md:px-10">
@if ($artworks && $artworks->isNotEmpty())
<div
data-react-masonry-gallery
data-artworks='@json($galleryArtworks)'
data-gallery-type="today-downloads"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="{{ $artworks->perPage() }}"
class="min-h-32"
></div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<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>
</div>
@endif
</div>
@endsection
@push('styles')
<style>
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,77 @@
{{--
Explore index uses ExploreLayout.
Displays an artwork grid with hero header, mode tabs, pagination.
--}}
@extends('layouts.nova.explore-layout')
@section('explore-grid')
{{-- ══ EGS §11: FEATURED SPOTLIGHT ROW ══ --}}
@if(!empty($spotlight) && $spotlight->isNotEmpty())
<div class="px-6 md:px-10 pt-6 pb-2">
<div class="flex items-center gap-2 mb-4">
<span class="text-xs font-semibold uppercase tracking-widest text-amber-400"> Featured Today</span>
<span class="flex-1 border-t border-white/10"></span>
</div>
<div class="flex gap-4 overflow-x-auto nb-scrollbar-none pb-2">
@foreach($spotlight as $item)
<a href="{{ !empty($item->id) ? route('art.show', ['id' => $item->id, 'slug' => $item->slug ?? null]) : '#' }}"
class="group relative flex-none w-44 md:w-52 rounded-xl overflow-hidden
bg-neutral-800 border border-white/10 hover:border-amber-400/40
hover:shadow-lg hover:shadow-amber-500/10 transition-all duration-200"
title="{{ $item->name ?? '' }}">
{{-- Thumbnail --}}
<div class="aspect-[4/3] overflow-hidden bg-neutral-900">
<img
src="{{ $item->thumb_url ?? '' }}"
@if(!empty($item->thumb_srcset)) srcset="{{ $item->thumb_srcset }}" @endif
sizes="(max-width: 768px) 176px, 208px"
alt="{{ $item->name ?? 'Featured artwork' }}"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
{{-- Label overlay --}}
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent px-3 py-2">
<p class="text-xs font-medium text-white truncate leading-snug">{{ $item->name ?? '' }}</p>
@if(!empty($item->uname))
<p class="text-[10px] text-neutral-400 truncate">@{{ $item->uname }}</p>
@endif
</div>
</a>
@endforeach
</div>
</div>
@endif
@php
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'thumb' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'profile_url' => $art->profile_url ?? null,
'published_as_type' => $art->published_as_type ?? null,
'publisher' => $art->publisher ?? null,
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'slug' => $art->slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
])->values();
@endphp
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="explore"
data-limit="24"
class="min-h-32 px-6 md:px-10 py-6"
></div>
@endsection

View File

@@ -0,0 +1,332 @@
@extends('layouts.nova.content-layout')
@section('page-content')
<div class="max-w-3xl space-y-10">
{{-- Intro --}}
<div>
<p class="text-neutral-400 text-sm mb-2">Last updated: March 1, 2026</p>
<p class="text-neutral-300 leading-relaxed">
Answers to the questions we hear most often. If something isn't covered here, feel free to reach out to
any <a href="/staff" class="text-sky-400 hover:underline">staff member</a> we're happy to help.
</p>
</div>
{{-- Table of Contents --}}
<nav class="rounded-xl border border-white/10 bg-white/[0.03] p-5">
<p class="text-xs font-semibold uppercase tracking-widest text-neutral-400 mb-3">Contents</p>
<ol class="space-y-1.5 text-sm text-sky-400">
<li><a href="#about" class="hover:underline">01 About Skinbase</a></li>
<li><a href="#uploading" class="hover:underline">02 Uploading &amp; Submissions</a></li>
<li><a href="#copyright" class="hover:underline">03 Copyright &amp; Photoskins</a></li>
<li><a href="#skinning" class="hover:underline">04 Skinning Help</a></li>
<li><a href="#account" class="hover:underline">05 Account &amp; Profile</a></li>
<li><a href="#community" class="hover:underline">06 Community &amp; Forums</a></li>
<li><a href="#policies" class="hover:underline">07 Policies &amp; Conduct</a></li>
</ol>
</nav>
{{-- 01 About Skinbase --}}
<section id="about">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">01</span>
About Skinbase
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">What is Skinbase?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Skinbase is a community gallery for desktop customisation skins, themes, wallpapers, icons, and
more. Members upload their own creations, collect favourites, leave feedback, and discuss all things
design in the forums. We've been online since 2001 and still going strong.
</dd>
</div>
<div>
<dt class="font-medium text-white">Is Skinbase free to use?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Yes, completely. Browsing and downloading are free without an account. Registering (also free)
unlocks uploading, commenting, favourites, collections, and messaging.
</dd>
</div>
<div>
<dt class="font-medium text-white">Who runs Skinbase?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Skinbase is maintained by a small volunteer <a href="/staff" class="text-sky-400 hover:underline">staff team</a>.
Staff moderate uploads, help members, and keep the lights on. There is no corporate ownership
this is a community project.
</dd>
</div>
<div>
<dt class="font-medium text-white">How can I support Skinbase?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
The best support is participation upload your work, leave constructive comments, report problems,
and invite other creators. You can also help by flagging rule-breaking content so staff can review it quickly.
</dd>
</div>
</dl>
</section>
{{-- 02 Uploading & Submissions --}}
<section id="uploading">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">02</span>
Uploading &amp; Submissions
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">What file types are accepted?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Skins and resources are generally uploaded as <strong class="text-white">.zip</strong> archives.
Preview images are accepted as JPG, PNG, or WebP. Wallpapers may be uploaded directly as image files.
Check the upload form for the exact size and type limits per category.
</dd>
</div>
<div>
<dt class="font-medium text-white">Is there a file size limit?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Yes. The current limit is displayed on the upload page. If your file exceeds the limit, try removing
any unnecessary assets from the archive before re-uploading.
</dd>
</div>
<div>
<dt class="font-medium text-white">Why was my upload removed?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Uploads are removed when they break the <a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>
most commonly for containing photographs you don't own (photoskins), missing preview images, or
violating copyright. You will usually receive a message explaining the reason.
If you believe a removal was in error, contact a staff member.
</dd>
</div>
<div>
<dt class="font-medium text-white">Can I upload work-in-progress skins?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
You may share works-in-progress in the forums for feedback. The main gallery is intended for
finished, download-ready submissions only.
</dd>
</div>
</dl>
</section>
{{-- 03 Copyright & Photoskins --}}
<section id="copyright">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">03</span>
Copyright &amp; Photoskins
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">What is a photoskin?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
A photoskin is a skin that uses photographic images typically sourced from the internet as its
primary visual element. Because those photos belong to the photographer (or their publisher), using
them without permission is copyright infringement.
</dd>
</div>
<div>
<dt class="font-medium text-white">Can I upload photoskins?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
<strong class="text-white">No.</strong> Photoskins are not allowed on Skinbase. All artwork in a
skin must be created by you, or you must include written proof of permission from the original
artist inside the zip file. Stock images with a licence that explicitly permits use in
redistributed works are allowed include a copy of that licence in the zip.
</dd>
</div>
<div>
<dt class="font-medium text-white">Can I base my skin on someone else's artwork?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Only with documented permission. Include the permission statement (email, forum post, etc.) inside
your zip file and note it in your upload description. Staff may still remove the work if we cannot
verify the permission.
</dd>
</div>
<div>
<dt class="font-medium text-white">Someone uploaded my artwork without permission. What do I do?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Use the Report button on the artwork's page, or contact a
<a href="/staff" class="text-sky-400 hover:underline">staff member</a> directly.
Provide a link to the infringing upload and evidence that you are the copyright holder.
We take copyright complaints seriously and act promptly.
</dd>
</div>
</dl>
</section>
{{-- 04 Skinning Help --}}
<section id="skinning">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">04</span>
Skinning Help
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">How do I make a skin?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Every application is different, but skins generally consist of a folder of images and small
text/config files. A good starting point is to unpack an existing skin (many Winamp skins are
simply renamed <code class="text-sky-300">.zip</code> files), study the structure, then replace the
images with your own artwork. Check the application's official documentation for its exact format.
</dd>
</div>
<div>
<dt class="font-medium text-white">How do I apply a Windows theme?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
To change the visual style of Windows you typically need a third-party tool such as
<strong class="text-white">WindowBlinds</strong>, <strong class="text-white">SecureUxTheme</strong>,
or a patched <code class="text-sky-300">uxtheme.dll</code>. Install your chosen tool, download a
compatible theme from Skinbase, then follow the tool's instructions to apply it.
</dd>
</div>
<div>
<dt class="font-medium text-white">Where can I get help with a specific application?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
The forums are the best place there are dedicated sections for popular skinnable applications.
You can also check the comments on popular skins for tips from other members.
</dd>
</div>
<div>
<dt class="font-medium text-white">What image editing software do skinners use?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
The community uses a wide range of tools. Popular choices include
<strong class="text-white">Adobe Photoshop</strong>, <strong class="text-white">GIMP</strong> (free),
<strong class="text-white">Affinity Designer</strong>, <strong class="text-white">Figma</strong>,
and <strong class="text-white">Krita</strong> (free). The best tool is the one you're comfortable with.
</dd>
</div>
</dl>
</section>
{{-- 05 Account & Profile --}}
<section id="account">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">05</span>
Account &amp; Profile
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">How do I set a profile picture?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Go to <strong class="text-white">Settings Avatar</strong>, choose an image from your device, and
save. Your avatar appears on your profile page and next to all your comments.
</dd>
</div>
<div>
<dt class="font-medium text-white">Can I change my username?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Username changes are handled by staff. Send a message to a
<a href="/staff" class="text-sky-400 hover:underline">staff member</a> with your requested new name.
We reserve the right to decline requests that are inappropriate or conflict with an existing account.
</dd>
</div>
<div>
<dt class="font-medium text-white">How do I delete my account?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Account deletion requests must be sent to staff. Please be aware that your publicly submitted
artwork may remain in the gallery under your username unless you also request removal of specific
uploads. See our <a href="/privacy-policy" class="text-sky-400 hover:underline">Privacy Policy</a>
for details on data retention.
</dd>
</div>
<div>
<dt class="font-medium text-white">I forgot my password. How do I reset it?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Use the <strong class="text-white">Forgot password?</strong> link on the login page. An email with
a reset link will be sent to the address on your account. If you no longer have access to that
email, contact a staff member for assistance.
</dd>
</div>
</dl>
</section>
{{-- 06 Community & Forums --}}
<section id="community">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">06</span>
Community &amp; Forums
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">Do I need an account to use the forums?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Guests can read most forum threads without an account. Posting, replying, and creating new topics
require a registered account.
</dd>
</div>
<div>
<dt class="font-medium text-white">Can I promote my own work in the forums?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Yes there are dedicated showcase and feedback sections. Limit self-promotion to those areas and
avoid spamming multiple threads with the same content.
</dd>
</div>
<div>
<dt class="font-medium text-white">How do I report a bad comment or forum post?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Every comment and post has a Report link. Use it to flag content that breaks the rules; staff will
review it promptly. For urgent issues, message a
<a href="/staff" class="text-sky-400 hover:underline">staff member</a> directly.
</dd>
</div>
<div>
<dt class="font-medium text-white">What are the messaging rules?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Private messaging is for genuine one-to-one communication. Do not use it to harass, solicit, or
send unsolicited promotional material. Violations can result in messaging being disabled on your account.
</dd>
</div>
</dl>
</section>
{{-- 07 Policies & Conduct --}}
<section id="policies">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">07</span>
Policies &amp; Conduct
</h2>
<dl class="space-y-6">
<div>
<dt class="font-medium text-white">Are there many rules to follow?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
We keep the rules straightforward: respect everyone, only upload work you own or have permission to
share, and keep it drama-free. The full list is in our
<a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>.
</dd>
</div>
<div>
<dt class="font-medium text-white">What happens if I break the rules?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Depending on severity, staff may issue a warning, remove the offending content, temporarily
restrict your account, or permanently ban you. Serious offences (harassment, illegal content)
result in an immediate permanent ban with no prior warning.
</dd>
</div>
<div>
<dt class="font-medium text-white">How do I appeal a ban or removed upload?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Contact a senior staff member and explain the situation calmly. Provide any supporting evidence.
Staff decisions can be reversed when new information comes to light, but appeals submitted
aggressively or repeatedly will not be reconsidered.
</dd>
</div>
<div>
<dt class="font-medium text-white">I still can't find what I need. What now?</dt>
<dd class="mt-1.5 text-sm text-neutral-400 leading-relaxed">
Send a private message to any <a href="/staff" class="text-sky-400 hover:underline">staff member</a>
or post in the Help section of the forums. Someone from the community will usually respond
quickly.
</dd>
</div>
</dl>
</section>
{{-- Footer note --}}
<div class="rounded-xl border border-white/10 bg-white/[0.03] p-5 text-sm text-neutral-400 leading-relaxed">
This FAQ is reviewed periodically. For legal matters such as copyright, data, or account deletion,
please refer to our <a href="/privacy-policy" class="text-sky-400 hover:underline">Privacy Policy</a>
and <a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>,
or contact the <a href="/staff" class="text-sky-400 hover:underline">staff team</a> directly.
</div>
</div>
@endsection

View File

@@ -0,0 +1,97 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Featured Artworks';
$page_meta_description = 'Browse staff-picked and community-highlighted artwork in the shared gallery feed.';
$page_canonical = request()->fullUrl();
$gallery_type = 'featured';
$useUnifiedSeo = true;
$featuredBreadcrumbs = collect([
(object) ['name' => 'Featured', 'url' => request()->path()],
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $featuredBreadcrumbs;
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'slug' => $art->slug ?? null,
'url' => $art->url ?? null,
'thumb' => $art->thumb_url ?? null,
'thumb_url' => $art->thumb_url ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'profile_url' => $art->profile_url ?? null,
'published_as_type' => $art->published_as_type ?? null,
'publisher' => $art->publisher ?? null,
'maturity' => $art->maturity ?? null,
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
])->values();
$galleryNextPageUrl = $artworks->nextPageUrl();
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Featured"
:title="$page_title ?? 'Featured Artworks'"
icon="fa-star"
:breadcrumbs="$featuredBreadcrumbs"
description="Browse staff-picked and community-highlighted artwork in the shared gallery feed."
headerClass="pb-6"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-2 text-sm">
@foreach($artworkTypes as $k => $label)
<a
href="{{ url()->current() }}?type={{ (int) $k }}"
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {{ (int) $k === (int) $type ? 'bg-amber-500/20 text-amber-200 ring-1 ring-amber-400/30' : 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white' }}"
>
{{ $label }}
</a>
@endforeach
</div>
</x-slot>
</x-nova-page-header>
<section class="px-6 pt-8 md:px-10">
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="featured"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="39"
class="min-h-32"
></div>
</section>
@endsection
@push('styles')
<style>
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,46 @@
@extends('layouts.nova')
@php($useUnifiedSeo = true)
@push('head')
{{-- Preload hero image for faster LCP --}}
@if(!empty($props['hero']['thumb']) || !empty($props['hero']['thumb_lg']))
<link
rel="preload"
as="image"
href="{{ $props['hero']['thumb_lg'] ?? $props['hero']['thumb'] }}"
@if(!empty($props['hero']['thumb_srcset'])) imagesrcset="{{ $props['hero']['thumb_srcset'] }}" imagesizes="100vw" @endif
fetchpriority="high"
>
@elseif(!empty($props['hero']['thumb']))
<link rel="preload" as="image" href="{{ $props['hero']['thumb'] }}" fetchpriority="high">
@endif
@endpush
@section('main-class', '')
@section('content')
@include('web.home.hero', ['artwork' => $props['hero'] ?? null])
{{-- Inline props for the React component (avoids data-attribute length limits) --}}
<script id="homepage-props" type="application/json">
{!! json_encode($props, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
</script>
<div id="homepage-root">
@if(!empty($props['is_logged_in']))
@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'],
])
@endif
</div>
@vite(['resources/js/Pages/Home/HomePage.jsx'])
@endsection

View File

@@ -0,0 +1,46 @@
{{-- Featured row use Nova cards for consistent layout with browse/gallery --}}
<section class="px-6 pt-8 pb-6 md:px-10">
<div class="flex items-center gap-2 mb-6">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-amber-500/15 text-amber-400">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/></svg>
</span>
<h2 class="text-base font-semibold text-white/90 tracking-wide uppercase">Featured</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
@if(!empty($featured))
<div>
@include('web.partials._artwork_card', ['art' => $featured])
</div>
@else
<div class="rounded-2xl ring-1 ring-white/5 bg-white/[0.03] p-4">
<p class="text-sm text-neutral-400">No featured artwork set.</p>
</div>
@endif
@if(!empty($memberFeatured))
<div>
@include('web.partials._artwork_card', ['art' => $memberFeatured])
</div>
@else
<div class="rounded-2xl ring-1 ring-white/5 bg-white/[0.03] p-4">
<p class="text-sm text-neutral-400">No member featured artwork.</p>
</div>
@endif
<div>
<div class="group relative flex flex-col overflow-hidden rounded-2xl ring-1 ring-white/5 bg-white/[0.03] shadow-lg h-full">
<a href="{{ route('register') }}" title="Join Skinbase" class="block shrink-0">
<img src="/gfx/sb_join.jpg" alt="Join SkinBase Community" class="w-full h-48 object-cover">
</a>
<div class="flex flex-col flex-1 p-5 text-center">
<div class="text-lg font-semibold text-white/90">Join Skinbase World</div>
<p class="mt-2 text-sm text-neutral-400 flex-1">Join our community upload, share and explore curated photography and skins.</p>
<a href="{{ route('register') }}" class="mt-4 inline-block px-4 py-2 rounded-lg bg-sky-500 hover:bg-sky-400 transition-colors text-white text-sm font-medium">Create an account</a>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,69 @@
@php
$heroArtwork = $artwork ?? null;
$fallbackImage = 'https://files.skinbase.org/default/missing_lg.webp';
$heroImage = $heroArtwork['thumb_lg'] ?? $heroArtwork['thumb'] ?? $fallbackImage;
$heroImageSrcset = $heroArtwork['thumb_srcset'] ?? null;
$heroImageSizes = '100vw';
@endphp
@if (!$heroArtwork)
<section class="relative flex min-h-[62vh] max-h-[420px] w-full items-end overflow-hidden bg-nova-900 md:min-h-[38vh] md:max-h-[460px]">
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-nova-900 via-nova-900/60 to-transparent"></div>
<div class="relative z-10 w-full px-6 pb-7 sm:px-10 lg:px-16">
<h1 class="text-2xl font-bold tracking-tight text-white sm:text-4xl">
Skinbase Nova
</h1>
<p class="mt-2 max-w-xl text-sm text-soft">
Discover. Create. Inspire.
</p>
<div class="mt-4 flex flex-wrap gap-3">
<a href="/discover/trending" class="btn-accent-solid rounded-xl px-5 py-2 text-sm font-semibold">Explore Trending</a>
</div>
</div>
</section>
@else
<section class="group relative flex min-h-[62vh] max-h-[420px] w-full items-end overflow-hidden bg-nova-900 md:min-h-[38vh] md:max-h-[460px]">
<picture>
<img
src="{{ $heroImage }}"
@if($heroImageSrcset) srcset="{{ $heroImageSrcset }}" sizes="{{ $heroImageSizes }}" @endif
alt="{{ $heroArtwork['title'] ?? 'Featured artwork' }}"
class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
fetchpriority="high"
loading="eager"
decoding="sync"
/>
</picture>
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-nova-900 via-nova-900/55 to-transparent"></div>
<div class="relative z-10 w-full px-6 pb-7 sm:px-10 lg:px-16">
<p class="mb-1.5 text-xs font-semibold uppercase tracking-widest text-accent">
Featured Artwork
</p>
<h1 class="text-2xl font-bold tracking-tight text-white drop-shadow sm:text-4xl lg:text-5xl">
{{ $heroArtwork['title'] ?? 'Untitled' }}
</h1>
<p class="mt-1.5 text-sm text-soft">
by
<a href="{{ $heroArtwork['url'] ?? '#' }}" class="text-nova-200 transition hover:text-white">
{{ $heroArtwork['author'] ?? 'Artist' }}
</a>
</p>
<div class="mt-4 flex flex-wrap gap-3">
<a
href="/discover/trending"
class="btn-accent-solid rounded-xl px-5 py-2 text-sm font-semibold"
>
Explore Trending
</a>
<a
href="{{ $heroArtwork['url'] ?? '#' }}"
class="rounded-xl border border-nova-600 px-5 py-2 text-sm font-semibold text-nova-200 shadow transition hover:border-nova-400 hover:text-white"
>
View Artwork
</a>
</div>
</div>
</section>
@endif

View File

@@ -0,0 +1,142 @@
{{-- News and forum columns --}}
@php
use Carbon\Carbon;
use Illuminate\Support\Str;
@endphp
<section class="px-6 pb-14 pt-2 md:px-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
{{-- ── LEFT: Forum News ── --}}
<div class="space-y-1">
<div class="flex items-center gap-2 mb-5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-sky-500/15 text-sky-400">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h10M7 12h6m-6 4h4M5 20H4a2 2 0 01-2-2V6a2 2 0 012-2h16a2 2 0 012 2v12a2 2 0 01-2 2h-1l-4 4-4-4z"/></svg>
</span>
<h2 class="text-base font-semibold text-white/90 tracking-wide uppercase">Forum News</h2>
</div>
@forelse ($forumNews as $item)
<article class="group rounded-xl bg-white/[0.03] border border-white/[0.06] hover:border-sky-500/30 hover:bg-white/[0.05] transition-all duration-200 p-4">
<a href="{{ route('forum.thread.show', ['thread' => $item->topic_id, 'slug' => Str::slug($item->topic ?? '')]) }}"
class="block text-sm font-semibold text-white/90 group-hover:text-sky-300 transition-colors leading-snug mb-1 line-clamp-2">
{{ $item->topic }}
</a>
<div class="flex items-center gap-3 text-xs text-white/40 mb-2">
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
{{ $item->uname }}
</span>
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ Carbon::parse($item->post_date)->format('j M Y') }}
</span>
</div>
@if (!empty($item->preview))
<p class="text-xs text-white/50 leading-relaxed line-clamp-3">{{ Str::limit(strip_tags($item->preview), 200) }}</p>
@endif
</article>
@empty
<div class="rounded-xl bg-white/[0.03] border border-white/[0.06] p-6 text-sm text-white/40 text-center">
No forum news available.
</div>
@endforelse
</div>
{{-- ── RIGHT: Site News + Info + Forum Activity ── --}}
<div class="space-y-8">
{{-- Site News --}}
<div>
<div class="flex items-center gap-2 mb-5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-violet-500/15 text-violet-400">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z"/><path stroke-linecap="round" stroke-linejoin="round" d="M13 4v6h6"/></svg>
</span>
<h2 class="text-base font-semibold text-white/90 tracking-wide uppercase">Site News</h2>
</div>
@forelse ($ourNews as $news)
@php $nid = floor($news->news_id / 100); @endphp
<article class="group rounded-xl bg-white/[0.03] border border-white/[0.06] hover:border-violet-500/30 hover:bg-white/[0.05] transition-all duration-200 p-4 mb-3 last:mb-0">
<a href="/news/{{ $news->news_id }}/{{ Str::slug($news->headline ?? '') }}"
class="block text-sm font-semibold text-white/90 group-hover:text-violet-300 transition-colors leading-snug mb-1 line-clamp-2">
{{ $news->headline }}
</a>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-white/40 mb-3">
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
{{ $news->uname }}
</span>
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ Carbon::parse($news->create_date)->format('j M Y') }}
</span>
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
{{ number_format($news->views) }}
</span>
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
{{ $news->num_comments }}
</span>
</div>
@if (!empty($news->picture))
<div class="flex gap-3">
<img src="/archive/news/{{ $nid }}/{{ $news->picture }}"
class="w-20 h-14 object-cover rounded-lg flex-shrink-0 ring-1 ring-white/10"
alt="{{ e($news->headline) }}" loading="lazy">
<p class="text-xs text-white/50 leading-relaxed line-clamp-3">{!! Str::limit(strip_tags($news->preview ?? ''), 180) !!}</p>
</div>
@else
<p class="text-xs text-white/50 leading-relaxed line-clamp-3">{!! Str::limit(strip_tags($news->preview ?? ''), 240) !!}</p>
@endif
</article>
@empty
<div class="rounded-xl bg-white/[0.03] border border-white/[0.06] p-6 text-sm text-white/40 text-center">
No news available.
</div>
@endforelse
</div>
{{-- About Skinbase --}}
<div class="rounded-xl bg-gradient-to-br from-sky-500/10 to-violet-500/10 border border-white/[0.07] p-5">
<div class="flex items-center gap-2 mb-3">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-md bg-sky-500/20 text-sky-400">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</span>
<h3 class="text-sm font-semibold text-white/80">About Skinbase</h3>
</div>
<p class="text-xs text-white/55 leading-relaxed">
Skinbase is dedicated to <span class="text-white/80 font-medium">Photography</span>, <span class="text-white/80 font-medium">Wallpapers</span> and <span class="text-white/80 font-medium">Skins</span> for popular applications on Windows, macOS, Linux, iOS and Android.
Browse categories, discover curated artwork, and download everything for free.
</p>
</div>
{{-- Latest Forum Activity --}}
<div>
<div class="flex items-center gap-2 mb-4">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-emerald-500/15 text-emerald-400">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/></svg>
</span>
<h2 class="text-base font-semibold text-white/90 tracking-wide uppercase">Forum Activity</h2>
</div>
<div class="rounded-xl border border-white/[0.06] overflow-hidden divide-y divide-white/[0.05]">
@forelse ($latestForumActivity as $topic)
<a href="{{ route('forum.thread.show', ['thread' => $topic->topic_id, 'slug' => Str::slug($topic->topic ?? '')]) }}"
class="flex items-center justify-between gap-3 px-4 py-3 text-sm text-white/70 hover:bg-white/[0.04] hover:text-white transition-colors group">
<span class="truncate group-hover:text-emerald-300 transition-colors">{{ $topic->topic }}</span>
<span class="flex-shrink-0 inline-flex items-center gap-1 text-xs text-white/35 group-hover:text-white/60 transition-colors">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
{{ $topic->numPosts }}
</span>
</a>
@empty
<div class="px-4 py-5 text-sm text-white/40 text-center">No recent forum activity.</div>
@endforelse
</div>
</div>
</div>{{-- end right column --}}
</div>
</section>

View File

@@ -0,0 +1,67 @@
@if(!empty($showWelcomeSpacer))
<div class="mt-10 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div class="h-20 animate-pulse rounded-[28px] border border-white/10 bg-nova-800/70"></div>
</div>
@endif
@foreach(($variants ?? []) as $variant)
@if($variant === 'tags')
<section class="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div class="mb-5 h-8 w-48 animate-pulse rounded-xl bg-nova-800/70"></div>
<div class="flex flex-wrap gap-2">
<div class="h-9 w-24 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-32 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-28 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-36 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-24 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-32 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-28 animate-pulse rounded-full bg-nova-800/70"></div>
<div class="h-9 w-36 animate-pulse rounded-full bg-nova-800/70"></div>
</div>
</section>
@elseif($variant === 'cta')
<section class="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div class="h-40 animate-pulse rounded-[28px] border border-white/10 bg-nova-800/70"></div>
</section>
@else
@php
$showSubtitle = in_array($variant, ['collections', 'groups', 'news'], true);
$gridClass = match ($variant) {
'creators' => 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
'news' => 'grid-cols-1',
'categories' => 'grid-cols-2 lg:grid-cols-4',
'collections' => 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3',
'groups' => 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4',
default => 'grid-cols-2 xl:grid-cols-4',
};
$cardClass = match ($variant) {
'categories' => 'h-28 rounded-2xl',
'news' => 'h-24 rounded-2xl',
'creators' => 'h-64 rounded-2xl',
'collections', 'groups' => 'h-80 rounded-[28px]',
default => 'aspect-[4/3] rounded-2xl',
};
$cardCount = match ($variant) {
'creators' => 6,
'news' => 4,
default => 4,
};
@endphp
<section class="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div class="mb-5 flex items-center justify-between gap-4">
<div>
<div class="h-8 w-48 animate-pulse rounded-xl bg-nova-800/70"></div>
@if($showSubtitle)
<div class="mt-3 h-4 w-80 max-w-full animate-pulse rounded bg-nova-800/60"></div>
@endif
</div>
<div class="hidden h-5 w-24 animate-pulse rounded bg-nova-800/60 sm:block"></div>
</div>
<div class="grid gap-4 {{ $gridClass }}">
@for($i = 0; $i < $cardCount; $i++)
<div class="animate-pulse bg-nova-800/70 {{ $cardClass }}"></div>
@endfor
</div>
</section>
@endif
@endforeach

View File

@@ -0,0 +1,84 @@
@php
$gridV2 = request()->query('grid') === 'v2';
$seoPage = (int) request()->query('page', 1);
$seoBase = url()->current();
$seoCanonical = $seoPage > 1 ? $seoBase . '?page=' . $seoPage : $seoBase;
$seoPrev = $seoPage > 1
? ($seoPage === 2 ? $seoBase : $seoBase . '?page=' . ($seoPage - 1))
: null;
$seoNext = (isset($latestUploads) && method_exists($latestUploads, 'hasMorePages') && $latestUploads->hasMorePages())
? $seoBase . '?page=' . ($seoPage + 1)
: null;
$homeUploadsItems = collect(method_exists($latestUploads ?? null, 'items') ? $latestUploads->items() : ($latestUploads ?? []));
$homeGalleryArtworks = $homeUploadsItems->map(fn ($upload) => [
'id' => $upload->id ?? null,
'name' => $upload->name ?? $upload->title ?? null,
'slug' => $upload->slug ?? \Illuminate\Support\Str::slug($upload->name ?? $upload->title ?? 'artwork'),
'url' => $upload->url ?? ((isset($upload->id) && $upload->id) ? '/art/' . $upload->id . '/' . ($upload->slug ?? \Illuminate\Support\Str::slug($upload->name ?? $upload->title ?? 'artwork')) : '#'),
'thumb' => $upload->thumb ?? $upload->thumb_url ?? null,
'thumb_url' => $upload->thumb_url ?? $upload->thumb ?? null,
'thumb_srcset' => $upload->thumb_srcset ?? null,
'uname' => $upload->uname ?? $upload->author_name ?? '',
'username' => $upload->username ?? $upload->uname ?? '',
'avatar_url' => $upload->avatar_url ?? null,
'content_type_name' => $upload->content_type_name ?? '',
'content_type_slug' => $upload->content_type_slug ?? '',
'category_name' => $upload->category_name ?? '',
'category_slug' => $upload->category_slug ?? '',
'width' => $upload->width ?? null,
'height' => $upload->height ?? null,
'published_at' => !empty($upload->published_at)
? (method_exists($upload->published_at, 'toIsoString') ? $upload->published_at->toIsoString() : (string) $upload->published_at)
: null,
]);
$homeGalleryNextPageUrl = method_exists($latestUploads ?? null, 'nextPageUrl') ? $latestUploads->nextPageUrl() : null;
@endphp
@push('head')
<link rel="canonical" href="{{ $seoCanonical ?? url()->current() }}">
@if(!empty($seoPrev ?? null))<link rel="prev" href="{{ $seoPrev }}">@endif
@if(!empty($seoNext ?? null))<link rel="next" href="{{ $seoNext }}">@endif
@endpush
{{-- Latest uploads grid use same Nova gallery layout as /browse --}}
<section class="px-6 pb-10 pt-6 md:px-10">
<div
data-react-masonry-gallery
data-artworks='@json($homeGalleryArtworks)'
data-gallery-type="home-uploads"
@if ($homeGalleryNextPageUrl) data-next-page-url="{{ $homeGalleryNextPageUrl }}" @endif
data-limit="{{ method_exists($latestUploads ?? null, 'perPage') ? $latestUploads->perPage() : $homeGalleryArtworks->count() }}"
class="min-h-32"
></div>
</section>
@push('styles')
@if(! ($gridV2 ?? false))
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-auto-rows: 8px;
gap: 1rem;
}
@media (min-width: 768px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
[data-nova-gallery].is-enhanced [data-gallery-pagination] { display: none; }
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
</style>
@endif
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,82 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Member Photos';
$page_meta_description = 'Artwork submitted by the Skinbase community, presented in the shared Nova gallery feed.';
$page_canonical = route('members.photos', request()->query());
$gallery_type = 'member-photos';
$useUnifiedSeo = true;
$memberPhotoBreadcrumbs = collect([
(object) ['name' => 'Members', 'url' => route('creators.top')],
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $memberPhotoBreadcrumbs;
$memberPhotoItems = collect(method_exists($artworks ?? null, 'items') ? $artworks->items() : ($artworks ?? []));
$memberPhotoGallery = $memberPhotoItems->map(fn ($art) => [
'id' => $art->id ?? null,
'name' => $art->name ?? $art->title ?? 'Artwork',
'slug' => $art->slug ?? \Illuminate\Support\Str::slug($art->name ?? $art->title ?? 'artwork'),
'url' => $art->url ?? ((isset($art->id) && $art->id) ? '/art/' . $art->id . '/' . ($art->slug ?? \Illuminate\Support\Str::slug($art->name ?? $art->title ?? 'artwork')) : '#'),
'thumb' => $art->thumb ?? $art->thumb_url ?? null,
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? $art->author ?? '',
'username' => $art->username ?? $art->uname ?? '',
'avatar_url' => $art->avatar_url ?? null,
'content_type_name' => $art->content_type_name ?? '',
'content_type_slug' => $art->content_type_slug ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'published_at' => !empty($art->published_at)
? (method_exists($art->published_at, 'toIsoString') ? $art->published_at->toIsoString() : (string) $art->published_at)
: null,
]);
$memberPhotosNextPageUrl = method_exists($artworks ?? null, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Members"
:title="$page_title ?? 'Member Photos'"
icon="fa-images"
:breadcrumbs="$memberPhotoBreadcrumbs"
description="Artwork submitted by the Skinbase community, presented in the shared Nova gallery feed."
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-sky-500/10 text-sky-200 ring-1 ring-sky-400/25">
<span class="w-1.5 h-1.5 rounded-full bg-sky-300"></span>
Community uploads
</span>
</x-slot>
</x-nova-page-header>
{{-- ── Artwork grid ── --}}
<div class="px-6 pb-16 md:px-10">
@if ($memberPhotoGallery->isNotEmpty())
<div
data-react-masonry-gallery
data-artworks='@json($memberPhotoGallery)'
data-gallery-type="member-photos"
@if ($memberPhotosNextPageUrl) data-next-page-url="{{ $memberPhotosNextPageUrl }}" @endif
data-limit="{{ method_exists($artworks ?? null, 'perPage') ? $artworks->perPage() : $memberPhotoGallery->count() }}"
class="min-h-32"
></div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No artworks found.</p>
</div>
@endif
</div>
@endsection
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,18 @@
{{--
Static page uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = $page->title;
@endphp
@section('page-content')
<article class="max-w-3xl">
<div class="prose prose-invert prose-headings:text-white prose-a:text-sky-400 prose-p:text-white/70 max-w-none">
{!! $page->body !!}
</div>
</article>
@endsection

View File

@@ -0,0 +1 @@
<x-artwork-card :art="$art" />

View File

@@ -0,0 +1,11 @@
@if($arts && count($arts))
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-5">
@foreach($arts as $art)
<x-artwork-card :art="$art" />
@endforeach
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No uploads for this date.</p>
</div>
@endif

View File

@@ -0,0 +1,392 @@
@extends('layouts.nova.content-layout')
@section('page-content')
{{-- Table of contents --}}
<div class="max-w-3xl">
<p class="text-sm text-white/40 mb-1">Last updated: <time datetime="2026-03-01">March 1, 2026</time></p>
<p class="text-white/60 text-sm leading-relaxed mb-8">
This Privacy Policy explains how Skinbase ("we", "us", "our") collects, uses, stores, and protects
information about you when you use our website at <strong class="text-white">skinbase.org</strong>.
By using Skinbase you agree to the practices described in this policy.
</p>
{{-- TOC --}}
<nav class="mb-10 rounded-xl border border-white/[0.08] bg-white/[0.03] px-6 py-5">
<h2 class="text-xs font-semibold uppercase tracking-widest text-white/40 mb-3">Contents</h2>
<ol class="space-y-1.5 text-sm text-sky-400">
<li><a href="#information-we-collect" class="hover:text-sky-300 hover:underline transition-colors">1. Information We Collect</a></li>
<li><a href="#how-we-use-information" class="hover:text-sky-300 hover:underline transition-colors">2. How We Use Your Information</a></li>
<li><a href="#cookies" class="hover:text-sky-300 hover:underline transition-colors">3. Cookies &amp; Tracking</a></li>
<li><a href="#sharing" class="hover:text-sky-300 hover:underline transition-colors">4. Sharing of Information</a></li>
<li><a href="#user-content" class="hover:text-sky-300 hover:underline transition-colors">5. User-Generated Content</a></li>
<li><a href="#data-retention" class="hover:text-sky-300 hover:underline transition-colors">6. Data Retention</a></li>
<li><a href="#security" class="hover:text-sky-300 hover:underline transition-colors">7. Security</a></li>
<li><a href="#your-rights" class="hover:text-sky-300 hover:underline transition-colors">8. Your Rights</a></li>
<li><a href="#advertising" class="hover:text-sky-300 hover:underline transition-colors">9. Advertising</a></li>
<li><a href="#third-party-links" class="hover:text-sky-300 hover:underline transition-colors">10. Third-Party Links</a></li>
<li><a href="#children" class="hover:text-sky-300 hover:underline transition-colors">11. Children's Privacy</a></li>
<li><a href="#changes" class="hover:text-sky-300 hover:underline transition-colors">12. Changes to This Policy</a></li>
<li><a href="#contact" class="hover:text-sky-300 hover:underline transition-colors">13. Contact Us</a></li>
</ol>
</nav>
{{-- Sections --}}
<div class="space-y-10">
{{-- 1 --}}
<section id="information-we-collect">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">01</span>
Information We Collect
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
We collect information in two ways: information you give us directly, and information
collected automatically as you use the site.
</p>
<h3 class="text-base font-semibold text-white mt-5 mb-2">Information you provide</h3>
<ul class="list-disc list-inside space-y-1.5 text-sm text-white/70 pl-2">
<li><strong class="text-white/90">Account registration</strong> username, email address, and password (stored as a secure hash).</li>
<li><strong class="text-white/90">Profile information</strong> display name, avatar, bio, website URL, and location if you choose to provide them.</li>
<li><strong class="text-white/90">Uploaded content</strong> artworks, wallpapers, skins, and photographs, along with their titles, descriptions, and tags.</li>
<li><strong class="text-white/90">Communications</strong> messages sent through features such as private messaging, forum posts, comments, and bug reports.</li>
</ul>
<h3 class="text-base font-semibold text-white mt-5 mb-2">Information collected automatically</h3>
<ul class="list-disc list-inside space-y-1.5 text-sm text-white/70 pl-2">
<li><strong class="text-white/90">Log data</strong> IP address, browser type and version, operating system, referring URL, pages visited, and timestamps.</li>
<li><strong class="text-white/90">Usage data</strong> download counts, favourite actions, search queries, and interaction events used to improve recommendations.</li>
<li><strong class="text-white/90">Cookies &amp; local storage</strong> see Section 3 for full details.</li>
</ul>
</section>
{{-- 2 --}}
<section id="how-we-use-information">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">02</span>
How We Use Your Information
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">We use the information we collect to:</p>
<ul class="list-disc list-inside space-y-1.5 text-sm text-white/70 pl-2">
<li>Provide, operate, and maintain the Skinbase service.</li>
<li>Authenticate your identity and keep your account secure.</li>
<li>Personalise your experience, including content recommendations.</li>
<li>Send transactional emails (password resets, email verification, notifications you subscribe to).</li>
<li>Moderate content and enforce our <a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>.</li>
<li>Analyse usage patterns to improve site performance and features.</li>
<li>Detect, prevent, and investigate fraud, abuse, or security incidents.</li>
<li>Comply with legal obligations.</li>
</ul>
<p class="mt-4 text-sm text-white/50">
We will never sell your personal data or use it for purposes materially different from those
stated above without first obtaining your explicit consent.
</p>
{{-- Lawful basis table (GDPR Art. 13(1)(c)) --}}
<h3 class="text-base font-semibold text-white mt-6 mb-3">Lawful basis for processing (GDPR Art. 6)</h3>
<div class="overflow-hidden rounded-lg border border-white/[0.08]">
<table class="w-full text-sm">
<thead class="bg-white/[0.05]">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">Processing activity</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">Lawful basis</th>
</tr>
</thead>
<tbody class="divide-y divide-white/[0.05]">
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80">Account registration &amp; authentication</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(b) Performance of contract</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80">Delivering and operating the Service</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(b) Performance of contract</td>
</tr>
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80">Transactional emails (password reset, verification)</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(b) Performance of contract</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80">Security, fraud prevention, abuse detection</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(f) Legitimate interests</td>
</tr>
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80">Analytics &amp; site-performance monitoring</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(f) Legitimate interests</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80">Essential cookies (session, CSRF, remember-me)</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(f) Legitimate interests</td>
</tr>
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80">Third-party advertising cookies</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(a) <strong class="text-white/90">Consent</strong> (via cookie banner)</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80">Compliance with legal obligations</td>
<td class="px-4 py-3 text-white/60">Art. 6(1)(c) Legal obligation</td>
</tr>
</tbody>
</table>
</div>
</section>
{{-- 3 --}}
<section id="cookies">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">03</span>
Cookies &amp; Tracking
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-4">
Skinbase uses cookies small text files stored in your browser to deliver a reliable,
personalised experience. No cookies are linked to sensitive personal data.
</p>
<div class="overflow-hidden rounded-lg border border-white/[0.08]">
<table class="w-full text-sm">
<thead class="bg-white/[0.05]">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">Cookie</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">Purpose</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-white/40">Duration</th>
</tr>
</thead>
<tbody class="divide-y divide-white/[0.05]">
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80 font-mono text-xs">skinbase_session</td>
<td class="px-4 py-3 text-white/60">Authentication session identifier</td>
<td class="px-4 py-3 text-white/50">Browser session</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80 font-mono text-xs">XSRF-TOKEN</td>
<td class="px-4 py-3 text-white/60">Cross-site request forgery protection</td>
<td class="px-4 py-3 text-white/50">Browser session</td>
</tr>
<tr class="bg-white/[0.02]">
<td class="px-4 py-3 text-white/80 font-mono text-xs">remember_web_*</td>
<td class="px-4 py-3 text-white/60">"Remember me" persistent login</td>
<td class="px-4 py-3 text-white/50">30 days</td>
</tr>
<tr>
<td class="px-4 py-3 text-white/80 font-mono text-xs">__gads, ar_debug,<br>DSID, IDE, NID</td>
<td class="px-4 py-3 text-white/60">Google AdSense interest-based ad targeting &amp; frequency capping. Only loaded after you accept cookies. (See Section 9)</td>
<td class="px-4 py-3 text-white/50">Up to 13 months</td>
</tr>
</tbody>
</table>
</div>
<p class="mt-3 text-sm text-white/50">
You can disable cookies in your browser settings. Doing so may prevent some features
(such as staying logged in) from working correctly.
</p>
</section>
{{-- 4 --}}
<section id="sharing">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">04</span>
Sharing of Information
</h2>
<p class="text-white/70 text-sm leading-relaxed">
We do not sell or rent your personal data. We may share information only in the following
limited circumstances:
</p>
<ul class="mt-3 list-disc list-inside space-y-1.5 text-sm text-white/70 pl-2">
<li><strong class="text-white/90">Legal requirements</strong> if required by law, court order, or governmental authority.</li>
<li><strong class="text-white/90">Protection of rights</strong> to enforce our policies, prevent fraud, or protect the safety of our users or the public.</li>
<li><strong class="text-white/90">Service providers</strong> trusted third-party vendors (e.g. hosting, email delivery, analytics) who are contractually bound to handle data only as instructed by us.</li>
<li><strong class="text-white/90">Business transfers</strong> in the event of a merger, acquisition, or sale of assets, you will be notified via email and/or a prominent notice on the site.</li>
</ul>
</section>
{{-- 5 --}}
<section id="user-content">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">05</span>
User-Generated Content
</h2>
<p class="text-white/70 text-sm leading-relaxed">
Artworks, comments, forum posts, and other content you upload or publish on Skinbase are
publicly visible. Do not include personal information (phone numbers, home addresses, etc.)
in public content. You retain ownership of your original work; by uploading you grant
Skinbase a non-exclusive licence to display and distribute it as part of the service.
You may delete your own content at any time from your dashboard.
</p>
<p class="mt-3 text-sm text-white/50">
Content found to infringe copyright or violate our rules will be removed.
To report a submission, please <a href="/bug-report" class="text-sky-400 hover:underline">contact a staff member</a>.
</p>
</section>
{{-- 6 --}}
<section id="data-retention">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">06</span>
Data Retention
</h2>
<p class="text-white/70 text-sm leading-relaxed">
We retain your account data for as long as your account is active. If you delete your
account, we will remove or anonymise your personal data within <strong class="text-white/90">30 days</strong>,
except where we are required to retain it for legal or fraud-prevention purposes.
Anonymised aggregate statistics (e.g. download counts) may be retained indefinitely.
Server log files containing IP addresses are rotated and deleted after <strong class="text-white/90">90 days</strong>.
</p>
</section>
{{-- 7 --}}
<section id="security">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">07</span>
Security
</h2>
<p class="text-white/70 text-sm leading-relaxed">
We implement industry-standard measures to protect your information, including:
</p>
<ul class="mt-3 list-disc list-inside space-y-1.5 text-sm text-white/70 pl-2">
<li>HTTPS (TLS) encryption for all data in transit.</li>
<li>Bcrypt hashing for all stored passwords we never store passwords in plain text.</li>
<li>CSRF protection on all state-changing requests.</li>
<li>Rate limiting and account lockouts to resist brute-force attacks.</li>
</ul>
<p class="mt-3 text-sm text-white/50">
No method of transmission over the Internet is 100% secure. If you believe your account
has been compromised, please <a href="/bug-report" class="text-sky-400 hover:underline">contact us immediately</a>.
</p>
</section>
{{-- 8 --}}
<section id="your-rights">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">08</span>
Your Rights
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Depending on where you live, you may have certain rights over your personal data:
</p>
<div class="grid sm:grid-cols-2 gap-3">
@foreach ([
['Access', 'Request a copy of the personal data we hold about you.'],
['Rectification', 'Correct inaccurate or incomplete data via your account settings.'],
['Erasure', 'Request deletion of your account and associated personal data.'],
['Portability', 'Receive your data in a structured, machine-readable format.'],
['Restriction', 'Ask us to limit how we process your data in certain circumstances.'],
['Objection', 'Object to processing based on legitimate interests or for direct marketing.'],
] as [$right, $desc])
<div class="rounded-lg border border-white/[0.07] bg-white/[0.03] px-4 py-3">
<p class="text-sm font-semibold text-white mb-0.5">{{ $right }}</p>
<p class="text-xs text-white/50">{{ $desc }}</p>
</div>
@endforeach
</div>
<p class="mt-4 text-sm text-white/50">
To exercise any of these rights, please <a href="/bug-report" class="text-sky-400 hover:underline">contact us</a>.
We will respond within 30 days. You also have the right to lodge a complaint with your
local data protection authority.
</p>
</section>
{{-- 9 --}}
<section id="advertising">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">09</span>
Advertising
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Skinbase uses <strong class="text-white/90">Google AdSense</strong> (operated by Google LLC,
1600 Amphitheatre Parkway, Mountain View, CA 94043, USA) to display advertisements. Google AdSense
may use cookies and web beacons to collect information about your browsing activity in order to
serve interest-based (personalised) ads.
</p>
<p class="text-white/70 text-sm leading-relaxed mb-3">
<strong class="text-white/90">Consent required.</strong> Google AdSense cookies are only loaded
after you click <em>Accept all</em> in the cookie consent banner. If you choose
<em>Essential only</em>, no advertising cookies will be placed.
You can withdraw consent at any time by clicking <strong class="text-white/90">Cookie Preferences</strong>
in the footer.
</p>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Data collected by Google AdSense (such as browser type, pages visited, and ad interactions) is
processed by Google under
<a href="https://policies.google.com/privacy" class="text-sky-400 hover:underline" target="_blank" rel="noopener noreferrer">Google's Privacy Policy</a>.
Skinbase does not share any personally identifiable information with Google AdSense beyond what is
automatically collected through the ad script.
</p>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Google's use of advertising cookies can be managed at
<a href="https://www.google.com/settings/ads" class="text-sky-400 hover:underline" target="_blank" rel="noopener noreferrer">google.com/settings/ads</a>,
or you may opt out of personalised advertising through the
<a href="https://optout.aboutads.info/" class="text-sky-400 hover:underline" target="_blank" rel="noopener noreferrer">Digital Advertising Alliance opt-out</a>.
</p>
<p class="mt-1 text-sm text-white/50">
Registered members may see reduced advertising frequency depending on their account status.
</p>
</section>
{{-- 10 --}}
<section id="third-party-links">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">10</span>
Third-Party Links
</h2>
<p class="text-white/70 text-sm leading-relaxed">
Skinbase may contain links to external websites. We are not responsible for the privacy
practices or content of those sites and encourage you to review their privacy policies
before disclosing any personal information.
</p>
</section>
{{-- 11 --}}
<section id="children">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">11</span>
Children's Privacy
</h2>
<p class="text-white/70 text-sm leading-relaxed">
Skinbase is a general-audience website. In compliance with the Children's Online Privacy
Protection Act (COPPA) we do not knowingly collect personal information from children
under the age of <strong class="text-white/90">13</strong>. If we become aware that a
child under 13 has registered, we will promptly delete their account and data.
If you believe a child has provided us with personal information, please
<a href="/bug-report" class="text-sky-400 hover:underline">contact us</a>.
</p>
</section>
{{-- 12 --}}
<section id="changes">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">12</span>
Changes to This Policy
</h2>
<p class="text-white/70 text-sm leading-relaxed">
We may update this Privacy Policy from time to time. When we do, we will revise the
"Last updated" date at the top of this page. For material changes we will notify
registered members by email and/or by a prominent notice on the site. We encourage you
to review this policy periodically. Continued use of Skinbase after changes are posted
constitutes your acceptance of the revised policy.
</p>
</section>
{{-- 13 --}}
<section id="contact">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">13</span>
Contact Us
</h2>
<p class="text-white/70 text-sm leading-relaxed">
If you have any questions, concerns, or requests regarding this Privacy Policy or our
data practices, please reach out via our
<a href="/bug-report" class="text-sky-400 hover:underline">contact form</a> or by
sending a private message to any <a href="/staff" class="text-sky-400 hover:underline">staff member</a>.
We aim to respond to all privacy-related enquiries within <strong class="text-white/90">10 business days</strong>.
</p>
<div class="mt-6 rounded-lg border border-sky-500/20 bg-sky-500/5 px-5 py-4 text-sm text-sky-300">
<p class="font-semibold mb-1">Data Controller</p>
<p class="text-sky-300/70">
Skinbase.org &mdash; operated by the Skinbase team.<br>
Contact: <a href="/bug-report" class="underline hover:text-sky-200">via contact form</a>
</p>
</div>
</section>
</div>
</div>
@endsection

View File

@@ -0,0 +1,127 @@
@extends('layouts.nova.content-layout')
@push('head')
{{-- Global RSS alternate links discoverable by feed readers --}}
<link rel="alternate" type="application/rss+xml" title="Skinbase Latest Artworks" href="{{ url('/rss') }}">
<link rel="alternate" type="application/rss+xml" title="Skinbase Trending Artworks" href="{{ url('/rss/discover/trending') }}">
<link rel="alternate" type="application/rss+xml" title="Skinbase Blog" href="{{ url('/rss/blog') }}">
@foreach ($feeds as $key => $feed)
<link rel="alternate" type="application/rss+xml" title="{{ $feed['title'] }} — Skinbase" href="{{ url($feed['url']) }}">
@endforeach
@endpush
@section('page-content')
<div x-data="{ copied: null }" class="max-w-3xl space-y-10">
{{-- Introduction --}}
<p class="text-neutral-400 text-sm leading-relaxed">
Subscribe to Skinbase RSS feeds in your feed reader, Discord bot, or automation tool.
Every feed returns valid RSS&nbsp;2.0 XML with preview images and artwork links.
</p>
{{-- Grouped feed list --}}
@if (!empty($feed_groups))
@foreach ($feed_groups as $groupKey => $group)
<div>
<h2 class="text-base font-semibold text-neutral-300 uppercase tracking-wider mb-3">
{{ $group['label'] }}
</h2>
<ul class="divide-y divide-neutral-800 rounded-lg border border-neutral-800 overflow-hidden">
@foreach ($group['feeds'] as $feed)
<li class="flex items-start gap-4 px-5 py-4 bg-nova-900/50 hover:bg-nova-800/60 transition-colors">
<svg class="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19.01 7.38 20 6.18 20C4.98 20 4 19.01 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white">{{ $feed['title'] }}</p>
@if (!empty($feed['description']))
<p class="text-xs text-neutral-500 mt-0.5">{{ $feed['description'] }}</p>
@endif
<p class="text-xs text-neutral-600 truncate mt-1 font-mono">{{ url($feed['url']) }}</p>
</div>
<div class="flex-shrink-0 flex items-center gap-2">
<button
type="button"
@click="navigator.clipboard.writeText('{{ url($feed['url']) }}').then(() => { copied = '{{ $feed['url'] }}'; setTimeout(() => copied = null, 2000) })"
class="rounded-md border border-neutral-700 px-3 py-1.5 text-xs text-neutral-400 hover:border-neutral-500 hover:text-white transition-colors"
:class="copied === '{{ $feed['url'] }}' ? 'border-green-600 !text-green-400' : ''"
>
<span x-show="copied !== '{{ $feed['url'] }}'">Copy URL</span>
<span x-show="copied === '{{ $feed['url'] }}'" x-cloak> Copied</span>
</button>
<a href="{{ $feed['url'] }}"
target="_blank"
rel="noopener noreferrer"
class="rounded-md border border-neutral-700 px-3 py-1.5 text-xs text-neutral-400 hover:border-orange-500 hover:text-orange-400 transition-colors">
Open
</a>
</div>
</li>
@endforeach
</ul>
</div>
@endforeach
@else
{{-- Fallback: flat feeds list --}}
<ul class="divide-y divide-neutral-800 rounded-lg border border-neutral-800 overflow-hidden">
@foreach ($feeds as $key => $feed)
<li class="flex items-center gap-4 px-5 py-4 bg-nova-900/50 hover:bg-nova-800/60 transition-colors">
<svg class="h-6 w-6 flex-shrink-0 text-orange-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19.01 7.38 20 6.18 20C4.98 20 4 19.01 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white">{{ $feed['title'] }}</p>
<p class="text-xs text-neutral-500 truncate">{{ url($feed['url']) }}</p>
</div>
<a href="{{ $feed['url'] }}"
class="flex-shrink-0 rounded-md border border-neutral-700 px-3 py-1.5 text-xs text-neutral-400 hover:border-orange-500 hover:text-orange-400 transition-colors">
Subscribe
</a>
</li>
@endforeach
</ul>
@endif
{{-- Tag & Creator feed instructions --}}
<div class="rounded-lg border border-neutral-800 bg-nova-900/30 px-5 py-4 space-y-2">
<h2 class="text-sm font-semibold text-white">Tag &amp; Creator Feeds</h2>
<p class="text-xs text-neutral-400 leading-relaxed">
Subscribe to any tag or creator using the dynamic URL patterns below:
</p>
<ul class="space-y-1 text-xs font-mono text-neutral-300">
<li><span class="text-neutral-500 mr-2">Tag:</span>{{ url('/rss/tag/') }}<em class="text-orange-400 not-italic">{tag-slug}</em></li>
<li><span class="text-neutral-500 mr-2">Creator:</span>{{ url('/rss/creator/') }}<em class="text-orange-400 not-italic">{username}</em></li>
</ul>
<p class="text-xs text-neutral-500 mt-2">
Examples:
<a href="/rss/tag/digital-art" class="text-neutral-300 hover:text-orange-400 underline" target="_blank">/rss/tag/digital-art</a>
&bull;
<a href="/rss/creator/gregor" class="text-neutral-300 hover:text-orange-400 underline" target="_blank">/rss/creator/gregor</a>
</p>
</div>
{{-- About RSS --}}
<div class="prose prose-invert prose-sm max-w-none">
<h2>About RSS</h2>
<p>
RSS is a widely supported web feed format. By subscribing to a Skinbase
RSS feed you can follow updates in any feed reader, wire up Discord bots,
or power autoposting workflows without visiting the site.
</p>
<h3>How to subscribe</h3>
<p>
Copy a feed URL above and paste it into your feed reader (e.g. Feedly, Inoreader,
NetNewsWire) or any tool that supports RSS&nbsp;2.0.
</p>
<h3>Feed format</h3>
<p>
All feeds return RSS&nbsp;2.0 XML with <code>application/rss+xml</code> content-type,
UTF-8 encoding, preview thumbnails via <code>&lt;enclosure&gt;</code> and
<code>&lt;media:content&gt;</code>, and a hard limit of 20 items per feed.
</p>
</div>
</div>
@endsection

View File

@@ -0,0 +1,251 @@
@extends('layouts.nova.content-layout')
@section('page-content')
<div class="max-w-3xl">
<p class="text-sm text-white/40 mb-1">Last updated: <time datetime="2026-03-01">March 1, 2026</time></p>
<p class="text-white/60 text-sm leading-relaxed mb-8">
Skinbase is a creative community built on respect and trust. These rules apply to all members
and all content. By registering or uploading you agree to follow them. They are intentionally
kept minimal so that the most important ones are easy to remember <strong class="text-white">be respectful,
upload only what you own, and have fun.</strong>
</p>
{{-- TOC --}}
<nav class="mb-10 rounded-xl border border-white/[0.08] bg-white/[0.03] px-6 py-5">
<h2 class="text-xs font-semibold uppercase tracking-widest text-white/40 mb-3">Contents</h2>
<ol class="space-y-1.5 text-sm text-sky-400">
<li><a href="#community-conduct" class="hover:text-sky-300 hover:underline transition-colors">1. Community Conduct</a></li>
<li><a href="#ownership" class="hover:text-sky-300 hover:underline transition-colors">2. Ownership &amp; Copyright</a></li>
<li><a href="#licence" class="hover:text-sky-300 hover:underline transition-colors">3. Licence to Skinbase</a></li>
<li><a href="#submission-quality" class="hover:text-sky-300 hover:underline transition-colors">4. Submission Quality</a></li>
<li><a href="#prohibited-content" class="hover:text-sky-300 hover:underline transition-colors">5. Prohibited Content</a></li>
<li><a href="#ripping" class="hover:text-sky-300 hover:underline transition-colors">6. Ripping (Copyright Theft)</a></li>
<li><a href="#accounts" class="hover:text-sky-300 hover:underline transition-colors">7. Accounts &amp; Identity</a></li>
<li><a href="#moderation" class="hover:text-sky-300 hover:underline transition-colors">8. Moderation &amp; Enforcement</a></li>
<li><a href="#appeals" class="hover:text-sky-300 hover:underline transition-colors">9. Appeals</a></li>
<li><a href="#liability" class="hover:text-sky-300 hover:underline transition-colors">10. Liability</a></li>
</ol>
</nav>
<div class="space-y-10">
{{-- 1 --}}
<section id="community-conduct">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">01</span>
Community Conduct
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Skinbase is a friendly, general-audience community. Treat every member and guest as you
would wish to be treated yourself.
</p>
<ul class="list-disc list-inside space-y-2 text-sm text-white/70 pl-2">
<li>Be respectful in comments, messages, forum posts, and all other interactions.</li>
<li>Constructive criticism is welcome; personal attacks, harassment, or bullying are not.</li>
<li>No hate speech, discrimination, or content targeting individuals based on race, ethnicity, religion, gender, sexual orientation, disability, or nationality.</li>
<li>No spam this includes repetitive comments, self-promotion outside designated areas, and unsolicited advertising in private messages.</li>
<li>Keep drama off the site. Disputes should be resolved respectfully or escalated to a <a href="/staff" class="text-sky-400 hover:underline">staff member</a>.</li>
</ul>
</section>
{{-- 2 --}}
<section id="ownership">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">02</span>
Ownership &amp; Copyright
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
You retain ownership of everything you create and upload. By submitting, you confirm that:
</p>
<ul class="list-disc list-inside space-y-2 text-sm text-white/70 pl-2">
<li>The work is entirely your own creation, <strong class="text-white/90">or</strong> you have explicit written permission from the original author for any third-party assets used.</li>
<li>If third-party assets are included, proof of permission must be included in the zip file.</li>
<li>The submission does not violate any trademark, copyright, or other intellectual property right.</li>
</ul>
<p class="mt-3 text-sm text-white/50">
Uploads found to infringe copyright will be removed. Repeat infringers will have their
accounts terminated. To report a suspected infringement, use our
<a href="/bug-report" class="text-sky-400 hover:underline">contact form</a>.
</p>
</section>
{{-- 3 --}}
<section id="licence">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">03</span>
Licence to Skinbase
</h2>
<p class="text-white/70 text-sm leading-relaxed">
By uploading your work you grant Skinbase a <strong class="text-white/90">non-exclusive, royalty-free licence</strong>
to display, distribute, and promote your content as part of the service for example,
displaying it in galleries, featuring it on the homepage, or including it in promotional
material for Skinbase. This licence exists only to allow the site to function and does
<strong class="text-white/90">not</strong> transfer ownership.
</p>
<p class="mt-3 text-sm text-white/50">
The site is free you don't pay to store your work, and we don't charge others to
download it. You may delete your uploads at any time from your dashboard.
</p>
</section>
{{-- 4 --}}
<section id="submission-quality">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">04</span>
Submission Quality
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Every submission represents the Skinbase community. Please put care into what you publish:
</p>
<ul class="list-disc list-inside space-y-2 text-sm text-white/70 pl-2">
<li><strong class="text-white/90">Test before uploading.</strong> Incomplete or broken zip files will be removed.</li>
<li><strong class="text-white/90">Full-size screenshots only.</strong> Our server auto-generates thumbnails do not pre-scale your preview image.</li>
<li><strong class="text-white/90">Accurate categorisation.</strong> Choose the correct content type and category to help others find your work.</li>
<li><strong class="text-white/90">Meaningful title &amp; description.</strong> Titles like "skin1" or "untitled" are discouraged; a short description helps your work get discovered.</li>
<li><strong class="text-white/90">Appropriate tags.</strong> Add relevant tags, but do not keyword-stuff.</li>
</ul>
</section>
{{-- 5 --}}
<section id="prohibited-content">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">05</span>
Prohibited Content
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
The following will be removed immediately and may result in account suspension or permanent termination:
</p>
<div class="grid sm:grid-cols-2 gap-3">
@foreach ([
['Pornography &amp; explicit nudity', 'Frontal nudity is not accepted. Exceptional artistic work with incidental nudity may be considered on a case-by-case basis.'],
['Hate &amp; discriminatory content', 'Content that demeans or attacks people based on protected characteristics.'],
['Violence &amp; gore', 'Graphic depictions of real-world violence or gratuitous gore.'],
['Malware &amp; harmful files', 'Any executable or zip that contains malware, spyware, or harmful scripts.'],
['Personal information', 'Posting another person\'s private data (doxxing) without consent.'],
['Illegal content', 'Anything that violates applicable law, including DMCA violations.'],
] as [$title, $desc])
<div class="rounded-lg border border-white/[0.07] bg-white/[0.02] px-4 py-3">
<p class="text-sm font-semibold text-white mb-0.5">{!! $title !!}</p>
<p class="text-xs text-white/50">{{ $desc }}</p>
</div>
@endforeach
</div>
</section>
{{-- 6 --}}
<section id="ripping">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">06</span>
Ripping (Copyright Theft)
</h2>
<p class="text-white/70 text-sm leading-relaxed">
"Ripping" means uploading another artist's work in whole or in part without their
explicit permission. This includes extracting assets from commercial software, games, or
other skins and re-releasing them as your own. Ripped submissions will be removed and the
uploader's account will be reviewed. Repeat offenders will be permanently banned.
</p>
<div class="mt-4 rounded-lg border border-amber-500/20 bg-amber-500/5 px-5 py-4 text-sm text-amber-300">
<p class="font-semibold mb-1">Photo-based skins</p>
<p class="text-amber-300/70">
Using photographs found on the internet without the photographer's consent constitutes
copyright infringement, even if the skin itself is original artwork.
Only use photos you took yourself, or images with a licence that explicitly permits
use in derivative works (e.g. CC0 or a compatible Creative Commons licence).
</p>
</div>
</section>
{{-- 7 --}}
<section id="accounts">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">07</span>
Accounts &amp; Identity
</h2>
<ul class="list-disc list-inside space-y-2 text-sm text-white/70 pl-2">
<li>One account per person. Duplicate accounts created to evade a suspension or ban will be terminated.</li>
<li>Do not impersonate staff, other members, or real individuals.</li>
<li>Keep your contact email address up to date it is used for important account notifications.</li>
<li>You are responsible for all activity that occurs under your account. Keep your password secure.</li>
<li>Accounts that have been inactive for more than 3 years and contain no uploads may be reclaimed for the username pool.</li>
</ul>
</section>
{{-- 8 --}}
<section id="moderation">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">08</span>
Moderation &amp; Enforcement
</h2>
<p class="text-white/70 text-sm leading-relaxed mb-3">
Skinbase <a href="/staff" class="text-sky-400 hover:underline">staff</a> may take any of
the following actions in response to rule violations:
</p>
<div class="grid sm:grid-cols-3 gap-3">
@foreach ([
['Warning', 'A private message from staff explaining the violation.'],
['Content removal', 'Removal of the offending upload, comment, or post.'],
['Temporary suspension', 'Account access restricted for a defined period.'],
['Permanent ban', 'Account terminated for severe or repeated violations.'],
['IP block', 'Used in cases of persistent abuse or ban evasion.'],
['Legal referral', 'For serious illegal activity, authorities may be notified.'],
] as [$action, $desc])
<div class="rounded-lg border border-white/[0.07] bg-white/[0.03] px-4 py-3">
<p class="text-sm font-semibold text-white mb-0.5">{{ $action }}</p>
<p class="text-xs text-white/50">{{ $desc }}</p>
</div>
@endforeach
</div>
<p class="mt-4 text-sm text-white/50">
Skinbase reserves the right to remove any content or terminate any account at any time,
with or without prior notice, at staff discretion.
</p>
</section>
{{-- 9 --}}
<section id="appeals">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">09</span>
Appeals
</h2>
<p class="text-white/70 text-sm leading-relaxed">
If you believe a moderation action was made in error, you may appeal by contacting a
senior staff member via the <a href="/bug-report" class="text-sky-400 hover:underline">contact form</a>
or by sending a private message to an <a href="/staff" class="text-sky-400 hover:underline">admin</a>.
Please include your username, the content or account involved, and a clear explanation
of why you believe the decision was incorrect. We aim to review all appeals within
<strong class="text-white/90">5 business days</strong>.
</p>
</section>
{{-- 10 --}}
<section id="liability">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">
<span class="text-sky-400 font-mono text-base">10</span>
Liability
</h2>
<p class="text-white/70 text-sm leading-relaxed">
Skinbase is provided "as is". We make no warranties regarding uptime, data integrity,
or fitness for a particular purpose. We are not responsible for user-generated content
when you upload something, the legal and moral responsibility lies with you. We will
act promptly on valid takedown requests and reports of illegal content, but we cannot
pre-screen every submission.
</p>
</section>
</div>
{{-- Footer note --}}
<div class="mt-12 rounded-xl border border-white/[0.07] bg-white/[0.02] px-6 py-5 text-sm text-white/50">
<p>
Questions about these rules? Send a message to any
<a href="/staff" class="text-sky-400 hover:underline">staff member</a>
or use our <a href="/bug-report" class="text-sky-400 hover:underline">contact form</a>.
We're here to help not to catch you out.
</p>
</div>
</div>
@endsection

View File

@@ -0,0 +1,174 @@
@extends('layouts.nova')
@php
use Illuminate\Support\Str;
// One accent colour set per content-type (cycles if more than 4)
$accents = [
['ring' => 'ring-sky-500/30', 'bg' => 'bg-sky-500/10', 'text' => 'text-sky-400', 'badge' => 'bg-sky-500/15 text-sky-300', 'pill' => 'hover:bg-sky-500/15 hover:text-sky-300', 'dot' => 'bg-sky-400', 'border' => 'border-sky-500/25'],
['ring' => 'ring-violet-500/30', 'bg' => 'bg-violet-500/10', 'text' => 'text-violet-400', 'badge' => 'bg-violet-500/15 text-violet-300', 'pill' => 'hover:bg-violet-500/15 hover:text-violet-300', 'dot' => 'bg-violet-400', 'border' => 'border-violet-500/25'],
['ring' => 'ring-amber-500/30', 'bg' => 'bg-amber-500/10', 'text' => 'text-amber-400', 'badge' => 'bg-amber-500/15 text-amber-300', 'pill' => 'hover:bg-amber-500/15 hover:text-amber-300', 'dot' => 'bg-amber-400', 'border' => 'border-amber-500/25'],
['ring' => 'ring-emerald-500/30', 'bg' => 'bg-emerald-500/10','text' => 'text-emerald-400','badge' => 'bg-emerald-500/15 text-emerald-300','pill' => 'hover:bg-emerald-500/15 hover:text-emerald-300','dot' => 'bg-emerald-400','border' => 'border-emerald-500/25'],
];
// Map content-type slug → icon SVG paths
$typeIcons = [
'photography' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>',
'wallpapers' => '<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
'skins' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',
'other' => '<path stroke-linecap="round" stroke-linejoin="round" d="M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V7.414A2 2 0 0020.414 6L15 .586A2 2 0 0013.586 0H5z"/>',
];
$defaultIcon = '<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>';
@endphp
@section('content')
{{-- ── Hero header ── --}}
<div class="px-6 pt-10 pb-6 md:px-10">
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-white/30 mb-1">Skinbase</p>
<h1 class="text-3xl font-bold text-white leading-tight">Browse Sections</h1>
<p class="mt-1 text-sm text-white/50">Explore all artwork categories photography, wallpapers, skins and more.</p>
</div>
{{-- Quick-jump anchor links --}}
<nav class="flex flex-wrap gap-2" aria-label="Section jump links">
@foreach ($contentTypes as $i => $ct)
@php $a = $accents[$i % count($accents)]; @endphp
<a href="#section-{{ $ct->slug }}"
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium border border-white/[0.08] bg-white/[0.04] {{ $a['text'] }} hover:{{ $a['bg'] }} transition-colors">
<span class="w-1.5 h-1.5 rounded-full {{ $a['dot'] }}"></span>
{{ $ct->name }}
</a>
@endforeach
</nav>
</div>
</div>
{{-- ── Content type sections ── --}}
<div class="px-6 pb-16 md:px-10 space-y-14">
@forelse ($contentTypes as $i => $ct)
@php
$a = $accents[$i % count($accents)];
$icon = $typeIcons[strtolower($ct->slug)] ?? $defaultIcon;
$totalCount = $artworkCountsByType[$ct->id] ?? 0;
$roots = $ct->rootCategories;
@endphp
<section id="section-{{ $ct->slug }}" class="scroll-mt-20">
{{-- Section heading ── --}}
<div class="flex items-center gap-3 mb-6">
<div class="flex-shrink-0 w-10 h-10 rounded-xl {{ $a['bg'] }} {{ $a['text'] }} flex items-center justify-center ring-1 {{ $a['ring'] }}">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.75">
{!! $icon !!}
</svg>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-3 flex-wrap">
<h2 class="text-xl font-bold text-white">{{ $ct->name }}</h2>
@if ($totalCount > 0)
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $a['badge'] }}">
{{ number_format($totalCount) }} artworks
</span>
@endif
</div>
@if (!empty($ct->description))
<p class="mt-0.5 text-sm text-white/45 leading-snug">{{ $ct->description }}</p>
@endif
</div>
<a href="/{{ strtolower($ct->slug) }}"
class="hidden sm:inline-flex flex-shrink-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium border {{ $a['border'] }} {{ $a['text'] }} {{ $a['bg'] }} hover:brightness-110 transition-all">
Browse all
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
</a>
</div>
{{-- Separator line --}}
<div class="h-px bg-white/[0.06] mb-6"></div>
@if ($roots->isEmpty())
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-6 py-8 text-center text-sm text-white/35">
No categories available yet.
</div>
@else
{{-- Root category cards grid --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
@foreach ($roots as $root)
@php $subCats = $root->children; @endphp
<div class="group flex flex-col rounded-xl border border-white/[0.06] bg-white/[0.03] hover:border-white/[0.12] hover:bg-white/[0.05] transition-all duration-200 overflow-hidden">
{{-- Card header --}}
<div class="px-4 pt-4 pb-3 border-b border-white/[0.05]">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<a href="{{ $root->url }}"
class="block text-sm font-semibold text-white/90 group-hover:{{ $a['text'] }} transition-colors leading-snug truncate">
{{ $root->name }}
</a>
@if (!empty($root->description))
<p class="mt-1 text-xs text-white/40 leading-relaxed line-clamp-2">{{ $root->description }}</p>
@endif
</div>
@if (($root->artwork_count ?? 0) > 0)
<span class="flex-shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium {{ $a['badge'] }} whitespace-nowrap">
{{ number_format($root->artwork_count) }}
</span>
@endif
</div>
</div>
{{-- Sub-category pills --}}
<div class="px-4 py-3 flex-1">
@if ($subCats->isEmpty())
<span class="text-xs text-white/25 italic">No subcategories</span>
@else
<div class="flex flex-wrap gap-1.5">
@foreach ($subCats as $sub)
<a href="{{ $sub->url }}"
title="{{ $sub->name }}{{ ($sub->artwork_count ?? 0) > 0 ? ' · ' . number_format($sub->artwork_count) . ' artworks' : '' }}"
class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-white/55 bg-white/[0.04] border border-white/[0.06] transition-colors {{ $a['pill'] }}">
{{ $sub->name }}
@if (($sub->artwork_count ?? 0) > 0)
<span class="text-white/30 text-[10px]">{{ number_format($sub->artwork_count) }}</span>
@endif
</a>
@endforeach
</div>
@endif
</div>
{{-- Card footer link --}}
<div class="px-4 pb-3 pt-1">
<a href="{{ $root->url }}"
class="text-xs {{ $a['text'] }} opacity-60 hover:opacity-100 transition-opacity inline-flex items-center gap-1">
View all
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
</a>
</div>
</div>
@endforeach
</div>
{{-- Mobile browse-all link --}}
<div class="mt-4 sm:hidden text-center">
<a href="/{{ strtolower($ct->slug) }}"
class="inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium border {{ $a['border'] }} {{ $a['text'] }} {{ $a['bg'] }}">
Browse all {{ $ct->name }}
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
</a>
</div>
@endif
</section>
@empty
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No sections available.</p>
</div>
@endforelse
</div>
@endsection

View File

@@ -0,0 +1,83 @@
@extends('layouts.nova.content-layout')
@section('page-content')
<div class="max-w-3xl">
<p class="text-sm text-white/40 mb-1">Last updated: <time datetime="2026-03-01">March 1, 2026</time></p>
<p class="text-neutral-300 text-sm leading-relaxed mb-6">
Our volunteer staff help keep Skinbase running from moderation and technical maintenance to community support.
If you need assistance, reach out to any team member listed below or use the <a href="/contact" class="text-sky-400 hover:underline">contact form</a>.
</p>
</div>
@if ($staffByRole->isEmpty())
<div class="max-w-md rounded-lg border border-neutral-800 bg-nova-900/50 px-8 py-10 text-center">
<svg class="mx-auto mb-3 h-10 w-10 text-neutral-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0"/>
</svg>
<p class="text-neutral-400 text-sm">We're building our team. Check back soon!</p>
</div>
@else
<div class="space-y-12">
@foreach ($roleLabels as $roleSlug => $roleLabel)
@if ($staffByRole->has($roleSlug))
<section>
<h2 class="text-base font-semibold uppercase tracking-widest text-accent border-b border-neutral-800 pb-2 mb-6">
{{ $roleLabel }}
</h2>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($staffByRole[$roleSlug] as $member)
@php
$avatarUrl = $member->profile?->avatar_url;
$profileUrl = '/@' . $member->username;
@endphp
<div class="flex gap-4 rounded-lg border border-neutral-800 bg-nova-900/50 p-5 hover:border-neutral-700 transition-colors">
{{-- Avatar --}}
<a href="{{ $profileUrl }}" class="flex-shrink-0">
@if ($avatarUrl)
<img src="{{ $avatarUrl }}"
alt="{{ $member->username }}"
class="h-16 w-16 rounded-full object-cover ring-2 ring-neutral-700">
@else
<div class="h-16 w-16 rounded-full bg-neutral-800 flex items-center justify-center ring-2 ring-neutral-700">
<span class="text-xl font-semibold text-neutral-400 uppercase">
{{ substr($member->username, 0, 1) }}
</span>
</div>
@endif
</a>
{{-- Info --}}
<div class="min-w-0 flex-1">
<a href="{{ $profileUrl }}"
class="font-semibold text-white hover:text-accent transition-colors truncate block">
{{ $member->username }}
</a>
@if ($member->name && $member->name !== $member->username)
<p class="text-xs text-neutral-500 mt-0.5 truncate">{{ $member->name }}</p>
@endif
<span class="mt-2 inline-block rounded-full px-2 py-0.5 text-xs font-medium
{{ $roleSlug === 'admin' ? 'bg-accent/10 text-accent' : 'bg-neutral-800 text-neutral-400' }}">
{{ ucfirst($roleSlug) }}
</span>
@if ($member->profile?->bio)
<p class="mt-2 text-xs text-neutral-400 line-clamp-2">{{ $member->profile->bio }}</p>
@endif
</div>
</div>
@endforeach
</div>
</section>
@endif
@endforeach
</div>
@endif
{{-- Footer note: contact staff --}}
<div class="mt-10 rounded-xl border border-white/10 bg-white/[0.03] p-4 text-sm text-neutral-400">
Need help? Start with the <a href="/contact" class="text-sky-400 hover:underline">Contact / Apply</a> form or send a private message to any staff member.
</div>
@endsection

View File

@@ -0,0 +1,54 @@
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Story Analytics';
$hero_description = 'Performance metrics for "' . $story->title . '".';
@endphp
@section('page-content')
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Views</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($metrics['views']) }}</p>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Likes</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($metrics['likes']) }}</p>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Comments</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($metrics['comments']) }}</p>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Read Time</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($metrics['read_time']) }} min</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-3">
<div class="rounded-xl border border-gray-700 bg-gray-900/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Views (7 days)</p>
<p class="mt-2 text-xl font-semibold text-sky-300">{{ number_format($metrics['views_last_7_days']) }}</p>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-900/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Views (30 days)</p>
<p class="mt-2 text-xl font-semibold text-emerald-300">{{ number_format($metrics['views_last_30_days']) }}</p>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-900/70 p-4 shadow-lg">
<p class="text-xs uppercase tracking-wide text-gray-400">Estimated Read Minutes</p>
<p class="mt-2 text-xl font-semibold text-violet-300">{{ number_format($metrics['estimated_total_read_minutes']) }}</p>
</div>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4 shadow-lg">
<div class="flex flex-wrap gap-3 text-sm">
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="rounded-xl 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.preview', ['story' => $story->id]) }}" class="rounded-xl border border-gray-500/40 bg-gray-700/30 px-3 py-2 text-gray-100 transition hover:scale-[1.02]">Preview story</a>
@if($story->status === 'published' || $story->status === 'scheduled')
<a href="{{ route('stories.show', ['slug' => $story->slug]) }}" class="rounded-xl border border-emerald-500/40 bg-emerald-500/10 px-3 py-2 text-emerald-200 transition hover:scale-[1.02]">Open published page</a>
@endif
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,98 @@
{{--
Author stories page /stories/author/{username}
Uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$authorDisplayName = $author->user?->username ?? $author->name;
$hero_title = 'Stories by ' . $authorDisplayName;
$hero_description = 'All stories and interviews by ' . $authorDisplayName . ' on Skinbase.';
@endphp
@section('page-content')
{{-- Author spotlight --}}
<div class="flex items-center gap-5 rounded-xl border border-white/[0.06] bg-white/[0.02] p-6 mb-10">
@if($author->avatar_url)
<img src="{{ $author->avatar_url }}" alt="{{ $author->name }}"
class="w-16 h-16 rounded-full object-cover border-2 border-white/10 flex-shrink-0" />
@else
<div class="w-16 h-16 rounded-full bg-nova-700 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-user text-xl text-white/30"></i>
</div>
@endif
<div class="min-w-0">
<h2 class="text-lg font-semibold text-white">{{ $author->name }}</h2>
@if($author->bio)
<p class="mt-1 text-sm text-white/50 line-clamp-2">{{ $author->bio }}</p>
@endif
@if($author->user)
<a href="{{ $author->profile_url }}" class="mt-2 inline-flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 transition-colors">
View profile <i class="fa-solid fa-arrow-right text-[10px]"></i>
</a>
@endif
</div>
</div>
{{-- Stories grid --}}
@if($stories->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach($stories as $story)
<a href="{{ $story->url }}"
class="group block rounded-xl border border-white/[0.06] bg-white/[0.02] overflow-hidden hover:bg-white/[0.05] transition-colors">
@if($story->cover_url)
<div class="aspect-video bg-nova-800 overflow-hidden">
<img src="{{ $story->cover_url }}" alt="{{ $story->title }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy" />
</div>
@else
<div class="aspect-video bg-gradient-to-br from-sky-900/20 to-indigo-900/20 flex items-center justify-center">
<i class="fa-solid fa-feather-pointed text-3xl text-white/15"></i>
</div>
@endif
<div class="p-5">
@if($story->tags->isNotEmpty())
<div class="flex flex-wrap gap-1.5 mb-3">
@foreach($story->tags->take(3) as $tag)
<span class="rounded-full px-2 py-0.5 text-[11px] font-medium bg-sky-500/10 text-sky-400 border border-sky-500/20">
#{{ $tag->name }}
</span>
@endforeach
</div>
@endif
<h2 class="text-base font-semibold text-white group-hover:text-sky-300 transition-colors line-clamp-2 leading-snug">
{{ $story->title }}
</h2>
@if($story->excerpt)
<p class="mt-2 text-sm text-white/45 line-clamp-2">{{ $story->excerpt }}</p>
@endif
<div class="mt-4 flex items-center gap-2 text-xs text-white/30">
@if($story->published_at)
<time datetime="{{ $story->published_at->toIso8601String() }}">
{{ $story->published_at->format('M j, Y') }}
</time>
<span>·</span>
@endif
<span>{{ $story->reading_time }} min read</span>
</div>
</div>
</a>
@endforeach
</div>
<div class="mt-10 flex justify-center">
{{ $stories->withQueryString()->links() }}
</div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-16 text-center">
<i class="fa-solid fa-feather-pointed text-4xl text-white/20 mb-4 block"></i>
<p class="text-white/40 text-sm">No published stories from this author yet.</p>
<a href="/stories" class="mt-4 inline-block text-sm text-sky-400 hover:text-sky-300 transition-colors">
Browse all stories
</a>
</div>
@endif
@endsection

View File

@@ -0,0 +1,34 @@
@extends('layouts.nova.content-layout')
@php
$hero_title = ucfirst(str_replace('_', ' ', $category)) . ' Stories';
$hero_description = 'Stories in the ' . str_replace('_', ' ', $category) . ' category.';
@endphp
@section('page-content')
<div class="space-y-8">
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-4">
<div class="flex flex-wrap gap-2">
@foreach($categories as $item)
<a href="{{ route('stories.category', $item['slug']) }}" class="rounded-lg border px-3 py-2 text-sm {{ $item['slug'] === $category ? 'border-sky-400/50 bg-sky-500/10 text-sky-200' : 'border-gray-700 bg-gray-800 text-gray-300 hover:text-white' }}">
{{ $item['name'] }}
</a>
@endforeach
</div>
</div>
@if($stories->isNotEmpty())
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($stories as $story)
<x-story-card :story="$story" />
@endforeach
</div>
<div class="mt-8">{{ $stories->links() }}</div>
@else
<div class="rounded-xl border border-gray-700 bg-gray-800/50 px-8 py-16 text-center text-sm text-gray-300">
No stories found in this category.
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,65 @@
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Create Story';
$hero_description = 'Write a new creator story for your audience.';
@endphp
@section('page-content')
<div class="mx-auto max-w-3xl rounded-xl border border-gray-700 bg-gray-800/60 p-6">
<form method="POST" action="{{ route('creator.stories.store') }}" class="space-y-5">
@csrf
<div>
<label class="mb-1 block text-sm text-gray-200">Title</label>
<input name="title" value="{{ old('title') }}" required class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white" />
</div>
<div>
<label class="mb-1 block text-sm text-gray-200">Cover image URL</label>
<input name="cover_image" value="{{ old('cover_image') }}" class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white" />
</div>
<div>
<label class="mb-1 block text-sm text-gray-200">Excerpt</label>
<textarea name="excerpt" rows="3" class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white">{{ old('excerpt') }}</textarea>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div>
<label class="mb-1 block text-sm text-gray-200">Story type</label>
<select name="story_type" class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white">
@foreach($storyTypes as $type)
<option value="{{ $type['slug'] }}">{{ $type['name'] }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-sm text-gray-200">Status</label>
<select name="status" class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white">
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="scheduled">Scheduled</option>
<option value="archived">Archived</option>
</select>
</div>
</div>
<div>
<label class="mb-1 block text-sm text-gray-200">Tags</label>
<select name="tags[]" multiple class="min-h-28 w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white">
@foreach($tags as $tag)
<option value="{{ $tag->id }}">{{ $tag->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="mb-1 block text-sm text-gray-200">Content (Markdown/HTML)</label>
<textarea name="content" rows="14" required class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-white" placeholder="Write your story...">{{ old('content') }}</textarea>
</div>
<button class="rounded-lg border border-sky-500/40 bg-sky-500/10 px-4 py-2 text-sky-200 transition hover:scale-[1.02]">Save story</button>
</form>
</div>
@endsection

View File

@@ -0,0 +1,30 @@
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Stories by @' . $creator->username;
$hero_description = 'Creator stories published by @' . $creator->username . '.';
@endphp
@section('page-content')
<div class="space-y-8">
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-6">
<h2 class="text-xl font-semibold tracking-tight text-white">Creator Stories</h2>
<p class="mt-2 text-sm text-gray-300">Browse long-form stories from this creator.</p>
<a href="/{{ '@' . strtolower((string) $creator->username) }}" class="mt-3 inline-flex text-sm text-sky-300 hover:text-sky-200">View profile</a>
</div>
@if($stories->isNotEmpty())
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($stories as $story)
<x-story-card :story="$story" />
@endforeach
</div>
<div class="mt-8">{{ $stories->links() }}</div>
@else
<div class="rounded-xl border border-gray-700 bg-gray-800/50 px-8 py-16 text-center text-sm text-gray-300">
No stories published by this creator yet.
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,129 @@
@extends('layouts.nova')
@push('head')
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
@endpush
@section('content')
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(100vh-64px)]">
<main class="w-full">
<x-nova-page-header
section="Creator"
title="My Stories"
icon="fa-pen-nib"
:breadcrumbs="collect([
(object) ['name' => 'Creator', 'url' => route('creator.stories.index')],
(object) ['name' => 'My Stories', 'url' => route('creator.stories.index')],
])"
description="Drafts, published stories, and archived work in one creator dashboard."
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<a href="{{ route('creator.stories.create') }}"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-pen text-xs"></i>
Write Story
</a>
</x-slot>
</x-nova-page-header>
<section class="px-6 pb-16 pt-8 md:px-10">
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4">
<p class="text-xs uppercase tracking-widest text-white/35">Drafts</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($drafts->count()) }}</p>
</div>
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4">
<p class="text-xs uppercase tracking-widest text-white/35">Published</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($publishedStories->count()) }}</p>
</div>
<div class="rounded-xl border border-white/[0.08] bg-white/[0.03] p-4 sm:col-span-2 xl:col-span-1">
<p class="text-xs uppercase tracking-widest text-white/35">Archived</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($archivedStories->count()) }}</p>
</div>
</div>
<div class="space-y-8">
<section class="rounded-xl border border-white/[0.06] bg-white/[0.02] p-5 shadow-lg">
<h3 class="mb-4 text-base font-semibold text-white">Drafts</h3>
@if($drafts->isEmpty())
<p class="text-sm text-gray-400">No drafts yet.</p>
@else
<div class="grid gap-3 md:grid-cols-2">
@foreach($drafts as $story)
<article class="rounded-xl border border-white/[0.06] bg-black/20 p-4 transition hover:bg-white/[0.03]">
<div class="flex items-start justify-between gap-3">
<h4 class="text-sm font-semibold text-white">{{ $story->title }}</h4>
<span class="rounded-full border border-white/[0.08] px-2 py-1 text-xs uppercase tracking-wide text-white/55">{{ str_replace('_', ' ', $story->status) }}</span>
</div>
<p class="mt-2 text-xs text-gray-400">Last edited {{ optional($story->updated_at)->diffForHumans() }}</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">Rejected: {{ \Illuminate\Support\Str::limit($story->rejected_reason, 180) }}</p>
@endif
<div class="mt-3 flex flex-wrap gap-3 text-xs">
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="text-sky-300 hover:text-sky-200">Edit</a>
<a href="{{ route('creator.stories.preview', ['story' => $story->id]) }}" class="text-gray-300 hover:text-white">Preview</a>
<form method="POST" action="{{ route('creator.stories.submit-review', ['story' => $story->id]) }}">
@csrf
<button class="text-amber-300 hover:text-amber-200">Submit Review</button>
</form>
</div>
</article>
@endforeach
</div>
@endif
</section>
<section class="rounded-xl border border-white/[0.06] bg-white/[0.02] p-5 shadow-lg">
<h3 class="mb-4 text-base font-semibold text-white">Published Stories</h3>
@if($publishedStories->isEmpty())
<p class="text-sm text-gray-400">No published stories yet.</p>
@else
<div class="grid gap-3 md:grid-cols-2">
@foreach($publishedStories as $story)
<article class="rounded-xl border border-white/[0.06] bg-black/20 p-4 transition hover:bg-white/[0.03]">
<div class="flex items-start justify-between gap-3">
<h4 class="text-sm font-semibold text-white">{{ $story->title }}</h4>
<span class="rounded-full border border-emerald-500/40 px-2 py-1 text-xs uppercase tracking-wide text-emerald-200">{{ str_replace('_', ' ', $story->status) }}</span>
</div>
<p class="mt-2 text-xs text-gray-400">{{ number_format((int) $story->views) }} views · {{ number_format((int) $story->likes_count) }} likes</p>
<div class="mt-3 flex flex-wrap gap-3 text-xs">
<a href="{{ route('stories.show', ['slug' => $story->slug]) }}" class="text-sky-300 hover:text-sky-200">View</a>
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="text-gray-300 hover:text-white">Edit</a>
<a href="{{ route('creator.stories.analytics', ['story' => $story->id]) }}" class="text-violet-300 hover:text-violet-200">Analytics</a>
</div>
</article>
@endforeach
</div>
@endif
</section>
<section class="rounded-xl border border-white/[0.06] bg-white/[0.02] p-5 shadow-lg">
<h3 class="mb-4 text-base font-semibold text-white">Archived Stories</h3>
@if($archivedStories->isEmpty())
<p class="text-sm text-gray-400">No archived stories.</p>
@else
<div class="grid gap-3 md:grid-cols-2">
@foreach($archivedStories as $story)
<article class="rounded-xl border border-white/[0.06] bg-black/20 p-4 transition hover:bg-white/[0.03]">
<h4 class="text-sm font-semibold text-white">{{ $story->title }}</h4>
<p class="mt-2 text-xs text-gray-400">Archived {{ optional($story->updated_at)->diffForHumans() }}</p>
<div class="mt-3 flex flex-wrap gap-3 text-xs">
<a href="{{ route('creator.stories.edit', ['story' => $story->id]) }}" class="text-sky-300 hover:text-sky-200">Open</a>
</div>
</article>
@endforeach
</div>
@endif
</section>
</div>
</section>
</main>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,61 @@
@extends('layouts.nova.content-layout')
@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.';
@endphp
@section('page-content')
@php
$initialContent = $story->content;
if (is_string($initialContent)) {
$decodedContent = json_decode($initialContent, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decodedContent)) {
$initialContent = [
'type' => 'doc',
'content' => [
['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => strip_tags($initialContent)]],],
],
];
} else {
$initialContent = $decodedContent;
}
}
$storyPayload = [
'id' => $story->id,
'title' => old('title', (string) $story->title),
'excerpt' => old('excerpt', (string) ($story->excerpt ?? '')),
'cover_image' => old('cover_image', (string) ($story->cover_image ?? '')),
'story_type' => old('story_type', (string) ($story->story_type ?? 'creator_story')),
'tags_csv' => old('tags_csv', (string) ($story->tags?->pluck('name')->implode(', ') ?? '')),
'meta_title' => old('meta_title', (string) ($story->meta_title ?? $story->title ?? '')),
'meta_description' => old('meta_description', (string) ($story->meta_description ?? $story->excerpt ?? '')),
'canonical_url' => old('canonical_url', (string) ($story->canonical_url ?? '')),
'og_image' => old('og_image', (string) ($story->og_image ?? $story->cover_image ?? '')),
'status' => old('status', (string) ($story->status ?? 'draft')),
'scheduled_for' => old('scheduled_for', optional($story->scheduled_for)->format('Y-m-d\\TH:i')),
'content' => $initialContent,
];
$endpointPayload = [
'create' => url('/api/stories/create'),
'update' => url('/api/stories/update'),
'autosave' => url('/api/stories/autosave'),
'uploadImage' => url('/api/story/upload-image'),
'artworks' => url('/api/story/artworks'),
'previewBase' => url('/creator/stories'),
'analyticsBase' => url('/creator/stories'),
];
@endphp
<div class="mx-auto max-w-7xl" id="story-editor-react-root"
data-mode="{{ $mode }}"
data-story='@json($storyPayload)'
data-story-types='@json($storyTypes)'
data-endpoints='@json($endpointPayload)'>
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-6 text-gray-200 shadow-lg">
Loading editor...
</div>
</div>
@endsection

View File

@@ -0,0 +1,188 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $page_title,
'description' => $page_meta_description,
'url' => $page_canonical,
])
->build();
@endphp
@extends('layouts.nova')
@section('content')
@php
$currentCategory = (string) (request()->route('category') ?? request()->query('category', ''));
$storyTabs = [
['label' => '🔥 Trending', 'href' => '#trending'],
['label' => '🚀 New & Hot', 'href' => '#featured'],
['label' => '⭐ Best', 'href' => '#latest'],
['label' => '🕐 Latest', 'href' => '#latest'],
];
@endphp
<x-nova-page-header
section="Stories"
title="Browse Stories"
icon="fa-newspaper"
:breadcrumbs="$breadcrumbs ?? collect()"
description="List of all published stories across tutorials, creator journals, interviews, and project breakdowns."
contentClass="max-w-3xl"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<a href="{{ route('creator.stories.create') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors">
<i class="fa-solid fa-pen-nib text-xs"></i>
Write Story
</a>
</x-slot>
</x-nova-page-header>
<div class="border-b border-white/10 bg-nova-900/90 backdrop-blur-md">
<div class="px-6 md:px-10">
<nav data-stories-tabs class="flex items-center gap-0 -mb-px nb-scrollbar-none overflow-x-auto" role="tablist" aria-label="Stories sections">
@foreach($storyTabs as $index => $tab)
<a href="{{ $tab['href'] }}"
data-stories-tab
data-target="{{ ltrim($tab['href'], '#') }}"
role="tab"
aria-selected="{{ $index === 0 ? 'true' : 'false' }}"
class="relative whitespace-nowrap px-5 py-4 text-sm font-medium {{ $index === 0 ? 'text-white' : 'text-neutral-400 hover:text-white' }}">
{{ $tab['label'] }}
<span data-tab-indicator class="absolute bottom-0 left-0 right-0 h-0.5 {{ $index === 0 ? 'bg-accent scale-x-100' : 'bg-transparent scale-x-0' }} transition-transform duration-300 origin-left rounded-full"></span>
</a>
@endforeach
</nav>
</div>
</div>
<div class="border-b border-white/10 bg-nova-900/70">
<div class="px-6 md:px-10 py-6">
<div class="flex gap-3 overflow-x-auto nb-scrollbar-none pb-1">
<a href="{{ route('stories.index') }}" class="whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-semibold transition-colors {{ $currentCategory === '' ? 'bg-orange-500 text-white' : 'border border-white/10 bg-white/[0.05] text-white/70 hover:bg-white/[0.1] hover:text-white' }}">All</a>
@foreach($categories as $category)
<a href="{{ route('stories.category', $category['slug']) }}" class="whitespace-nowrap rounded-full px-3 py-1.5 text-sm transition-colors {{ $currentCategory === $category['slug'] ? 'bg-orange-500 text-white font-semibold' : 'border border-white/10 bg-white/[0.05] text-white/70 hover:bg-white/[0.1] hover:text-white' }}">
{{ $category['name'] }}
</a>
@endforeach
</div>
</div>
</div>
<div class="px-6 pb-16 md:px-10">
<div class="space-y-10">
@if($featured)
<section id="featured" class="overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70 shadow-lg">
<a href="{{ route('stories.show', $featured->slug) }}" class="grid gap-0 lg:grid-cols-2">
<div class="aspect-video overflow-hidden bg-gray-900">
@if($featured->cover_url)
<img src="{{ $featured->cover_url }}" alt="{{ $featured->title }}" class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" />
@endif
</div>
<div class="flex flex-col justify-center space-y-4 p-6">
<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>
</div>
</a>
</section>
@endif
<section id="trending">
<div class="mb-5 flex items-center justify-between">
<h3 class="text-2xl font-semibold tracking-tight text-white">Trending Stories</h3>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($trendingStories as $story)
<x-story-card :story="$story" />
@endforeach
</div>
</section>
<section>
<h3 class="mb-5 text-2xl font-semibold tracking-tight text-white">Categories</h3>
<div class="flex flex-wrap gap-2">
@foreach($categories as $category)
<a href="{{ route('stories.category', $category['slug']) }}" class="rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 transition-colors hover:border-sky-500/40 hover:text-white">
{{ $category['name'] }}
</a>
@endforeach
</div>
</section>
<section id="latest">
<h3 class="mb-5 text-2xl font-semibold tracking-tight text-white">Latest Stories</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($latestStories as $story)
<x-story-card :story="$story" />
@endforeach
</div>
<div class="mt-8">
{{ $latestStories->links() }}
</div>
</section>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
var tabLinks = Array.from(document.querySelectorAll('[data-stories-tab]'));
if (!tabLinks.length) return;
function setActiveTab(targetId) {
tabLinks.forEach(function (link) {
var isActive = link.getAttribute('data-target') === targetId;
link.setAttribute('aria-selected', isActive ? 'true' : 'false');
link.classList.toggle('text-white', isActive);
link.classList.toggle('text-neutral-400', !isActive);
var indicator = link.querySelector('[data-tab-indicator]');
if (!indicator) return;
indicator.classList.toggle('bg-accent', isActive);
indicator.classList.toggle('bg-transparent', !isActive);
indicator.classList.toggle('scale-x-100', isActive);
indicator.classList.toggle('scale-x-0', !isActive);
});
}
tabLinks.forEach(function (link) {
link.addEventListener('click', function () {
var targetId = link.getAttribute('data-target');
if (targetId) setActiveTab(targetId);
});
});
var sectionIds = Array.from(new Set(tabLinks.map(function (link) {
return link.getAttribute('data-target');
}).filter(Boolean)));
var sections = sectionIds
.map(function (id) { return document.getElementById(id); })
.filter(Boolean);
if (!sections.length) return;
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
setActiveTab(entry.target.id);
}
});
}, {
root: null,
rootMargin: '-35% 0px -55% 0px',
threshold: 0.01
});
sections.forEach(function (section) { observer.observe(section); });
});
</script>
@endpush

View File

@@ -0,0 +1,51 @@
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Story Preview';
$hero_description = 'This preview mirrors the final published layout.';
@endphp
@section('page-content')
<div class="mx-auto grid max-w-7xl gap-6 lg:grid-cols-12">
<article class="lg:col-span-8">
<div class="overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70 shadow-lg">
@if($story->cover_image)
<img src="{{ $story->cover_image }}" alt="{{ $story->title }}" class="h-72 w-full object-cover" />
@endif
<div class="p-6">
<div class="mb-4 flex flex-wrap items-center gap-3 text-xs text-gray-300">
<span class="rounded-full border border-gray-600 px-2 py-1">{{ str_replace('_', ' ', $story->story_type) }}</span>
<span>{{ (int) $story->reading_time }} min read</span>
<span class="rounded-full border border-amber-500/40 px-2 py-1 text-amber-200">Preview</span>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-white">{{ $story->title }}</h1>
@if($story->excerpt)
<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">
{!! $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>
</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

@@ -0,0 +1,127 @@
@extends('layouts.nova.content-layout')
@php
$storySummary = $story->excerpt ?: \Illuminate\Support\Str::limit(trim(strip_tags($safeContent)), 160);
$storyUrl = $story->canonical_url ?: route('stories.show', ['slug' => $story->slug]);
$creatorName = $story->creator?->display_name ?: $story->creator?->username ?: 'Unknown creator';
$metaDescription = $story->meta_description ?: $storySummary;
$metaTitle = $story->meta_title ?: $story->title;
$ogImage = $story->og_image ?: $story->cover_url;
$creatorFollowProps = $story->creator ? [
'username' => $story->creator->username,
'following' => (bool) ($storySocialProps['state']['is_following_creator'] ?? false),
'followers_count' => (int) ($storySocialProps['creator']['followers_count'] ?? 0),
] : null;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->og(
type: 'article',
title: $metaTitle,
description: $metaDescription,
url: $storyUrl,
image: $ogImage,
)
->addJsonLd(array_filter([
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $story->title,
'description' => $metaDescription,
'image' => $ogImage,
'author' => [
'@type' => 'Person',
'name' => $creatorName,
],
'datePublished' => optional($story->published_at)->toIso8601String(),
'dateModified' => optional($story->updated_at)->toIso8601String(),
'mainEntityOfPage' => $storyUrl,
], fn (mixed $value): bool => $value !== null && $value !== ''))
->build();
@endphp
@section('page-hero')
<div class="hidden" aria-hidden="true"></div>
@endsection
@section('page-content')
<div class="mx-auto grid max-w-7xl gap-8 lg:grid-cols-12">
<article class="lg:col-span-8">
<div class="overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70">
@if($story->cover_url)
<img src="{{ $story->cover_url }}" alt="{{ $story->title }}" class="h-72 w-full object-cover" loading="lazy" />
@endif
<div class="p-6">
<div class="mb-4 flex flex-wrap items-center gap-3 text-xs text-gray-300">
<span class="rounded-full border border-gray-600 px-2 py-1">{{ str_replace('_', ' ', $story->story_type) }}</span>
@if($story->published_at)
<span>{{ $story->published_at->format('M d, Y') }}</span>
@endif
<span>{{ (int) $story->reading_time }} min read</span>
</div>
<h1 class="text-3xl font-semibold leading-tight tracking-tight text-white">{{ $story->title }}</h1>
@if($story->excerpt)
<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">
{!! $safeContent !!}
</div>
</div>
</div>
<section class="mt-8" id="story-social-root" data-props='@json($storySocialProps)'></section>
</article>
<aside class="space-y-8 lg:col-span-4">
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Creator</h3>
<a href="{{ route('stories.creator', ['username' => $story->creator?->username]) }}" class="mt-2 inline-flex items-center gap-2 text-base text-white hover:text-sky-300">
<span>{{ $story->creator?->display_name ?: $story->creator?->username }}</span>
</a>
@if($story->creator)
<div class="mt-4" id="story-creator-follow-root" data-props='@json($creatorFollowProps)'></div>
@endif
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Tags</h3>
<div class="mt-3 flex flex-wrap gap-2">
@forelse($story->tags as $tag)
<a href="{{ route('stories.tag', ['tag' => $tag->slug]) }}" class="rounded-full border border-gray-600 px-2 py-1 text-xs text-gray-200 hover:border-sky-400 hover:text-sky-300">#{{ $tag->name }}</a>
@empty
<span class="text-sm text-gray-400">No tags</span>
@endforelse
</div>
</div>
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Report Story</h3>
@auth
<form method="POST" action="{{ url('/api/reports') }}" class="mt-3 space-y-3">
@csrf
<input type="hidden" name="target_type" value="story" />
<input type="hidden" name="target_id" value="{{ $story->id }}" />
<textarea name="reason" rows="3" class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-sm text-white" placeholder="Reason for report"></textarea>
<button class="w-full rounded-lg border border-rose-500/40 bg-rose-500/10 px-3 py-2 text-sm text-rose-200 transition hover:scale-[1.01]">Submit report</button>
</form>
@else
<p class="mt-3 text-sm text-gray-400">
<a href="{{ route('login') }}" class="text-sky-300 hover:text-sky-200">Sign in</a> to report this story.
</p>
@endauth
</div>
@if($relatedStories->isNotEmpty())
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-5">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-300">Related Stories</h3>
<div class="space-y-3">
@foreach($relatedStories as $item)
<a href="{{ route('stories.show', ['slug' => $item->slug]) }}" class="block rounded-lg border border-gray-700 bg-gray-900/50 p-3 text-sm text-gray-200 hover:border-sky-400 hover:text-white">{{ $item->title }}</a>
@endforeach
</div>
</div>
@endif
</aside>
</div>
@endsection

View File

@@ -0,0 +1,35 @@
{{--
Tag stories page /stories/tag/{tag}
Uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = '#' . $storyTag->name;
$hero_description = 'Stories tagged with "' . $storyTag->name . '".';
@endphp
@section('page-content')
<div class="space-y-8">
<div class="rounded-xl border border-gray-700 bg-gray-800/60 p-6">
<h2 class="text-xl font-semibold tracking-tight text-white">Tagged Stories</h2>
<p class="mt-2 text-sm text-gray-300">Browsing stories tagged with <span class="text-sky-300">#{{ $storyTag->name }}</span>.</p>
</div>
@if($stories->isNotEmpty())
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($stories as $story)
<x-story-card :story="$story" />
@endforeach
</div>
<div class="mt-8">{{ $stories->links() }}</div>
@else
<div class="rounded-xl border border-gray-700 bg-gray-800/50 px-8 py-16 text-center text-sm text-gray-300">
No stories found for this tag.
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,299 @@
@extends('layouts.nova')
@php
$useUnifiedSeo = true;
$hero_title = 'Tags';
$hero_description = 'Browse all artwork tags on Skinbase.';
$breadcrumbs = $breadcrumbs ?? collect([
(object) ['name' => 'Browse', 'url' => '/browse'],
(object) ['name' => 'Tags', 'url' => '/tags'],
]);
@endphp
@section('content')
@php
$query = trim((string) ($query ?? ''));
$featuredTags = $featuredTags ?? collect();
$risingTags = $risingTags ?? collect();
$tagStats = $tagStats ?? ['active' => 0, 'usage' => 0, 'matching' => $tags->total(), 'recent_clicks' => 0];
$topFeaturedTag = $featuredTags->first();
@endphp
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(100vh-64px)]">
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 overflow-hidden">
<div class="absolute left-[-6rem] top-10 h-56 w-56 rounded-full bg-sky-500/10 blur-3xl"></div>
<div class="absolute right-[-4rem] top-24 h-64 w-64 rounded-full bg-cyan-400/10 blur-3xl"></div>
<div class="absolute left-1/3 top-56 h-48 w-48 rounded-full bg-emerald-400/10 blur-3xl"></div>
</div>
<main class="w-full">
<x-nova-page-header
section="Browse"
title="Tags"
icon="fa-tags"
:breadcrumbs="$breadcrumbs"
:description="$hero_description"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
@include('gallery._browse_nav', ['section' => 'tags', 'includeTags' => true])
</x-slot>
</x-nova-page-header>
<section class="px-6 pb-16 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.55fr)_minmax(320px,0.9fr)]">
<div class="overflow-hidden rounded-[1.75rem] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.18),_transparent_38%),linear-gradient(135deg,rgba(13,19,30,0.98),rgba(8,14,24,0.92))] shadow-[0_24px_80px_rgba(3,7,18,0.32)]">
<div class="grid gap-8 p-6 md:p-8 xl:grid-cols-[minmax(0,1.2fr)_minmax(260px,0.8fr)] xl:items-end">
<div class="space-y-6">
<div class="inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200">
<span class="h-2 w-2 rounded-full bg-sky-300"></span>
Explore by vibe, medium, and theme
</div>
<div class="space-y-3">
<h2 class="max-w-2xl text-3xl font-semibold tracking-tight text-white md:text-4xl xl:text-[2.8rem] xl:leading-[1.05]">
Find collections faster with a cleaner tag browsing experience.
</h2>
<p class="max-w-2xl text-sm leading-6 text-white/64 md:text-base">
Jump into the most used themes on Skinbase, search by keyword, and move from discovery to relevant artwork feeds without scanning an endless wall of chips.
</p>
</div>
<form method="GET" action="{{ route('tags.index') }}" class="space-y-3" data-tags-search-form>
<label for="tags-search" class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">Search tags</label>
<div class="flex flex-col gap-3 sm:flex-row">
<div class="relative flex-1" data-tags-search-root data-search-endpoint="/api/tags/search" data-popular-endpoint="/api/tags/popular">
<i class="fa-solid fa-magnifying-glass pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-sm text-white/35"></i>
<input
id="tags-search"
type="search"
role="combobox"
name="q"
value="{{ $query }}"
placeholder="Search aesthetics, games, styles..."
autocomplete="off"
spellcheck="false"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="tags-search-suggestions"
data-tags-search-input
class="h-12 w-full rounded-2xl border border-white/10 bg-black/25 pl-11 pr-4 text-sm text-white placeholder:text-white/30 focus:border-sky-400/45 focus:outline-none focus:ring-2 focus:ring-sky-400/20"
>
<div id="tags-search-suggestions" data-tags-search-panel class="absolute left-0 right-0 top-[calc(100%+0.75rem)] z-20 hidden overflow-hidden rounded-2xl border border-white/10 bg-[linear-gradient(180deg,rgba(13,19,30,0.98),rgba(8,14,24,0.98))] shadow-[0_18px_50px_rgba(3,7,18,0.36)]">
<div class="border-b border-white/8 px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.24em] text-white/38" data-tags-search-title>
Suggested tags
</div>
<div class="max-h-72 overflow-y-auto p-2" data-tags-search-results role="listbox"></div>
</div>
</div>
<div class="flex gap-3 sm:shrink-0">
<button type="submit" class="inline-flex h-12 items-center justify-center rounded-2xl bg-sky-500 px-5 text-sm font-semibold text-slate-950 transition hover:bg-sky-400">
Search
</button>
@if($query !== '')
<a href="{{ route('tags.index') }}" class="inline-flex h-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] px-5 text-sm font-semibold text-white/72 transition hover:bg-white/[0.08] hover:text-white">
Reset
</a>
@endif
</div>
</div>
</form>
@if($risingTags->isNotEmpty())
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">Popular right now</p>
<p class="text-xs text-white/35">{{ $risingTags->count() }} quick jumps tuned by recent clicks</p>
</div>
<div class="flex flex-wrap gap-2.5">
@foreach($risingTags as $tag)
<a href="{{ route('tags.show', $tag->slug) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-3.5 py-2 text-sm text-white/72 transition hover:border-sky-400/30 hover:bg-sky-400/10 hover:text-white">
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-white/[0.08] text-[11px] text-sky-200">#</span>
<span>{{ $tag->name }}</span>
</a>
@endforeach
</div>
</div>
@endif
</div>
<div class="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/40">Active tags</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($tagStats['active']) }}</p>
<p class="mt-2 text-sm text-white/45">Browsable tags with active artwork associations.</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/40">Total usage</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($tagStats['usage']) }}</p>
<p class="mt-2 text-sm text-white/45">Tag assignments used across the catalog.</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/40">Matching now</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($tagStats['matching']) }}</p>
<p class="mt-2 text-sm text-white/45">
{{ $query !== '' ? 'Results for your current search term.' : 'The current catalog available to browse.' }}
</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/40">Recent clicks</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($tagStats['recent_clicks'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-white/45">Last 14 days of tag discovery clicks used to tune highlights.</p>
</div>
</div>
</div>
</div>
<aside class="rounded-[1.75rem] border border-white/[0.08] bg-white/[0.04] p-6 shadow-[0_16px_60px_rgba(3,7,18,0.22)] backdrop-blur-sm">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">Featured tag</p>
<h3 class="mt-2 text-2xl font-semibold text-white">{{ $topFeaturedTag?->name ?? 'No featured tag yet' }}</h3>
</div>
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-sky-400/12 text-sky-200">
<i class="fa-solid fa-wand-magic-sparkles text-lg"></i>
</div>
</div>
@if($topFeaturedTag)
<p class="mt-4 text-sm leading-6 text-white/58">
One of the strongest tags in current discovery behavior, with {{ number_format($topFeaturedTag->artworks_count) }} artworks currently attached.
</p>
<a href="{{ route('tags.show', $topFeaturedTag->slug) }}" class="mt-6 inline-flex items-center gap-2 rounded-2xl bg-white px-4 py-2.5 text-sm font-semibold text-slate-950 transition hover:bg-sky-100">
Open #{{ $topFeaturedTag->slug }}
<i class="fa-solid fa-arrow-right text-xs"></i>
</a>
@else
<p class="mt-4 text-sm leading-6 text-white/58">Tag highlights will appear here as soon as the catalog has enough data.</p>
@endif
<div class="mt-8 border-t border-white/10 pt-6">
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">Browse tips</p>
<div class="mt-4 space-y-3 text-sm text-white/56">
<div class="rounded-2xl border border-white/8 bg-black/20 px-4 py-3">
Use broad tags like <span class="text-white">anime</span> or <span class="text-white">minimal</span> to start wide, then narrow down inside each feed.
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 px-4 py-3">
Search matches both display names and slugs, so shorthand and full names work.
</div>
</div>
</div>
</aside>
</div>
@if($featuredTags->isNotEmpty())
<div class="mt-8 rounded-[1.75rem] border border-white/[0.08] bg-white/[0.03] p-6 md:p-7">
<div class="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">Editor's picks</p>
<h2 class="mt-2 text-2xl font-semibold text-white">High-signal tags worth exploring first</h2>
</div>
<p class="text-sm text-white/48">Ranked by recent discovery clicks first, then usage and artwork volume.</p>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($featuredTags as $index => $tag)
<a href="{{ route('tags.show', $tag->slug) }}" class="group rounded-[1.35rem] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] p-5 transition duration-200 hover:-translate-y-0.5 hover:border-sky-400/30 hover:bg-sky-400/[0.08]">
<div class="flex items-start justify-between gap-3">
<span class="inline-flex h-9 min-w-9 items-center justify-center rounded-xl bg-sky-400/12 px-3 text-sm font-semibold text-sky-200">
{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}
</span>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-white/42">
{{ number_format((int) ($tag->recent_clicks ?? 0)) }} recent clicks
</span>
</div>
<div class="mt-6 flex items-center gap-2 text-white">
<i class="fa-solid fa-hashtag text-sky-300"></i>
<h3 class="text-xl font-semibold tracking-tight">{{ $tag->name }}</h3>
</div>
<p class="mt-3 text-sm leading-6 text-white/56">
{{ number_format($tag->artworks_count) }} artworks tagged. Open the feed to see the strongest matches first.
</p>
<div class="mt-6 inline-flex items-center gap-2 text-sm font-medium text-white/72 transition group-hover:text-white">
Browse tag
<i class="fa-solid fa-arrow-right text-xs transition group-hover:translate-x-0.5"></i>
</div>
</a>
@endforeach
</div>
</div>
@endif
<div class="mt-8 rounded-[1.75rem] border border-white/[0.08] bg-white/[0.02] p-6 md:p-7">
<div class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-white/40">All tags</p>
<h2 class="mt-2 text-2xl font-semibold text-white">
{{ $query !== '' ? 'Search results' : 'Browse the full catalog' }}
</h2>
</div>
<p class="text-sm text-white/46">
Showing {{ number_format($tags->count()) }} of {{ number_format($tags->total()) }} tags.
@if($query !== '')
<span>Matching &quot;{{ $query }}&quot;.</span>
@endif
</p>
</div>
@if($tags->isNotEmpty())
<div class="mt-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
@foreach($tags as $tag)
<a href="{{ route('tags.show', $tag->slug) }}" class="group flex min-h-[132px] flex-col justify-between rounded-[1.25rem] border border-white/[0.08] bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))] p-4 transition duration-200 hover:border-sky-400/28 hover:bg-sky-400/[0.07] hover:shadow-[0_16px_40px_rgba(14,165,233,0.08)]">
<div class="flex items-start justify-between gap-3">
<div class="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-white/[0.05] text-sky-300 transition group-hover:bg-sky-400/12">
<i class="fa-solid fa-hashtag text-sm"></i>
</div>
<span class="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-white/38">
{{ number_format($tag->artworks_count) }} artworks
</span>
</div>
<div class="mt-5">
<h3 class="text-lg font-semibold tracking-tight text-white">{{ $tag->name }}</h3>
<p class="mt-1 text-sm text-white/44">#{{ $tag->slug }}</p>
</div>
<div class="mt-5 flex items-center justify-between text-sm text-white/58 transition group-hover:text-white/78">
<span>{{ number_format($tag->usage_count) }} total uses</span>
<i class="fa-solid fa-arrow-up-right-from-square text-xs"></i>
</div>
</a>
@endforeach
</div>
<div class="mt-10 flex justify-center">
{{ $tags->withQueryString()->links() }}
</div>
@else
<div class="mt-6 rounded-[1.4rem] border border-dashed border-white/12 bg-black/20 px-8 py-12 text-center">
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-white/[0.05] text-sky-300">
<i class="fa-solid fa-tags text-xl"></i>
</div>
<h3 class="mt-5 text-xl font-semibold text-white">No tags matched this search</h3>
<p class="mx-auto mt-2 max-w-xl text-sm leading-6 text-white/50">
Try a broader keyword, remove punctuation, or reset the search to return to the full tag catalog.
</p>
<div class="mt-6 flex justify-center">
<a href="{{ route('tags.index') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white/72 transition hover:bg-white/[0.08] hover:text-white">
View all tags
<i class="fa-solid fa-arrow-right text-xs"></i>
</a>
</div>
</div>
@endif
</div>
</section>
</main>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,380 @@
@extends('layouts.nova.content-layout')
@section('page-content')
<div class="max-w-3xl space-y-10">
{{-- Intro --}}
<div>
<p class="text-neutral-400 text-sm mb-2">Last updated: March 1, 2026</p>
<p class="text-neutral-300 leading-relaxed">
These Terms of Service ("Terms") govern your access to and use of Skinbase ("we", "us", "our",
"the Service") at <strong class="text-white">skinbase.org</strong>. By creating an account or
using the Service in any way, you agree to be bound by these Terms. If you do not agree, do not
use Skinbase.
</p>
</div>
{{-- Table of Contents --}}
<nav class="rounded-xl border border-white/10 bg-white/[0.03] p-5">
<p class="text-xs font-semibold uppercase tracking-widest text-neutral-400 mb-3">Contents</p>
<ol class="space-y-1.5 text-sm text-sky-400">
<li><a href="#acceptance" class="hover:underline">01 Acceptance of Terms</a></li>
<li><a href="#the-service" class="hover:underline">02 The Service</a></li>
<li><a href="#accounts" class="hover:underline">03 Accounts &amp; Eligibility</a></li>
<li><a href="#content-licence" class="hover:underline">04 Your Content &amp; Licence Grant</a></li>
<li><a href="#prohibited" class="hover:underline">05 Prohibited Conduct</a></li>
<li><a href="#copyright" class="hover:underline">06 Copyright &amp; DMCA</a></li>
<li><a href="#our-ip" class="hover:underline">07 Skinbase Intellectual Property</a></li>
<li><a href="#disclaimers" class="hover:underline">08 Disclaimers</a></li>
<li><a href="#liability" class="hover:underline">09 Limitation of Liability</a></li>
<li><a href="#indemnification" class="hover:underline">10 Indemnification</a></li>
<li><a href="#termination" class="hover:underline">11 Termination</a></li>
<li><a href="#governing-law" class="hover:underline">12 Governing Law</a></li>
<li><a href="#changes" class="hover:underline">13 Changes to These Terms</a></li>
<li><a href="#contact" class="hover:underline">14 Contact</a></li>
</ol>
</nav>
{{-- 01 --}}
<section id="acceptance">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">01</span>
Acceptance of Terms
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
By accessing or using Skinbase whether by browsing the site, registering an account, uploading
content, or any other interaction you confirm that you have read, understood, and agree to
these Terms and our <a href="/privacy-policy" class="text-sky-400 hover:underline">Privacy Policy</a>,
which is incorporated into these Terms by reference. If you are using Skinbase on behalf of an
organisation, you represent that you have authority to bind that organisation to these Terms.
</p>
</section>
{{-- 02 --}}
<section id="the-service">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">02</span>
The Service
</h2>
<p class="text-sm text-neutral-400 leading-relaxed mb-4">
Skinbase is a community platform for sharing and discovering desktop customisation artwork
including skins, themes, wallpapers, icons, and related resources. The Service includes the
website, galleries, forums, messaging, comments, and any other features we provide.
</p>
<p class="text-sm text-neutral-400 leading-relaxed">
We reserve the right to modify, suspend, or discontinue any part of the Service at any time
with or without notice. We will not be liable to you or any third party for any modification,
suspension, or discontinuation of the Service.
</p>
</section>
{{-- 03 --}}
<section id="accounts">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">03</span>
Accounts &amp; Eligibility
</h2>
<div class="space-y-4 text-sm text-neutral-400 leading-relaxed">
<p>
<strong class="text-white">Age.</strong> You must be at least <strong class="text-white">13 years old</strong>
to create a Skinbase account. If you are under 18, you represent that you have your parent's or
guardian's permission to use the Service.
</p>
<p>
<strong class="text-white">Accurate information.</strong> You agree to provide accurate, current, and
complete information when registering and to keep your account information up to date.
</p>
<p>
<strong class="text-white">Account security.</strong> You are responsible for maintaining the confidentiality
of your password and for all activity that occurs under your account. Notify us immediately at
<a href="/bug-report" class="text-sky-400 hover:underline">skinbase.org/bug-report</a> if you believe
your account has been compromised.
</p>
<p>
<strong class="text-white">One account per person.</strong> You may not create multiple accounts to
circumvent bans or restrictions, or to misrepresent your identity to other users.
</p>
<p>
<strong class="text-white">Account transfer.</strong> Accounts are personal and non-transferable.
You may not sell, trade, or give away your account.
</p>
</div>
</section>
{{-- 04 --}}
<section id="content-licence">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">04</span>
Your Content &amp; Licence Grant
</h2>
<div class="space-y-4 text-sm text-neutral-400 leading-relaxed">
<p>
<strong class="text-white">Ownership.</strong> You retain ownership of any original artwork,
skins, themes, or other creative works ("Your Content") that you upload to Skinbase. These Terms
do not transfer any intellectual property rights to us.
</p>
<p>
<strong class="text-white">Licence to Skinbase.</strong> By uploading or publishing Your Content
on Skinbase, you grant us a worldwide, non-exclusive, royalty-free, sublicensable licence to
host, store, reproduce, display, distribute, and make Your Content available as part of the
Service, including in thumbnails, feeds, promotional materials, and search results. This licence
exists only for as long as Your Content remains on the Service.
</p>
<p>
<strong class="text-white">Licence to other users.</strong> Unless you specify otherwise in your
upload description, Your Content may be downloaded and used by other users for personal,
non-commercial use. You are responsible for clearly communicating any additional licence terms
or restrictions within your upload.
</p>
<p>
<strong class="text-white">Representations.</strong> By submitting Your Content you represent and
warrant that: (a) you own or have all necessary rights to the content; (b) the content does not
infringe any third-party intellectual property, privacy, or publicity rights; and (c) the content
complies with these Terms and our
<a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>.
</p>
<p>
<strong class="text-white">Removal.</strong> You may delete Your Content from your account at any
time via your dashboard. Upon deletion, the content will be removed from public view within a
reasonable time, though cached copies may persist briefly.
</p>
</div>
</section>
{{-- 05 --}}
<section id="prohibited">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">05</span>
Prohibited Conduct
</h2>
<p class="text-sm text-neutral-400 leading-relaxed mb-4">
You agree not to use the Service to:
</p>
<ul class="space-y-2 text-sm text-neutral-400 leading-relaxed list-disc list-inside pl-2">
<li>Upload content that infringes any copyright, trademark, patent, trade secret, or other proprietary right.</li>
<li>Upload photographs or photoskins using images you do not own or have permission to use (see our <a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>).</li>
<li>Harass, threaten, bully, stalk, or intimidate any person.</li>
<li>Post content that is defamatory, obscene, pornographic, hateful, or promotes violence or illegal activity.</li>
<li>Impersonate any person or entity, or falsely claim affiliation with any person, entity, or Skinbase staff.</li>
<li>Distribute spam, chain letters, unsolicited commercial messages, or phishing content.</li>
<li>Attempt to gain unauthorised access to any part of the Service, other accounts, or our systems.</li>
<li>Use automated tools (bots, scrapers, crawlers) to access the Service without prior written permission.</li>
<li>Interfere with or disrupt the integrity or performance of the Service or the data contained therein.</li>
<li>Collect or harvest personal information about other users without their consent.</li>
<li>Use the Service for any unlawful purpose or in violation of applicable laws or regulations.</li>
</ul>
<p class="mt-4 text-sm text-neutral-500">
Violations may result in content removal, account suspension, or a permanent ban. Serious violations
may be reported to relevant authorities.
</p>
</section>
{{-- 06 --}}
<section id="copyright">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">06</span>
Copyright &amp; DMCA
</h2>
<div class="space-y-4 text-sm text-neutral-400 leading-relaxed">
<p>
Skinbase respects intellectual property rights. We respond to valid notices of copyright
infringement in accordance with applicable law, including the Digital Millennium Copyright Act (DMCA).
</p>
<p>
<strong class="text-white">To report infringement:</strong> If you believe your copyrighted work has
been copied and is accessible on the Service in a way that constitutes infringement, please contact
a <a href="/staff" class="text-sky-400 hover:underline">staff member</a> or use our
<a href="/bug-report" class="text-sky-400 hover:underline">contact form</a>. Your notice must include:
</p>
<ul class="list-disc list-inside pl-2 space-y-1.5">
<li>A description of the copyrighted work you claim has been infringed.</li>
<li>A description of where the infringing material is located on Skinbase (with URL).</li>
<li>Your contact information (name, email address).</li>
<li>A statement that you have a good-faith belief that the use is not authorised.</li>
<li>A statement, under penalty of perjury, that the information in your notice is accurate and that you are the copyright owner or authorised to act on their behalf.</li>
</ul>
<p>
<strong class="text-white">Counter-notices:</strong> If your content was removed in error, you may
submit a counter-notice to a staff member including your identification details, description of the
removed content, and a statement under penalty of perjury that you have a good-faith belief the
content was removed in error.
</p>
<p>
<strong class="text-white">Repeat infringers:</strong> We will terminate the accounts of users who
are determined to be repeat infringers.
</p>
</div>
</section>
{{-- 07 --}}
<section id="our-ip">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">07</span>
Skinbase Intellectual Property
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
The Skinbase name, logo, website design, software, and all Skinbase-produced content are owned by
Skinbase and are protected by copyright, trademark, and other intellectual property laws. Nothing in
these Terms grants you any right to use the Skinbase name, logo, or branding without our prior written
consent. You may not copy, modify, distribute, sell, or lease any part of our Service or included
software, nor may you reverse-engineer or attempt to extract the source code of the Service.
</p>
</section>
{{-- 08 --}}
<section id="disclaimers">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">08</span>
Disclaimers
</h2>
<div class="rounded-xl border border-amber-500/20 bg-amber-500/5 px-5 py-4 mb-4">
<p class="text-sm text-amber-300/90 leading-relaxed">
THE SERVICE IS PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND UNINTERRUPTED OR ERROR-FREE OPERATION.
</p>
</div>
<div class="space-y-3 text-sm text-neutral-400 leading-relaxed">
<p>
We do not warrant that the Service will be uninterrupted, secure, or free of errors, viruses,
or other harmful components. We do not endorse any user-submitted content and are not responsible
for its accuracy, legality, or appropriateness.
</p>
<p>
Downloaded files are provided by third-party users. You download and install any content at your own
risk. Always scan downloaded files with up-to-date antivirus software.
</p>
</div>
</section>
{{-- 09 --}}
<section id="liability">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">09</span>
Limitation of Liability
</h2>
<div class="rounded-xl border border-amber-500/20 bg-amber-500/5 px-5 py-4 mb-4">
<p class="text-sm text-amber-300/90 leading-relaxed">
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, SKINBASE AND ITS OPERATORS, STAFF, AND
CONTRIBUTORS SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR
PUNITIVE DAMAGES INCLUDING BUT NOT LIMITED TO LOSS OF DATA, LOSS OF PROFITS, OR LOSS OF
GOODWILL ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR YOUR USE OF THE SERVICE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
</p>
</div>
<p class="text-sm text-neutral-400 leading-relaxed">
Our total liability to you for any claim arising out of or relating to these Terms or the Service
shall not exceed the greater of (a) the amount you paid us in the twelve months prior to the claim,
or (b) USD $50. Some jurisdictions do not allow limitations on implied warranties or exclusion of
incidental/consequential damages, so the above limitations may not apply to you.
</p>
</section>
{{-- 10 --}}
<section id="indemnification">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">10</span>
Indemnification
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
You agree to indemnify, defend, and hold harmless Skinbase, its operators, staff, and contributors
from and against any claims, liabilities, damages, losses, and expenses (including reasonable legal
fees) arising out of or in any way connected with: (a) your access to or use of the Service;
(b) Your Content; (c) your violation of these Terms; or (d) your violation of any rights of another
person or entity.
</p>
</section>
{{-- 11 --}}
<section id="termination">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">11</span>
Termination
</h2>
<div class="space-y-3 text-sm text-neutral-400 leading-relaxed">
<p>
<strong class="text-white">By you.</strong> You may close your account at any time by contacting
a <a href="/staff" class="text-sky-400 hover:underline">staff member</a>. Account deletion requests
are processed within 30 days.
</p>
<p>
<strong class="text-white">By us.</strong> We may suspend or terminate your account immediately
and without notice if we determine, in our sole discretion, that you have violated these Terms,
the <a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>,
or applicable law. We may also terminate accounts that have been inactive for an extended period,
with prior notice where practicable.
</p>
<p>
<strong class="text-white">Effect of termination.</strong> Upon termination, your right to access
the Service ceases immediately. Publicly uploaded content may remain on the Service unless you
separately request its removal. Sections of these Terms that by their nature should survive
termination (including Sections 4, 6, 7, 8, 9, 10, and 12) shall survive.
</p>
</div>
</section>
{{-- 12 --}}
<section id="governing-law">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">12</span>
Governing Law
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
These Terms are governed by and construed in accordance with applicable law. Any dispute arising
under or in connection with these Terms that cannot be resolved informally shall be submitted to
the exclusive jurisdiction of the competent courts in the applicable jurisdiction. Nothing in this
section limits your rights under mandatory consumer-protection or data-protection laws of your
country of residence.
</p>
</section>
{{-- 13 --}}
<section id="changes">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">13</span>
Changes to These Terms
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
We may update these Terms from time to time. When we make material changes, we will revise the
"Last updated" date at the top of this page and, where the changes are significant, notify
registered members by email and/or a prominent notice on the site. Your continued use of the
Service after any changes take effect constitutes your acceptance of the revised Terms. If you do
not agree to the revised Terms, you must stop using the Service.
</p>
</section>
{{-- 14 --}}
<section id="contact">
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-6">
<span class="text-sky-400 font-mono text-base">14</span>
Contact
</h2>
<p class="text-sm text-neutral-400 leading-relaxed">
If you have questions about these Terms, please contact us via our
<a href="/bug-report" class="text-sky-400 hover:underline">contact form</a> or by sending a
private message to any <a href="/staff" class="text-sky-400 hover:underline">staff member</a>.
We aim to respond to all legal enquiries within <strong class="text-white">10 business days</strong>.
</p>
<div class="mt-6 rounded-lg border border-sky-500/20 bg-sky-500/5 px-5 py-4 text-sm text-sky-300">
<p class="font-semibold mb-1">Skinbase.org</p>
<p class="text-sky-300/70">
Operated by the Skinbase team.<br>
Contact: <a href="/bug-report" class="underline hover:text-sky-200">via contact form</a> &nbsp;|&nbsp;
<a href="/staff" class="underline hover:text-sky-200">Staff page</a>
</p>
</div>
</section>
{{-- Footer note --}}
<div class="rounded-xl border border-white/10 bg-white/[0.03] p-5 text-sm text-neutral-400 leading-relaxed">
These Terms of Service should be read alongside our
<a href="/privacy-policy" class="text-sky-400 hover:underline">Privacy Policy</a> and
<a href="/rules-and-guidelines" class="text-sky-400 hover:underline">Rules &amp; Guidelines</a>,
which together form the complete agreement between you and Skinbase.
</div>
</div>
@endsection

View File

@@ -0,0 +1,194 @@
@extends('layouts.nova')
@php
$page_title = $page_title ?? 'Latest Artworks';
$page_meta_description = 'Fresh public uploads across skins, photography, wallpapers, and the rest of the Skinbase catalog.';
$page_canonical = route('uploads.latest', request()->query());
$gallery_type = 'latest-uploads';
$useUnifiedSeo = true;
$latestBreadcrumbs = collect([
(object) ['name' => 'Uploads', 'url' => route('uploads.latest')],
(object) ['name' => $page_title, 'url' => $page_canonical],
]);
$breadcrumbs = $latestBreadcrumbs;
$cursorStateLabel = request()->filled('cursor') ? 'Browsing archive' : 'Latest slice';
$cursorStateCopy = request()->filled('cursor')
? 'You are viewing an older slice of the cursor-based feed.'
: 'You are viewing the newest public uploads first.';
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'thumb' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'content_type_name' => $art->content_type_name ?? '',
'content_type_slug' => $art->content_type_slug ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'slug' => $art->slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'published_at' => optional($art->published_at)?->toIsoString() ?? null,
'url' => isset($art->id) ? '/art/' . $art->id . '/' . ($art->slug ?: \Illuminate\Support\Str::slug($art->name ?? 'artwork')) : '#',
])->values();
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@section('content')
<x-nova-page-header
section="Uploads"
:title="$page_title ?? 'Latest Artworks'"
icon="fa-sparkles"
:breadcrumbs="$latestBreadcrumbs"
description="Fresh public uploads across skins, photography, wallpapers, and the rest of the Skinbase catalog."
headerClass="pb-6"
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-3">
<a
href="{{ route('uploads.daily') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white transition-colors"
>
<i class="fa-solid fa-calendar-day text-sky-300"></i>
Daily Uploads
</a>
<a
href="{{ route('discover.fresh') }}"
class="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium border border-sky-400/20 bg-sky-400/10 text-sky-100 hover:bg-sky-400/15 transition-colors"
>
<i class="fa-solid fa-compass text-sky-300"></i>
Discover Fresh
</a>
</div>
</x-slot>
</x-nova-page-header>
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
<div class="overflow-hidden rounded-[1.5rem] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_34%),linear-gradient(135deg,rgba(11,17,27,0.96),rgba(10,16,24,0.88))] p-5 shadow-[0_20px_70px_rgba(3,7,18,0.24)] md:p-6">
<div class="flex flex-wrap items-center gap-3 text-sm text-white/58">
<span class="inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200">
<span class="h-2 w-2 rounded-full bg-sky-300"></span>
Live feed
</span>
<span class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-medium text-white/60">
Ordered by newest first
</span>
</div>
<div class="mt-5 grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/40">This page</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($artworks->count()) }}</p>
<p class="mt-2 text-sm text-white/45">Artworks loaded in the current slice.</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/40">Position</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ $cursorStateLabel }}</p>
<p class="mt-2 text-sm text-white/45">{{ $cursorStateCopy }}</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.05] p-4 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/40">Per page</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($artworks->perPage()) }}</p>
<p class="mt-2 text-sm text-white/45">Balanced for a fast modern gallery load.</p>
</div>
</div>
</div>
<aside class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
<div class="rounded-[1.5rem] border border-white/[0.08] bg-white/[0.03] p-5 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/38">Gallery mode</p>
<h2 class="mt-3 text-lg font-semibold text-white">Nova Gallery</h2>
<p class="mt-2 text-sm leading-6 text-white/52">A denser card wall with the same Skinbase artwork cards used across discover and browse surfaces.</p>
</div>
<div class="rounded-[1.5rem] border border-white/[0.08] bg-white/[0.03] p-5 backdrop-blur-sm">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/38">Content mix</p>
<h2 class="mt-3 text-lg font-semibold text-white">All public uploads</h2>
<p class="mt-2 text-sm leading-6 text-white/52">Skins, photography, wallpapers, and everything else released publicly, sorted strictly by recency.</p>
</div>
</aside>
</div>
</section>
<section class="px-6 pb-16 pt-8 md:px-10">
<div
data-react-masonry-gallery
data-artworks='@json($galleryArtworks)'
data-gallery-type="latest-uploads"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="{{ $artworks->perPage() }}"
class="min-h-32"
></div>
</section>
@endsection
@push('styles')
<style>
[data-nova-gallery].is-enhanced [data-gallery-grid] {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
grid-auto-rows: 8px;
gap: 1rem;
}
@media (min-width: 768px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
[data-nova-gallery].is-enhanced [data-gallery-pagination] {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
}
[data-nova-gallery].is-enhanced [data-gallery-pagination] ul {
display: inline-flex;
gap: 0.25rem;
align-items: center;
padding: 0;
margin: 0;
list-style: none;
}
[data-nova-gallery].is-enhanced [data-gallery-pagination] li a,
[data-nova-gallery].is-enhanced [data-gallery-pagination] li span {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
border-radius: 0.75rem;
padding: 0 0.75rem;
background: rgba(255,255,255,0.03);
color: #e6eef8;
border: 1px solid rgba(255,255,255,0.04);
text-decoration: none;
font-size: 0.875rem;
}
[data-gallery-skeleton].is-loading { display: grid !important; grid-template-columns: inherit; gap: 1rem; }
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush