Save workspace changes
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
»
|
||||
<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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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="' . e(now()->toDateString()) . '">' . 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
|
||||
@@ -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
|
||||
@@ -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 & Submissions</a></li>
|
||||
<li><a href="#copyright" class="hover:underline">03 — Copyright & Photoskins</a></li>
|
||||
<li><a href="#skinning" class="hover:underline">04 — Skinning Help</a></li>
|
||||
<li><a href="#account" class="hover:underline">05 — Account & Profile</a></li>
|
||||
<li><a href="#community" class="hover:underline">06 — Community & Forums</a></li>
|
||||
<li><a href="#policies" class="hover:underline">07 — Policies & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Guidelines</a>,
|
||||
or contact the <a href="/staff" class="text-sky-400 hover:underline">staff team</a> directly.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
<x-artwork-card :art="$art" />
|
||||
@@ -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
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 — 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
|
||||
@@ -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 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 & 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>
|
||||
•
|
||||
<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 2.0.
|
||||
</p>
|
||||
<h3>Feed format</h3>
|
||||
<p>
|
||||
All feeds return RSS 2.0 XML with <code>application/rss+xml</code> content-type,
|
||||
UTF-8 encoding, preview thumbnails via <code><enclosure></code> and
|
||||
<code><media:content></code>, and a hard limit of 20 items per feed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -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 & 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 & Identity</a></li>
|
||||
<li><a href="#moderation" class="hover:text-sky-300 hover:underline transition-colors">8. Moderation & 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 & 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 & 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 & explicit nudity', 'Frontal nudity is not accepted. Exceptional artistic work with incidental nudity may be considered on a case-by-case basis.'],
|
||||
['Hate & discriminatory content', 'Content that demeans or attacks people based on protected characteristics.'],
|
||||
['Violence & gore', 'Graphic depictions of real-world violence or gratuitous gore.'],
|
||||
['Malware & 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 & 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 & 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "{{ $query }}".</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
|
||||
@@ -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 & Eligibility</a></li>
|
||||
<li><a href="#content-licence" class="hover:underline">04 — Your Content & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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> |
|
||||
<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 & Guidelines</a>,
|
||||
which together form the complete agreement between you and Skinbase.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user