login update
This commit is contained in:
@@ -10,6 +10,14 @@
|
||||
|
||||
<x-auth-session-status class="mt-4 mb-2 text-green-300" :status="session('status')" />
|
||||
|
||||
@if($errors->has('oauth'))
|
||||
<div class="rounded-lg bg-red-900/40 border border-red-500/40 px-4 py-3 text-sm text-red-300 mb-4">
|
||||
{{ $errors->first('oauth') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@include('auth.partials.social-login')
|
||||
|
||||
<form method="POST" action="{{ route('login') }}" class="space-y-5">
|
||||
@csrf
|
||||
|
||||
|
||||
34
resources/views/auth/partials/social-login.blade.php
Normal file
34
resources/views/auth/partials/social-login.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
{{-- Social login / register buttons for Google, Apple, Discord --}}
|
||||
<div class="space-y-3">
|
||||
{{-- Google --}}
|
||||
<a
|
||||
href="{{ route('oauth.redirect', 'google') }}"
|
||||
class="flex items-center justify-center gap-3 w-full rounded-lg border border-white/15 px-4 py-3 text-sm font-medium text-white hover:border-white/30 hover:bg-white/5 transition-colors duration-150"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="w-5 h-5 shrink-0" aria-hidden="true">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
<span>Continue with Google</span>
|
||||
</a>
|
||||
|
||||
{{-- Discord --}}
|
||||
<a
|
||||
href="{{ route('oauth.redirect', 'discord') }}"
|
||||
class="flex items-center justify-center gap-3 w-full rounded-lg border border-white/15 px-4 py-3 text-sm font-medium text-white hover:border-white/30 hover:bg-white/5 transition-colors duration-150"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="w-5 h-5 shrink-0" aria-hidden="true" fill="#5865F2">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.04.033.05a19.89 19.89 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
<span>Continue with Discord</span>
|
||||
</a>
|
||||
|
||||
{{-- Divider --}}
|
||||
<div class="relative flex items-center py-1">
|
||||
<div class="flex-grow border-t border-white/10"></div>
|
||||
<span class="mx-3 text-xs text-white/40 whitespace-nowrap">{{ $dividerLabel ?? 'or continue with email' }}</span>
|
||||
<div class="flex-grow border-t border-white/10"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,7 +7,13 @@
|
||||
<h2 class="text-2xl font-semibold mb-2 text-white">Create Account</h2>
|
||||
|
||||
<p class="text-sm text-white/60 mb-6">Start with your email. You’ll choose a password and username after verification.</p>
|
||||
@if($errors->has('oauth'))
|
||||
<div class="rounded-lg bg-red-900/40 border border-red-500/40 px-4 py-3 text-sm text-red-300 mb-4">
|
||||
{{ $errors->first('oauth') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@include('auth.partials.social-login', ['dividerLabel' => 'or register with email'])
|
||||
<form method="POST" action="{{ route('register') }}" class="space-y-5">
|
||||
@csrf
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
$avatarUserId = $art->user->id ?? $art->user_id ?? null;
|
||||
$avatarHash = $art->user->profile->avatar_hash ?? $art->avatar_hash ?? null;
|
||||
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($avatarUserId ?? 0), $avatarHash, 40);
|
||||
$avatarUrl = \App\Support\AvatarUrl::forUser((int) ($avatarUserId ?? 0), $avatarHash, 64);
|
||||
|
||||
$license = trim((string) ($art->license ?? 'Standard'));
|
||||
$resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : '')));
|
||||
|
||||
@@ -1,25 +1,92 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto py-8 max-w-3xl">
|
||||
<h1 class="text-2xl font-semibold mb-6">People I Follow</h1>
|
||||
|
||||
@if($following->isEmpty())
|
||||
<p class="text-sm text-gray-500">You are not following anyone yet. <a href="{{ route('discover.trending') }}" class="underline">Discover creators</a></p>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($following as $f)
|
||||
<a href="{{ $f->profile_url }}" class="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 transition">
|
||||
<img src="{{ $f->avatar_url }}" alt="{{ $f->uname }}" class="w-10 h-10 rounded-full object-cover">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium truncate">{{ $f->uname }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $f->uploads }} uploads · {{ $f->followers_count }} followers · followed {{ \Carbon\Carbon::parse($f->followed_at)->diffForHumans() }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
<div class="px-6 pt-10 pb-16 md:px-10">
|
||||
<div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-white/30 mb-1">Dashboard</p>
|
||||
<h1 class="text-3xl font-bold text-white leading-tight">People I Follow</h1>
|
||||
<p class="mt-1 text-sm text-white/50">Creators and members you follow, with quick stats and recent follow time.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('discover.trending') }}"
|
||||
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-compass text-xs"></i>
|
||||
Discover creators
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($following->isEmpty())
|
||||
<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">You are not following anyone yet.</p>
|
||||
<a href="{{ route('discover.trending') }}" class="mt-4 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-compass text-xs"></i>
|
||||
Start following creators
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$firstFollow = $following->getCollection()->first();
|
||||
$latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at)
|
||||
? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans()
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<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">Following</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($following->total()) }}</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">On this page</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ number_format($following->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">Last followed</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-white">{{ $latestFollowedAt ?? '—' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/[0.06] overflow-hidden">
|
||||
<div class="grid grid-cols-[1fr_auto_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">Creator</span>
|
||||
<span class="hidden sm:block text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Stats</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-widest text-white/30 text-right">Followed</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-white/[0.04]">
|
||||
@foreach($following as $f)
|
||||
@php
|
||||
$displayName = $f->name ?: $f->uname;
|
||||
@endphp
|
||||
<a href="{{ $f->profile_url }}"
|
||||
class="grid grid-cols-[1fr_auto_auto] items-center gap-4 px-5 py-4 hover:bg-white/[0.03] transition-colors">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<img src="{{ $f->avatar_url }}"
|
||||
alt="{{ $displayName }}"
|
||||
class="w-11 h-11 rounded-full object-cover flex-shrink-0 ring-1 ring-white/[0.10]"
|
||||
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-semibold text-white/90">{{ $displayName }}</div>
|
||||
@if(!empty($f->username))
|
||||
<div class="truncate text-xs text-white/35">{{ '@' . $f->username }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block text-right text-xs text-white/55">
|
||||
{{ number_format((int) $f->uploads) }} uploads · {{ number_format((int) $f->followers_count) }} followers
|
||||
</div>
|
||||
|
||||
<div class="text-right text-xs text-white/45 whitespace-nowrap">
|
||||
{{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : '—' }}
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
{{ $following->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -37,4 +37,10 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if(config('app.debug') && isset($exception))
|
||||
<div class="mt-8 mx-auto max-w-4xl text-left bg-black/40 rounded-xl p-4 overflow-auto">
|
||||
<div class="font-semibold text-white/80 mb-3">Exception: {{ $exception->getMessage() }}</div>
|
||||
<pre class="text-xs text-white/60 whitespace-pre-wrap">{{ $exception->getTraceAsString() }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<link rel="next" href="{{ $page_rel_next }}" />
|
||||
@endisset
|
||||
|
||||
{{-- Global RSS feed discovery --}}
|
||||
<link rel="alternate" type="application/rss+xml" title="Skinbase Latest Artworks" href="{{ url('/rss') }}">
|
||||
|
||||
<!-- Icons (kept for now to preserve current visual output) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||
|
||||
|
||||
@@ -223,7 +223,10 @@
|
||||
$routeEditProfile = Route::has('dashboard.profile')
|
||||
? route('dashboard.profile')
|
||||
: (Route::has('settings') ? route('settings') : '/settings');
|
||||
$routePublicProfile = Route::has('profile.show') ? route('profile.show', ['username' => $toolbarUsername]) : '/@'.$toolbarUsername;
|
||||
// Guard: username may be null for OAuth users still in onboarding.
|
||||
$routePublicProfile = $toolbarUsername !== ''
|
||||
? (Route::has('profile.show') ? route('profile.show', ['username' => $toolbarUsername]) : '/@'.$toolbarUsername)
|
||||
: route('setup.username.create');
|
||||
@endphp
|
||||
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeUpload }}">
|
||||
@@ -333,7 +336,10 @@
|
||||
|
||||
@php
|
||||
$mobileUsername = strtolower((string) (Auth::user()->username ?? ''));
|
||||
$mobileProfile = Route::has('profile.show') ? route('profile.show', ['username' => $mobileUsername]) : '/@'.$mobileUsername;
|
||||
// Guard: username may be null for OAuth users still in onboarding.
|
||||
$mobileProfile = $mobileUsername !== ''
|
||||
? (Route::has('profile.show') ? route('profile.show', ['username' => $mobileUsername]) : '/@'.$mobileUsername)
|
||||
: route('setup.username.create');
|
||||
@endphp
|
||||
<div class="pt-1 pb-1 px-3 text-[11px] font-semibold uppercase tracking-widest text-sb-muted">My Account</div>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/studio/artworks">
|
||||
|
||||
62
resources/views/privacy/data-deletion.blade.php
Normal file
62
resources/views/privacy/data-deletion.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
@extends('layouts.nova.content-layout')
|
||||
|
||||
@section('page-title', 'Data Deletion')
|
||||
|
||||
@section('page-content')
|
||||
<div class="max-w-3xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
||||
<p class="text-sm text-white/40 mb-1">Last updated: <time datetime="2026-03-05">March 5, 2026</time></p>
|
||||
|
||||
<p class="text-white/60 text-sm leading-relaxed mb-8">This page explains how users can delete their account and request removal of their personal data. Follow the steps below depending on whether you still have access to your account.</p>
|
||||
|
||||
<nav class="mb-8 rounded-xl border border-white/[0.08] bg-white/[0.02] px-6 py-4">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-widest text-white/40 mb-2">Contents</h2>
|
||||
<ol class="space-y-1 text-sm text-sky-400">
|
||||
<li><a href="#signed-in" class="hover:text-sky-300 hover:underline">If you can sign in</a></li>
|
||||
<li><a href="#cant-sign-in" class="hover:text-sky-300 hover:underline">If you cannot sign in</a></li>
|
||||
<li><a href="#what-we-remove" class="hover:text-sky-300 hover:underline">What we remove</a></li>
|
||||
<li><a href="#providers" class="hover:text-sky-300 hover:underline">Providers</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="space-y-10">
|
||||
<section id="signed-in">
|
||||
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">If you can sign in</h2>
|
||||
<div class="text-sm text-neutral-400 leading-relaxed">
|
||||
<ol class="list-decimal list-inside space-y-2">
|
||||
<li>Sign in to your account.</li>
|
||||
<li>Go to your <strong>Settings</strong> → <strong>Account</strong> page. {{ Route::has('settings') ? '(You can open <a href="'.route('settings').'" class="text-sky-400 hover:underline">Settings</a>.)' : '' }}</li>
|
||||
<li>Use the <strong>Delete account</strong> action to request permanent removal. Follow the confirmation steps — some actions are irreversible.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="cant-sign-in">
|
||||
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">If you cannot sign in</h2>
|
||||
<div class="text-sm text-neutral-400 leading-relaxed">
|
||||
<p class="mb-3">If you no longer have access to the email address on your account or cannot sign in, please contact our support team so we can verify ownership and process the request.</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>Your account username or profile URL (if known)</li>
|
||||
<li>The email address you used to register (if known)</li>
|
||||
<li>A short explanation why you cannot sign in</li>
|
||||
</ul>
|
||||
<p class="mt-3">Contact us via the <a href="{{ Route::has('contact') ? route('contact') : url('/contact') }}" class="text-sky-400 hover:underline">Contact</a> page or the staff contact methods listed on the site.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="what-we-remove">
|
||||
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">What we remove</h2>
|
||||
<div class="text-sm text-neutral-400 leading-relaxed">
|
||||
<p>When an account is deleted we remove personal data associated with that account in accordance with our retention and legal obligations. Publicly visible content you uploaded may remain unless you request its removal specifically.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="providers">
|
||||
<h2 class="flex items-center gap-2 text-xl font-bold text-white border-b border-white/10 pb-3 mb-4">Providers</h2>
|
||||
<div class="text-sm text-neutral-400 leading-relaxed">
|
||||
<p>If you are adding this URL to an OAuth provider's developer settings for data-deletion instructions, use: <strong>{{ url('/data-deletion') }}</strong></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
39
resources/views/rss/channel.blade.php
Normal file
39
resources/views/rss/channel.blade.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<rss version="2.0"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:media="http://search.yahoo.com/mrss/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<channel>
|
||||
<title>{{ htmlspecialchars($channelTitle) }}</title>
|
||||
<link>{{ $channelLink }}</link>
|
||||
<description>{{ htmlspecialchars($channelDescription) }}</description>
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>{{ $buildDate }}</lastBuildDate>
|
||||
<atom:link href="{{ $feedUrl }}" rel="self" type="application/rss+xml" />
|
||||
@foreach ($items as $item)
|
||||
<item>
|
||||
<title><![CDATA[{{ $item['title'] }}]]></title>
|
||||
<link>{{ $item['link'] }}</link>
|
||||
<guid isPermaLink="true">{{ $item['guid'] }}</guid>
|
||||
@if (!empty($item['pubDate']))
|
||||
<pubDate>{{ $item['pubDate'] }}</pubDate>
|
||||
@endif
|
||||
@if (!empty($item['author']))
|
||||
<dc:creator><![CDATA[{{ $item['author'] }}]]></dc:creator>
|
||||
@endif
|
||||
@if (!empty($item['category']))
|
||||
<category><![CDATA[{{ $item['category'] }}]]></category>
|
||||
@endif
|
||||
<description><![CDATA[{!! $item['description'] !!}]]></description>
|
||||
@if (!empty($item['enclosure']))
|
||||
<enclosure url="{{ $item['enclosure']['url'] }}"
|
||||
length="{{ $item['enclosure']['length'] }}"
|
||||
type="{{ $item['enclosure']['type'] }}" />
|
||||
@endif
|
||||
@if (!empty($item['enclosure']))
|
||||
<media:content url="{{ $item['enclosure']['url'] }}" medium="image" />
|
||||
@endif
|
||||
</item>
|
||||
@endforeach
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -3,6 +3,7 @@
|
||||
@push('head')
|
||||
<link rel="canonical" href="{{ $page_canonical }}">
|
||||
<meta name="robots" content="{{ $page_robots ?? 'index,follow' }}">
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ $tag->name }} Artworks — Skinbase" href="{{ url('/rss/tag/' . $tag->slug) }}">
|
||||
@if(!empty($ogImage))
|
||||
<meta property="og:image" content="{{ $ogImage }}">
|
||||
<meta property="og:image:alt" content="{{ $tag->name }} artworks on Skinbase">
|
||||
|
||||
@@ -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