login update

This commit is contained in:
2026-03-05 11:24:37 +01:00
parent 5a33ca55a1
commit f6772f673b
67 changed files with 10640 additions and 116 deletions

View File

@@ -36,9 +36,61 @@
{{-- ── Leaderboard ── --}}
<div class="px-6 pb-16 md:px-10">
@php $offset = ($authors->currentPage() - 1) * $authors->perPage(); @endphp
@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 gap-3">
<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">
<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 class="mt-1 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 --}}
@@ -52,13 +104,13 @@
{{-- Rows --}}
<div class="divide-y divide-white/[0.04]">
@foreach ($authors as $i => $author)
@foreach ($tableAuthors as $i => $author)
@php
$rank = $offset + $i + 1;
$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, null, 40);
$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">

View File

@@ -21,9 +21,52 @@
{{-- ── Leaderboard ── --}}
<div class="px-6 pb-16 md:px-10">
@php $offset = ($rows->currentPage() - 1) * $rows->perPage(); @endphp
@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 --}}
@@ -35,11 +78,11 @@
{{-- Rows --}}
<div class="divide-y divide-white/[0.04]">
@foreach ($rows as $i => $row)
@foreach ($tableRows as $i => $row)
@php
$rank = $offset + $i + 1;
$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, 40);
$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">

View File

@@ -21,9 +21,58 @@
</div>
<div class="px-6 pb-16 md:px-10">
@php $offset = ($creators->currentPage() - 1) * $creators->perPage(); @endphp
@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 gap-3">
<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">
<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 class="mt-1 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>
@@ -32,13 +81,13 @@
</div>
<div class="divide-y divide-white/[0.04]">
@foreach ($creators as $i => $creator)
@foreach ($tableCreators as $i => $creator)
@php
$rank = $offset + $i + 1;
$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, 40);
$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">

View File

@@ -1,12 +1,69 @@
@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 class="max-w-2xl space-y-10">
<div x-data="{ copied: null }" class="max-w-3xl space-y-10">
{{-- Feed list --}}
<div>
<h2 class="text-lg font-semibold text-white mb-4">Available Feeds</h2>
{{-- Introduction --}}
<p class="text-neutral-400 text-sm leading-relaxed">
Subscribe to Skinbase RSS feeds in your feed reader, Discord bot, or automation tool.
Every feed returns valid RSS&nbsp;2.0 XML with preview images and artwork links.
</p>
{{-- Grouped feed list --}}
@if (!empty($feed_groups))
@foreach ($feed_groups as $groupKey => $group)
<div>
<h2 class="text-base font-semibold text-neutral-300 uppercase tracking-wider mb-3">
{{ $group['label'] }}
</h2>
<ul class="divide-y divide-neutral-800 rounded-lg border border-neutral-800 overflow-hidden">
@foreach ($group['feeds'] as $feed)
<li class="flex items-start gap-4 px-5 py-4 bg-nova-900/50 hover:bg-nova-800/60 transition-colors">
<svg class="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-400" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19.01 7.38 20 6.18 20C4.98 20 4 19.01 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white">{{ $feed['title'] }}</p>
@if (!empty($feed['description']))
<p class="text-xs text-neutral-500 mt-0.5">{{ $feed['description'] }}</p>
@endif
<p class="text-xs text-neutral-600 truncate mt-1 font-mono">{{ url($feed['url']) }}</p>
</div>
<div class="flex-shrink-0 flex items-center gap-2">
<button
type="button"
@click="navigator.clipboard.writeText('{{ url($feed['url']) }}').then(() => { copied = '{{ $feed['url'] }}'; setTimeout(() => copied = null, 2000) })"
class="rounded-md border border-neutral-700 px-3 py-1.5 text-xs text-neutral-400 hover:border-neutral-500 hover:text-white transition-colors"
:class="copied === '{{ $feed['url'] }}' ? 'border-green-600 !text-green-400' : ''"
>
<span x-show="copied !== '{{ $feed['url'] }}'">Copy URL</span>
<span x-show="copied === '{{ $feed['url'] }}'" x-cloak> Copied</span>
</button>
<a href="{{ $feed['url'] }}"
target="_blank"
rel="noopener noreferrer"
class="rounded-md border border-neutral-700 px-3 py-1.5 text-xs text-neutral-400 hover:border-orange-500 hover:text-orange-400 transition-colors">
Open
</a>
</div>
</li>
@endforeach
</ul>
</div>
@endforeach
@else
{{-- Fallback: flat feeds list --}}
<ul class="divide-y divide-neutral-800 rounded-lg border border-neutral-800 overflow-hidden">
@foreach ($feeds as $key => $feed)
<li class="flex items-center gap-4 px-5 py-4 bg-nova-900/50 hover:bg-nova-800/60 transition-colors">
@@ -24,40 +81,47 @@
</li>
@endforeach
</ul>
@endif
{{-- Tag & Creator feed instructions --}}
<div class="rounded-lg border border-neutral-800 bg-nova-900/30 px-5 py-4 space-y-2">
<h2 class="text-sm font-semibold text-white">Tag &amp; Creator Feeds</h2>
<p class="text-xs text-neutral-400 leading-relaxed">
Subscribe to any tag or creator using the dynamic URL patterns below:
</p>
<ul class="space-y-1 text-xs font-mono text-neutral-300">
<li><span class="text-neutral-500 mr-2">Tag:</span>{{ url('/rss/tag/') }}<em class="text-orange-400 not-italic">{tag-slug}</em></li>
<li><span class="text-neutral-500 mr-2">Creator:</span>{{ url('/rss/creator/') }}<em class="text-orange-400 not-italic">{username}</em></li>
</ul>
<p class="text-xs text-neutral-500 mt-2">
Examples:
<a href="/rss/tag/digital-art" class="text-neutral-300 hover:text-orange-400 underline" target="_blank">/rss/tag/digital-art</a>
&bull;
<a href="/rss/creator/gregor" class="text-neutral-300 hover:text-orange-400 underline" target="_blank">/rss/creator/gregor</a>
</p>
</div>
{{-- About RSS --}}
<div class="prose prose-invert prose-sm max-w-none">
<h2>About RSS</h2>
<p>
RSS is a family of web feed formats used to publish frequently updated digital content,
such as blogs, news feeds, or upload streams. By subscribing to an RSS feed you can
follow Skinbase updates in your favourite feed reader without needing to visit the site.
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 one of the feed URLs above and paste it into your feed reader (e.g. Feedly, Inoreader,
or any app that supports RSS 2.0). The reader will automatically check for new content and
notify you of updates.
Copy a feed URL above and paste it into your feed reader (e.g. Feedly, Inoreader,
NetNewsWire) or any tool that supports RSS&nbsp;2.0.
</p>
<h3>Feed formats</h3>
<ul>
<li>Really Simple Syndication (RSS 2.0)</li>
<li>Rich Site Summary (RSS 0.91, RSS 1.0)</li>
<li>RDF Site Summary (RSS 0.9 and 1.0)</li>
</ul>
<h3>Feed format</h3>
<p>
RSS delivers its information as an XML file. Our feeds include title, description,
author, publication date, and a media thumbnail for each item.
All feeds return RSS&nbsp;2.0 XML with <code>application/rss+xml</code> content-type,
UTF-8 encoding, preview thumbnails via <code>&lt;enclosure&gt;</code> and
<code>&lt;media:content&gt;</code>, and a hard limit of 20 items per feed.
</p>
</div>
</div>
@push('head')
@foreach ($feeds as $key => $feed)
<link rel="alternate" type="application/rss+xml" title="{{ $feed['title'] }} — Skinbase" href="{{ url($feed['url']) }}">
@endforeach
@endpush
@endsection

View File

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

View File

@@ -0,0 +1,155 @@
{{--
Stories index /stories
Uses ContentLayout.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = 'Skinbase Stories';
$hero_description = 'Artist interviews, community spotlights, tutorials and announcements.';
@endphp
@push('head')
{{-- WebSite / Blog structured data --}}
<script type="application/ld+json">
{!! json_encode([
'@context' => 'https://schema.org',
'@type' => 'Blog',
'name' => 'Skinbase Stories',
'description' => 'Artist interviews, community spotlights, tutorials and announcements from Skinbase.',
'url' => url('/stories'),
'publisher' => ['@type' => 'Organization', 'name' => 'Skinbase', 'url' => url('/')],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>
@endpush
@section('page-content')
{{-- Featured story hero --}}
@if($featured)
<a href="{{ $featured->url }}"
class="group block rounded-2xl border border-white/[0.06] bg-white/[0.02] overflow-hidden hover:bg-white/[0.04] transition-colors mb-10">
<div class="md:flex">
@if($featured->cover_url)
<div class="md:w-1/2 aspect-video md:aspect-auto overflow-hidden bg-nova-900">
<img src="{{ $featured->cover_url }}" alt="{{ $featured->title }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="eager" />
</div>
@else
<div class="md:w-1/2 aspect-video md:aspect-auto bg-gradient-to-br from-sky-900/40 to-purple-900/40 flex items-center justify-center">
<i class="fa-solid fa-star text-4xl text-white/20"></i>
</div>
@endif
<div class="md:w-1/2 p-8 flex flex-col justify-center">
<div class="mb-3">
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium bg-yellow-400/10 text-yellow-300 border border-yellow-400/20">
<i class="fa-solid fa-star text-[10px]"></i> Featured
</span>
</div>
<h2 class="text-2xl font-bold text-white group-hover:text-sky-300 transition-colors leading-snug">
{{ $featured->title }}
</h2>
@if($featured->excerpt)
<p class="mt-3 text-sm text-white/50 line-clamp-3">{{ $featured->excerpt }}</p>
@endif
<div class="mt-5 flex items-center gap-3 text-xs text-white/30">
@if($featured->author)
<span class="flex items-center gap-1.5">
@if($featured->author->avatar_url)
<img src="{{ $featured->author->avatar_url }}" alt="{{ $featured->author->name }}"
class="w-5 h-5 rounded-full object-cover" />
@endif
{{ $featured->author->name }}
</span>
<span>·</span>
@endif
@if($featured->published_at)
<time datetime="{{ $featured->published_at->toIso8601String() }}">
{{ $featured->published_at->format('M j, Y') }}
</time>
<span>·</span>
@endif
<span>{{ $featured->reading_time }} min read</span>
</div>
</div>
</div>
</a>
@endif
{{-- Stories grid --}}
@if($stories->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach($stories as $story)
@if($featured && $story->id === $featured->id)
@continue
@endif
<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">
{{-- Tags --}}
@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->author)
<span class="flex items-center gap-1.5">
@if($story->author->avatar_url)
<img src="{{ $story->author->avatar_url }}" alt="{{ $story->author->name }}"
class="w-4 h-4 rounded-full object-cover" />
@endif
{{ $story->author->name }}
</span>
<span>·</span>
@endif
@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 stories published yet. Check back soon!</p>
</div>
@endif
@endsection

View File

@@ -0,0 +1,229 @@
{{--
Single story page /stories/{slug}
Uses ContentLayout.
Includes: Hero, Article content, Author box, Related stories, Share buttons.
--}}
@extends('layouts.nova.content-layout')
@php
$hero_title = $story->title;
@endphp
@push('head')
{{-- OpenGraph --}}
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ $story->title }}" />
<meta property="og:description" content="{{ $story->meta_excerpt }}" />
@if($story->cover_url)
<meta property="og:image" content="{{ $story->cover_url }}" />
@endif
<meta property="og:url" content="{{ $story->url }}" />
<meta property="og:site_name" content="Skinbase" />
@if($story->published_at)
<meta property="article:published_time" content="{{ $story->published_at->toIso8601String() }}" />
@endif
@if($story->updated_at)
<meta property="article:modified_time" content="{{ $story->updated_at->toIso8601String() }}" />
@endif
{{-- Twitter / X card --}}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ $story->title }}" />
<meta name="twitter:description" content="{{ $story->meta_excerpt }}" />
@if($story->cover_url)
<meta name="twitter:image" content="{{ $story->cover_url }}" />
@endif
{{-- Article structured data (schema.org) --}}
<script type="application/ld+json">
{!! json_encode(array_filter([
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => $story->title,
'description' => $story->meta_excerpt,
'image' => $story->cover_url,
'datePublished' => $story->published_at?->toIso8601String(),
'dateModified' => $story->updated_at?->toIso8601String(),
'mainEntityOfPage' => $story->url,
'author' => $story->author ? [
'@type' => 'Person',
'name' => $story->author->name,
] : [
'@type' => 'Organization',
'name' => 'Skinbase',
],
'publisher' => [
'@type' => 'Organization',
'name' => 'Skinbase',
'url' => url('/'),
],
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>
@endpush
@section('page-content')
<div class="max-w-3xl mx-auto">
{{-- ── HERO ──────────────────────────────────────────────────────────────── --}}
@if($story->cover_url)
<div class="rounded-2xl overflow-hidden mb-8 aspect-video">
<img src="{{ $story->cover_url }}" alt="{{ $story->title }}"
class="w-full h-full object-cover"
loading="eager" />
</div>
@endif
{{-- Meta bar --}}
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-white/40 mb-6">
@if($story->author)
<a href="{{ $story->author->profile_url }}"
class="flex items-center gap-2 hover:text-white/60 transition-colors">
@if($story->author->avatar_url)
<img src="{{ $story->author->avatar_url }}" alt="{{ $story->author->name }}"
class="w-6 h-6 rounded-full object-cover" />
@endif
<span>{{ $story->author->name }}</span>
</a>
<span>·</span>
@endif
@if($story->published_at)
<time datetime="{{ $story->published_at->toIso8601String() }}">
{{ $story->published_at->format('F j, Y') }}
</time>
<span>·</span>
@endif
<span>{{ $story->reading_time }} min read</span>
<span>·</span>
<span><i class="fa-regular fa-eye mr-1 text-xs"></i>{{ number_format($story->views) }}</span>
</div>
{{-- Tags --}}
@if($story->tags->isNotEmpty())
<div class="flex flex-wrap gap-2 mb-8">
@foreach($story->tags as $tag)
<a href="{{ $tag->url }}"
class="rounded-full px-3 py-1 text-xs font-medium bg-sky-500/10 text-sky-400 border border-sky-500/20 hover:bg-sky-500/20 transition-colors">
#{{ $tag->name }}
</a>
@endforeach
</div>
@endif
{{-- ── ARTICLE CONTENT ──────────────────────────────────────────────────── --}}
<article class="prose prose-invert prose-headings:text-white prose-a:text-sky-400 hover:prose-a:text-sky-300 prose-p:text-white/70 prose-strong:text-white prose-blockquote:border-sky-500 prose-blockquote:text-white/60 max-w-none">
@if($story->content)
{!! $story->content !!}
@else
<p class="text-white/40 italic">Content not available.</p>
@endif
</article>
{{-- ── SHARE BUTTONS ────────────────────────────────────────────────────── --}}
<div class="mt-12 pt-8 border-t border-white/[0.06]">
<p class="text-sm text-white/40 mb-4">Share this story</p>
<div class="flex flex-wrap gap-3">
<a href="https://twitter.com/intent/tweet?text={{ urlencode($story->title) }}&url={{ urlencode($story->url) }}"
target="_blank" rel="noopener"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.03] px-4 py-2 text-sm text-white/60 hover:bg-white/[0.07] hover:text-white transition-colors">
<i class="fa-brands fa-x-twitter text-xs"></i> Share on X
</a>
<a href="https://www.reddit.com/submit?title={{ urlencode($story->title) }}&url={{ urlencode($story->url) }}"
target="_blank" rel="noopener"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.03] px-4 py-2 text-sm text-white/60 hover:bg-white/[0.07] hover:text-white transition-colors">
<i class="fa-brands fa-reddit text-xs"></i> Reddit
</a>
<button type="button"
onclick="navigator.clipboard.writeText('{{ $story->url }}').then(() => { this.textContent = '✓ Copied!'; setTimeout(() => { this.innerHTML = '<i class=\'fa-regular fa-link text-xs\'></i> Copy link'; }, 2000); })"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.03] px-4 py-2 text-sm text-white/60 hover:bg-white/[0.07] hover:text-white transition-colors">
<i class="fa-regular fa-link text-xs"></i> Copy link
</button>
</div>
</div>
{{-- ── AUTHOR BOX ───────────────────────────────────────────────────────── --}}
@if($story->author)
<div class="mt-12 rounded-2xl border border-white/[0.06] bg-white/[0.02] p-6">
<div class="flex items-start gap-5">
@if($story->author->avatar_url)
<img src="{{ $story->author->avatar_url }}" alt="{{ $story->author->name }}"
class="w-16 h-16 rounded-full object-cover border 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 flex-1">
<p class="text-xs uppercase tracking-widest text-white/30 mb-1">About the author</p>
<h3 class="text-lg font-semibold text-white">{{ $story->author->name }}</h3>
@if($story->author->bio)
<p class="mt-2 text-sm text-white/55">{{ $story->author->bio }}</p>
@endif
<div class="mt-4 flex flex-wrap gap-3">
@if($story->author->user)
<a href="{{ $story->author->profile_url }}"
class="inline-flex items-center gap-1.5 text-sm text-sky-400 hover:text-sky-300 transition-colors">
More from this artist <i class="fa-solid fa-arrow-right text-xs"></i>
</a>
@endif
<a href="/stories/author/{{ $story->author->user?->username ?? urlencode($story->author->name) }}"
class="inline-flex items-center gap-1.5 text-sm text-white/40 hover:text-white/60 transition-colors">
All stories
</a>
</div>
</div>
</div>
</div>
@endif
{{-- ── RELATED STORIES ─────────────────────────────────────────────────── --}}
@if($related->isNotEmpty())
<div class="mt-12">
<h2 class="text-lg font-semibold text-white mb-6">Related Stories</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
@foreach($related as $rel)
<a href="{{ $rel->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($rel->cover_url)
<div class="aspect-video overflow-hidden bg-nova-800">
<img src="{{ $rel->cover_url }}" alt="{{ $rel->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-2xl text-white/15"></i>
</div>
@endif
<div class="p-4">
<h3 class="text-sm font-medium text-white group-hover:text-sky-300 transition-colors line-clamp-2 leading-snug">
{{ $rel->title }}
</h3>
<div class="mt-2 flex items-center gap-2 text-xs text-white/30">
@if($rel->published_at)
<time datetime="{{ $rel->published_at->toIso8601String() }}">
{{ $rel->published_at->format('M j, Y') }}
</time>
<span>·</span>
@endif
<span>{{ $rel->reading_time }} min read</span>
</div>
</div>
</a>
@endforeach
</div>
</div>
@endif
{{-- Back link --}}
<div class="mt-12 pt-8 border-t border-white/[0.06]">
<a href="/stories" 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 Stories
</a>
</div>
</div>
@endsection

View File

@@ -0,0 +1,68 @@
{{--
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 . '" on Skinbase.';
@endphp
@section('page-content')
@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">
<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->author)
<span>{{ $story->author->name }}</span>
<span>·</span>
@endif
@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-tag text-4xl text-white/20 mb-4 block"></i>
<p class="text-white/40 text-sm">No stories found for this tag.</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