login update
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 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 & 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 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 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 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>
|
||||
|
||||
@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
|
||||
|
||||
98
resources/views/web/stories/author.blade.php
Normal file
98
resources/views/web/stories/author.blade.php
Normal 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
|
||||
155
resources/views/web/stories/index.blade.php
Normal file
155
resources/views/web/stories/index.blade.php
Normal 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
|
||||
229
resources/views/web/stories/show.blade.php
Normal file
229
resources/views/web/stories/show.blade.php
Normal 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
|
||||
68
resources/views/web/stories/tag.blade.php
Normal file
68
resources/views/web/stories/tag.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user