Save workspace changes

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

View File

@@ -0,0 +1,2 @@
{{-- Shim for legacy artwork card includes. Points to new web partial. --}}
@include('web.partials._artwork_card')

View File

@@ -0,0 +1,17 @@
@extends('layouts.nova')
@php
use Illuminate\Support\Str;
use Carbon\Carbon;
use App\Services\LegacyService;
@endphp
@section('content')
<div class="container-fluid legacy-page">
@include('legacy::home.featured')
@include('legacy::home.uploads')
@include('legacy::home.news')
</div>
@endsection

View File

@@ -0,0 +1,46 @@
{{-- Featured row --}}
<div class="row featured-row">
<div class="col-md-4 col-sm-12">
<div class="featured-card effect2">
<div class="card-header">Featured Artwork</div>
<div class="card-body text-center">
<a href="/art/{{ data_get($featured, 'id') }}/{{ Str::slug(data_get($featured, 'name') ?? 'artwork') }}" class="thumb-link">
@php
$fthumb = data_get($featured, 'thumb_url') ?? data_get($featured, 'thumb');
@endphp
<img src="{{ $fthumb }}" class="img-responsive featured-img" alt="{{ data_get($featured, 'name') }}">
</a>
<div class="featured-title">{{ data_get($featured, 'name') }}</div>
<div class="featured-author">by {{ data_get($featured, 'uname') }}</div>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12">
<div class="featured-card effect2">
<div class="card-header">Featured by Members Vote</div>
<div class="card-body text-center">
<a href="/art/{{ data_get($memberFeatured, 'id') }}/{{ Str::slug(data_get($memberFeatured, 'name') ?? 'artwork') }}" class="thumb-link">
@php
$mthumb = data_get($memberFeatured, 'thumb_url') ?? data_get($memberFeatured, 'thumb');
@endphp
<img src="{{ $mthumb }}" class="img-responsive featured-img" alt="{{ data_get($memberFeatured, 'name') }}">
</a>
<div class="featured-title">{{ data_get($memberFeatured, 'name') }}</div>
<div class="featured-author">by {{ data_get($memberFeatured, 'uname') }}</div>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12">
<div class="featured-card join-card effect2">
<div class="card-header">Join to Skinbase World</div>
<div class="card-body text-center">
<a href="{{ route('register') }}" title="Join Skinbase">
<img src="/gfx/sb_join.jpg" alt="Join SkinBase Community" class="img-responsive join-img center-block">
</a>
<div class="join-text">Join to Skinbase and be part of our great community! We have big collection of high quality Photography, Wallpapers and Skins for popular applications.</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
{{-- News and forum columns --}}
<div class="row news-row">
<div class="col-sm-6">
@php
use Carbon\Carbon;
use Illuminate\Support\Str;
@endphp
@forelse ($forumNews as $item)
<div class="panel panel-skinbase effect2">
<div class="panel-heading"><h4 class="panel-title">{{ $item->topic }}</h4></div>
<div class="panel-body">
<div class="text-muted news-head">
Written by {{ $item->uname }} on {{ Carbon::parse($item->post_date)->format('j F Y \@ H:i') }}
</div>
{!! Str::limit(strip_tags($item->preview ?? ''), 240, '...') !!}
<br>
<a class="clearfix btn btn-xs btn-info" href="{{ route('forum.thread.show', ['thread' => $item->topic_id, 'slug' => Str::slug($item->topic ?? '')]) }}" title="{{ strip_tags($item->topic) }}">More</a>
</div>
</div>
@empty
<p>No forum news available.</p>
@endforelse
</div>
<div class="col-sm-6">
@forelse ($ourNews as $news)
<div class="panel panel-skinbase effect2">
<div class="panel-heading"><h3 class="panel-title">{{ $news->headline }}</h3></div>
<div class="panel-body">
<div class="text-muted news-head">
<i class="fa fa-user"></i> {{ $news->uname }}
<i class="fa fa-calendar"></i> {{ Carbon::parse($news->create_date)->format('j F Y \@ H:i') }}
<i class="fa fa-info"></i> {{ $news->category_name }}
<i class="fa fa-info"></i> {{ $news->views }} reads
<i class="fa fa-comment"></i> {{ $news->num_comments }} comments
</div>
@if (!empty($news->picture))
@php $nid = floor($news->news_id / 100); @endphp
<div class="col-md-4">
<img src="/archive/news/{{ $nid }}/{{ $news->picture }}" class="img-responsive" alt="{{ $news->headline }}">
</div>
<div class="col-md-8">
{!! $news->preview !!}
</div>
@else
{!! $news->preview !!}
@endif
<a class="clearfix btn btn-xs btn-info text-white" href="/news/{{ $news->news_id }}/{{ Str::slug($news->headline ?? '') }}">More</a>
</div>
</div>
@empty
<p>No news available.</p>
@endforelse
{{-- Site info --}}
<div class="panel panel-default">
<div class="panel-heading"><strong>Info</strong></div>
<div class="panel-body">
<h4>Photography, Wallpapers and Skins... Thats Skinbase</h4>
<p>Skinbase is the site dedicated to <strong>Photography</strong>, <strong>Wallpapers</strong> and <strong>Skins</strong> for <u>popular applications</u> for every major operating system like Windows, Mac OS X, Linux, iOS and Android</p>
<em>Our members every day uploads new artworks to our site, so don&apos;t hesitate and check Skinbase frequently for updates. We also have forum where you can discuss with other members with anything.</em>
<p>On the site toolbar you can click on Categories and start browsing our atwork (<i>photo</i>, <i>desktop themes</i>, <i>pictures</i>) and of course you can <u>download</u> them for free!</p>
<p>We are also active on all major <b>social</b> sites, find us there too</p>
</div>
</div>
{{-- Latest forum activity --}}
<div class="panel panel-default activity-panel">
<div class="panel-heading"><strong>Latest Forum Activity</strong></div>
<div class="panel-body">
<div class="list-group effect2">
@forelse ($latestForumActivity as $topic)
<a class="list-group-item" href="{{ route('forum.thread.show', ['thread' => $topic->topic_id, 'slug' => Str::slug($topic->topic ?? '')]) }}">
{{ $topic->topic }} <span class="badge badge-info">{{ $topic->numPosts }}</span>
</a>
@empty
<p>No recent forum activity.</p>
@endforelse
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="gallery-grid">
@foreach ($latestUploads as $upload)
<div class="thumb-card effect2">
@php
$t = \App\Services\ThumbnailPresenter::present($upload, 'md');
$card = [
'id' => $t['id'] ?? null,
'title' => $t['title'] ?? 'Artwork',
'thumb' => $t['url'] ?? null,
'thumb_srcset' => $t['srcset'] ?? null,
];
@endphp
<x-artwork-card :art="$card" />
</div>
@endforeach
</div> <!-- end .gallery-grid -->

View File

@@ -0,0 +1,97 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Interview</h1>
<p>{{ $ar->headline ?? '' }}</p>
<div style="location_bar">
<a href="/interviews" class="btn btn-xs btn-default" title="List of all Interviews"><i class="fa fa-list fa-fw"></i> Interviews List</a>
</div>
</header>
</div>
<div class="row">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-body">
<h1 class="skinTitle">{{ $ar->headline }}</h1>
<div>{!! $ar->tekst !!}</div>
</div>
</div>
<h2 class="skinTitle">User Comments</h2>
@foreach($comments as $comment)
@php
$local_date = date('j.F.y @ H:i:s', strtotime($comment->datum));
$user_id = $comment->user_id ?? null;
@endphp
<table width="100%" cellspacing="1" cellpadding="1" class="MojText" border="0" style="background:#eee;">
<tr>
<td rowspan="3" valign="top" width="100" style="background:#fff">
@if(!empty($comment->user_id) && !empty($comment->icon))
<div align="center"><a href="/profile/{{ $comment->user_id }}"><img src="{{ \App\Support\AvatarUrl::forUser((int) $comment->user_id, null, 50) }}" width="50" height="50" border="0" alt="" /></a></div>
@endif
<br/>Posted by: <b><a href="/profile/{{ $comment->user_id ?? '' }}">{{ $comment->author }}</a></b><br/>
Posts: {{ $postCounts[$comment->author] ?? 0 }}
<div align="center"><img src="/gfx/member_stars/{{ $comment->user_type ?? 0 }}.jpg" title="" /></div>
</td>
<td valign="top" height="10" style="background:#eee">&nbsp;{{ $local_date }}</td>
</tr>
<tr>
<td height="50" style="background:#fff;padding-left:13px; padding-right:3px;">{!! $comment->tekst !!}</td>
</tr>
<tr>
<td valign="bottom" align="center" height="10" style="background:#fff;">
@if(!empty($comment->signature))
{!! nl2br(e($comment->signature)) !!}
@endif
</td>
</tr>
</table>
<br />
@endforeach
@php
$status = $_SESSION['web_login']['status'] ?? false;
$user_type = $_SESSION['web_login']['user_type'] ?? 0;
@endphp
@if(!$status || $user_type < 2)
<h1>Please login first</h1>
@else
<h2 class="skinTitle">Write comment</h2>
<form action="{{ url()->current() }}" method="post">
@csrf
<textarea name="comment" style="width:98%; height:215px;" class="textarea"></textarea><br />
<input type="hidden" name="interview_id" value="{{ $ar->id }}">
<input type="hidden" name="action" value="store">
<button type="submit" class="btn btn-success">Submit</button>
</form>
@endif
</div>
<div class="col-md-3">
<div style="margin-left:4px; float:right;width:300px;border-left :dotted 1px #eee; padding-left:10px;">
<br/><br/>
@php \App\Banner::ShowBanner300x250(); @endphp
<br/><br/>
@if(!empty($ar->username))
@php $username = DB::table('users')->where('uname', $ar->username)->value('uname'); @endphp
<div class="interviewGuy">{{ $username }} Gallery (random order):</div><br/>
@foreach($artworks as $artwork)
@php $nid = (int)($artwork->id / 100); @endphp
<a href="/art/{{ $artwork->id }}/{{ \Illuminate\Support\Str::slug($artwork->name ?? '') }}">
<img src="/files/archive/shots/{{ $nid }}/{{ $artwork->picture }}" alt="" style="max-width:100%;" />
</a>
<br/><br/>
@endforeach
@endif
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,41 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">{{ $page_title ?? 'Interviews' }}</h1>
<p>List of interviews and their authors.</p>
</header>
</div>
<div class="mb-3">
@php \App\Banner::ShowResponsiveAd(); @endphp
</div>
<div class="panel panel-default">
<div class="panel-body">
<table class="table table-bordered table-striped">
<tbody>
@foreach($interviews as $interview)
<tr>
<td style="width:60px;">
@if(!empty($interview->icon))
<a href="/profile/{{ $interview->user_id }}/{{ \Illuminate\Support\Str::slug($interview->uname ?? '') }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $interview->user_id, null, 50) }}" width="50" height="50" alt="">
</a>
@else
<img src="/gfx/avatar.jpg" alt="">
@endif
</td>
<td>
<a href="/interview/{{ $interview->id }}/{{ \Illuminate\Support\Str::slug($interview->headline ?? '') }}">{{ $interview->headline }}</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,30 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Latest Artworks</h1>
<p>List of recently uploaded Artworks - <strong>Skins</strong>, <strong>Photography</strong> and <strong>Wallpapers</strong>.</p>
</header>
</div>
<div class="panel panel-default uploads-panel effect2">
<div class="panel-body">
<div class="gallery-grid">
@if($artworks)
@foreach($artworks as $art)
<x-artwork-card :art="$art" />
@endforeach
@else
<p class="text-muted">No artworks found.</p>
@endif
</div>
</div>
</div>
<div class="paginationMenu text-center">
@if($artworks){{ $artworks->withQueryString()->links('pagination::bootstrap-4') }}@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,64 @@
@extends('layouts.nova')
@php
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => route('legacy.latest_comments')],
]);
@endphp
@section('content')
<x-nova-page-header
section="Comments"
:title="$page_title"
icon="fa-comments"
:breadcrumbs="$headerBreadcrumbs"
description="List of artwork with latest comments received."
headerClass="pb-6"
/>
<div class="container-fluid legacy-page pt-8">
<div class="masonry">
@foreach ($comments as $comment)
<div class="masonry_item col-sm-6 col-md-4">
<div class="comment_box effect3">
<div class="cb_image">
<a href="/profile/{{ $comment->commenter_id }}/{{ rawurlencode($comment->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $comment->commenter_id, null, 50) }}" width="50" height="50" class="comment_avatar" alt="{{ $comment->uname }}">
</a>
</div>
<div class="bubble_comment panel panel-skinbase">
<div class="panel-heading">
<div class="pull-right">{{
\Carbon\Carbon::parse($comment->datetime)->diffForHumans() }}
</div>
<h5 class="panel-title">Comment by: <a href="/profile/{{ $comment->commenter_id }}/{{ rawurlencode($comment->uname) }}">{{ $comment->uname }}</a></h5>
</div>
<div class="panel-body">
<div class="comment_box_image">
<a href="/art/{{ $comment->id }}/{{ $comment->artwork_slug }}">
<img src="{{ $comment->thumb }}" alt="{{ $comment->name }}" class="img-thumbnail img-responsive">
</a>
</div>
<div class="comment_text mt-2">
{!! nl2br(e($comment->comment_description)) !!}
</div>
@if (!empty($comment->signature))
<div class="panel-footer comment-footer mt-2">
{!! nl2br(e($comment->signature)) !!}
</div>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
<div class="paginationMenu text-center">
{{ $comments->links('pagination::bootstrap-4') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,66 @@
@extends('layouts.nova')
@php
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => route('legacy.monthly_commentators')],
]);
@endphp
@section('content')
<x-nova-page-header
section="Comments"
:title="$page_title"
icon="fa-ranking-star"
:breadcrumbs="$headerBreadcrumbs"
description="List of users who post the most comments in the current month."
headerClass="pb-6"
/>
<div class="container-fluid legacy-page pt-8">
<div class="container-main">
<div class="panel panel-default effect2">
<div class="panel-body">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Thumb</th>
<th>Name</th>
<th>Level</th>
<th>Country</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
@foreach ($rows as $row)
<tr>
<td width="50" class="text-center">
<a href="/profile/{{ (int)$row->user_id }}/{{ rawurlencode($row->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $row->user_id, null, 32) }}" width="30" alt="{{ $row->uname }}">
</a>
</td>
<td>
<a href="/profile/{{ (int)$row->user_id }}/{{ rawurlencode($row->uname) }}">{{ $row->uname }}</a>
</td>
<td width="100" class="text-center">
<img src="/gfx/member_stars/{{ (int)$row->user_type }}.gif" alt="level">
</td>
<td width="100" class="text-center">
@if (!empty($row->country_flag))
<img width="20" title="{{ $row->country_name }}" src="/gfx/flags/{{ rawurlencode($row->country_flag) }}" alt="{{ $row->country_name }}">
@endif
</td>
<td width="100" class="text-center">{{ (int)$row->num_comments }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="paginationMenu text-center">
{{ $rows->withQueryString()->links('pagination::bootstrap-4') }}
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,77 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="page-heading">
<h1 class="page-header">{{ $news->headline ?? 'News' }}</h1>
</div>
<br>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-user"></i>
<a href="/profile/{{ $news->user_id ?? '' }}/{{ Str::slug($news->uname ?? '') }}">{{ $news->uname ?? 'Unknown' }}</a>
<i class="fa fa-calendar"></i>
{{ date('j.F.y @ H:i:s', strtotime($news->create_date ?? now())) }}
</div>
<div class="panel-body">
<h1 class="panel-title" style="font-size:26px;color:#000;">{!! nl2br(e($news->headline ?? '')) !!}</h1>
<br>
<p>{!! html_entity_decode(stripslashes($news->content ?? '')) !!}</p>
</div>
</div>
<h3 class="lead">User Comments:</h3>
@foreach($comments as $ar)
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-user"></i>
<a href="/profile/{{ $ar->user_id ?? '' }}/{{ Str::slug($ar->uname ?? '') }}">{{ $ar->uname ?? 'Unknown' }}</a>
<i class="fa fa-calendar"></i>
{{ date('j.F.y @ H:i:s', strtotime($ar->posted ?? now())) }}
</div>
<div class="panel-body">
@if(!empty($ar->icon))
<div style="float:right;padding-left:20px;">
<a href="/profile/{{ $ar->user_id ?? '' }}/{{ Str::slug($ar->uname ?? '') }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) ($ar->user_id ?? 0), null, 50) }}" width="50" height="50" alt="">
</a>
</div>
@endif
<div>{!! nl2br(e($ar->message ?? '')) !!}</div>
@if(!empty($ar->signature))
<div class="label label-info">{!! nl2br(e($ar->signature)) !!}</div>
@endif
</div>
</div>
@endforeach
{{-- Comment form (legacy) --}}
@php
$status = $_SESSION['web_login']['status'] ?? false;
$user_type = $_SESSION['web_login']['user_type'] ?? 0;
@endphp
@if(!$status && $user_type < 2)
<div class="alert alert-warning"><i class="fa fa-warning"></i> You have to login to write comment</div>
@else
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-comment"></i> Write comment</div>
<div class="panel-body">
<form action="/news/{{ $news->news_id ?? '' }}/{{ Str::slug($news->headline ?? '') }}" method="post" class="form">
@csrf
<textarea name="comment" style="width:100%; height:215px;" class="tinymce"></textarea><br>
<input type="hidden" name="news_id" value="{{ $news->news_id ?? '' }}">
<input type="hidden" name="confirm" value="true">
<button type="submit" class="btn btn-success">Post Comment</button>
</form>
</div>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,693 @@
@extends('layouts.nova')
@php
$uname = $user->username ?? $user->name ?? 'Unknown';
$displayName = $user->name ?? $uname;
$avatarUrl = \App\Support\AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 128);
$genderMap = [
'M' => ['label' => 'Male', 'icon' => 'fa-mars', 'color' => 'text-blue-400'],
'F' => ['label' => 'Female', 'icon' => 'fa-venus', 'color' => 'text-pink-400'],
'X' => ['label' => 'N/A', 'icon' => 'fa-question', 'color' => 'text-gray-400'],
];
$genderCode = strtoupper((string) ($profile?->gender ?? 'X'));
$gender = $genderMap[$genderCode] ?? $genderMap['X'];
$birthdate = null;
if ($profile?->birthdate) {
try {
$bd = \Carbon\Carbon::parse($profile->birthdate);
if ($bd->year > 1900) {
$birthdate = $bd->format('F d, Y');
}
} catch (\Throwable) {}
}
$website = $profile?->website ?? null;
if ($website && !preg_match('#^https?://#i', $website)) {
$website = 'https://' . $website;
}
$about = $profile?->about ?? null;
$lastVisit = null;
if ($user->last_visit_at) {
try { $lastVisit = \Carbon\Carbon::parse($user->last_visit_at); } catch (\Throwable) {}
}
$socialIcons = [
'twitter' => ['icon' => 'fa-brands fa-x-twitter', 'label' => 'X / Twitter'],
'deviantart' => ['icon' => 'fa-brands fa-deviantart', 'label' => 'DeviantArt'],
'instagram' => ['icon' => 'fa-brands fa-instagram', 'label' => 'Instagram'],
'behance' => ['icon' => 'fa-brands fa-behance', 'label' => 'Behance'],
'artstation' => ['icon' => 'fa-solid fa-palette', 'label' => 'ArtStation'],
'youtube' => ['icon' => 'fa-brands fa-youtube', 'label' => 'YouTube'],
'website' => ['icon' => 'fa-solid fa-link', 'label' => 'Website'],
];
$seoPage = max(1, (int) request()->query('page', 1));
$seoBase = url()->current();
$seoQ = request()->query(); unset($seoQ['page']);
$seoUrl = fn(int $p) => $seoBase . ($p > 1
? '?' . http_build_query(array_merge($seoQ, ['page' => $p]))
: (count($seoQ) ? '?' . http_build_query($seoQ) : ''));
$seoPrev = $seoPage > 1 ? $seoUrl($seoPage - 1) : null;
$seoNext = (isset($artworks) && method_exists($artworks, 'nextPageUrl')) ? $artworks->nextPageUrl() : null;
@endphp
@push('head')
<link rel="canonical" href="{{ $seoUrl($seoPage) }}">
@if($seoPrev)<link rel="prev" href="{{ $seoPrev }}">@endif
@if($seoNext)<link rel="next" href="{{ $seoNext }}">@endif
<meta name="robots" content="index,follow">
<meta property="og:title" content="Profile: {{ e($uname) }} Skinbase.org">
<meta property="og:image" content="{{ $avatarUrl }}">
<meta property="og:url" content="{{ $page_canonical ?? url()->current() }}">
<style>
.profile-hero-bg {
position: relative;
overflow: hidden;
}
.profile-hero-bg::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(77,163,255,.12), transparent 60%),
radial-gradient(ellipse at 80% 20%, rgba(224,122,33,.08), transparent 50%);
pointer-events: none;
z-index: 1;
}
.nova-panel {
background: var(--panel-dark);
border: 1px solid var(--sb-line);
border-radius: 0.75rem;
overflow: hidden;
}
.nova-panel-header {
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--sb-line);
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.82rem;
color: var(--sb-text);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.nova-panel-body { padding: 1rem; }
.stat-item { text-align: center; padding: 0.5rem 0.75rem; }
.stat-item .stat-value {
font-size: 1.15rem;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
.stat-item .stat-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--sb-muted);
margin-top: 1px;
}
.profile-table td:first-child {
color: var(--sb-muted);
font-size: 0.75rem;
white-space: nowrap;
padding-right: 0.75rem;
padding-top: 0.45rem;
padding-bottom: 0.45rem;
}
.profile-table td:last-child {
color: var(--sb-text);
font-size: 0.82rem;
text-align: right;
}
.profile-table tr {
border-bottom: 1px solid rgba(42,42,51,0.5);
}
.profile-table tr:last-child { border-bottom: none; }
.profile-table td { vertical-align: middle; }
.comment-avatar {
width: 38px; height: 38px;
border-radius: 50%; object-fit: cover; flex-shrink: 0;
}
.follower-avatar {
width: 34px; height: 34px;
border-radius: 50%; object-fit: cover;
border: 1px solid var(--sb-line);
transition: opacity 0.2s;
}
.follower-avatar:hover { opacity: 0.85; }
.fav-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
gap: 4px;
}
.fav-grid a img {
width: 100%; aspect-ratio: 1; object-fit: cover;
border-radius: 4px; transition: opacity 0.2s;
display: block;
}
.fav-grid a:hover img { opacity: 0.82; }
.follow-btn { transition: all 0.2s ease; }
</style>
@endpush
@section('content')
{{-- ═══════════════════════════════════════════════════════════
PROFILE HERO
═══════════════════════════════════════════════════════════ --}}
<div class="profile-hero-bg border-b border-[--sb-line]"
@if(!empty($heroBgUrl))
style="background: url('{{ $heroBgUrl }}') center/cover no-repeat;"
@else
style="background: linear-gradient(135deg, rgba(15,23,36,1) 0%, rgba(21,30,46,1) 50%, rgba(9,16,26,1) 100%);"
@endif
>
{{-- Dark overlay so the content stays readable --}}
@if(!empty($heroBgUrl))
<div class="absolute inset-0 bg-gradient-to-r from-[#0f1724]/95 via-[#0f1724]/80 to-[#0f1724]/60"></div>
@endif
<div class="relative z-10 max-w-screen-xl mx-auto px-4 py-8">
<div class="flex flex-col sm:flex-row items-center sm:items-end gap-5">
{{-- Avatar --}}
<div class="shrink-0">
<img src="{{ $avatarUrl }}"
alt="{{ e($uname) }}"
class="w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover border-4 border-[--sb-line] shadow-lg">
</div>
{{-- Name + meta --}}
<div class="flex-1 text-center sm:text-left min-w-0">
<h1 class="text-2xl sm:text-3xl font-bold text-white leading-tight truncate">
{{ e($uname) }}
</h1>
@if($displayName && $displayName !== $uname)
<p class="text-[--sb-muted] text-sm mt-0.5">{{ e($displayName) }}</p>
@endif
@if($countryName)
<p class="text-[--sb-muted] text-sm mt-1 flex items-center justify-center sm:justify-start gap-1.5">
@if($profile?->country_code)
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
alt="{{ e($countryName) }}"
class="w-5 h-auto rounded-sm inline-block"
onerror="this.style.display='none'">
@endif
{{ e($countryName) }}
</p>
@endif
@if($lastVisit)
<p class="text-[--sb-muted] text-xs mt-1">
<i class="fa-solid fa-clock fa-fw mr-1"></i>
Last seen {{ $lastVisit->diffForHumans() }}
</p>
@endif
</div>
{{-- Action buttons --}}
<div class="shrink-0 flex flex-col gap-2 items-center sm:items-end">
@if(!$isOwner)
@auth
<div x-data="{
following: {{ $viewerIsFollowing ? 'true' : 'false' }},
count: {{ (int) $followerCount }},
loading: false,
hovering: false,
async toggle() {
this.loading = true;
try {
const r = await fetch('{{ route('profile.follow', ['username' => strtolower((string)$uname)]) }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Accept': 'application/json'
}
});
const d = await r.json();
if (r.ok) { this.following = d.following; this.count = d.follower_count; }
} catch(e) {}
this.loading = false;
}
}">
<button @click="toggle" @mouseenter="hovering=true" @mouseleave="hovering=false" :disabled="loading" class="follow-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border transition-all"
:class="following
? 'bg-green-500/10 border-green-500/40 text-green-400 hover:bg-red-500/10 hover:border-red-500/40 hover:text-red-400'
: 'bg-[--sb-blue]/10 border-[--sb-blue]/40 text-[--sb-blue] hover:bg-[--sb-blue]/20'">
<i class="fa-solid fa-fw"
:class="loading ? 'fa-circle-notch fa-spin' : (following ? (hovering ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus')"></i>
<span x-text="following ? (hovering ? 'Unfollow' : 'Following') : 'Follow'"></span>
<span class="text-xs opacity-60" x-text="'(' + count + ')'"></span>
</button>
</div>
@else
<a href="{{ route('login') }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border border-[--sb-blue]/40 text-[--sb-blue] hover:bg-[--sb-blue]/10 transition-all">
<i class="fa-solid fa-user-plus fa-fw"></i> Follow
<span class="text-xs opacity-60">({{ $followerCount }})</span>
</a>
@endauth
@else
<a href="{{ route('dashboard.profile') }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium border border-[--sb-line] text-[--sb-text] hover:bg-white/5 transition-all">
<i class="fa-solid fa-pen fa-fw"></i> Edit Profile
</a>
@endif
</div>
</div>
</div>
</div>
{{-- ═══════════════════════════════════════════════════════════
STATS STRIP
═══════════════════════════════════════════════════════════ --}}
<div class="bg-[--sb-panel2] border-b border-[--sb-line]">
<div class="max-w-screen-xl mx-auto px-4">
<div class="flex divide-x divide-[--sb-line] overflow-x-auto">
@foreach([
['value' => number_format($stats->uploads_count ?? 0), 'label' => 'Uploads', 'icon' => 'fa-cloud-arrow-up'],
['value' => number_format($stats->downloads_received_count ?? 0),'label' => 'Downloads', 'icon' => 'fa-download'],
['value' => number_format($stats->profile_views_count ?? 0), 'label' => 'Profile Views', 'icon' => 'fa-eye'],
['value' => number_format($followerCount), 'label' => 'Followers', 'icon' => 'fa-users'],
['value' => number_format($stats->following_count ?? 0), 'label' => 'Following', 'icon' => 'fa-user-check'],
['value' => number_format($stats->awards_received_count ?? 0), 'label' => 'Awards', 'icon' => 'fa-trophy'],
] as $si)
<div class="stat-item flex-1 py-3">
<div class="stat-value">{{ $si['value'] }}</div>
<div class="stat-label">
<i class="fa-solid {{ $si['icon'] }} fa-fw mr-0.5"></i>{{ $si['label'] }}
</div>
</div>
@endforeach
</div>
</div>
</div>
{{-- ═══════════════════════════════════════════════════════════
MAIN CONTENT
═══════════════════════════════════════════════════════════ --}}
<div class="max-w-screen-xl mx-auto px-4 py-6">
<div class="flex flex-col lg:flex-row gap-5">
{{-- ─── LEFT COLUMN (artworks) ─── --}}
<div class="flex-1 min-w-0 space-y-5">
{{-- Featured Artworks --}}
@if(isset($featuredArtworks) && $featuredArtworks->isNotEmpty())
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-star text-yellow-400 fa-fw"></i>
Featured Artworks
</div>
<div class="nova-panel-body">
<div class="flex flex-col md:flex-row gap-4">
@php $feat = $featuredArtworks->first() @endphp
{{-- Main featured --}}
<a href="/art/{{ $feat->id }}/{{ \Illuminate\Support\Str::slug($feat->name) }}"
class="flex-1 group block min-w-0">
<div class="overflow-hidden rounded-lg bg-black">
<img src="{{ $feat->thumb }}"
alt="{{ e($feat->name) }}"
class="w-full object-cover transition-transform duration-300 group-hover:scale-105"
style="aspect-ratio:4/3;">
</div>
<h4 class="mt-2 text-sm font-medium text-white truncate">{{ e($feat->name) }}</h4>
@if($feat->label)
<p class="text-xs text-[--sb-muted]">{{ e($feat->label) }}</p>
@endif
@if($feat->featured_at)
<p class="text-xs text-[--sb-muted] mt-0.5">
<i class="fa-solid fa-calendar fa-fw"></i>
Featured {{ \Carbon\Carbon::parse($feat->featured_at)->format('d M, Y') }}
</p>
@endif
</a>
{{-- Side featured (2nd & 3rd) --}}
@if($featuredArtworks->count() > 1)
<div class="md:w-44 space-y-2">
@foreach($featuredArtworks->slice(1) as $sideArt)
<a href="/art/{{ $sideArt->id }}/{{ \Illuminate\Support\Str::slug($sideArt->name) }}"
class="block group">
<div class="overflow-hidden rounded-md bg-black">
<img src="{{ $sideArt->thumb }}"
alt="{{ e($sideArt->name) }}"
class="w-full object-cover transition-transform duration-300 group-hover:scale-105"
style="aspect-ratio:16/9;">
</div>
<p class="text-xs text-[--sb-muted] mt-1 truncate">{{ e($sideArt->name) }}</p>
</a>
@endforeach
</div>
@endif
</div>
</div>
</div>
@endif
{{-- Newest Artworks --}}
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-images fa-fw text-[--sb-blue]"></i>
Newest Artworks
<a href="{{ !empty($user->username) ? route('profile.gallery', ['username' => strtolower((string) $user->username)]) : url('/gallery/'.$user->id.'/'.\Illuminate\Support\Str::slug($uname)) }}"
class="ml-auto text-xs text-[--sb-blue] hover:underline normal-case tracking-normal font-normal">
View Gallery <i class="fa-solid fa-arrow-right fa-fw"></i>
</a>
</div>
<div class="nova-panel-body">
@if(isset($artworks) && !$artworks->isEmpty())
<div class="gallery-grid grid grid-cols-2 gap-4"
data-nova-gallery
data-gallery-type="profile"
data-gallery-grid
data-profile-id="{{ $user->id }}">
@foreach($artworks as $art)
<x-artwork-card :art="$art" />
@endforeach
</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-6" data-gallery-skeleton></div>
@else
<p class="text-[--sb-muted] text-sm text-center py-8">
<i class="fa-solid fa-image fa-2x mb-3 block opacity-20"></i>
No artworks yet.
</p>
@endif
</div>
</div>
{{-- Favourites --}}
@if(isset($favourites) && $favourites->isNotEmpty())
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-heart fa-fw text-pink-400"></i>
Favourites
</div>
<div class="nova-panel-body">
<div class="grid grid-cols-2 gap-4">
@foreach($favourites as $fav)
<x-artwork-card :art="$fav" />
@endforeach
</div>
</div>
</div>
@endif
</div>{{-- end left --}}
{{-- ─── RIGHT SIDEBAR ─── --}}
<div class="lg:w-80 xl:w-96 shrink-0 space-y-4">
{{-- Profile Info --}}
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-id-card fa-fw text-[--sb-blue]"></i>
Profile
</div>
<div class="nova-panel-body">
<table class="profile-table w-full">
<tr>
<td>Username</td>
<td>{{ e($uname) }}</td>
</tr>
@if($displayName && $displayName !== $uname)
<tr><td>Real Name</td><td>{{ e($displayName) }}</td></tr>
@endif
<tr>
<td>Gender</td>
<td>
<i class="fa-solid {{ $gender['icon'] }} fa-fw {{ $gender['color'] }}"></i>
{{ $gender['label'] }}
</td>
</tr>
@if($birthdate)
<tr><td>Birthday</td><td>{{ $birthdate }}</td></tr>
@endif
@if($countryName)
<tr>
<td>Country</td>
<td class="flex items-center justify-end gap-1.5">
@if($profile?->country_code)
<img src="/gfx/flags/shiny/24/{{ rawurlencode($profile->country_code) }}.png"
alt="{{ e($countryName) }}"
class="w-4 h-auto rounded-sm"
onerror="this.style.display='none'">
@endif
{{ e($countryName) }}
</td>
</tr>
@endif
@if($website)
<tr>
<td>Website</td>
<td>
<a href="{{ e($website) }}" rel="nofollow noopener" target="_blank"
class="text-[--sb-blue] hover:underline text-xs inline-flex items-center gap-1">
<i class="fa-solid fa-link fa-fw"></i>
{{ e(parse_url($website, PHP_URL_HOST) ?? $website) }}
</a>
</td>
</tr>
@endif
@if($lastVisit)
<tr>
<td>Last Activity</td>
<td class="text-[11px]">
{{ $lastVisit->format('d.M.Y') }}
<i class="fa-solid fa-clock fa-fw ml-1 opacity-60"></i>
{{ $lastVisit->format('H:i') }}
</td>
</tr>
@endif
<tr>
<td>Member since</td>
<td>{{ $user->created_at ? $user->created_at->format('M Y') : 'N/A' }}</td>
</tr>
</table>
</div>
</div>
{{-- About Me --}}
@if($about)
<div class="nova-panel" x-data="{ expanded: false }">
<div class="nova-panel-header">
<i class="fa-solid fa-quote-left fa-fw text-purple-400"></i>
About Me
</div>
<div class="nova-panel-body">
<div class="text-sm text-[--sb-text] leading-relaxed"
:class="expanded ? '' : 'line-clamp-6'">
{!! nl2br(e($about)) !!}
</div>
@if(strlen($about) > 300)
<button @click="expanded = !expanded"
class="mt-2 text-xs text-[--sb-blue] hover:underline">
<span x-text="expanded ? '↑ Show less' : '↓ Read more'"></span>
</button>
@endif
</div>
</div>
@endif
{{-- Statistics --}}
@if($stats)
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-chart-bar fa-fw text-green-400"></i>
Statistics
</div>
<div class="nova-panel-body p-0">
<table class="profile-table w-full">
@foreach([
['Profile Views', number_format($stats->profile_views_count ?? 0), null],
['Uploads', number_format($stats->uploads_count ?? 0), null],
['Downloads', number_format($stats->downloads_received_count ?? 0), null],
['Page Views', number_format($stats->artwork_views_received_count ?? 0), null],
['Featured Works',number_format($stats->awards_received_count ?? 0), 'fa-star text-yellow-400'],
] as [$label, $value, $iconClass])
<tr>
<td class="pl-4">{{ $label }}</td>
<td class="pr-4">
{{ $value }}
@if($iconClass)<i class="fa-solid {{ $iconClass }} text-xs ml-1"></i>@endif
</td>
</tr>
@endforeach
</table>
</div>
</div>
@endif
{{-- Social Links --}}
@if(isset($socialLinks) && $socialLinks->isNotEmpty())
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-share-nodes fa-fw text-[--sb-blue]"></i>
Social Links
</div>
<div class="nova-panel-body flex flex-wrap gap-2">
@foreach($socialLinks as $platform => $link)
@php
$si = $socialIcons[$platform] ?? ['icon' => 'fa-solid fa-link', 'label' => ucfirst($platform)];
$href = str_starts_with($link->url, 'http') ? $link->url : ('https://' . $link->url);
@endphp
<a href="{{ e($href) }}" rel="nofollow noopener" target="_blank"
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs border border-[--sb-line] text-[--sb-text] hover:bg-white/5 hover:border-[--sb-blue]/40 transition-all">
<i class="{{ $si['icon'] }} fa-fw"></i>
{{ $si['label'] }}
</a>
@endforeach
</div>
</div>
@endif
{{-- Recent Followers --}}
@if(isset($recentFollowers) && $recentFollowers->isNotEmpty())
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-users fa-fw text-[--sb-blue]"></i>
Followers
<span class="ml-1 px-1.5 py-0.5 rounded text-xs bg-white/5 text-[--sb-muted]">
{{ number_format($followerCount) }}
</span>
<a href="/following/{{ $user->id }}/{{ \Illuminate\Support\Str::slug($uname) }}"
class="ml-auto text-xs text-[--sb-blue] hover:underline normal-case tracking-normal font-normal">
All
</a>
</div>
<div class="nova-panel-body">
<div class="flex flex-wrap gap-1.5">
@foreach($recentFollowers as $follower)
<a href="{{ $follower->profile_url }}" title="{{ e($follower->uname) }}">
<img src="{{ $follower->avatar_url }}"
alt="{{ e($follower->uname) }}"
class="follower-avatar"
onerror="this.src='{{ \App\Support\AvatarUrl::default() }}'">
</a>
@endforeach
</div>
</div>
</div>
@elseif($followerCount > 0)
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-users fa-fw text-[--sb-blue]"></i>
Followers
<span class="ml-1 px-1.5 py-0.5 rounded text-xs bg-white/5 text-[--sb-muted]">
{{ number_format($followerCount) }}
</span>
</div>
</div>
@endif
{{-- Profile Comments --}}
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-comments fa-fw text-orange-400"></i>
Comments
@if(isset($profileComments) && $profileComments->isNotEmpty())
<span class="ml-1 px-1.5 py-0.5 rounded text-xs bg-white/5 text-[--sb-muted]">
{{ $profileComments->count() }}
</span>
@endif
</div>
<div class="nova-panel-body">
@if(!isset($profileComments) || $profileComments->isEmpty())
<p class="text-[--sb-muted] text-xs text-center py-3">No comments yet.</p>
@else
<div class="space-y-4">
@foreach($profileComments as $comment)
<div class="flex gap-3">
<a href="{{ $comment->author_profile_url }}" class="shrink-0">
<img src="{{ $comment->author_avatar }}"
alt="{{ e($comment->author_name) }}"
class="comment-avatar"
onerror="this.src='{{ \App\Support\AvatarUrl::default() }}'">
</a>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<a href="{{ $comment->author_profile_url }}"
class="text-xs font-semibold text-[--sb-text] hover:text-[--sb-blue] transition-colors">
{{ e($comment->author_name) }}
</a>
<span class="text-[--sb-muted] text-[10px] ml-auto whitespace-nowrap">
{{ \Carbon\Carbon::parse($comment->created_at)->diffForHumans() }}
</span>
</div>
<p class="text-xs text-[--sb-text] leading-relaxed break-words">
{!! nl2br(e($comment->body)) !!}
</p>
@if(!empty($comment->author_signature))
<p class="text-[--sb-muted] text-[10px] mt-1 italic border-t border-[--sb-line] pt-1 opacity-70">
{!! nl2br(e($comment->author_signature)) !!}
</p>
@endif
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
{{-- Write Comment --}}
@auth
@if(auth()->id() !== $user->id)
<div class="nova-panel">
<div class="nova-panel-header">
<i class="fa-solid fa-pen fa-fw text-[--sb-blue]"></i>
Write a Comment
</div>
<div class="nova-panel-body">
@if(session('status') === 'Comment posted!')
<div class="mb-3 px-3 py-2 rounded-lg bg-green-500/10 border border-green-500/30 text-green-400 text-xs">
<i class="fa-solid fa-check fa-fw"></i> Comment posted!
</div>
@endif
<form method="POST"
action="{{ route('profile.comment', ['username' => strtolower((string)$uname)]) }}">
@csrf
<textarea name="body" rows="4" required minlength="2" maxlength="2000"
placeholder="Write a comment for {{ e($uname) }}..."
class="w-full bg-[--sb-bg] border border-[--sb-line] rounded-lg px-3 py-2 text-sm text-[--sb-text] placeholder:text-[--sb-muted]/60 resize-none focus:outline-none focus:border-[--sb-blue]/50 transition-colors"
>{{ old('body') }}</textarea>
@error('body')
<p class="mt-1 text-xs text-red-400">{{ $message }}</p>
@enderror
<div class="mt-2 text-right">
<button type="submit"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium bg-[--sb-blue]/90 hover:bg-[--sb-blue] text-white transition-colors">
<i class="fa-solid fa-paper-plane fa-fw"></i>
Post Comment
</button>
</div>
</form>
</div>
</div>
@endif
@else
<div class="nova-panel">
<div class="nova-panel-body text-center py-4">
<p class="text-[--sb-muted] text-sm">
<a href="{{ route('login') }}" class="text-[--sb-blue] hover:underline">Log in</a>
to leave a comment.
</p>
</div>
</div>
@endauth
</div>{{-- end right sidebar --}}
</div>{{-- end flex --}}
</div>{{-- end container --}}
@endsection
@push('scripts')
@endpush

View File

@@ -0,0 +1,63 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Received Comments</h1>
<p>List of comments left on your uploaded artworks.</p>
</header>
</div>
<div class="masonry">
@foreach($comments as $comment)
@php
$author = $comment->user;
$art = $comment->artwork;
$created = optional($comment->created_at);
@endphp
<div class="masonry_item col-sm-6 col-md-4">
<div class="comment_box effect3">
<div class="cb_image">
<a href="/profile/{{ (int)($author->id ?? $author->user_id) }}/{{ rawurlencode($author->name ?? $author->uname ?? '') }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) ($author->id ?? $author->user_id), null, 50) }}" width="50" height="50" class="comment_avatar" alt="{{ $author->name ?? $author->uname ?? '' }}">
</a>
</div>
<div class="bubble_comment panel panel-skinbase">
<div class="panel-heading">
<div class="pull-right">{{ $created ? $created->diffForHumans() . ' ago' : '' }}</div>
<h5 class="panel-title">Comment by: <a href="/profile/{{ (int)($author->id ?? $author->user_id) }}/{{ rawurlencode($author->name ?? $author->uname ?? '') }}">{{ $author->name ?? $author->uname ?? '' }}</a></h5>
</div>
<div class="panel-body">
<div class="comment_box_image">
@if($art)
<a href="/art/{{ $art->id }}/{{ Str::slug($art->name ?? '') }}">
<img src="{{ $art->thumb }}" class="img-thumbnail img-responsive" alt="{{ $art->name }}">
</a>
@endif
</div>
<div class="comment_text">{!! nl2br(e($comment->content ?? $comment->description ?? '')) !!}</div>
@if(!empty($author->signature))
<div class="panel-footer comment-footer">
<p>{!! nl2br(e($author->signature)) !!}</p>
</div>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
@if($comments->hasPages())
<div class="paginationMenu">
{{ $comments->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,82 @@
@extends('layouts.nova')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h1 class="page-header" style="margin:0;display:inline-block">Artwork Statistics</h1>
<span class="text-muted pull-right" style="margin-top:10px">
Uploaded: {{ method_exists($artworks, 'total') ? $artworks->total() : $artworks->count() }}
</span>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">Sort your uploads by popularity, downloads, comments, and date.</div>
</div>
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover" style="margin-bottom:0">
<thead>
<tr>
<th style="width:90px" class="text-center">Thumbnail</th>
<th>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'name']) }}">Name</a>
</th>
<th style="width:110px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'dls']) }}">Downloads</a>
</th>
<th style="width:95px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'comments']) }}">Reviews</a>
</th>
<th style="width:180px">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'category']) }}">Section</a>
</th>
<th style="width:130px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'date']) }}">Date</a>
</th>
</tr>
</thead>
<tbody>
@forelse($artworks as $art)
<tr>
<td class="text-center">
<a href="/art/{{ (int) $art->id }}" title="View">
<img
src="{{ $art->thumb_url ?? 'https://files.skinbase.org/default/missing_md.webp' }}"
@if(!empty($art->thumb_srcset)) srcset="{{ $art->thumb_srcset }}" @endif
sizes="70px"
alt="{{ $art->name ?? '' }}"
class="img-thumbnail"
style="width:70px;height:70px;object-fit:cover"
>
</a>
</td>
<td>
<a href="/art/{{ (int) $art->id }}">{{ $art->name ?? '' }}</a>
<div class="text-muted" style="font-size:12px">#{{ (int) $art->id }}</div>
</td>
<td class="text-center">{{ (int) ($art->dls ?? 0) }}</td>
<td class="text-center">{{ (int) ($art->num_comments ?? 0) }}</td>
<td>{{ $art->category_name ?? '' }}</td>
<td class="text-center">
@if(!empty($art->datum))
{{ is_string($art->datum) ? date('d.m.Y', strtotime($art->datum)) : '' }}
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6">No artworks found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="paginationMenu text-center">
{{ $artworks->links('pagination::bootstrap-3') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,79 @@
@extends('layouts.nova')
@php
$headerBreadcrumbs = collect([
(object) ['name' => $page_title ?? 'Today in History', 'url' => route('legacy.today_in_history')],
]);
$galleryArtworks = collect(method_exists($artworks, 'items') ? $artworks->items() : [])->map(fn ($art) => [
'id' => $art->id ?? null,
'name' => $art->name ?? null,
'slug' => $art->slug ?? null,
'url' => $art->url ?? $art->art_url ?? null,
'thumb' => $art->thumb_url ?? null,
'thumb_url' => $art->thumb_url ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? $art->uname ?? '',
'avatar_url' => $art->avatar_url ?? null,
'content_type_name' => $art->content_type_name ?? '',
'content_type_slug' => $art->content_type_slug ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
])->values();
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
@endphp
@section('content')
<x-nova-page-header
section="History"
:title="$page_title ?? 'Today in History'"
icon="fa-calendar-days"
:breadcrumbs="$headerBreadcrumbs"
:description="'Featured artworks uploaded on <span class=&quot;text-white/80 font-medium&quot;>' . e($todayLabel ?? now()->format('F j')) . '</span> in past years.'"
headerClass="pb-6"
/>
<section class="px-6 pt-8 md:px-10">
@if ($artworks && $artworks->count())
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="today-in-history"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="36"
class="min-h-32"
></div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No featured artworks were found for {{ $todayLabel ?? now()->format('F j') }}.</p>
<p class="text-white/25 text-xs mt-1">When historical features exist for this date, they will appear here.</p>
</div>
@endif
</section>
@endsection
@push('styles')
<style>
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,117 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Top Members</h1>
<p>Statistics of popular and active members at Skinbase</p>
</header>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>Most Active Members</strong></div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2">Username</th>
<th class="text-right">Uploads</th>
<th class="text-right">Total Downloads</th>
</tr>
</thead>
<tbody>
@foreach($topUsers as $u)
<tr>
<td width="80">
<a href="/profile/{{ $u->user_id }}/{{ \Illuminate\Support\Str::slug($u->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $u->user_id, null, 32) }}" width="30" height="30" alt="{{ $u->uname }}">
</a>
</td>
<td>
<a href="/profile/{{ $u->user_id }}/{{ \Illuminate\Support\Str::slug($u->uname) }}">{{ $u->uname }}</a>
</td>
<td class="text-right">{{ $u->uploads }}</td>
<td class="text-right">{{ $u->total_downloads }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>Most Followed Members</strong></div>
<div class="panel-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Thumb</th>
<th>Name</th>
<th>Followers</th>
</tr>
</thead>
<tbody>
@foreach($topFollowers as $f)
<tr>
<td width="60" align="center">
<a href="/profile/{{ $f->user_id }}/{{ \Illuminate\Support\Str::slug($f->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $f->user_id, null, 32) }}" width="30" alt="{{ $f->uname }}">
</a>
</td>
<td>
<a href="/profile/{{ $f->user_id }}/{{ \Illuminate\Support\Str::slug($f->uname) }}">{{ $f->uname }}</a>
</td>
<td align="center">{{ $f->num }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>Top Commentators</strong></div>
<div class="panel-body">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Thumb</th>
<th>Name</th>
<th>Level</th>
<th>Country</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
@foreach($topCommentators as $c)
<tr>
<td width="50" align="center">
<a href="/profile/{{ $c->user_id }}/{{ \Illuminate\Support\Str::slug($c->uname) }}">
<img src="{{ \App\Support\AvatarUrl::forUser((int) $c->user_id, null, 32) }}" width="30" alt="{{ $c->uname }}">
</a>
</td>
<td>
<a href="/profile/{{ $c->user_id }}/{{ \Illuminate\Support\Str::slug($c->uname) }}">{{ $c->uname }}</a>
</td>
<td width="100" align="center"><img src="/gfx/member_stars/{{ $c->user_type }}.gif" alt="level"></td>
<td width="100" align="center">
@if(!empty($c->country_flag))
<img width="20" src="/gfx/flags/{{ rawurlencode($c->country_flag) }}" title="{{ $c->country_name }}">
@endif
</td>
<td width="100" align="center">{{ $c->num_comments }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,79 @@
@extends('layouts.nova')
@php
$headerBreadcrumbs = collect([
(object) ['name' => $page_title, 'url' => route('legacy.top_favourites')],
]);
$galleryArtworks = collect(method_exists($artworks, 'items') ? $artworks->items() : [])->map(fn ($art) => [
'id' => $art->id ?? null,
'name' => $art->name ?? null,
'slug' => $art->slug ?? null,
'url' => $art->url ?? null,
'thumb' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->username ?? $art->uname ?? '',
'avatar_url' => $art->avatar_url ?? null,
'content_type_name' => $art->content_type_name ?? '',
'content_type_slug' => $art->content_type_slug ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'favourites' => (int) ($art->favourites ?? $art->num ?? 0),
])->values();
$galleryNextPageUrl = method_exists($artworks, 'nextPageUrl') ? $artworks->nextPageUrl() : null;
@endphp
@section('content')
<x-nova-page-header
section="Favourites"
:title="$page_title"
icon="fa-heart"
:breadcrumbs="$headerBreadcrumbs"
description="Most popular artworks which users added in their favourites."
headerClass="pb-6"
/>
<section class="px-6 pt-8 md:px-10">
@if ($artworks && $artworks->count())
<div
data-react-masonry-gallery
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="top-favourites"
@if ($galleryNextPageUrl) data-next-page-url="{{ $galleryNextPageUrl }}" @endif
data-limit="21"
class="min-h-32"
></div>
@else
<div class="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
<p class="text-white/40 text-sm">No artworks have been favourited yet.</p>
<p class="text-white/25 text-xs mt-1">Once members start saving artworks, they will appear here.</p>
</div>
@endif
</section>
@endsection
@push('styles')
<style>
@media (min-width: 1024px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (min-width: 2600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}
</style>
@endpush
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush

View File

@@ -0,0 +1,368 @@
@extends('layouts.nova')
@section('content')
@php
$birthDay = $birthDay ?? null;
$birthMonth = $birthMonth ?? null;
$birthYear = $birthYear ?? null;
$avatarUserId = (int) ($user->id ?? auth()->id() ?? 0);
$avatarHash = $user->icon
?? optional(auth()->user())->profile->avatar_hash
?? null;
$currentAvatarUrl = !empty($avatarHash)
? \App\Support\AvatarUrl::forUser($avatarUserId, $avatarHash, 128)
: \App\Support\AvatarUrl::default();
@endphp
@push('styles')
<style>
.edit-profile-hero { position: relative; overflow: hidden; }
</style>
@endpush
{{-- ── Hero background ──────────────────────────────────────────────── --}}
<div class="edit-profile-hero border-b border-[--sb-line]"
@if(!empty($heroBgUrl))
style="background: url('{{ $heroBgUrl }}') center/cover no-repeat;"
@else
style="background: linear-gradient(135deg, rgba(15,23,36,1) 0%, rgba(21,30,46,1) 50%, rgba(9,16,26,1) 100%);"
@endif
>
@if(!empty($heroBgUrl))
<div class="absolute inset-0 bg-gradient-to-r from-[#0f1724]/95 via-[#0f1724]/80 to-[#0f1724]/50"></div>
@endif
<div class="relative z-10 max-w-5xl mx-auto px-4 py-10 flex items-end gap-5">
<img src="{{ $currentAvatarUrl }}"
alt="Your avatar"
class="w-20 h-20 rounded-full object-cover border-4 border-[--sb-line] shadow-lg shrink-0">
<div>
<h1 class="text-2xl font-bold text-white">Edit Profile</h1>
<p class="text-sm text-[--sb-muted] mt-1">Manage your account settings</p>
</div>
</div>
</div>
<div class="min-h-screen text-white py-12">
<!-- Container -->
<div class="max-w-5xl mx-auto px-4">
@if ($errors->any())
<div class="mb-4 rounded-lg bg-red-700/10 border border-red-700/20 p-3 text-sm text-red-300">
<div class="font-semibold mb-2">Please fix the following errors:</div>
<ul class="list-disc pl-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- ================= Profile Card ================= -->
<div class="bg-nova-800 rounded-xl shadow-lg p-8 mb-10">
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- LEFT COLUMN -->
<div class="space-y-5">
<!-- Email -->
<div>
<label class="form-label">Email</label>
<input type="email" name="email"
class="form-input"
value="{{ old('email', auth()->user()->email) }}">
</div>
<!-- Username -->
<div>
<label class="form-label">Username</label>
<input type="text" name="username"
class="form-input"
value="{{ old('username', auth()->user()->username) }}"
readonly>
</div>
<!-- Real Name -->
<div>
<label class="form-label">Real Name</label>
<input type="text" name="name"
class="form-input"
value="{{ old('name', auth()->user()->name) }}">
</div>
<!-- Homepage -->
<div>
<label class="form-label">Homepage</label>
<input type="url" name="homepage"
class="form-input"
placeholder="https://"
value="{{ old('homepage', auth()->user()->homepage ?? auth()->user()->website ?? '') }}">
</div>
<!-- Birthday -->
<div>
<label class="form-label">Birthday</label>
<div class="grid grid-cols-3 gap-3">
@php
$currentYear = date('Y');
$startYear = $currentYear - 100;
$months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
@endphp
<select name="day" class="form-input" aria-label="Day">
<option value="">Day</option>
@for($d = 1; $d <= 31; $d++)
<option value="{{ $d }}" @if(intval(old('day', $birthDay)) == $d) selected @endif>{{ $d }}</option>
@endfor
</select>
<select name="month" class="form-input" aria-label="Month">
<option value="">Month</option>
@foreach($months as $idx => $m)
@php $val = $idx + 1; @endphp
<option value="{{ $val }}" @if(intval(old('month', $birthMonth)) == $val) selected @endif>{{ $m }}</option>
@endforeach
</select>
<select name="year" class="form-input" aria-label="Year">
<option value="">Year</option>
@for($y = $currentYear; $y >= $startYear; $y--)
<option value="{{ $y }}" @if(intval(old('year', $birthYear)) == $y) selected @endif>{{ $y }}</option>
@endfor
</select>
</div>
</div>
<!-- Gender -->
<div>
<label class="form-label">Gender</label>
<div class="flex gap-6 mt-2 text-soft">
<label><input type="radio" name="gender" value="m" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'm') checked @endif> Male</label>
<label><input type="radio" name="gender" value="f" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'f') checked @endif> Female</label>
<label><input type="radio" name="gender" value="x" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'x') checked @endif> N/A</label>
</div>
</div>
<!-- Country -->
<div>
<label class="form-label">Country</label>
<input type="text" name="country"
class="form-input"
value="{{ old('country', auth()->user()->country ?? auth()->user()->country_code ?? '') }}">
</div>
<!-- Preferences -->
<div class="flex gap-6 text-soft">
<label>
<input type="checkbox" name="mailing" @if(old('mailing', auth()->user()->mlist ?? false)) checked @endif>
Mailing List
</label>
<label>
<input type="checkbox" name="notify" @if(old('notify', auth()->user()->friend_upload_notice ?? false)) checked @endif>
Upload Notifications
</label>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="space-y-5">
<!-- Avatar -->
<div>
<label class="form-label">Avatar</label>
<div class="rounded-xl border border-sb-line bg-black/10 p-4">
<div class="flex items-center gap-4">
<img
id="avatarPreviewImage"
src="{{ $currentAvatarUrl }}"
alt="{{ $user->name ?? $user->username ?? 'Avatar' }}"
class="w-24 h-24 rounded-full object-cover ring-1 ring-white/10"
loading="lazy"
decoding="async"
>
<div class="min-w-0 flex-1">
<button
type="button"
id="avatarDropzone"
class="w-full rounded-lg border border-dashed border-sb-line px-4 py-4 text-left hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-sb-blue/50"
>
<div class="text-sm text-white/90">Drag & drop a new avatar here</div>
<div class="text-xs text-soft mt-1">or click to browse (JPG, PNG, WEBP)</div>
<div id="avatarFileName" class="text-xs text-sb-muted mt-2 truncate">No file selected</div>
</button>
<input id="avatarInput" type="file" name="avatar" class="sr-only" accept="image/jpeg,image/png,image/webp">
</div>
</div>
</div>
</div>
<!-- Emoticon -->
<div>
<label class="form-label">Emoticon</label>
<input type="file" name="emoticon" class="form-file">
</div>
<!-- Personal Picture -->
<div>
<label class="form-label">Personal Picture</label>
<input type="file" name="photo" class="form-file">
</div>
<!-- About -->
<div>
<label class="form-label">About Me</label>
<textarea name="about" rows="4"
class="form-textarea">{{ old('about', auth()->user()->about ?? auth()->user()->about_me ?? '') }}</textarea>
</div>
<!-- Signature -->
<div>
<label class="form-label">Signature</label>
<textarea name="signature" rows="3"
class="form-textarea">{{ old('signature', auth()->user()->signature ?? '') }}</textarea>
</div>
<!-- Description -->
<div>
<label class="form-label">Description</label>
<textarea name="description" rows="4"
class="form-textarea">{{ old('description', auth()->user()->description ?? '') }}</textarea>
</div>
</div>
</div>
<!-- Save Button -->
<div class="mt-8 text-right">
<button type="submit"
class="btn-primary">
Update Profile
</button>
</div>
</form>
</div>
<!-- ================= PASSWORD CARD ================= -->
<div class="bg-nova-800 rounded-xl shadow-lg p-8">
<h2 class="text-xl font-semibold mb-6">
Change Password
</h2>
<form method="POST" action="{{ route('profile.password') }}">
@csrf
@method('PUT')
<div class="space-y-5 max-w-md">
<div>
<label class="form-label">Current Password</label>
<input type="password" name="current_password"
class="form-input">
</div>
<div>
<label class="form-label">New Password</label>
<input type="password" name="password"
class="form-input">
</div>
<div>
<label class="form-label">Repeat Password</label>
<input type="password" name="password_confirmation"
class="form-input">
</div>
<button class="btn-secondary">
Change Password
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const input = document.getElementById('avatarInput');
const dropzone = document.getElementById('avatarDropzone');
const preview = document.getElementById('avatarPreviewImage');
const fileName = document.getElementById('avatarFileName');
if (!input || !dropzone || !preview || !fileName) {
return;
}
const updatePreview = (file) => {
if (!file || !file.type || !file.type.startsWith('image/')) {
return;
}
const objectUrl = URL.createObjectURL(file);
preview.src = objectUrl;
fileName.textContent = file.name;
preview.onload = () => URL.revokeObjectURL(objectUrl);
};
dropzone.addEventListener('click', () => input.click());
input.addEventListener('change', () => {
const file = input.files && input.files[0] ? input.files[0] : null;
if (file) {
updatePreview(file);
}
});
['dragenter', 'dragover'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
dropzone.classList.add('ring-2', 'ring-sb-blue/50');
});
});
['dragleave', 'drop'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
dropzone.classList.remove('ring-2', 'ring-sb-blue/50');
});
});
dropzone.addEventListener('drop', (event) => {
const file = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null;
if (!file) {
return;
}
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
updatePreview(file);
});
})();
</script>
@endpush

View File

@@ -0,0 +1,86 @@
@extends('layouts.nova')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h2 style="margin:0;display:inline-block">Edit Artwork</h2>
<a class="btn btn-default btn-xs pull-right" href="{{ route('dashboard.artworks.index') }}" style="margin-top:6px">
<i class="fa fa-arrow-left fa-fw"></i> Back to list
</a>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">
#{{ $artwork->id }} &middot; {{ $artwork->is_public ? 'Public' : 'Private' }}
</div>
</div>
@if(session('status'))
<div class="alert alert-success" role="status">{{ session('status') }}</div>
@endif
@if($errors->any())
<div class="alert alert-danger" role="alert">
<strong>Please fix the errors below.</strong>
</div>
@endif
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<form method="POST" action="{{ route('dashboard.artworks.update', $artwork->id) }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-7">
<div class="form-group @error('title') has-error @enderror">
<label for="title" class="control-label">Title</label>
<input id="title" type="text" name="title" class="form-control" value="{{ old('title', $artwork->title) }}" required>
@error('title')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<div class="form-group @error('description') has-error @enderror">
<label for="description" class="control-label">Description</label>
<textarea id="description" name="description" class="form-control" rows="8">{{ old('description', $artwork->description) }}</textarea>
@error('description')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="btn btn-success">
<i class="fa fa-save fa-fw"></i> Save Changes
</button>
</div>
<div class="col-md-5">
<label class="control-label">Preview</label>
<div class="thumbnail" style="margin-top:6px">
<img
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
@if(!empty($artwork->thumb_srcset)) srcset="{{ $artwork->thumb_srcset }}" @endif
sizes="(max-width: 992px) 100vw, 400px"
class="img-responsive"
alt="{{ $artwork->title }}"
>
</div>
<div class="form-group @error('file') has-error @enderror">
<label for="file" class="control-label">Replace image (optional)</label>
<input id="file" type="file" name="file" class="form-control" accept="image/*">
<p class="help-block">Max 100MB. Images only.</p>
@error('file')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<p class="text-muted" style="font-size:12px;margin-bottom:0">
Created: {{ optional($artwork->created_at)->format('d.m.Y') }}
</p>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,105 @@
@extends('layouts.nova')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h2 style="margin:0;display:inline-block">My Artworks</h2>
<span class="text-muted pull-right" style="margin-top:6px">
{{ method_exists($artworks, 'total') ? $artworks->total() : $artworks->count() }} items
</span>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">Manage and edit your uploads.</div>
</div>
@if(session('status'))
<div class="alert alert-success" role="status">{{ session('status') }}</div>
@endif
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover table-advanced" style="margin-bottom:0">
<thead>
<tr>
<th style="width:92px">Preview</th>
<th>Title</th>
<th style="width:120px" class="text-center">Created</th>
<th style="width:90px" class="text-center">Public</th>
<th style="width:95px" class="text-center">Approved</th>
<th style="width:150px" class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@forelse($artworks as $artwork)
<tr>
<td class="text-center">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}" title="Edit {{ $artwork->title }}">
<img
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
@if(!empty($artwork->thumb_srcset)) srcset="{{ $artwork->thumb_srcset }}" @endif
sizes="70px"
alt="{{ $artwork->title }}"
class="img-thumbnail"
style="width:70px;height:70px;object-fit:cover"
>
</a>
</td>
<td>
<div style="font-weight:700;line-height:1.2">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}">{{ $artwork->title }}</a>
</div>
<div class="text-muted" style="font-size:12px">#{{ $artwork->id }}</div>
@if(!empty($artwork->description))
<div class="text-muted" style="margin-top:4px;font-size:12px">
{{ str($artwork->description)->stripTags()->limit(120) }}
</div>
@endif
</td>
<td class="text-center">{{ optional($artwork->created_at)->format('d.m.Y') }}</td>
<td class="text-center">
@if($artwork->is_public)
<span class="label label-success">Yes</span>
@else
<span class="label label-default">No</span>
@endif
</td>
<td class="text-center">
@if($artwork->is_approved)
<span class="label label-primary">Yes</span>
@else
<span class="label label-warning">No</span>
@endif
</td>
<td class="text-center">
<div class="btn-group" role="group" aria-label="Actions">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}" class="btn btn-xs btn-default">
<i class="fa fa-pencil fa-fw"></i> Edit
</a>
<form method="POST" action="{{ route('dashboard.artworks.destroy', $artwork->id) }}" style="display:inline-block" onsubmit="return confirm('Delete this artwork?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-xs btn-danger">
<i class="fa fa-trash fa-fw"></i> Delete
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6">No artworks found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="paginationMenu text-center">
{{ $artworks->links('pagination::bootstrap-3') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,56 @@
@extends('layouts.nova')
@php
$presentMd = $presentMd ?? \App\Services\ThumbnailPresenter::present($artwork, 'md');
$presentLg = $presentLg ?? \App\Services\ThumbnailPresenter::present($artwork, 'lg');
$presentXl = $presentXl ?? \App\Services\ThumbnailPresenter::present($artwork, 'xl');
$presentSq = $presentSq ?? \App\Services\ThumbnailPresenter::present($artwork, 'sq');
$canonicalUrl = route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]);
$meta = $meta ?? [
'title' => trim((string) ($artwork->title ?? 'Artwork') . ' by ' . (string) ($artwork->user?->name ?? $artwork->user?->username ?? 'Unknown Author') . ' | Skinbase'),
'description' => (string) ($artwork->description ?? ''),
'canonical' => $canonicalUrl,
'og_image' => $presentXl['url'] ?? $presentLg['url'] ?? $presentMd['url'] ?? null,
'og_width' => $presentXl['width'] ?? $presentLg['width'] ?? null,
'og_height' => $presentXl['height'] ?? $presentLg['height'] ?? null,
];
$artworkData = $artworkData ?? [];
$relatedItems = $relatedItems ?? [];
$comments = $comments ?? [];
$groupSummary = $groupSummary ?? null;
$useUnifiedSeo = true;
@endphp
@push('head')
@php
$preloadSrcset = ($presentMd['url'] ?? '') . ' 640w, ' . ($presentLg['url'] ?? '') . ' 1280w, ' . ($presentXl['url'] ?? '') . ' 1920w';
@endphp
@if(!empty($presentLg['url']))
<link rel="preload" as="image"
href="{{ $presentLg['url'] }}"
imagesrcset="{{ trim($preloadSrcset) }}"
imagesizes="(min-width: 1280px) 1200px, (min-width: 768px) 90vw, 100vw">
@endif
@endpush
@section('content')
<div id="artwork-page"
data-artwork='@json($artworkData)'
data-related='@json($relatedItems)'
data-present-md='@json($presentMd)'
data-present-lg='@json($presentLg)'
data-present-xl='@json($presentXl)'
data-present-sq='@json($presentSq)'
data-cdn='@json(rtrim((string) config("cdn.files_url", "https://files.skinbase.org"), "/"))'
data-canonical='@json($meta["canonical"])'
data-comments='@json($comments)'
data-group-summary='@json($groupSummary)'
data-is-authenticated='@json(auth()->check())'>
</div>
@vite(['resources/js/Pages/ArtworkPage.jsx'])
@endsection

View File

@@ -0,0 +1,28 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Confirm Password</h2>
<p class="text-sm text-white/60 mb-6">Please confirm your password before continuing.</p>
<form method="POST" action="{{ route('password.confirm') }}" class="mt-4">
@csrf
<div>
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="current-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,35 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Reset Password</h2>
<p class="text-sm text-white/60 mb-6">Enter your email and we'll send a link to reset your password.</p>
@include('auth.partials.help-links', [
'title' => 'Need help recovering access?',
'description' => 'Use the auth guide for recovery basics or the troubleshooting page if the issue still feels broader than a reset link.',
'showLogin' => true,
])
<x-auth-session-status class="mt-4 mb-2 text-green-300" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}" class="mt-4">
@csrf
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<x-text-input id="email" name="email" type="email" :value="old('email')" required autofocus class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,88 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Login</h2>
<p class="text-sm text-white/60 mb-6">Access your Skinbase account.</p>
@include('auth.partials.help-links', [
'title' => 'Need help signing in?',
'description' => 'Use the signup and login guide or the troubleshooting page before spending time guessing at the wrong fix.',
'showReset' => true,
])
<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
@if($errors->has('bot'))
<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('bot') }}
</div>
@endif
@include('auth.partials.social-login')
<form method="POST" action="{{ route('login') }}" class="space-y-5" data-bot-form>
@csrf
<input type="text" name="homepage_url" value="" tabindex="-1" autocomplete="off" class="hidden" aria-hidden="true">
<input type="hidden" name="_bot_fingerprint" value="">
@php
$captchaProvider = $captcha['provider'] ?? 'turnstile';
$captchaSiteKey = $captcha['siteKey'] ?? '';
@endphp
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="you@example.com" value="{{ old('email') }}" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div>
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<input id="password" name="password" type="password" required placeholder="••••••••••" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
@if(($requiresCaptcha ?? false) && $captchaSiteKey !== '')
@if($captchaProvider === 'recaptcha')
<div class="g-recaptcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@elseif($captchaProvider === 'hcaptcha')
<div class="h-captcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@else
<div class="cf-turnstile" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@endif
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
@endif
<div class="flex items-center justify-between text-sm text-white/60">
<label class="flex items-center gap-2">
<input type="checkbox" name="remember" class="rounded bg-slate-800 border-white/20" />
Remember me
</label>
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="text-purple-400 hover:underline">Forgot password?</a>
@endif
</div>
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-400 hover:to-pink-400 text-white transition">Sign In</button>
<p class="text-sm text-center text-white/60">Dont have an account? <a href="{{ route('register') }}" class="text-purple-400 hover:underline">Create one</a></p>
</form>
</div>
</div>
</div>
@if(($requiresCaptcha ?? false) && (($captcha['siteKey'] ?? '') !== '') && (($captcha['scriptUrl'] ?? '') !== ''))
<script src="{{ $captcha['scriptUrl'] }}" async defer></script>
@endif
@include('partials.bot-fingerprint-script')
@endsection

View File

@@ -0,0 +1,24 @@
@php
$title = $title ?? 'Need help?';
$description = $description ?? 'Use the help guides when the fastest next step is still unclear.';
$showReset = $showReset ?? false;
$showLogin = $showLogin ?? false;
@endphp
<div class="mt-6 rounded-2xl border border-sky-400/15 bg-sky-400/10 p-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/80">{{ $title }}</p>
<p class="mt-2 text-sm leading-6 text-slate-200/85">{{ $description }}</p>
<div class="mt-4 flex flex-wrap gap-3 text-sm">
<a href="{{ route('help.auth') }}" class="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">Read signup and login help</a>
<a href="{{ route('help.troubleshooting') }}" class="rounded-full border border-white/10 bg-black/20 px-4 py-2 font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Open troubleshooting</a>
@if($showReset)
<a href="{{ route('password.request') }}" class="rounded-full border border-white/10 bg-black/20 px-4 py-2 font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Reset password</a>
@endif
@if($showLogin)
<a href="{{ route('login') }}" class="rounded-full border border-white/10 bg-black/20 px-4 py-2 font-semibold text-slate-200 transition hover:border-white/20 hover:bg-white/[0.05]">Open login</a>
@endif
</div>
</div>

View File

@@ -0,0 +1,27 @@
@php
$steps = [
'email' => 'Email',
'verified' => 'Verified',
'password' => 'Password',
'complete' => 'Username',
];
$currentIndex = array_search($currentStep ?? 'email', array_keys($steps), true);
if ($currentIndex === false) {
$currentIndex = 0;
}
@endphp
<div class="mb-6">
<div class="flex items-center justify-between text-xs sm:text-sm text-gray-600">
@foreach($steps as $key => $label)
@php $idx = array_search($key, array_keys($steps), true); @endphp
<span class="{{ $idx <= $currentIndex ? 'text-gray-900 font-semibold' : '' }}">
{{ $label }}
</span>
@endforeach
</div>
<div class="mt-2 h-2 w-full bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-gray-900 rounded-full" style="width: {{ (($currentIndex + 1) / count($steps)) * 100 }}%"></div>
</div>
</div>

View 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>

View File

@@ -0,0 +1,88 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<div class="mb-4 text-sm text-white/80">
<p class="font-medium text-white">Check your inbox</p>
@if($email !== '')
<p class="mt-1">We sent a verification link to <strong class="text-white">{{ $email }}</strong>.</p>
<p class="mt-1">Click the link in that email to continue setup.</p>
@else
<p class="mt-1">Enter your email to resend verification if needed.</p>
@endif
</div>
@if (session('status'))
<div class="mb-4 rounded-md border border-green-700/60 bg-green-900/20 px-3 py-2 text-sm text-green-300">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="mb-4 rounded-md border border-red-700/60 bg-red-900/20 px-3 py-2 text-sm text-red-300">
{{ $errors->first() }}
</div>
@endif
<form method="POST" action="{{ route('register.resend') }}" class="space-y-4" id="resend-form" data-resend-seconds="{{ (int) $resendSeconds }}">
@csrf
<div>
<x-input-label for="email" value="Email" class="text-sb-muted" />
<x-text-input id="email" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="email" name="email" :value="old('email', $email)" required autocomplete="email" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-between gap-3">
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
<a class="underline text-sm text-sb-muted hover:text-white" href="{{ route('login') }}">Back to login</a>
<a class="underline text-sm text-sb-muted hover:text-white" href="{{ route('register', ['email' => $email]) }}">Change email</a>
</div>
<x-primary-button id="resend-btn" class="justify-center" type="submit">
Resend verification email
</x-primary-button>
</div>
<p id="resend-timer" class="text-xs text-sb-muted"></p>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('resend-form');
const button = document.getElementById('resend-btn');
const timerText = document.getElementById('resend-timer');
if (!form || !button || !timerText) return;
let remaining = parseInt(form.dataset.resendSeconds || '0', 10);
const render = () => {
if (remaining > 0) {
button.setAttribute('disabled', 'disabled');
timerText.textContent = `You can resend in ${remaining}s.`;
} else {
button.removeAttribute('disabled');
timerText.textContent = 'Did not receive it? You can resend now.';
}
};
render();
if (remaining <= 0) return;
const interval = setInterval(() => {
remaining -= 1;
render();
if (remaining <= 0) {
clearInterval(interval);
}
}, 1000);
});
</script>
@endsection

View File

@@ -0,0 +1,67 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<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. Youll choose a password and username after verification.</p>
@include('auth.partials.help-links', [
'title' => 'Need help before you start?',
'description' => 'Use the signup and login guide if you want a clearer view of verification, recovery, or what happens after the first account step.',
])
@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
@if($errors->has('bot'))
<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('bot') }}
</div>
@endif
@include('auth.partials.social-login', ['dividerLabel' => 'or register with email'])
<form method="POST" action="{{ route('register') }}" class="space-y-5" data-bot-form>
@csrf
<input type="text" name="homepage_url" value="" tabindex="-1" autocomplete="off" class="hidden" aria-hidden="true">
<input type="hidden" name="_bot_fingerprint" value="">
@php
$captchaProvider = $captcha['provider'] ?? 'turnstile';
$captchaSiteKey = $captcha['siteKey'] ?? '';
@endphp
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="you@example.com" value="{{ old('email', $prefillEmail ?? '') }}" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
@if((($requiresCaptcha ?? false) || session('bot_captcha_required')) && $captchaSiteKey !== '')
@if($captchaProvider === 'recaptcha')
<div class="g-recaptcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@elseif($captchaProvider === 'hcaptcha')
<div class="h-captcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@else
<div class="cf-turnstile" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
@endif
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
@endif
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-cyan-500 to-sky-400 hover:from-cyan-400 hover:to-sky-300 text-slate-900 transition">Continue</button>
<p class="text-sm text-center text-white/60">Already registered? <a href="{{ route('login') }}" class="text-cyan-400 hover:underline">Sign in</a></p>
</form>
</div>
</div>
</div>
@if((($requiresCaptcha ?? false) || session('bot_captcha_required')) && (($captcha['siteKey'] ?? '') !== '') && (($captcha['scriptUrl'] ?? '') !== ''))
<script src="{{ $captcha['scriptUrl'] }}" async defer></script>
@endif
@include('partials.bot-fingerprint-script')
@endsection

View File

@@ -0,0 +1,47 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Reset Password</h2>
<p class="text-sm text-white/60 mb-6">Choose a new password for your account.</p>
<form method="POST" action="{{ route('password.store') }}" class="mt-4">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<x-text-input id="email" name="email" type="email" :value="old('email', $request->email)" required autofocus autocomplete="username" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" class="text-sb-muted" />
<x-text-input id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,43 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Set Your Password</h2>
<div class="mt-4 text-white/90">
@include('auth.partials.onboarding-progress', ['currentStep' => 'verified'])
@if (session('status'))
<div class="mb-4 rounded-md border border-green-700/60 bg-green-900/20 px-3 py-2 text-sm text-green-300">
{{ session('status') }}
</div>
@endif
<p class="mb-4 text-sm text-white/60">{{ __('Create a password for ') }}<strong>{{ $email }}</strong></p>
<form method="POST" action="{{ route('setup.password.store') }}">
@csrf
<div>
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
<p class="mt-2 text-xs text-sb-muted">{{ __('Minimum 10 characters, include at least one number and one symbol.') }}</p>
</div>
<div class="mt-4">
<label class="block text-sm mb-1 text-white/80" for="password_confirmation">Confirm Password</label>
<x-text-input id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
</div>
<div class="mt-6 flex justify-end">
<x-primary-button class="w-full sm:w-auto justify-center">
{{ __('Continue') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,42 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Choose Username</h2>
<div class="mt-4 text-white/90">
@include('auth.partials.onboarding-progress', ['currentStep' => 'password'])
@if (session('status'))
<div class="mb-4 rounded-md border border-green-700/60 bg-green-900/20 px-3 py-2 text-sm text-green-300">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="mb-4 rounded-md border border-red-700/60 bg-red-900/20 px-3 py-2 text-sm text-red-300">
{{ $errors->first() }}
</div>
@endif
<form method="POST" action="{{ route('setup.username.store') }}">
@csrf
<div>
<label class="block text-sm mb-1 text-white/80" for="username">Username</label>
<x-text-input id="username" name="username" type="text" :value="old('username', $username)" required autocomplete="username" data-username-field="true" data-availability-url="{{ route('api.username.availability') }}" data-availability-target="setup-username-availability" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<p id="setup-username-availability" class="mt-1 text-xs text-sb-muted"></p>
<x-input-error :messages="$errors->get('username')" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<x-primary-button class="w-full sm:w-auto justify-center">
{{ __('Complete Setup') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,37 @@
@extends('layouts.nova')
@section('content')
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Verify Your Email</h2>
<p class="text-sm text-white/60 mb-6">Before getting started, please verify your email address by clicking the link we sent you.</p>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 rounded-md border border-green-700/60 bg-green-900/20 px-3 py-2 text-sm text-green-300">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-sb-muted hover:text-white rounded-md">
{{ __('Log Out') }}
</button>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,140 @@
@extends('layouts.nova')
@section('content')
<!-- Nova main preview ported into Blade (server-rendered) -->
<div class="pt-0">
<div class="mx-auto w-full">
<div class="flex min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
<!-- SIDEBAR -->
<aside id="sidebar" class="hidden md:block w-72 shrink-0 border-r border-neutral-800 bg-nova-900/60 backdrop-blur-sm">
<div class="p-4">
<button class="w-full h-12 rounded-xl bg-white/5 hover:bg-white/7 border border-white/5 flex items-center gap-3 px-4">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</span>
<span class="text-sm text-white/90">Menu</span>
</button>
<div class="mt-6 text-sm text-neutral-400">
<div class="font-semibold text-white/80 mb-2">Main Categories:</div>
<ul class="space-y-2">
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">📷</span> Photography</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🖼️</span> Wallpapers</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🧩</span> Skins</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70"></span> Other</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Browse Subcategories:</div>
<ul class="space-y-2 sb-scrollbar max-h-56 overflow-auto pr-2">
<li><a class="hover:text-white" href="#">3D</a></li>
<li><a class="hover:text-white" href="#">Abstract</a></li>
<li><a class="hover:text-white" href="#">Animals</a></li>
<li><a class="hover:text-white" href="#">Anime</a></li>
<li><a class="hover:text-white" href="#">Art</a></li>
<li><a class="hover:text-white" href="#">Cars</a></li>
<li><a class="hover:text-white" href="#">Cartoon</a></li>
<li><a class="hover:text-white" href="#">Fantasy</a></li>
<li><a class="hover:text-white" href="#">Nature</a></li>
<li><a class="hover:text-white" href="#">Sci-Fi</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Daily Uploads <span class="text-neutral-400 font-normal">(245)</span></div>
<div class="rounded-xl bg-white/5 border border-white/5 overflow-hidden">
<button class="w-full px-4 py-3 text-left hover:bg-white/5">All</button>
<button class="w-full px-4 py-3 text-left hover:bg-white/5">Hot</button>
</div>
<a class="mt-4 inline-flex items-center gap-2 text-neutral-400 hover:text-white" href="#">
<span>Link, more</span>
<span class="opacity-60"></span>
</a>
</div>
</div>
</aside>
<!-- MAIN -->
<main class="flex-1">
<!-- Hero background -->
<div class="relative overflow-hidden nb-hero-radial">
<div class="absolute inset-0 opacity-35"></div>
<div class="relative px-6 py-8 md:px-10 md:py-10">
<div class="text-sm text-neutral-400">
<a class="hover:text-white" href="#">Wallpapers</a> <span class="opacity-50"></span> <span class="text-white/80">Fantasy</span>
</div>
<h1 class="mt-2 text-3xl md:text-4xl font-semibold tracking-tight text-white/95">Fantasy</h1>
<!-- Info card -->
<section class="mt-5 bg-white/5 border border-white/10 rounded-2xl shadow-lg">
<div class="p-5 md:p-6">
<div class="text-lg font-semibold text-white/90">Fantasy</div>
<p class="mt-2 text-sm leading-6 text-neutral-400">A small preview of the Nova layout, server-rendered for SEO and progressive enhancement.</p>
</div>
</section>
</div>
</div>
<!-- Grid -->
<section class="px-6 pb-10 md:px-10">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Card template (replace src with real thumbnails server-side) -->
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
<div class="aspect-[16/10] bg-gradient-to-br from-cyan-400/30 via-blue-500/20 to-purple-600/30">
<img src="/images/placeholder.jpg" alt="Fantasy artwork" width="1600" height="1000" loading="lazy" class="w-full h-full object-cover" />
</div>
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">Featured artwork</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
<div class="aspect-[16/10] bg-gradient-to-br from-emerald-400/25 via-teal-500/15 to-sky-500/25">
<img src="/images/placeholder.jpg" alt="Island artwork" width="1600" height="1000" loading="lazy" class="w-full h-full object-cover" />
</div>
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">Island</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
<div class="aspect-[16/10] bg-gradient-to-br from-fuchsia-400/20 via-rose-500/20 to-amber-500/20">
<img src="/images/placeholder.jpg" alt="Sunset artwork" width="1600" height="1000" loading="lazy" class="w-full h-full object-cover" />
</div>
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">Sunset</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-lg">
<div class="aspect-[16/10] bg-gradient-to-br from-indigo-400/20 via-slate-500/20 to-zinc-700/30">
<img src="/images/placeholder.jpg" alt="Dragon artwork" width="1600" height="1000" loading="lazy" class="w-full h-full object-cover" />
</div>
<div class="p-3 text-xs text-neutral-400 group-hover:text-white/80">Dragon</div>
</a>
</div>
<div class="flex justify-center mt-10">
<a href="/browse" class="px-10 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-white/90 shadow-lg">Browse All</a>
</div>
</section>
<!-- Nova color scale examples -->
<section class="px-6 pb-10 md:px-10 mt-8">
<h2 class="text-lg font-semibold mb-4">Nova color scale</h2>
<div class="grid grid-cols-2 sm:grid-cols-5 md:grid-cols-10 gap-3">
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-50 text-black">nova-50</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-100 text-black">nova-100</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-200 text-black">nova-200</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-300 text-black">nova-300</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-400 text-black">nova-400</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-500 text-white">nova-500</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-600 text-white">nova-600</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-700 text-white">nova-700</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-800 text-white">nova-800</div>
<div class="h-20 rounded-md flex items-center justify-center text-sm font-medium border border-white/5 bg-nova-900 text-white">nova-900</div>
</div>
</section>
</main>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,62 @@
@php
/**
* Legacy browse-categories view ported from oldSite/pages/browse-categories.page.php
* Variables: $categories (collection), $fixName (callable)
*/
@endphp
@extends('layouts.nova')
@section('content')
<div class="effect2">
<div class="page-heading">
<h1 class="page-header">Browse Categories</h1>
<p>List of all categories at Skinbase</p>
</div>
</div>
@if (class_exists('\App\Banner'))
<?php \App\Banner::ShowResponsiveAd(); ?>
@endif
@foreach ($contentTypes as $ct)
<div class="panel panel-skinbase effect2">
<div class="panel-heading">
<h2 class="panel-title">{{ $ct->name }}</h2>
</div>
<div class="panel-body">
<p>{!! $ct->description ?? '' !!}</p>
@php
$roots = $categoriesByType[$ct->slug] ?? $ct->rootCategories ?? collect();
@endphp
@if ($roots->isEmpty())
<div class="alert alert-info">No categories for this content type.</div>
@else
<ul class="browseList" style="list-style:none;padding:0;margin:0;">
@foreach ($roots as $category)
<li style="display:block;margin-bottom:8px;">
<h4>{{ $category->name }}</h4>
<p>{!! $category->description !!}</p>
<ul style="list-style:none;padding:0;margin:0;">
@foreach ($category->subcategories as $subcategory)
@php
$picture = $subcategory->image ?: "../cfolder15.gif";
$subcategoryName = $subcategory->name;
$subcategoryUrl = $subcategory->url;
@endphp
<li style="width:19%;display:inline-block;vertical-align:top;">
<img src="/gfx/icons/{{ $picture }}" width="15" height="15" alt="{{ $subcategoryName }}" />
<a href="{{ $subcategoryUrl }}" title="{{ $subcategoryName }}">{!! $subcategoryName !!}</a>
</li>
@endforeach
</ul>
</li>
@endforeach
</ul>
@endif
</div>
</div>
@endforeach
@endsection

View File

@@ -0,0 +1,133 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
<p class="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Nova Cards</p>
<h1 class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{{ $heading }}</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">{{ $subheading }}</p>
</div>
</section>
@if(!empty($challengeEntryItems ?? []))
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-2xl font-semibold tracking-[-0.03em] text-white">Entries</h2>
@auth
@if(!empty($challenge ?? null))
<button type="button" data-report-target-type="nova_card_challenge" data-report-target-id="{{ $challenge->id }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-flag"></i>
Report challenge
</button>
@endif
@endauth
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
@foreach($challengeEntryItems as $entry)
<div class="space-y-3">
@include('cards.partials.tile', ['card' => $entry['card']])
@auth
<button type="button" data-report-target-type="nova_card_challenge_entry" data-report-target-id="{{ $entry['id'] }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-flag"></i>
Report entry
</button>
@endauth
</div>
@endforeach
</div>
</div>
</section>
@endif
<section class="px-6 py-8 md:px-10">
<div class="grid gap-4 xl:grid-cols-2">
@foreach(($challenges ?? collect()) as $challengeItem)
<article class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 transition hover:border-sky-300/30 hover:bg-white/[0.06]">
<div class="flex items-center justify-between gap-3">
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">{{ strtoupper($challengeItem->status) }}</div>
<h2 class="mt-2 text-2xl font-semibold text-white">{{ $challengeItem->title }}</h2>
</div>
@if($challengeItem->featured)
<span class="rounded-full border border-amber-300/25 bg-amber-300/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">Featured</span>
@endif
</div>
@if($challengeItem->prompt)
<p class="mt-4 text-sm leading-7 text-slate-300">{{ $challengeItem->prompt }}</p>
@endif
@if($challengeItem->description)
<p class="mt-3 text-sm leading-7 text-slate-400">{{ $challengeItem->description }}</p>
@endif
<div class="mt-4 flex items-center justify-between text-xs text-slate-400">
<span>{{ number_format((int) $challengeItem->entries_count) }} entries</span>
<span>{{ optional($challengeItem->starts_at)->format('M j, Y') ?: 'Open date TBD' }}</span>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<a href="{{ route('cards.challenges.show', ['slug' => $challengeItem->slug]) }}" class="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i class="fa-solid fa-arrow-right"></i>
View challenge
</a>
@auth
<button type="button" data-report-target-type="nova_card_challenge" data-report-target-id="{{ $challengeItem->id }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-flag"></i>
Report challenge
</button>
@endauth
</div>
</article>
@endforeach
</div>
</section>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', () => {
const reportButtons = document.querySelectorAll('[data-report-target-type][data-report-target-id]')
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''
reportButtons.forEach((button) => {
button.addEventListener('click', () => {
const reason = window.prompt('Why are you reporting this Nova Cards item?')
if (!reason || !reason.trim()) {
return
}
const details = window.prompt('Add extra details for moderators (optional)')
fetch(@json(route('api.reports.store')), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
target_type: button.dataset.reportTargetType,
target_id: Number(button.dataset.reportTargetId),
reason: reason.trim(),
details: details && details.trim() ? details.trim() : null,
}),
}).then(async (response) => {
if (!response.ok) {
return
}
window.alert('Report submitted. Thank you.')
}).catch(() => {})
})
})
})
</script>
@endpush

View File

@@ -0,0 +1,64 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $collection['name'],
'description' => $meta['description'] ?? $collection['description'],
'url' => $meta['canonical'] ?? $collection['public_url'],
'creator' => [
'@type' => 'Person',
'name' => data_get($collection, 'owner.username'),
],
'mainEntity' => collect($collection['items'] ?? [])->map(fn ($item) => [
'@type' => 'CreativeWork',
'name' => data_get($item, 'card.title'),
'url' => data_get($item, 'card.public_url'),
])->values()->all(),
])
->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200">{{ $collection['cards_count'] }} cards</span>
@if(!empty($collection['official']))
<span class="inline-flex items-center rounded-full border border-amber-300/25 bg-amber-300/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">Official</span>
@endif
</div>
<h1 class="mt-4 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{{ $collection['name'] }}</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">{{ $collection['description'] ?: 'A curated Nova Cards collection.' }}</p>
<div class="mt-5 text-sm text-slate-400">
Curated by <a href="{{ route('cards.creator', ['username' => strtolower($collection['owner']['username'])]) }}" class="font-semibold text-sky-100 transition hover:text-white">@{{ $collection['owner']['username'] }}</a>
</div>
</div>
</section>
<section class="px-6 pt-8 md:px-10">
@if(empty($collection['items']))
<div class="rounded-[28px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-14 text-center">
<h2 class="text-2xl font-semibold text-white">No public cards in this collection yet</h2>
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-slate-300">Cards added here will appear once they are public and approved.</p>
</div>
@else
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
@foreach($collection['items'] as $item)
<div class="space-y-3">
@include('cards.partials.tile', ['card' => $item['card']])
@if(!empty($item['note']))
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm leading-6 text-slate-300">{{ $item['note'] }}</div>
@endif
</div>
@endforeach
</div>
@endif
</section>
@endsection

View File

@@ -0,0 +1,798 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $meta['title'] ?? 'Nova Cards - Skinbase Nova',
'description' => $meta['description'] ?? '',
'url' => $meta['canonical'] ?? route('cards.index'),
'isPartOf' => [
'@type' => 'WebSite',
'name' => config('app.name'),
'url' => url('/'),
],
'mainEntity' => collect($cards ?? [])->take(12)->map(function ($card) {
return [
'@type' => 'CreativeWork',
'name' => $card['title'] ?? null,
'url' => $card['public_url'] ?? null,
'creator' => [
'@type' => 'Person',
'name' => data_get($card, 'creator.username'),
],
];
})->values()->all(),
])
->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
<p class="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Nova Cards</p>
<h1 class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{{ $heading }}</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">{{ $subheading }}</p>
<div class="mt-6 flex flex-wrap gap-3">
<a href="{{ route('studio.cards.create') }}" class="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i class="fa-solid fa-plus"></i>
Create a card
</a>
<a href="{{ route('cards.popular') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-fire"></i>
Popular
</a>
<a href="{{ route('cards.remixed') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-code-branch"></i>
Remixed
</a>
<a href="{{ route('cards.remix-highlights') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-sparkles"></i>
Best remixes
</a>
<a href="{{ route('cards.editorial') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-feather-pointed"></i>
Editorial
</a>
<a href="{{ route('cards.seasonal') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-sun"></i>
Seasonal
</a>
<a href="{{ route('cards.challenges') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-trophy"></i>
Challenges
</a>
@if(($context ?? null) !== 'index')
<a href="{{ route('cards.index') }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-compass"></i>
Browse all cards
</a>
@endif
</div>
</div>
</section>
@if(($context ?? null) === 'index' && (!empty($featuredCards) || !empty($trendingCards)))
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-2">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Featured</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Editors picks</h2>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
@foreach($featuredCards as $card)
@include('cards.partials.tile', ['card' => $card])
@endforeach
</div>
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Trending</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Most viewed right now</h2>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
@foreach($trendingCards as $card)
@include('cards.partials.tile', ['card' => $card])
@endforeach
</div>
</div>
</div>
</section>
@endif
@if(in_array(($context ?? null), ['creator', 'creator-portfolio'], true) && !empty($creatorSummary))
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creator profile</p>
<h2 class="mt-1 text-2xl font-semibold text-white">{{ $creatorSummary['creator']['display_name'] }}</h2>
<p class="mt-2 text-sm leading-7 text-slate-300">{{ ($context ?? null) === 'creator-portfolio' ? 'A dedicated Nova Cards portfolio view with public works, signature themes, remix activity, and publishing history.' : 'A public snapshot of this creator\'s Nova Cards footprint, top styles, and strongest publishing signals.' }}</p>
</div>
<span class="rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100">{{ '@' . $creatorSummary['creator']['username'] }}</span>
</div>
<div class="mt-5 flex flex-wrap gap-2">
<a href="{{ route('cards.creator', ['username' => strtolower((string) $creatorSummary['creator']['username'])]) }}" class="inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition {{ ($context ?? null) === 'creator' ? 'border-sky-300/20 bg-sky-400/10 text-sky-100' : 'border-white/10 bg-white/[0.05] text-white hover:bg-white/[0.08]' }}">
<i class="fa-solid fa-user"></i>
Profile
</a>
<a href="{{ route('cards.creator.portfolio', ['username' => strtolower((string) $creatorSummary['creator']['username'])]) }}" class="inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition {{ ($context ?? null) === 'creator-portfolio' ? 'border-sky-300/20 bg-sky-400/10 text-sky-100' : 'border-white/10 bg-white/[0.05] text-white hover:bg-white/[0.08]' }}">
<i class="fa-solid fa-layer-group"></i>
Portfolio
</a>
</div>
<div class="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-6">
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Public cards</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_cards'] ?? 0) }}</div>
</div>
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Featured works</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_featured_cards'] ?? 0) }}</div>
</div>
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Views</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_views'] ?? 0) }}</div>
</div>
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Saves</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_saves'] ?? 0) }}</div>
</div>
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Remixes</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_remixes'] ?? 0) }}</div>
</div>
<div class="rounded-[22px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Challenge entries</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorSummary['stats']['total_challenge_entries'] ?? 0) }}</div>
</div>
</div>
<div class="mt-5 grid gap-5 lg:grid-cols-3">
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top styles</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorSummary['top_styles'] ?? []) as $style)
<a href="{{ route('cards.style', ['styleSlug' => $style['key']]) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $style['label'] }} <span class="text-xs text-slate-500">{{ $style['cards_count'] }}</span></a>
@empty
<span class="text-sm text-slate-500">No dominant style family yet.</span>
@endforelse
</div>
</div>
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top categories</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorSummary['top_categories'] ?? []) as $category)
<a href="{{ route('cards.category', ['categorySlug' => $category['slug']]) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $category['name'] }} <span class="text-xs text-slate-500">{{ $category['cards_count'] }}</span></a>
@empty
<span class="text-sm text-slate-500">No category signal yet.</span>
@endforelse
</div>
</div>
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top tags</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorSummary['top_tags'] ?? []) as $tag)
<a href="{{ route('cards.tag', ['tagSlug' => $tag['slug']]) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">#{{ $tag['name'] }} <span class="text-xs text-slate-500">{{ $tag['cards_count'] }}</span></a>
@empty
<span class="text-sm text-slate-500">No recurring tags yet.</span>
@endforelse
</div>
</div>
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Signature themes</div>
<div class="mt-4 grid gap-5 lg:grid-cols-2">
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top palettes</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorSummary['top_palettes'] ?? []) as $palette)
<a href="{{ route('cards.palette', ['paletteSlug' => $palette['key']]) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $palette['label'] }} <span class="text-xs text-slate-500">{{ $palette['cards_count'] }}</span></a>
@empty
<span class="text-sm text-slate-500">No signature palette family yet.</span>
@endforelse
</div>
</div>
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Signature moods</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorSummary['top_moods'] ?? []) as $mood)
<a href="{{ route('cards.mood', ['moodSlug' => $mood['key']]) }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $mood['label'] }} <span class="text-xs text-slate-500">{{ $mood['cards_count'] }}</span></a>
@empty
<span class="text-sm text-slate-500">No recurring mood signal yet.</span>
@endforelse
</div>
</div>
</div>
</div>
<div class="mt-5 grid gap-5 lg:grid-cols-2">
<div class="rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Portfolio depth</div>
<h3 class="mt-2 text-xl font-semibold text-white">Most remixed works</h3>
@if(!empty($creatorMostRemixedWorks))
<div class="mt-4 space-y-3">
@foreach($creatorMostRemixedWorks as $card)
<a href="{{ $card['public_url'] }}" class="flex items-start justify-between gap-3 rounded-[20px] border border-white/10 bg-[#08111f]/70 px-4 py-3 transition hover:border-white/20 hover:bg-[#0d1726]">
<div>
<div class="font-semibold text-white">{{ $card['title'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{{ $card['creator']['username'] ? '@' . $card['creator']['username'] : 'Creator' }}</div>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ number_format($card['remixes_count'] ?? 0) }} remixes</span>
</a>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Remix traction will appear here as this creator's cards are remixed by the community.</div>
@endif
</div>
<div class="rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Participation</div>
<h3 class="mt-2 text-xl font-semibold text-white">Challenge track record</h3>
@if(!empty($creatorChallengeHistory))
<div class="mt-4 space-y-3">
@foreach($creatorChallengeHistory as $entry)
<div class="rounded-[20px] border border-white/10 bg-[#08111f]/70 px-4 py-3">
<div class="flex items-start justify-between gap-3">
<div>
@if(!empty($entry['challenge_url']))
<a href="{{ $entry['challenge_url'] }}" class="font-semibold text-white transition hover:text-sky-100">{{ $entry['challenge_title'] }}</a>
@else
<div class="font-semibold text-white">{{ $entry['challenge_title'] }}</div>
@endif
<div class="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{{ $entry['official'] ? 'Official challenge' : ucfirst($entry['challenge_status'] ?: 'challenge') }}</div>
</div>
<span class="rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100">{{ $entry['status_label'] }}</span>
</div>
@if(!empty($entry['card_url']))
<div class="mt-3 text-sm text-slate-300">With <a href="{{ $entry['card_url'] }}" class="font-semibold text-sky-100 transition hover:text-white">{{ $entry['card_title'] }}</a></div>
@endif
</div>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Challenge entries and featured placements will appear here as this creator participates in Nova Cards challenges.</div>
@endif
</div>
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Audience traction</div>
<h3 class="mt-2 text-xl font-semibold text-white">Most liked works</h3>
@if(!empty($creatorMostLikedWorks))
<div class="mt-4 grid gap-3 lg:grid-cols-2">
@foreach($creatorMostLikedWorks as $card)
<a href="{{ $card['public_url'] }}" class="flex items-start justify-between gap-3 rounded-[20px] border border-white/10 bg-[#08111f]/70 px-4 py-3 transition hover:border-white/20 hover:bg-[#0d1726]">
<div>
<div class="font-semibold text-white">{{ $card['title'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{{ $card['creator']['username'] ? '@' . $card['creator']['username'] : 'Creator' }}</div>
</div>
<div class="flex flex-col items-end gap-1 text-right">
<span class="rounded-full border border-rose-300/20 bg-rose-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-rose-100">{{ number_format($card['likes_count'] ?? 0) }} likes</span>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ number_format($card['saves_count'] ?? 0) }} saves</span>
</div>
</a>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Audience favorites will appear here once this creator's cards start collecting likes and saves.</div>
@endif
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Remix activity</div>
<h3 class="mt-2 text-xl font-semibold text-white">Remix branches</h3>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Community branches</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorRemixActivity['total_cards_remixed_by_community'] ?? 0) }}</div>
</div>
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Published remixes</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($creatorRemixActivity['total_published_remixes'] ?? 0) }}</div>
</div>
</div>
@if(!empty($creatorRemixActivity['branches']))
<div class="mt-4 space-y-3">
@foreach($creatorRemixActivity['branches'] as $branch)
<div class="rounded-[20px] border border-white/10 bg-[#08111f]/70 px-4 py-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<a href="{{ $branch['card']['public_url'] }}" class="font-semibold text-white transition hover:text-sky-100">{{ $branch['card']['title'] }}</a>
<div class="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{{ $branch['branch_type'] }}</div>
</div>
<a href="{{ $branch['lineage_url'] }}" class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200 transition hover:bg-white/[0.08]">
<i class="fa-solid fa-code-branch"></i>
View lineage
</a>
</div>
<div class="mt-3 text-sm text-slate-300">Source: <span class="font-semibold text-white">{{ $branch['source_label'] }}</span></div>
<div class="mt-3 flex flex-wrap gap-3 text-xs text-slate-400">
<span>{{ number_format($branch['card']['remixes_count'] ?? 0) }} remixes</span>
<span>{{ number_format($branch['card']['likes_count'] ?? 0) }} likes</span>
<span>{{ number_format($branch['card']['saves_count'] ?? 0) }} saves</span>
</div>
</div>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Remix branch activity will appear here once this creator publishes remixes or their cards start branching.</div>
@endif
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Remix visualization</div>
<h3 class="mt-2 text-xl font-semibold text-white">Remix graph</h3>
@if(!empty($creatorRemixGraph))
<div class="mt-4 space-y-4">
@foreach($creatorRemixGraph as $branch)
<div>
<div class="flex flex-wrap items-center justify-between gap-3 text-sm">
<span class="font-semibold text-white">{{ $branch['root_title'] }}</span>
<span class="text-slate-400">{{ number_format($branch['cards_count']) }} cards · {{ number_format($branch['total_remixes']) }} remixes</span>
</div>
<div class="mt-2 h-3 overflow-hidden rounded-full bg-white/[0.06]">
<div class="h-full rounded-full bg-gradient-to-r from-sky-400 via-cyan-300 to-emerald-300" style="width: {{ $branch['width_percent'] }}%"></div>
</div>
<div class="mt-2 text-xs uppercase tracking-[0.16em] text-slate-500">Peak branch card: {{ $branch['peak_title'] }}</div>
</div>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Branch volume will chart here once this creator has remix families with visible activity.</div>
@endif
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Creator identity</div>
<h3 class="mt-2 text-xl font-semibold text-white">Preference signals</h3>
<div class="mt-4 grid gap-5 lg:grid-cols-2">
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top formats</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorPreferenceSignals['top_formats'] ?? []) as $format)
<span class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ $format['label'] }} <span class="text-xs text-slate-500">{{ $format['cards_count'] }}</span></span>
@empty
<span class="text-sm text-slate-500">No dominant format yet.</span>
@endforelse
</div>
</div>
<div>
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Favorite templates</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorPreferenceSignals['top_templates'] ?? []) as $template)
<span class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ $template['name'] }} <span class="text-xs text-slate-500">{{ $template['cards_count'] }}</span></span>
@empty
<span class="text-sm text-slate-500">No preferred template signal yet.</span>
@endforelse
</div>
</div>
</div>
<div class="mt-5 grid gap-3 sm:grid-cols-2">
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Preferred editor mode</div>
<div class="mt-2 text-lg font-semibold text-white">{{ $creatorPreferenceSignals['preferred_editor_mode']['label'] ?? 'No preference yet' }}</div>
@if(!empty($creatorPreferenceSignals['preferred_editor_mode']))
<div class="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{{ number_format($creatorPreferenceSignals['preferred_editor_mode']['cards_count']) }} cards</div>
@endif
</div>
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Saved presets</div>
<div class="mt-3 flex flex-wrap gap-2">
@forelse(($creatorPreferenceSignals['preset_counts'] ?? []) as $preset)
<span class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ $preset['label'] }} <span class="text-xs text-slate-500">{{ $preset['presets_count'] }}</span></span>
@empty
<span class="text-sm text-slate-500">No saved presets yet.</span>
@endforelse
</div>
</div>
</div>
</div>
<div class="mt-5 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Publishing history</div>
<h3 class="mt-2 text-xl font-semibold text-white">Recent timeline</h3>
@if(!empty($creatorTimeline))
<div class="mt-4 space-y-4">
@foreach($creatorTimeline as $event)
<div class="flex gap-4 rounded-[20px] border border-white/10 bg-[#08111f]/70 px-4 py-4">
<div class="mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-sky-300"></div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center justify-between gap-3">
<a href="{{ $event['card']['public_url'] }}" class="font-semibold text-white transition hover:text-sky-100">{{ $event['card']['title'] }}</a>
<span class="text-xs uppercase tracking-[0.16em] text-slate-500">{{ $event['card']['published_at'] ? \Illuminate\Support\Carbon::parse($event['card']['published_at'])->format('M j, Y') : 'Published' }}</span>
</div>
<div class="mt-2 flex flex-wrap gap-2">
@forelse($event['signals'] as $signal)
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ $signal }}</span>
@empty
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">Published</span>
@endforelse
</div>
<div class="mt-3 flex flex-wrap gap-3 text-xs text-slate-400">
<span>{{ number_format($event['card']['likes_count'] ?? 0) }} likes</span>
<span>{{ number_format($event['card']['saves_count'] ?? 0) }} saves</span>
<span>{{ number_format($event['card']['remixes_count'] ?? 0) }} remixes</span>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="mt-4 rounded-[20px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-6 text-sm text-slate-400">Recent publishing milestones will appear here once this creator has public card activity.</div>
@endif
</div>
</div>
<div class="space-y-6">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Featured works</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Staff-curated creator picks</h2>
</div>
</div>
@if(!empty($creatorFeaturedWorks))
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
@foreach($creatorFeaturedWorks as $card)
@include('cards.partials.tile', ['card' => $card])
@endforeach
</div>
@else
<div class="rounded-[22px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-sm text-slate-400">No explicit featured works yet. Staff-featured cards will appear here.</div>
@endif
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Featured collections</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Curated sets by this creator</h2>
</div>
</div>
@if(!empty($creatorFeaturedCollections))
<div class="space-y-3">
@foreach($creatorFeaturedCollections as $collection)
<a href="{{ $collection['public_url'] }}" class="block rounded-[22px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20 hover:bg-white/[0.05]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $collection['name'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{{ $collection['official'] ? 'Official collection' : '@' . ($collection['owner']['username'] ?? 'creator') }}</div>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ $collection['cards_count'] }} cards</span>
</div>
@if(!empty($collection['description']))
<div class="mt-2 text-sm text-slate-400">{{ $collection['description'] }}</div>
@endif
</a>
@endforeach
</div>
@else
<div class="rounded-[22px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-sm text-slate-400">No featured public collections yet.</div>
@endif
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creator highlights</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Strongest public works</h2>
</div>
</div>
@if(!empty($creatorHighlights))
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
@foreach($creatorHighlights as $card)
@include('cards.partials.tile', ['card' => $card])
@endforeach
</div>
@else
<div class="rounded-[22px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-sm text-slate-400">Highlights will appear as this creator publishes more public cards.</div>
@endif
</div>
</div>
</div>
</section>
@endif
@if(($context ?? null) === 'editorial' && (!empty($featuredCreators) || !empty($landingCollections) || (($landingChallenges ?? collect())->count() > 0)))
<section class="px-6 pt-8 md:px-10">
@if(!empty($featuredCreators))
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creators</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Featured creators</h2>
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@foreach($featuredCreators as $creator)
<a href="{{ $creator['public_url'] }}" class="block rounded-[22px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20 hover:bg-white/[0.05]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $creator['display_name'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">@{{ $creator['username'] }}</div>
</div>
<span class="rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100">Staff pick</span>
</div>
<div class="mt-3 grid grid-cols-3 gap-2 text-center text-xs text-slate-300">
<div class="rounded-2xl border border-white/10 bg-white/[0.04] px-2 py-3">
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-500">Cards</div>
<div class="mt-1 text-sm font-semibold text-white">{{ number_format($creator['public_cards_count']) }}</div>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.04] px-2 py-3">
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-500">Featured</div>
<div class="mt-1 text-sm font-semibold text-white">{{ number_format($creator['featured_cards_count']) }}</div>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.04] px-2 py-3">
<div class="text-[10px] uppercase tracking-[0.14em] text-slate-500">Views</div>
<div class="mt-1 text-sm font-semibold text-white">{{ number_format($creator['total_views_count']) }}</div>
</div>
</div>
</a>
@endforeach
</div>
</div>
@endif
<div class="mt-6 grid gap-6 xl:grid-cols-2">
@if(!empty($landingCollections))
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Collections</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Featured collections</h2>
</div>
<div class="space-y-3">
@foreach($landingCollections as $collection)
<a href="{{ $collection['public_url'] }}" class="block rounded-[22px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20 hover:bg-white/[0.05]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $collection['name'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{{ $collection['official'] ? 'Official collection' : '@' . ($collection['owner']['username'] ?? 'creator') }}</div>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ $collection['cards_count'] }} cards</span>
</div>
@if(!empty($collection['description']))
<div class="mt-2 text-sm text-slate-400">{{ $collection['description'] }}</div>
@endif
</a>
@endforeach
</div>
</div>
@endif
@if(($landingChallenges ?? collect())->count() > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Challenges</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Editorial challenge picks</h2>
</div>
<div class="space-y-3">
@foreach($landingChallenges as $challenge)
<a href="{{ route('cards.challenges.show', ['slug' => $challenge->slug]) }}" class="block rounded-[22px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20 hover:bg-white/[0.05]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $challenge->title }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{{ ucfirst((string) $challenge->status) }}{{ $challenge->official ? ' · Official' : '' }}</div>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ (int) $challenge->entries_count }} entries</span>
</div>
@if(!empty($challenge->description))
<div class="mt-2 text-sm text-slate-400">{{ $challenge->description }}</div>
@endif
</a>
@endforeach
</div>
</div>
@endif
</div>
</section>
@endif
@if(($context ?? null) === 'seasonal' && count($seasonalHubs ?? []) > 0)
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Seasonal hubs</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Recurring themes</h2>
</div>
<div class="flex flex-wrap gap-2">
@foreach($seasonalHubs as $hub)
<span class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ $hub['label'] }}</span>
@endforeach
</div>
</div>
</section>
@endif
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Latest</p>
<h2 class="mt-1 text-2xl font-semibold text-white">{{ in_array(($context ?? null), ['creator', 'creator-portfolio'], true) ? (($context ?? null) === 'creator-portfolio' ? 'Portfolio works' : 'All published works') : 'Published cards' }}</h2>
</div>
</div>
@if(empty($cards))
<div class="rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-14 text-center">
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-[24px] border border-white/12 bg-white/[0.05] text-slate-400">
<i class="fa-solid fa-id-card text-3xl"></i>
</div>
<h3 class="mt-5 text-2xl font-semibold text-white">No public cards yet</h3>
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-slate-300">As creators publish their Nova Cards, they will appear here with crawlable quote text and preview imagery.</p>
</div>
@else
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
@foreach($cards as $card)
@include('cards.partials.tile', ['card' => $card])
@endforeach
</div>
@endif
@if(isset($pagination) && method_exists($pagination, 'links'))
<div class="mt-6">
{{ $pagination->links() }}
</div>
@endif
</div>
<aside class="space-y-6">
@if(($context ?? null) === 'index' && count($categories ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Categories</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($categories as $category)
<a href="{{ route('cards.category', ['categorySlug' => $category->slug]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $category->name }}</a>
@endforeach
</div>
</div>
@endif
@if(($context ?? null) === 'index' && count($tags ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Popular tags</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($tags as $tag)
<a href="{{ route('cards.tag', ['tagSlug' => $tag->slug]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">#{{ $tag->name }}</a>
@endforeach
</div>
</div>
@endif
@if(in_array(($context ?? null), ['index', 'mood'], true) && count($moodFamilies ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Mood families</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($moodFamilies as $mood)
<a href="{{ route('cards.mood', ['moodSlug' => $mood['key']]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $mood['label'] }}</a>
@endforeach
</div>
</div>
@endif
@if(in_array(($context ?? null), ['index', 'style'], true) && count($styleFamilies ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Style families</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($styleFamilies as $style)
<a href="{{ route('cards.style', ['styleSlug' => $style['key']]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $style['label'] }}</a>
@endforeach
</div>
</div>
@endif
@if(in_array(($context ?? null), ['index', 'style', 'palette'], true) && count($paletteFamilies ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Palette families</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($paletteFamilies as $palette)
<a href="{{ route('cards.palette', ['paletteSlug' => $palette['key']]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">{{ $palette['label'] }}</a>
@endforeach
</div>
</div>
@endif
@if(in_array(($context ?? null), ['index', 'seasonal'], true) && count($seasonalHubs ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Seasonal hubs</p>
<div class="mt-4 flex flex-wrap gap-2">
@foreach($seasonalHubs as $hub)
<span class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ $hub['label'] }}</span>
@endforeach
</div>
</div>
@endif
@if(($context ?? null) === 'index' && count($collections ?? []) > 0)
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Collections</p>
<div class="mt-4 space-y-3">
@foreach($collections as $collection)
<a href="{{ $collection['public_url'] }}" class="block rounded-[22px] border border-white/10 bg-white/[0.03] p-4 transition hover:border-white/20 hover:bg-white/[0.05]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $collection['name'] }}</div>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{{ $collection['official'] ? 'Official collection' : '@' . ($collection['owner']['username'] ?? 'creator') }}</div>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-200">{{ $collection['cards_count'] }} cards</span>
</div>
@if(!empty($collection['description']))
<div class="mt-2 text-sm text-slate-400">{{ $collection['description'] }}</div>
@endif
</a>
@endforeach
</div>
</div>
@endif
@if(in_array(($context ?? null), ['creator', 'creator-portfolio'], true) && !empty($creatorSummary))
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creator pages</p>
<div class="mt-4 space-y-2 text-sm">
<a href="{{ route('cards.creator', ['username' => strtolower((string) $creatorSummary['creator']['username'])]) }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Profile overview</a>
<a href="{{ route('cards.creator.portfolio', ['username' => strtolower((string) $creatorSummary['creator']['username'])]) }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Portfolio page</a>
</div>
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creator signals</p>
<div class="mt-4 space-y-3 text-sm text-slate-300">
<div class="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<span>Total likes</span>
<span class="font-semibold text-white">{{ number_format($creatorSummary['stats']['total_likes'] ?? 0) }}</span>
</div>
<div class="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<span>Total saves</span>
<span class="font-semibold text-white">{{ number_format($creatorSummary['stats']['total_saves'] ?? 0) }}</span>
</div>
<div class="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">
<span>Total remixes</span>
<span class="font-semibold text-white">{{ number_format($creatorSummary['stats']['total_remixes'] ?? 0) }}</span>
</div>
</div>
</div>
@endif
@if(in_array(($context ?? null), ['remixed', 'remix-highlights'], true))
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Remix discovery</p>
<div class="mt-4 space-y-2 text-sm">
<a href="{{ route('cards.remixed') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Latest remixes</a>
<a href="{{ route('cards.remix-highlights') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Best remixes</a>
</div>
</div>
@endif
@if(in_array(($context ?? null), ['editorial', 'seasonal'], true))
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Discovery landings</p>
<div class="mt-4 space-y-2 text-sm">
<a href="{{ route('cards.editorial') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Editorial picks</a>
<a href="{{ route('cards.seasonal') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Seasonal cards</a>
</div>
</div>
@endif
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">V2 resources</p>
<div class="mt-4 space-y-2 text-sm">
<a href="{{ route('cards.templates') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Template packs</a>
<a href="{{ route('cards.assets') }}" class="block rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-slate-200 transition hover:bg-white/[0.08]">Asset packs</a>
</div>
</div>
</aside>
</div>
</section>
@endsection

View File

@@ -0,0 +1,71 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $meta['title'] ?? 'Nova Card Lineage - Skinbase Nova',
'description' => $meta['description'] ?? 'Trace the remix lineage for this Nova Card.',
'url' => $meta['canonical'] ?? route('cards.lineage', ['slug' => $card['slug'], 'id' => $card['id']]),
'mainEntity' => collect($familyCards ?? [])->map(fn ($familyCard) => [
'@type' => 'CreativeWork',
'name' => data_get($familyCard, 'title'),
'url' => data_get($familyCard, 'public_url'),
])->values()->all(),
])
->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
<p class="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Remix lineage</p>
<h1 class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{{ $card['title'] }}</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">Trace this card back to its root, then browse the rest of the family that grew from the same original.</p>
</div>
</section>
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Trail</p>
<div class="mt-4 flex flex-wrap items-center gap-3">
@foreach($trail as $index => $item)
<a href="{{ $item['public_url'] }}" class="rounded-2xl border {{ $item['id'] === $card['id'] ? 'border-sky-300/35 bg-sky-400/10 text-sky-100' : 'border-white/10 bg-white/[0.03] text-white' }} px-4 py-3 text-sm font-semibold transition hover:bg-white/[0.05]">{{ $item['title'] }}</a>
@if($index < count($trail) - 1)
<span class="text-slate-500"><i class="fa-solid fa-arrow-right"></i></span>
@endif
@endforeach
</div>
</div>
</section>
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Root card</p>
<div class="mt-4">
@include('cards.partials.tile', ['card' => $rootCard])
</div>
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Family variants</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Cards in this remix branch</h2>
</div>
<a href="{{ $card['public_url'] }}" class="rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">Back to card</a>
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
@foreach($familyCards as $familyCard)
@include('cards.partials.tile', ['card' => $familyCard])
@endforeach
</div>
</div>
</div>
</section>
@endsection

View File

@@ -0,0 +1,22 @@
<a href="{{ $card['public_url'] }}" class="group overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.03] transition hover:-translate-y-1 hover:border-sky-300/25 hover:bg-white/[0.05]">
@if(!empty($card['preview_url']))
<img src="{{ $card['preview_url'] }}" alt="{{ $card['title'] }}" class="aspect-[4/5] w-full object-cover transition duration-500 group-hover:scale-[1.02]" loading="lazy" />
@endif
<div class="p-4">
<div class="flex items-center justify-between gap-3">
<h3 class="truncate text-lg font-semibold tracking-[-0.03em] text-white">{{ $card['title'] }}</h3>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200">{{ $card['format'] }}</span>
</div>
@if(!empty($card['lineage']['original_card']))
<div class="mt-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">Remix</div>
@endif
<p class="mt-2 line-clamp-3 text-sm leading-6 text-slate-300">{{ $card['quote_text'] }}</p>
<div class="mt-4 flex items-center justify-between gap-3 text-xs text-slate-400">
<span>{{ $card['creator']['name'] ?: ('@' . $card['creator']['username']) }}</span>
<span>{{ number_format($card['likes_count'] ?? 0) }} likes</span>
</div>
@if(!empty($card['lineage']['original_card']) || (int) ($card['remixes_count'] ?? 0) > 0)
<div class="mt-3 text-xs text-sky-100/85">View lineage</div>
@endif
</div>
</a>

View File

@@ -0,0 +1,59 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[34px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-8">
<p class="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/75">Nova Cards</p>
<h1 class="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{{ $heading }}</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">{{ $subheading }}</p>
</div>
</section>
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,360px)]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<h2 class="text-2xl font-semibold tracking-[-0.03em] text-white">Official packs</h2>
<div class="mt-4 grid gap-4 md:grid-cols-2">
@foreach($packs as $pack)
<div class="rounded-[24px] border border-white/10 bg-white/[0.03] p-5">
<div class="flex items-center justify-between gap-3">
<h3 class="text-lg font-semibold text-white">{{ $pack['name'] ?? $pack['slug'] }}</h3>
@if(!empty($pack['official']))
<span class="rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100">Official</span>
@endif
</div>
@if(!empty($pack['description']))
<p class="mt-3 text-sm leading-7 text-slate-300">{{ $pack['description'] }}</p>
@endif
<div class="mt-4 text-xs text-slate-400">{{ strtoupper($pack['type'] ?? $resourceType) }} pack</div>
</div>
@endforeach
</div>
</div>
<aside class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<h2 class="text-2xl font-semibold tracking-[-0.03em] text-white">{{ $resourceType === 'template' ? 'Included templates' : 'Pack notes' }}</h2>
@if($resourceType === 'template')
<div class="mt-4 space-y-3">
@foreach($templates as $template)
<div class="rounded-[18px] border border-white/10 bg-white/[0.03] px-4 py-3">
<div class="text-sm font-semibold text-white">{{ $template['name'] }}</div>
<div class="mt-1 text-xs text-slate-400">{{ $template['description'] }}</div>
</div>
@endforeach
</div>
@else
<p class="mt-4 text-sm leading-7 text-slate-300">Asset packs surface inside the v2 studio editor as official decorative sources, and can be layered with template packs for challenge-ready or remix-ready compositions.</p>
@endif
</aside>
</div>
</section>
@endsection

View File

@@ -0,0 +1,394 @@
@php
$useUnifiedSeo = true;
$seo = \App\Support\Seo\SeoDataBuilder::fromArray(
app(\App\Support\Seo\SeoFactory::class)->fromViewData(get_defined_vars())
)
->og(
type: 'article',
title: $meta['title'] ?? ($card['title'] . ' - Nova Cards - Skinbase Nova'),
description: $meta['description'] ?? $card['quote_text'],
url: $meta['canonical'] ?? $card['public_url'],
image: $card['og_preview_url'] ?? $card['preview_url'] ?? null,
)
->addJsonLd([
'@context' => 'https://schema.org',
'@type' => 'CreativeWork',
'name' => $card['title'],
'headline' => $card['title'],
'description' => $meta['description'] ?? $card['quote_text'],
'url' => $meta['canonical'] ?? $card['public_url'],
'image' => array_values(array_filter([$card['og_preview_url'] ?? null, $card['preview_url'] ?? null])),
'genre' => $card['format'] ?? null,
'keywords' => collect($card['tags'] ?? [])->pluck('name')->values()->all(),
'datePublished' => $card['published_at'] ?? null,
'dateModified' => $card['updated_at'] ?? null,
'creator' => [
'@type' => 'Person',
'name' => data_get($card, 'creator.username'),
'url' => !empty(data_get($card, 'creator.username')) ? route('cards.creator', ['username' => strtolower(data_get($card, 'creator.username'))]) : null,
],
'publisher' => [
'@type' => 'Organization',
'name' => config('app.name'),
'url' => url('/'),
],
])
->build();
@endphp
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@section('content')
<section class="px-6 pt-8 md:px-10">
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(320px,420px)]">
<div class="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-5 shadow-[0_24px_70px_rgba(2,6,23,0.32)] md:p-6">
@if(!empty($card['preview_url']))
<img src="{{ $card['preview_url'] }}" alt="{{ $card['title'] }}" class="w-full rounded-[24px] border border-white/10 object-cover" />
@endif
</div>
<div class="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.18)]">
<div class="flex flex-wrap items-center gap-2">
@if(!empty($card['featured']))
<span class="inline-flex items-center rounded-full border border-amber-300/25 bg-amber-300/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">Featured</span>
@endif
<span class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200">{{ $card['format'] }}</span>
@if(!empty($card['category']))
<a href="{{ route('cards.category', ['categorySlug' => $card['category']['slug']]) }}" class="inline-flex items-center rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100">{{ $card['category']['name'] }}</a>
@endif
</div>
<h1 class="mt-4 text-3xl font-semibold tracking-[-0.04em] text-white md:text-4xl">{{ $card['title'] }}</h1>
<blockquote class="mt-5 text-lg leading-8 text-slate-100 md:text-xl">{{ $card['quote_text'] }}</blockquote>
@if(!empty($card['quote_author']))
<p class="mt-4 text-sm font-semibold uppercase tracking-[0.22em] text-sky-100"> {{ $card['quote_author'] }}</p>
@endif
@if(!empty($card['quote_source']))
<p class="mt-2 text-sm text-slate-400">Source: {{ $card['quote_source'] }}</p>
@endif
@if(!empty($card['description']))
<p class="mt-5 text-sm leading-7 text-slate-300">{{ $card['description'] }}</p>
@endif
@if(!empty($card['lineage']['original_card']))
<div class="mt-5 rounded-[20px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300">
Remixed from
<a href="{{ route('cards.show', ['slug' => $card['lineage']['original_card']['slug'], 'id' => $card['lineage']['original_card']['id']]) }}" class="font-semibold text-sky-100 transition hover:text-white">{{ $card['lineage']['original_card']['title'] }}</a>
</div>
@endif
@if(!empty($card['lineage']['original_card']) || (int) ($card['remixes_count'] ?? 0) > 0)
<div class="mt-4">
<a href="{{ route('cards.lineage', ['slug' => $card['slug'], 'id' => $card['id']]) }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-diagram-project"></i>
View remix lineage
</a>
</div>
@endif
<div class="mt-6 rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Creator</p>
<div class="mt-3 flex items-center justify-between gap-3">
<div>
<div class="text-base font-semibold text-white">{{ $card['creator']['name'] ?: ('@' . $card['creator']['username']) }}</div>
<a href="{{ route('cards.creator', ['username' => strtolower($card['creator']['username'])]) }}" class="text-sm text-slate-400 transition hover:text-slate-200">@{{ $card['creator']['username'] }}</a>
</div>
<div class="text-right text-xs text-slate-400">
<div>{{ number_format($card['views_count']) }} views</div>
<div>{{ number_format($card['shares_count']) }} shares</div>
<div>{{ number_format($card['likes_count']) }} likes</div>
</div>
</div>
</div>
<div class="mt-5 grid gap-3 sm:grid-cols-3">
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] px-4 py-3 text-center">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Likes</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($card['likes_count']) }}</div>
</div>
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] px-4 py-3 text-center">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Saved</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($card['saves_count']) }}</div>
</div>
<div class="rounded-[20px] border border-white/10 bg-white/[0.03] px-4 py-3 text-center">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Remixes</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ number_format($card['remixes_count']) }}</div>
</div>
</div>
@if(!empty($card['tags']))
<div class="mt-5 flex flex-wrap gap-2">
@foreach($card['tags'] as $tag)
<a href="{{ route('cards.tag', ['tagSlug' => $tag['slug']]) }}" class="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200 transition hover:bg-white/[0.08]">#{{ $tag['name'] }}</a>
@endforeach
</div>
@endif
<div class="mt-6 flex flex-wrap gap-3">
<button type="button" data-copy-card-link="{{ $card['public_url'] }}" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-link"></i>
Copy link
</button>
@auth
<button type="button" data-card-like class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm font-semibold text-rose-100 transition hover:bg-rose-400/15">
<i class="fa-solid fa-heart"></i>
Like
</button>
<button type="button" data-card-favorite class="inline-flex items-center gap-2 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-3 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/15">
<i class="fa-solid fa-star"></i>
Favorite
</button>
<button type="button" data-card-save class="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i class="fa-solid fa-bookmark"></i>
Save
</button>
@if(!empty($card['allow_remix']))
<button type="button" data-card-remix class="inline-flex items-center gap-2 rounded-2xl border border-violet-300/20 bg-violet-400/10 px-4 py-3 text-sm font-semibold text-violet-100 transition hover:bg-violet-400/15">
<i class="fa-solid fa-code-branch"></i>
Remix
</button>
@endif
<button type="button" data-card-report class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<i class="fa-solid fa-flag"></i>
Report
</button>
@endauth
@if(!empty($card['allow_download']) && !empty($card['preview_url']))
<a href="{{ $card['preview_url'] }}" download data-card-download-link class="inline-flex items-center gap-2 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-400/15">
<i class="fa-solid fa-download"></i>
Download preview
</a>
@endif
</div>
</div>
</div>
</section>
<section id="comments" class="px-6 pt-8 md:px-10">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<div class="flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Discussion</p>
<h2 class="mt-1 text-2xl font-semibold text-white">Comments</h2>
</div>
<span class="rounded-full border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-200">{{ count($comments ?? []) }}</span>
</div>
@if(session('status'))
<div class="mt-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-100">{{ session('status') }}</div>
@endif
@auth
<form action="{{ route('cards.comments.store', ['card' => $card['id']]) }}" method="post" class="mt-5 space-y-3">
@csrf
<textarea name="body" rows="4" required minlength="2" maxlength="4000" class="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" placeholder="Write a comment about this card..."></textarea>
<button type="submit" class="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i class="fa-solid fa-comment"></i>
Post comment
</button>
</form>
@else
<div class="mt-5 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300">Sign in to comment on this Nova Card.</div>
@endauth
<div class="mt-6 space-y-4">
@forelse($comments as $comment)
<article id="comment-{{ $comment['id'] }}" class="rounded-[24px] border border-white/10 bg-white/[0.03] p-4">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3">
<img src="{{ $comment['user']['avatar_url'] }}" alt="{{ $comment['user']['display'] }}" class="h-12 w-12 rounded-2xl border border-white/10 object-cover" />
<div>
<a href="{{ $comment['user']['profile_url'] }}" class="text-base font-semibold text-white transition hover:text-sky-100">{{ $comment['user']['display'] }}</a>
<div class="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">{{ $comment['time_ago'] }}</div>
</div>
</div>
<div class="flex items-center gap-2">
@if($comment['can_report'])
<button type="button" data-card-comment-report="{{ $comment['id'] }}" class="rounded-2xl border border-white/10 bg-white/[0.05] px-3 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.08]">Report</button>
@endif
@if($comment['can_delete'])
<form action="{{ route('cards.comments.destroy', ['card' => $card['id'], 'comment' => $comment['id']]) }}" method="post">
@csrf
@method('DELETE')
<button type="submit" class="rounded-2xl border border-rose-300/20 bg-rose-400/10 px-3 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-rose-100 transition hover:bg-rose-400/15">Delete</button>
</form>
@endif
</div>
</div>
<div class="mt-4 text-sm leading-7 text-slate-300">{!! $comment['rendered_content'] !!}</div>
</article>
@empty
<div class="rounded-2xl border border-dashed border-white/12 bg-white/[0.03] px-4 py-8 text-center text-sm text-slate-400">No comments yet.</div>
@endforelse
</div>
</div>
</section>
@foreach([
'Related in category' => $relatedByCategory,
'Related by tags' => $relatedByTags,
'More from creator' => $moreFromCreator,
] as $sectionTitle => $items)
@if(!empty($items))
<section class="px-6 pt-8 md:px-10">
<div class="rounded-[28px] border border-white/10 bg-white/[0.04] p-5">
<h2 class="text-2xl font-semibold tracking-[-0.03em] text-white">{{ $sectionTitle }}</h2>
<div class="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
@foreach($items as $item)
@include('cards.partials.tile', ['card' => $item])
@endforeach
</div>
</div>
</section>
@endif
@endforeach
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', () => {
const copyButton = document.querySelector('[data-copy-card-link]')
const downloadButton = document.querySelector('[data-card-download-link]')
const likeButton = document.querySelector('[data-card-like]')
const favoriteButton = document.querySelector('[data-card-favorite]')
const saveButton = document.querySelector('[data-card-save]')
const remixButton = document.querySelector('[data-card-remix]')
const reportButton = document.querySelector('[data-card-report]')
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''
const postEvent = (url, method = 'POST') => {
fetch(url, {
method,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
},
credentials: 'same-origin',
keepalive: true,
}).catch(() => {})
}
if (copyButton) {
copyButton.addEventListener('click', async () => {
try {
await navigator.clipboard?.writeText(copyButton.dataset.copyCardLink || '')
postEvent(@json(route('api.cards.share', ['id' => $card['id']])))
} catch (error) {
window.prompt('Copy this link', copyButton.dataset.copyCardLink || '')
}
})
}
if (downloadButton) {
downloadButton.addEventListener('click', () => {
postEvent(@json(route('api.cards.download', ['id' => $card['id']])))
})
}
if (likeButton) {
likeButton.addEventListener('click', () => postEvent(@json(route('api.cards.like', ['id' => $card['id']]))))
}
if (favoriteButton) {
favoriteButton.addEventListener('click', () => postEvent(@json(route('api.cards.favorite', ['id' => $card['id']]))))
}
if (saveButton) {
saveButton.addEventListener('click', () => postEvent(@json(route('api.cards.save', ['id' => $card['id']]))))
}
if (remixButton) {
remixButton.addEventListener('click', () => {
fetch(@json(route('api.cards.remix', ['id' => $card['id']])), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
},
credentials: 'same-origin',
}).then(async (response) => {
if (!response.ok) {
return
}
const payload = await response.json()
if (payload?.data?.id) {
window.location.assign(`/studio/cards/${payload.data.id}/edit`)
}
}).catch(() => {})
})
}
if (reportButton) {
reportButton.addEventListener('click', () => {
const reason = window.prompt('Why are you reporting this card?')
if (!reason || !reason.trim()) {
return
}
const details = window.prompt('Add extra details for moderators (optional)')
fetch(@json(route('api.reports.store')), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
target_type: 'nova_card',
target_id: @json($card['id']),
reason: reason.trim(),
details: details && details.trim() ? details.trim() : null,
}),
}).then(async (response) => {
if (!response.ok) {
return
}
window.alert('Report submitted. Thank you.')
}).catch(() => {})
})
}
document.querySelectorAll('[data-card-comment-report]').forEach((button) => {
button.addEventListener('click', () => {
const reason = window.prompt('Why are you reporting this comment?')
if (!reason || !reason.trim()) {
return
}
const details = window.prompt('Add extra details for moderators (optional)')
fetch(@json(route('api.reports.store')), {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
target_type: 'nova_card_comment',
target_id: Number(button.dataset.cardCommentReport),
reason: reason.trim(),
details: details && details.trim() ? details.trim() : null,
}),
}).then(async (response) => {
if (!response.ok) {
return
}
window.alert('Report submitted. Thank you.')
}).catch(() => {})
})
})
})
</script>
@endpush

View File

@@ -0,0 +1,18 @@
@extends('layouts.nova')
@push('head')
<meta name="csrf-token" content="{{ csrf_token() }}" />
@vite(['resources/js/collections.jsx'])
<style>
body.page-collections main { padding-top: 4rem; }
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.body.classList.add('page-collections')
})
</script>
@endpush
@section('content')
@inertia
@endsection

View File

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

View File

@@ -0,0 +1,33 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="page-heading">
<h1 class="page-header">{{ $page_title }}</h1>
</div>
<br>
<div class="mb-3">{!! $adHtml !!}</div>
<div class="panel panel-skinbase effect2">
<div class="panel-body">
{!! $chatHtml !!}
</div>
</div>
<ul class="smileyList">
@foreach ($smileys as $smiley)
@php
$codeJs = json_encode($smiley->code);
$imgSrc = '/gfx/smiles/' . rawurlencode($smiley->picture ?? '');
$alt = e($smiley->emotion ?? '');
@endphp
<li>
<img style="cursor:pointer;" onclick="put_smiley_chat({{ $codeJs }}, 'chat_txt');" alt="{{ $alt }}" src="{{ $imgSrc }}">
</li>
@endforeach
</ul>
</div>
@endsection

View File

@@ -0,0 +1,28 @@
{{--
<x-ad-unit slot="1234567890" />
<x-ad-unit slot="1234567890" format="rectangle" class="my-6" />
Props:
slot AdSense ad slot ID (required)
format AdSense data-ad-format (default: auto)
class additional wrapper classes
Renders nothing when:
- GOOGLE_ADSENSE_PUBLISHER_ID is not set in .env
- User has not given consent (handled client-side via CSS class .ads-disabled)
--}}
@php
$publisherId = config('services.google_adsense.publisher_id');
@endphp
@if($publisherId)
<div class="ad-unit-wrapper {{ $attributes->get('class', '') }}">
<ins class="adsbygoogle"
style="display:block"
data-ad-client="{{ $publisherId }}"
data-ad-slot="{{ $slot }}"
data-ad-format="{{ $format ?? 'auto' }}"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
@endif

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,247 @@
@props([
'art',
'loading' => 'lazy',
'fetchpriority' => null,
'imageSizes' => '(max-width: 640px) 50vw, (max-width: 1024px) 33vw, (max-width: 1536px) 25vw, 320px',
])
@php
if (isset($art) && (is_array($art) || $art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection)) {
$first = null;
if (is_array($art) && !\Illuminate\Support\Arr::isAssoc($art)) {
$first = reset($art);
} elseif ($art instanceof Illuminate\Support\Collection || $art instanceof Illuminate\Database\Eloquent\Collection) {
$first = $art->first();
}
if ($first) {
$art = $first;
}
}
if (is_array($art) && \Illuminate\Support\Arr::isAssoc($art)) {
$art = (object) $art;
}
$title = trim((string) ($art->name ?? $art->title ?? 'Untitled artwork'));
$publisherType = (string) (data_get($art, 'publisher.type') ?? ($art->published_as_type ?? ''));
$isGroupPublisher = $publisherType === 'group';
$author = trim((string) (
($isGroupPublisher ? data_get($art, 'publisher.name') : null)
?? $art->uname
?? $art->author_name
?? $art->author
?? ($art->user->name ?? null)
?? ($art->user->username ?? null)
?? 'Skinbase'
));
$username = $isGroupPublisher
? ''
: trim((string) (
$art->username
?? ($art->user->username ?? null)
?? ''
));
$rawContentType = trim((string) (
$art->content_type_name
?? $art->content_type
?? $art->content_type_slug
?? ''
));
$contentType = match (strtolower($rawContentType)) {
'artworks', 'artwork' => 'Artwork',
'wallpapers', 'wallpaper' => 'Wallpaper',
'skins', 'skin' => 'Skin',
'photography', 'photo', 'photos' => 'Photography',
'other' => 'Other',
default => $rawContentType !== ''
?
Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $rawContentType))
: '',
};
$category = trim((string) ($art->category_name ?? $art->category ?? ''));
$avatarUserId = $art->user->id ?? $art->user_id ?? null;
$avatarHash = $art->user->profile->avatar_hash ?? $art->avatar_hash ?? null;
$avatarUrl = trim((string) (
($isGroupPublisher ? data_get($art, 'publisher.avatar_url') : null)
?? ($art->avatar_url ?? null)
?? ''
));
if ($avatarUrl === '') {
$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) : '')));
$safeInt = function ($value, $fallback = 0) {
if (is_numeric($value)) {
return (int) $value;
}
if (is_array($value)) {
return count($value);
}
if (is_object($value)) {
if (method_exists($value, 'count')) {
return (int) $value->count();
}
if ($value instanceof Countable) {
return (int) count($value);
}
}
return (int) $fallback;
};
$likes = $safeInt($art->likes ?? $art->favourites ?? 0);
$comments = $safeInt($art->comments_count ?? $art->comment_count ?? $art->comments ?? 0);
$thumbUrl = is_object($art) && method_exists($art, 'thumbUrl') ? $art->thumbUrl('md') : null;
$imgSrc = (string) ($art->thumb ?? $art->thumbnail_url ?? $thumbUrl ?? '/images/placeholder.jpg');
$imgSrcset = (string) ($art->thumb_srcset ?? $art->thumbnail_srcset ?? $imgSrc);
$imgAvifSrcset = (string) ($art->thumb_avif_srcset ?? $imgSrcset);
$imgWebpSrcset = (string) ($art->thumb_webp_srcset ?? $imgSrcset);
$imgSizes = trim((string) $imageSizes);
$resolveDimension = function ($value, string $field, $fallback) {
if (is_numeric($value)) {
return (int) $value;
}
if (is_array($value)) {
$current = reset($value);
return is_numeric($current) ? (int) $current : (int) $fallback;
}
if (is_object($value)) {
if (method_exists($value, 'first')) {
$first = $value->first();
if (is_object($first) && isset($first->{$field})) {
return (int) ($first->{$field} ?: $fallback);
}
}
if (isset($value->{$field})) {
return (int) $value->{$field};
}
}
return (int) $fallback;
};
// Use stored dimensions when available; otherwise leave ratio unconstrained
// so the thumbnail displays at its natural proportions (no 1:1 or 16:9 forcing).
$hasDimensions = ($art->width ?? 0) > 0 && ($art->height ?? 0) > 0;
$imgWidth = $hasDimensions ? max(1, $resolveDimension($art->width, 'width', 0)) : null;
$imgHeight = $hasDimensions ? max(1, $resolveDimension($art->height, 'height', 0)) : null;
$imgAspectRatio = $hasDimensions ? ($imgWidth . ' / ' . $imgHeight) : null;
$contentUrl = $imgSrc;
$cardUrl = (string) ($art->url ?? '');
if ($cardUrl === '' || $cardUrl === '#') {
if (isset($art->id) && is_numeric($art->id)) {
$cardUrl = '/art/' . (int) $art->id . '/' . \Illuminate\Support\Str::slug($title);
} else {
$cardUrl = '#';
}
}
$authorUrl = trim((string) (
($isGroupPublisher ? data_get($art, 'publisher.profile_url') : null)
?? ($art->profile_url ?? null)
?? ($username !== '' ? '/@' . strtolower($username) : '')
));
if ($authorUrl === '') {
$authorUrl = null;
}
$metaParts = [];
if ($contentType !== '') {
$metaParts[] = $contentType;
}
if ($category !== '') {
$metaParts[] = $category;
}
if ($resolution !== '') {
$metaParts[] = $resolution;
}
$maturity = data_get($art, 'maturity');
if (is_array($maturity)) {
$maturity = (object) $maturity;
}
$shouldHide = (bool) data_get($maturity, 'should_hide', false);
$shouldBlur = (bool) data_get($maturity, 'should_blur', false);
$isMatureArtwork = (bool) data_get($maturity, 'is_mature_effective', false);
@endphp
@unless($shouldHide)
<article class="nova-card gallery-item artwork" itemscope itemtype="https://schema.org/ImageObject"
data-art-id="{{ $art->id ?? '' }}"
data-art-url="{{ $cardUrl }}"
data-art-title="{{ e($title) }}"
data-art-img="{{ $imgSrc }}">
<meta itemprop="name" content="{{ $title }}">
<meta itemprop="contentUrl" content="{{ $contentUrl }}">
<meta itemprop="creator" content="{{ $author }}">
<meta itemprop="license" content="{{ $license }}">
<a href="{{ $cardUrl }}" itemprop="url" class="group relative block overflow-hidden rounded-2xl ring-1 ring-white/5 bg-black/20 shadow-lg shadow-black/40 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70">
@if($category !== '')
<div class="absolute left-3 top-3 z-30 rounded-md bg-black/55 px-2 py-1 text-xs text-white backdrop-blur-sm">{{ $category }}</div>
@endif
@if($shouldBlur && $isMatureArtwork)
<div class="absolute right-3 top-3 z-30 rounded-md bg-amber-500/85 px-2 py-1 text-xs font-semibold text-black shadow-lg shadow-black/30 backdrop-blur-sm">Mature</div>
@endif
<div class="nova-card-media relative overflow-hidden bg-neutral-900"@if($imgAspectRatio) style="aspect-ratio: {{ $imgAspectRatio }};"@endif>
<div class="absolute inset-0 bg-gradient-to-br from-white/10 via-white/5 to-transparent pointer-events-none"></div>
<picture>
<source srcset="{{ $imgAvifSrcset }}" @if($imgSizes !== '') sizes="{{ $imgSizes }}" @endif type="image/avif">
<source srcset="{{ $imgWebpSrcset }}" @if($imgSizes !== '') sizes="{{ $imgSizes }}" @endif type="image/webp">
<img
src="{{ $imgSrc }}"
srcset="{{ $imgSrcset }}"
@if($imgSizes !== '') sizes="{{ $imgSizes }}" @endif
loading="{{ $loading }}"
decoding="{{ $loading === 'eager' ? 'sync' : 'async' }}"
@if($fetchpriority) fetchpriority="{{ $fetchpriority }}" @endif
@if($loading !== 'eager') data-blur-preview @endif
alt="{{ e($title) }}"
@if($imgWidth) width="{{ $imgWidth }}" @endif
@if($imgHeight) height="{{ $imgHeight }}" @endif
class="{{ $imgAspectRatio ? 'h-full w-full object-cover' : 'w-full h-auto' }} transition-[transform,filter] duration-300 ease-out {{ $shouldBlur ? 'blur-xl scale-[1.08]' : 'group-hover:scale-[1.04]' }}"
itemprop="thumbnailUrl"
/>
</picture>
@if($shouldBlur && $isMatureArtwork)
<div class="pointer-events-none absolute inset-0 z-10 bg-black/25"></div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/85 via-black/45 to-transparent px-3 py-3 text-xs font-semibold uppercase tracking-[0.18em] text-white/90">
Mature content blurred
</div>
@endif
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100">
<div class="truncate text-sm font-semibold text-white">{{ $title }}</div>
<div class="mt-1 flex items-start justify-between gap-3 text-xs text-white/80">
<span class="flex min-w-0 items-start gap-3">
<img src="{{ $avatarUrl }}" alt="Avatar of {{ e($author) }}" class="w-9 h-9 rounded-full object-cover">
<span class="min-w-0 flex-1">
<span class="block truncate">{{ $author }}@if($username !== '') <span class="text-white/60">{{ '@' . $username }}</span>@endif</span>
@if(!empty($metaParts))
<span class="mt-0.5 block truncate text-[11px] text-white/70">{{ implode(' • ', $metaParts) }}</span>
@endif
</span>
</span>
<span class="shrink-0"> {{ $likes }} · 💬 {{ $comments }}</span>
</div>
</div>
</div>
<span class="sr-only">{{ $title }} by {{ $author }}</span>
</a>
</article>
@endunless

View File

@@ -0,0 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@@ -0,0 +1,10 @@
@php
// Usage: <x-avatar :user="$user" size="128" />
$size = $size ?? 128;
$profile = $user->profile ?? null;
$hash = $profile->avatar_hash ?? null;
$src = \App\Support\AvatarUrl::forUser((int) $user->id, $hash, (int) $size);
$alt = $alt ?? ($user->username ?? 'avatar');
$class = $class ?? 'rounded-full';
@endphp
<img src="{{ $src }}" alt="{{ $alt }}" loading="lazy" decoding="async" class="{{ $class }}" width="{{ $size }}" height="{{ $size }}">

View File

@@ -0,0 +1,23 @@
{{--
Breadcrumb component with schema.org structured data.
@param \Illuminate\Support\Collection $breadcrumbs
Collection of objects with ->name and ->url properties.
--}}
@php
$breadcrumbTrail = \App\Support\Seo\BreadcrumbTrail::normalize($breadcrumbs ?? collect());
@endphp
@if($breadcrumbTrail !== [])
<nav class="flex items-center gap-1.5 flex-wrap text-sm text-neutral-400" aria-label="Breadcrumb">
@foreach($breadcrumbTrail as $crumb)
@if(!$loop->first)
<span class="opacity-40" aria-hidden="true"></span>
@endif
@if(!$loop->last)
<a class="hover:text-white transition-colors" href="{{ $crumb['url'] }}">{{ $crumb['name'] }}</a>
@else
<span class="text-white/70" aria-current="page">{{ $crumb['name'] }}</span>
@endif
@endforeach
</nav>
@endif

View File

@@ -0,0 +1,5 @@
@props(['max' => '3xl'])
<div {{ $attributes->merge(['class' => "mx-auto px-6 md:px-10 max-w-{$max}"]) }}>
{{ $slot }}
</div>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,68 @@
@props([
'name',
'value' => null,
'options' => [],
])
<div
x-data="{
open: false,
value: @js((string) ($value ?? '')),
options: @js(collect($options)->map(fn ($option) => [
'value' => (string) ($option['value'] ?? ''),
'label' => (string) ($option['label'] ?? ''),
])->values()->all()),
labelFor(selectedValue) {
const match = this.options.find((option) => option.value === selectedValue)
return match ? match.label : (this.options[0]?.label ?? '')
},
select(nextValue) {
this.value = nextValue
this.open = false
},
}"
class="relative"
@click.outside="open = false"
@keydown.escape.window="open = false"
>
<input type="hidden" name="{{ $name }}" x-model="value">
<button
type="button"
@click="open = !open"
class="flex w-full items-center justify-between gap-3 rounded-xl border border-white/[0.08] bg-black/20 px-4 py-3 text-left text-sm text-white transition-colors hover:border-white/[0.14] focus:border-sky-400/40 focus:outline-none"
:aria-expanded="open.toString()"
>
<span class="truncate" x-text="labelFor(value)"></span>
<i class="fa-solid fa-chevron-down text-xs text-white/40 transition-transform" :class="open ? 'rotate-180' : ''"></i>
</button>
<div
x-cloak
x-show="open"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-1"
class="absolute left-0 right-0 z-50 mt-2 overflow-hidden rounded-2xl border border-white/[0.08] bg-slate-950/95 shadow-[0_24px_70px_rgba(0,0,0,0.45)] backdrop-blur"
style="display: none;"
>
<div class="p-2">
<template x-for="option in options" :key="option.value">
<button
type="button"
@click="select(option.value)"
class="flex w-full items-center justify-between rounded-xl px-3 py-2.5 text-left text-sm transition-colors"
:class="value === option.value
? 'bg-sky-400/20 text-white'
: 'text-white/75 hover:bg-white/[0.06] hover:text-white'"
>
<span x-text="option.label"></span>
<i x-show="value === option.value" class="fa-solid fa-check text-xs text-sky-200"></i>
</button>
</template>
</div>
</div>
</div>

View File

@@ -0,0 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -0,0 +1,35 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@@ -0,0 +1 @@
@include('forum.components.category-card', ['category' => $category])

View File

@@ -0,0 +1 @@
@include('forum.thread.components.attachment-list', ['attachments' => $attachments])

View File

@@ -0,0 +1 @@
@include('forum.thread.components.author-badge', ['user' => $user])

View File

@@ -0,0 +1 @@
@include('forum.thread.components.breadcrumbs', ['thread' => $thread, 'category' => $category])

View File

@@ -0,0 +1 @@
@include('forum.thread.components.post-card', ['post' => $post, 'thread' => $thread ?? null, 'isOp' => $isOp ?? false])

View File

@@ -0,0 +1,13 @@
<div class="max-w-4xl mx-auto py-12 text-center">
@if(isset($title))
<h1 class="text-3xl font-extrabold text-white">{{ $title }}</h1>
@endif
@if(isset($subtitle))
<p class="mt-3 text-sm text-neutral-300">{{ $subtitle }}</p>
@endif
@if($slot->isNotEmpty())
<div class="mt-6">{{ $slot }}</div>
@endif
</div>

View File

@@ -0,0 +1,9 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@@ -0,0 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
{{ $value ?? $slot }}
</label>

View File

@@ -0,0 +1,78 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,71 @@
@props([
'section' => null,
'title' => null,
'icon' => null,
'breadcrumbs' => collect(),
'description' => null,
'showSection' => true,
'showIcon' => true,
'showTitle' => true,
'showBreadcrumbs' => true,
'showDescription' => true,
'headerClass' => '',
'innerClass' => '',
'contentClass' => '',
'actionsClass' => '',
'sectionClass' => '',
'titleClass' => '',
'iconClass' => '',
'descriptionClass' => '',
])
@php
$headerBreadcrumbs = $breadcrumbs instanceof \Illuminate\Support\Collection
? $breadcrumbs
: collect($breadcrumbs ?? []);
$hasActions = isset($actions) && trim((string) $actions) !== '';
$resolvedHeaderClass = trim('px-6 pt-10 pb-7 md:px-10 border-b border-white/[0.06] bg-gradient-to-b from-sky-500/[0.04] to-transparent ' . $headerClass);
$resolvedInnerClass = trim('flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between ' . $innerClass);
$resolvedContentClass = trim('max-w-3xl min-w-0 ' . $contentClass);
$resolvedActionsClass = trim('flex flex-col items-start gap-3 lg:items-end ' . $actionsClass);
$resolvedSectionClass = trim('text-xs font-semibold uppercase tracking-widest text-white/30 mb-1 ' . $sectionClass);
$resolvedTitleClass = trim('text-3xl font-bold text-white leading-tight flex items-center gap-3 ' . $titleClass);
$resolvedIconClass = trim('text-sky-400 text-2xl ' . $iconClass);
$resolvedDescriptionClass = trim('mt-1 text-sm text-white/50 ' . $descriptionClass);
@endphp
<header {{ $attributes->class([$resolvedHeaderClass]) }}>
<div class="{{ $resolvedInnerClass }}">
<div class="{{ $resolvedContentClass }}">
@if($showSection && filled($section))
<p class="{{ $resolvedSectionClass }}">{{ $section }}</p>
@endif
@if($showTitle && filled($title))
<h1 class="{{ $resolvedTitleClass }}">
@if($showIcon && filled($icon))
<i class="fa-solid {{ $icon }} {{ $resolvedIconClass }}"></i>
@endif
<span class="min-w-0">{{ $title }}</span>
</h1>
@endif
@if($showBreadcrumbs && $headerBreadcrumbs->isNotEmpty())
<div class="mt-3">
@include('components.breadcrumbs', ['breadcrumbs' => $headerBreadcrumbs])
</div>
@endif
@if($showDescription && filled($description))
<p class="{{ $resolvedDescriptionClass }}">{!! $description !!}</p>
@endif
</div>
@if($hasActions)
<div class="{{ $resolvedActionsClass }}">
{{ $actions }}
</div>
@endif
</div>
</header>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,8 @@
<div class="nova-skeleton-card" aria-hidden="true">
<div class="nova-skeleton-media"></div>
<div class="nova-skeleton-body">
<div class="nova-skeleton-line w-3/4"></div>
<div class="nova-skeleton-line w-1/2"></div>
<div class="nova-skeleton-pill w-20"></div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
@props(['story'])
<a href="{{ route('stories.show', $story->slug) }}"
class="group block overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70 shadow-lg transition-transform duration-200 hover:scale-[1.02] hover:border-sky-500/40">
@if($story->cover_url)
<div class="aspect-video overflow-hidden bg-gray-900">
<img src="{{ $story->cover_url }}" alt="{{ $story->title }}" class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" loading="lazy" />
</div>
@else
<div class="aspect-video bg-gradient-to-br from-gray-900 via-slate-900 to-sky-950"></div>
@endif
<div class="space-y-3 p-4">
<h3 class="line-clamp-2 text-lg font-bold text-white">{{ $story->title }}</h3>
<div class="flex items-center gap-2 text-sm text-gray-300">
<img
src="{{ $story->creator?->profile?->avatar_hash ? \App\Support\AvatarUrl::forUser((int) $story->creator->id, $story->creator->profile->avatar_hash, 48) : \App\Support\AvatarUrl::default() }}"
alt="{{ $story->creator?->username ?? 'Creator' }}"
class="h-6 w-6 rounded-full border border-gray-600 object-cover"
/>
<span>{{ $story->creator?->username ?? 'Unknown creator' }}</span>
@if($story->published_at)
<span class="text-gray-500"></span>
<time datetime="{{ $story->published_at->toIso8601String() }}" class="text-gray-400">
{{ $story->published_at->format('M j, Y') }}
</time>
@endif
</div>
<div class="flex items-center gap-3 text-xs text-gray-400">
<span>{{ $story->reading_time }} min read</span>
<span>{{ number_format((int) $story->likes_count) }} likes</span>
<span>{{ number_format((int) $story->comments_count) }} comments</span>
<span>{{ number_format((int) $story->views) }} views</span>
</div>
</div>
</a>

View File

@@ -0,0 +1,3 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>

View File

@@ -0,0 +1,28 @@
@extends('layouts.nova')
@push('head')
@vite(['resources/js/dashboard/index.jsx'])
@endpush
@section('main-class', 'pt-20')
@section('content')
<div
id="dashboard-root"
data-username="{{ $dashboard_user_name }}"
data-is-creator="{{ $dashboard_is_creator ? '1' : '0' }}"
data-level="{{ $dashboard_level ?? 1 }}"
data-rank="{{ $dashboard_rank ?? 'Newbie' }}"
data-received-comments-count="{{ (int) ($dashboard_received_comments_count ?? 0) }}"
data-overview='@json($dashboard_overview ?? [])'
data-preferences='@json($dashboard_preferences ?? [])'
></div>
@if (session('status'))
<div class="mx-auto max-w-7xl px-4 mt-4">
<div class="rounded-lg border border-emerald-500/30 bg-emerald-900/20 px-4 py-3 text-sm text-emerald-100" role="status">
{{ session('status') }}
</div>
</div>
@endif
@endsection

View File

@@ -0,0 +1,40 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-1">My Awards</h1>
<p class="text-sm text-soft mb-6">Artworks of yours that have received awards from the community.</p>
@if($artworks->isEmpty())
<div class="flex flex-col items-center justify-center py-20 text-center">
<i class="fa-solid fa-trophy text-4xl text-sb-muted mb-4"></i>
<p class="text-soft">None of your artworks have received awards yet.</p>
<a href="/browse" class="mt-4 text-sm text-accent hover:underline">Browse artworks for inspiration</a>
</div>
@else
<section data-nova-gallery data-gallery-type="dashboard-awards">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" data-gallery-grid>
@foreach($artworks as $art)
<div class="relative gallery-item">
<x-artwork-card :art="$art" />
@php $stat = $art->awardStat @endphp
@if($stat && ($stat->gold_count + $stat->silver_count + $stat->bronze_count) > 0)
<div class="absolute left-2 top-2 z-40 flex gap-1 text-xs font-bold">
@if($stat->gold_count) <span class="rounded px-1.5 py-0.5 bg-yellow-500/80 text-black">🥇 {{ $stat->gold_count }}</span> @endif
@if($stat->silver_count) <span class="rounded px-1.5 py-0.5 bg-neutral-400/80 text-black">🥈 {{ $stat->silver_count }}</span> @endif
@if($stat->bronze_count) <span class="rounded px-1.5 py-0.5 bg-amber-700/80 text-white">🥉 {{ $stat->bronze_count }}</span> @endif
</div>
@endif
</div>
@endforeach
</div>
<div class="mt-6" data-gallery-pagination>{{ $artworks->links() }}</div>
<div class="hidden" data-gallery-skeleton-template aria-hidden="true">
<x-skeleton.artwork-card />
</div>
<div class="hidden mt-8" data-gallery-skeleton></div>
</section>
@endif
</div>
@endsection

View File

@@ -0,0 +1,244 @@
@extends('layouts.nova')
@section('content')
<x-nova-page-header
section="Dashboard"
title="Received Comments"
icon="fa-comments"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Comments', 'url' => route('dashboard.comments.received')],
])"
description="A clean inbox for feedback on your artworks, with quick scanning, filtering, and direct artwork context."
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-2">
<a href="{{ route('community.activity') }}"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-wave-square text-xs"></i>
Community activity
</a>
</div>
</x-slot>
</x-nova-page-header>
<div class="px-6 pb-16 pt-8 md:px-10">
<div class="mb-6 flex flex-wrap items-center gap-2 rounded-2xl border border-white/[0.06] bg-white/[0.025] p-2 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
<a href="{{ route('dashboard.comments.received') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition {{ request()->routeIs('dashboard.comments.received') ? 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-400/20' : 'text-white/60 hover:bg-white/[0.05] hover:text-white' }}">
<i class="fa-solid fa-inbox"></i>
Received
</a>
<a href="{{ route('messages.index') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
<i class="fa-solid fa-envelope"></i>
Messages
</a>
<a href="{{ route('dashboard.notifications') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
<i class="fa-solid fa-bell"></i>
Notifications
</a>
</div>
@if(($freshlyClearedCount ?? 0) > 0)
<div class="mb-6 rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-5 py-4 text-sm text-emerald-100 shadow-[0_12px_30px_rgba(16,185,129,0.12)]">
Marked {{ number_format((int) $freshlyClearedCount) }} new {{ (int) $freshlyClearedCount === 1 ? 'comment' : 'comments' }} as read when you opened this inbox.
</div>
@endif
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-2xl border border-sky-400/20 bg-sky-500/10 p-5 shadow-[0_18px_45px_rgba(10,132,255,0.10)]">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-200/75">Total comments</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['total'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-sky-100/70">All comments on your approved artworks.</p>
</div>
<div class="rounded-2xl border border-emerald-400/20 bg-emerald-500/10 p-5">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-emerald-200/75">Last 7 days</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['recent'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-emerald-100/70">Recent conversation momentum.</p>
</div>
<div class="rounded-2xl border border-violet-400/20 bg-violet-500/10 p-5">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-200/75">Unique commenters</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['commenters'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-violet-100/70">Distinct people engaging with your work.</p>
</div>
<div class="rounded-2xl border border-amber-400/20 bg-amber-500/10 p-5">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-amber-200/75">Active artworks</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($stats['artworks'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-amber-100/70">Artworks that received comments.</p>
</div>
</div>
<div class="mb-6 rounded-[26px] border border-white/[0.07] bg-white/[0.03] p-4 shadow-[0_18px_45px_rgba(0,0,0,0.18)] md:p-5">
<form method="GET" action="{{ route('dashboard.comments.received') }}" class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex flex-1 flex-col gap-3 sm:flex-row">
<label class="relative flex-1">
<span class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-white/30">
<i class="fa-solid fa-magnifying-glass"></i>
</span>
<input
type="search"
name="q"
value="{{ $search }}"
placeholder="Search comment text, artwork title, or username"
class="w-full rounded-2xl border border-white/[0.08] bg-black/20 py-3 pl-11 pr-4 text-sm text-white placeholder:text-white/30 outline-none transition focus:border-sky-400/40 focus:bg-black/30"
>
</label>
<select
name="sort"
class="rounded-2xl border border-white/[0.08] bg-black/20 px-4 py-3 text-sm text-white outline-none transition focus:border-sky-400/40 focus:bg-black/30"
>
<option value="newest" @selected($sort === 'newest')>Newest first</option>
<option value="oldest" @selected($sort === 'oldest')>Oldest first</option>
</select>
</div>
<div class="flex items-center gap-2">
<button
type="submit"
class="inline-flex items-center gap-2 rounded-2xl bg-sky-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-sky-400"
>
<i class="fa-solid fa-sliders"></i>
Apply
</button>
@if($search !== '' || $sort !== 'newest')
<a
href="{{ route('dashboard.comments.received') }}"
class="inline-flex items-center gap-2 rounded-2xl border border-white/[0.08] bg-white/[0.03] px-4 py-3 text-sm font-medium text-white/65 transition hover:bg-white/[0.07] hover:text-white"
>
<i class="fa-solid fa-rotate-left"></i>
Reset
</a>
@endif
</div>
</form>
</div>
@if($comments->isEmpty())
<div class="rounded-[28px] border border-white/[0.07] bg-white/[0.025] px-8 py-16 text-center shadow-[0_18px_45px_rgba(0,0,0,0.18)]">
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35">
<i class="fa-regular fa-comments text-2xl"></i>
</div>
<h2 class="mt-5 text-2xl font-semibold text-white">No comments found</h2>
<p class="mx-auto mt-3 max-w-xl text-sm leading-6 text-white/45">
@if($search !== '')
Nothing matched your current search. Try a shorter phrase, a username, or reset the filters.
@else
When members comment on your artworks, they will appear here with artwork previews and quick context.
@endif
</p>
</div>
@else
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
@foreach($comments as $comment)
@php
$author = $comment->user;
$artwork = $comment->artwork;
$profileUrl = !empty($author?->username)
? '/@' . $author->username
: '/profile/' . (int) ($author?->id ?? 0);
$artworkUrl = $artwork
? '/art/' . (int) $artwork->id . '/' . \Illuminate\Support\Str::slug($artwork->title ?? $artwork->name ?? 'artwork')
: null;
@endphp
<article class="group flex h-full flex-col overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.025] shadow-[0_18px_45px_rgba(0,0,0,0.18)] transition hover:border-sky-400/20 hover:bg-white/[0.035]">
<div class="border-b border-white/[0.06] bg-black/20">
@if($artwork)
<a href="{{ $artworkUrl }}" class="block">
<img
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
alt="{{ $artwork->title ?? $artwork->name ?? 'Artwork' }}"
class="h-52 w-full object-cover transition duration-500 group-hover:scale-[1.02]"
>
</a>
@else
<div class="flex h-52 items-center justify-center text-white/25">
<i class="fa-regular fa-image text-4xl"></i>
</div>
@endif
</div>
<div class="flex flex-1 flex-col p-5 md:p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div class="flex min-w-0 items-center gap-3">
<a href="{{ $profileUrl }}" class="shrink-0">
<img
src="{{ \App\Support\AvatarUrl::forUser((int) ($author?->id ?? 0), $author?->profile?->avatar_hash, 64) }}"
alt="{{ $author?->name ?? $author?->username ?? 'Member' }}"
class="h-12 w-12 rounded-full object-cover ring-1 ring-white/[0.10]"
>
</a>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<a href="{{ $profileUrl }}" class="truncate text-base font-semibold text-white transition hover:text-sky-300">
{{ $author?->name ?? $author?->username ?? 'Unknown member' }}
</a>
@if(!empty($author?->username))
<span class="truncate text-xs font-medium uppercase tracking-[0.18em] text-white/30">{{ '@' . $author->username }}</span>
@endif
</div>
<p class="mt-1 text-sm text-white/45">
{{ optional($comment->created_at)->diffForHumans() ?? 'Unknown time' }}
</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
<span class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-white/60">
<i class="fa-solid fa-message"></i>
Comment #{{ $comment->id }}
</span>
@if($artworkUrl)
<a href="{{ $artworkUrl }}" class="inline-flex items-center gap-1.5 rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-sky-200 transition hover:bg-sky-500/20">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
Open artwork
</a>
@endif
</div>
</div>
<div class="mt-5 rounded-2xl border border-white/[0.06] bg-black/20 p-4 md:p-5">
<div class="prose prose-invert prose-sm max-w-none prose-p:text-white/80 prose-a:text-sky-300 prose-strong:text-white prose-code:text-sky-200">
{!! $comment->getDisplayHtml() !!}
</div>
</div>
<div class="mt-auto pt-5">
<div class="flex flex-col gap-3 border-t border-white/[0.06] pt-4 md:flex-row md:items-center md:justify-between">
<div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-white/30">Artwork</p>
@if($artwork)
<a href="{{ $artworkUrl }}" class="mt-1 block truncate text-sm font-medium text-white/80 transition hover:text-sky-300">
{{ $artwork->title ?? $artwork->name ?? 'Untitled artwork' }}
</a>
@else
<p class="mt-1 text-sm text-white/35">This artwork is no longer available.</p>
@endif
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-white/45">
<span class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.03] px-3 py-1">
<i class="fa-regular fa-clock"></i>
{{ optional($comment->created_at)->format('M j, Y H:i') ?? 'Unknown date' }}
</span>
</div>
</div>
</div>
</div>
</article>
@endforeach
</div>
<div class="mt-8 flex justify-center">
{{ $comments->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,76 @@
@extends('layouts.nova')
@php
$galleryItems = collect($artworks->items())->map(fn ($art) => [
'id' => $art->id ?? null,
'name' => $art->name ?? $art->title ?? null,
'title' => $art->title ?? $art->name ?? null,
'thumb' => $art->thumb ?? $art->thumb_url ?? null,
'thumb_url' => $art->thumb_url ?? $art->thumb ?? null,
'slug' => $art->slug ?? '',
'author' => $art->author ?? '',
'uname' => $art->uname ?? $art->author ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'likes' => $art->likes ?? 0,
'comments_count' => $art->comments_count ?? 0,
])->values();
@endphp
@push('scripts')
@vite('resources/js/entry-masonry-gallery.jsx')
@endpush
@section('content')
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
<main class="w-full">
<x-nova-page-header
section="Dashboard"
title="My Favourites"
icon="fa-heart"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Favourites', 'url' => route('dashboard.favorites')],
])"
description="Artworks you saved, displayed in the same gallery layout as Browse."
actionsClass="lg:pt-8"
>
<x-slot name="actions">
@if($artworks->total() > 0)
<div class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-sm text-white/60">
<i class="fa-solid fa-images text-xs text-sky-300"></i>
<span>{{ number_format($artworks->total()) }} artworks</span>
</div>
@endif
</x-slot>
</x-nova-page-header>
<section class="px-6 pb-10 pt-8 md:px-10">
@if($artworks->isEmpty())
<div class="rounded-xl border border-white/10 bg-white/5 p-8 text-center text-white/60">
You have no favourites yet.
</div>
@else
<div
data-react-masonry-gallery
data-artworks='@json($galleryItems)'
data-gallery-type="dashboard-favorites"
@if($artworks->nextPageUrl()) data-next-page-url="{{ $artworks->nextPageUrl() }}" @endif
data-limit="20"
class="min-h-32"
></div>
@endif
</section>
</main>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,288 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
<main class="w-full">
<x-nova-page-header
section="Dashboard"
title="People Following Me"
icon="fa-users"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Followers', 'url' => route('dashboard.followers')],
])"
description="A clearer view of who follows you, who you follow back, and who still needs a response."
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-3">
<a href="{{ route('dashboard.following') }}"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-user-check text-xs"></i>
People I follow
</a>
<a href="{{ route('discover.trending') }}"
class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-compass text-xs"></i>
Discover creators
</a>
</div>
</x-slot>
</x-nova-page-header>
<section class="px-6 pb-16 pt-8 md:px-10">
@php
$newestFollower = $followers->getCollection()->first();
$newestFollowerName = $newestFollower ? ($newestFollower->name ?: $newestFollower->uname) : null;
$latestFollowedAt = $newestFollower && !empty($newestFollower->followed_at)
? \Carbon\Carbon::parse($newestFollower->followed_at)->diffForHumans()
: null;
@endphp
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">Total followers</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['total_followers']) }}</p>
<p class="mt-2 text-xs text-white/40">People currently following your profile</p>
</div>
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">Followed back</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['following_back']) }}</p>
<p class="mt-2 text-xs text-white/40">Followers you also follow</p>
</div>
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">Not followed back</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['not_followed']) }}</p>
<p class="mt-2 text-xs text-white/40">Followers still waiting on your follow-back</p>
</div>
<div class="rounded-2xl border border-sky-400/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.12),rgba(255,255,255,0.03))] p-5 shadow-[0_16px_60px_rgba(14,165,233,0.08)]">
<p class="text-xs uppercase tracking-widest text-sky-100/60">Newest follower</p>
<p class="mt-2 truncate text-xl font-semibold text-white">{{ $newestFollowerName ?? '—' }}</p>
<p class="mt-2 text-xs text-sky-50/60">{{ $latestFollowedAt ?? 'No recent follower activity' }}</p>
</div>
</div>
<div class="mb-6 rounded-2xl border border-white/[0.06] bg-white/[0.03] p-4 shadow-[0_16px_60px_rgba(0,0,0,0.12)]">
@php
$sortOptions = [
['value' => 'recent', 'label' => 'Most recent'],
['value' => 'oldest', 'label' => 'Oldest first'],
['value' => 'name', 'label' => 'Name A-Z'],
['value' => 'uploads', 'label' => 'Most uploads'],
['value' => 'followers', 'label' => 'Most followers'],
];
$relationshipOptions = [
['value' => 'all', 'label' => 'All followers'],
['value' => 'following-back', 'label' => 'I follow back'],
['value' => 'not-followed', 'label' => 'Not followed back'],
];
@endphp
<form method="GET" action="{{ route('dashboard.followers') }}" class="grid gap-4 lg:grid-cols-[minmax(0,1.35fr)_220px_220px_auto] lg:items-end">
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Search follower</span>
<div class="relative">
<i class="fa-solid fa-magnifying-glass pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-white/30"></i>
<input
type="text"
name="q"
value="{{ $filters['q'] }}"
placeholder="Search by username or display name"
class="w-full rounded-xl border border-white/[0.08] bg-black/20 py-3 pl-10 pr-4 text-sm text-white placeholder:text-white/30 focus:border-sky-400/40 focus:outline-none"
>
</div>
</label>
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Sort by</span>
<x-dashboard.filter-select
name="sort"
:value="$filters['sort']"
:options="$sortOptions"
/>
</label>
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Relationship</span>
<x-dashboard.filter-select
name="relationship"
:value="$filters['relationship']"
:options="$relationshipOptions"
/>
</label>
<div class="flex flex-wrap gap-3 lg:justify-end">
<button type="submit" class="inline-flex items-center gap-2 rounded-xl border border-sky-400/30 bg-sky-400/10 px-4 py-3 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-sliders text-xs"></i>
Apply
</button>
@if($filters['q'] !== '' || $filters['sort'] !== 'recent' || $filters['relationship'] !== 'all')
<a href="{{ route('dashboard.followers') }}" class="inline-flex items-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-rotate-left text-xs"></i>
Reset
</a>
@endif
</div>
</form>
<div class="mt-4 flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white/50">{{ number_format($followers->count()) }} visible on this page</span>
@if($filters['q'] !== '')
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-sky-100/80">Search: {{ $filters['q'] }}</span>
@endif
@if($filters['relationship'] !== 'all')
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-emerald-100/80">
{{ $filters['relationship'] === 'following-back' ? 'Following back only' : 'Not followed back' }}
</span>
@endif
</div>
</div>
@if($followers->isEmpty())
<div class="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center shadow-[0_20px_80px_rgba(0,0,0,0.18)]">
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] text-white/60">
<i class="fa-solid fa-users text-lg"></i>
</div>
<h2 class="text-xl font-semibold text-white">No followers match these filters</h2>
<p class="mx-auto mt-2 max-w-xl text-sm text-white/45">
Try resetting the filters, or discover more creators and activity to grow your audience.
</p>
<div class="mt-6 flex flex-wrap items-center justify-center gap-3">
<a href="{{ route('dashboard.followers') }}" class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-rotate-left text-xs"></i>
Reset filters
</a>
<a href="{{ route('discover.trending') }}" class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-compass text-xs"></i>
Explore creators
</a>
</div>
</div>
@else
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
@foreach($followers as $f)
@php
$displayName = $f->name ?: $f->uname;
$profileUsername = strtolower((string) ($f->username ?? ''));
@endphp
<article class="group overflow-hidden rounded-2xl border border-white/[0.06] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0.02))] shadow-[0_18px_70px_rgba(0,0,0,0.14)] transition-all hover:-translate-y-0.5 hover:border-white/[0.10] hover:shadow-[0_24px_90px_rgba(0,0,0,0.20)]">
<div class="flex items-start justify-between gap-4 border-b border-white/[0.05] px-5 py-5">
<a href="{{ $f->profile_url }}" class="min-w-0 flex-1">
<div class="flex items-center gap-4 min-w-0">
<img src="{{ $f->avatar_url }}"
alt="{{ $displayName }}"
class="h-14 w-14 flex-shrink-0 rounded-2xl object-cover ring-1 ring-white/[0.10]"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h2 class="truncate text-base font-semibold text-white/95 group-hover:text-white">{{ $displayName }}</h2>
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
Follows you
</span>
@if($f->is_following_back)
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-sky-100">
Mutual
</span>
@endif
</div>
@if(!empty($f->username))
<div class="mt-1 truncate text-xs text-white/35">{{ '@' . $f->username }}</div>
@endif
<div class="mt-2 text-xs text-white/45">
Followed you {{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : 'recently' }}
</div>
</div>
</div>
</a>
<div class="shrink-0">
@if(!empty($profileUsername))
<div x-data="{
following: {{ $f->is_following_back ? 'true' : 'false' }},
count: {{ (int) $f->followers_count }},
loading: false,
hovering: false,
async toggle() {
this.loading = true;
try {
const r = await fetch('{{ route('profile.follow', ['username' => $profileUsername]) }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Accept': 'application/json'
}
});
const d = await r.json();
if (r.ok) {
this.following = d.following;
this.count = d.follower_count;
}
} catch (e) {
}
this.loading = false;
}
}">
<button @click="toggle"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
:disabled="loading"
class="inline-flex items-center gap-2 rounded-xl border px-3.5 py-2 text-sm font-medium transition-all"
:class="following
? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-100 hover:border-rose-400/30 hover:bg-rose-400/10 hover:text-rose-100'
: 'border-sky-400/30 bg-sky-400/10 text-sky-100 hover:bg-sky-400/15 hover:border-sky-300/40'">
<i class="fa-solid fa-fw text-xs"
:class="loading ? 'fa-circle-notch fa-spin' : (following ? (hovering ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus')"></i>
<span x-text="following ? (hovering ? 'Unfollow' : 'Following back') : 'Follow back'"></span>
</button>
</div>
@endif
</div>
</div>
<div class="grid gap-3 px-5 py-4 sm:grid-cols-3">
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Uploads</p>
<p class="mt-1 text-lg font-semibold text-white">{{ number_format((int) $f->uploads) }}</p>
</div>
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Followers</p>
<p class="mt-1 text-lg font-semibold text-white">{{ number_format((int) $f->followers_count) }}</p>
</div>
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Relationship</p>
<p class="mt-1 text-sm font-semibold {{ $f->is_following_back ? 'text-emerald-200' : 'text-amber-200' }}">
{{ $f->is_following_back ? 'Mutual follow' : 'Follower only' }}
</p>
</div>
</div>
<div class="flex items-center justify-between gap-3 border-t border-white/[0.05] px-5 py-4 text-xs text-white/45">
<span>
{{ $f->is_following_back && !empty($f->followed_back_at)
? 'You followed back ' . \Carbon\Carbon::parse($f->followed_back_at)->diffForHumans()
: 'Not followed back yet' }}
</span>
<a href="{{ $f->profile_url }}" class="inline-flex items-center gap-2 font-medium text-white/70 transition-colors hover:text-white">
View profile
<i class="fa-solid fa-arrow-right text-[10px]"></i>
</a>
</div>
</article>
@endforeach
</div>
<div class="mt-8 flex justify-center">
{{ $followers->links() }}
</div>
@endif
</section>
</main>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,289 @@
@extends('layouts.nova')
@section('content')
<div class="container-fluid legacy-page">
<div class="pt-0">
<div class="mx-auto w-full">
<div class="relative min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
<main class="w-full">
<x-nova-page-header
section="Dashboard"
title="People I Follow"
icon="fa-user-group"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Following', 'url' => route('dashboard.following')],
])"
description="Creators and members you follow, with quick stats and recent follow time."
actionsClass="lg:pt-8"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-3">
<a href="{{ route('dashboard.followers') }}"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-users text-xs"></i>
My followers
</a>
<a href="{{ route('discover.trending') }}"
class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-compass text-xs"></i>
Discover creators
</a>
</div>
</x-slot>
</x-nova-page-header>
<section class="px-6 pb-16 pt-8 md:px-10">
@php
$firstFollow = $following->getCollection()->first();
$latestFollowedAt = $firstFollow && !empty($firstFollow->followed_at)
? \Carbon\Carbon::parse($firstFollow->followed_at)->diffForHumans()
: null;
$latestFollowedName = $firstFollow ? ($firstFollow->name ?: $firstFollow->uname) : null;
@endphp
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">Following</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['total_following']) }}</p>
<p class="mt-2 text-xs text-white/40">People you currently follow</p>
</div>
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">Mutual follows</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['mutual']) }}</p>
<p class="mt-2 text-xs text-white/40">People who follow you back</p>
</div>
<div class="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-5 shadow-[0_16px_60px_rgba(0,0,0,0.16)]">
<p class="text-xs uppercase tracking-widest text-white/35">One-way follows</p>
<p class="mt-2 text-3xl font-semibold text-white">{{ number_format($summary['one_way']) }}</p>
<p class="mt-2 text-xs text-white/40">People you follow who do not follow back</p>
</div>
<div class="rounded-2xl border border-sky-400/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.12),rgba(255,255,255,0.03))] p-5 shadow-[0_16px_60px_rgba(14,165,233,0.08)]">
<p class="text-xs uppercase tracking-widest text-sky-100/60">Latest followed</p>
<p class="mt-2 truncate text-xl font-semibold text-white">{{ $latestFollowedName ?? '—' }}</p>
<p class="mt-2 text-xs text-sky-50/60">{{ $latestFollowedAt ?? 'No recent follow activity' }}</p>
</div>
</div>
<div class="mb-6 rounded-2xl border border-white/[0.06] bg-white/[0.03] p-4 shadow-[0_16px_60px_rgba(0,0,0,0.12)]">
@php
$sortOptions = [
['value' => 'recent', 'label' => 'Most recent'],
['value' => 'oldest', 'label' => 'Oldest first'],
['value' => 'name', 'label' => 'Name A-Z'],
['value' => 'uploads', 'label' => 'Most uploads'],
['value' => 'followers', 'label' => 'Most followers'],
];
$relationshipOptions = [
['value' => 'all', 'label' => 'Everyone I follow'],
['value' => 'mutual', 'label' => 'Mutual follows'],
['value' => 'one-way', 'label' => 'Not following me back'],
];
@endphp
<form method="GET" action="{{ route('dashboard.following') }}" class="grid gap-4 lg:grid-cols-[minmax(0,1.35fr)_220px_220px_auto] lg:items-end">
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Search creator</span>
<div class="relative">
<i class="fa-solid fa-magnifying-glass pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-xs text-white/30"></i>
<input
type="text"
name="q"
value="{{ $filters['q'] }}"
placeholder="Search by username or display name"
class="w-full rounded-xl border border-white/[0.08] bg-black/20 py-3 pl-10 pr-4 text-sm text-white placeholder:text-white/30 focus:border-sky-400/40 focus:outline-none"
>
</div>
</label>
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Sort by</span>
<x-dashboard.filter-select
name="sort"
:value="$filters['sort']"
:options="$sortOptions"
/>
</label>
<label class="block">
<span class="mb-2 block text-xs font-semibold uppercase tracking-widest text-white/35">Relationship</span>
<x-dashboard.filter-select
name="relationship"
:value="$filters['relationship']"
:options="$relationshipOptions"
/>
</label>
<div class="flex flex-wrap gap-3 lg:justify-end">
<button type="submit" class="inline-flex items-center gap-2 rounded-xl border border-sky-400/30 bg-sky-400/10 px-4 py-3 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-sliders text-xs"></i>
Apply
</button>
@if($filters['q'] !== '' || $filters['sort'] !== 'recent' || $filters['relationship'] !== 'all')
<a href="{{ route('dashboard.following') }}" class="inline-flex items-center gap-2 rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-rotate-left text-xs"></i>
Reset
</a>
@endif
</div>
</form>
<div class="mt-4 flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-white/50">{{ number_format($following->count()) }} visible on this page</span>
@if($filters['q'] !== '')
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-sky-100/80">Search: {{ $filters['q'] }}</span>
@endif
@if($filters['relationship'] !== 'all')
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-emerald-100/80">
{{ $filters['relationship'] === 'mutual' ? 'Mutual follows only' : 'Not following you back' }}
</span>
@endif
</div>
</div>
@if($following->isEmpty())
<div class="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center shadow-[0_20px_80px_rgba(0,0,0,0.18)]">
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] text-white/60">
<i class="fa-solid fa-user-group text-lg"></i>
</div>
<h2 class="text-xl font-semibold text-white">No followed creators match these filters</h2>
<p class="mx-auto mt-2 max-w-xl text-sm text-white/45">
Try resetting the filters, or discover more creators to build a stronger network.
</p>
<div class="mt-6 flex flex-wrap items-center justify-center gap-3">
<a href="{{ route('dashboard.following') }}" class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white">
<i class="fa-solid fa-rotate-left text-xs"></i>
Reset filters
</a>
<a href="{{ route('discover.trending') }}" class="inline-flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-400/10 px-4 py-2 text-sm font-medium text-sky-100 transition-colors hover:border-sky-300/40 hover:bg-sky-400/15">
<i class="fa-solid fa-compass text-xs"></i>
Discover creators
</a>
</div>
</div>
@else
<div class="grid gap-5 lg:grid-cols-2 2xl:grid-cols-3">
@foreach($following as $f)
@php
$displayName = $f->name ?: $f->uname;
$profileUsername = strtolower((string) ($f->username ?? ''));
@endphp
<article
x-data="{
following: true,
count: {{ (int) $f->followers_count }},
loading: false,
hovering: false,
async toggle() {
this.loading = true;
try {
const r = await fetch('{{ route('profile.follow', ['username' => $profileUsername]) }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Accept': 'application/json'
}
});
const d = await r.json();
if (r.ok) {
this.following = d.following;
this.count = d.follower_count;
}
} catch (e) {
}
this.loading = false;
}
}"
:class="following ? 'opacity-100' : 'opacity-50'"
class="group overflow-hidden rounded-2xl border border-white/[0.06] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0.02))] shadow-[0_18px_70px_rgba(0,0,0,0.14)] transition-all hover:-translate-y-0.5 hover:border-white/[0.10] hover:shadow-[0_24px_90px_rgba(0,0,0,0.20)]">
<div class="flex items-start justify-between gap-4 border-b border-white/[0.05] px-5 py-5">
<a href="{{ $f->profile_url }}" class="min-w-0 flex-1">
<div class="flex items-center gap-4 min-w-0">
<img src="{{ $f->avatar_url }}"
alt="{{ $displayName }}"
class="h-14 w-14 flex-shrink-0 rounded-2xl object-cover ring-1 ring-white/[0.10]"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h2 class="truncate text-base font-semibold text-white/95 group-hover:text-white">{{ $displayName }}</h2>
<span class="inline-flex items-center rounded-full border border-sky-400/20 bg-sky-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-sky-100">
You follow
</span>
@if($f->follows_you)
<span class="inline-flex items-center rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
Mutual
</span>
@endif
</div>
@if(!empty($f->username))
<div class="mt-1 truncate text-xs text-white/35">{{ '@' . $f->username }}</div>
@endif
<div class="mt-2 text-xs text-white/45">
You followed {{ !empty($f->followed_at) ? \Carbon\Carbon::parse($f->followed_at)->diffForHumans() : 'recently' }}
</div>
</div>
</div>
</a>
<div class="shrink-0">
@if(!empty($profileUsername))
<div>
<button @click="toggle"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
:disabled="loading"
class="inline-flex items-center gap-2 rounded-xl border border-emerald-400/25 bg-emerald-400/10 px-3.5 py-2 text-sm font-medium text-emerald-100 transition-all hover:border-rose-400/30 hover:bg-rose-400/10 hover:text-rose-100"
:class="!following ? 'border-white/[0.08] bg-white/[0.04] text-white/60' : ''">
<i class="fa-solid fa-fw text-xs"
:class="loading ? 'fa-circle-notch fa-spin' : (following ? (hovering ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus')"></i>
<span x-text="following ? (hovering ? 'Unfollow' : 'Following') : 'Follow back'"></span>
</button>
</div>
@endif
</div>
</div>
<div class="grid gap-3 px-5 py-4 sm:grid-cols-3">
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Uploads</p>
<p class="mt-1 text-lg font-semibold text-white">{{ number_format((int) $f->uploads) }}</p>
</div>
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Followers</p>
<p class="mt-1 text-lg font-semibold text-white" x-text="typeof count !== 'undefined' ? Number(count).toLocaleString() : '{{ number_format((int) $f->followers_count) }}'"></p>
</div>
<div class="rounded-xl border border-white/[0.06] bg-black/10 p-3">
<p class="text-[11px] font-medium uppercase tracking-wide text-white/35">Relationship</p>
<p class="mt-1 text-sm font-semibold {{ $f->follows_you ? 'text-emerald-200' : 'text-amber-200' }}">
{{ $f->follows_you ? 'Mutual follow' : 'You follow them' }}
</p>
</div>
</div>
<div class="flex items-center justify-between gap-3 border-t border-white/[0.05] px-5 py-4 text-xs text-white/45">
<span>
{{ $f->follows_you && !empty($f->follows_you_at)
? 'They followed you ' . \Carbon\Carbon::parse($f->follows_you_at)->diffForHumans()
: 'They do not follow you back yet' }}
</span>
<a href="{{ $f->profile_url }}" class="inline-flex items-center gap-2 font-medium text-white/70 transition-colors hover:text-white">
View profile
<i class="fa-solid fa-arrow-right text-[10px]"></i>
</a>
</div>
</article>
@endforeach
</div>
<div class="mt-8 flex justify-center">
{{ $following->links() }}
</div>
@endif
</section>
</main>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,32 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">My Gallery</h1>
@if($artworks->isEmpty())
<p class="text-sm text-gray-500">You have not uploaded any artworks yet.</p>
@else
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
@foreach($artworks as $art)
<div class="space-y-3">
<x-artwork-card :art="$art" />
<div class="rounded-xl border border-white/5 bg-white/[0.03] px-3 py-2">
<div class="text-xs text-soft">Published: {{ optional($art->published_at)->format('Y-m-d') }}</div>
<div class="mt-2 flex gap-2">
<a href="{{ route('dashboard.artworks.edit', ['id' => $art->id]) }}" class="text-xs px-2 py-1 bg-black/10 rounded">Edit</a>
<form method="POST" action="{{ route('dashboard.artworks.destroy', ['id' => $art->id]) }}" onsubmit="return confirm('Really delete this artwork?');">
@csrf
@method('DELETE')
<button type="submit" class="text-xs px-2 py-1 bg-red-600 text-white rounded">Delete</button>
</form>
</div>
</div>
</div>
@endforeach
</div>
<div class="mt-6">{{ $artworks->links() }}</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,187 @@
@extends('layouts.nova')
@section('content')
<x-nova-page-header
section="Dashboard"
title="Notifications"
icon="fa-bell"
:breadcrumbs="collect([
(object) ['name' => 'Dashboard', 'url' => '/dashboard'],
(object) ['name' => 'Notifications', 'url' => route('dashboard.notifications')],
])"
description="A dedicated feed for follows, likes, comments, and system events so you can triage account activity quickly."
>
<x-slot name="actions">
<form id="mark-all-notifications-read-form" class="contents">
<button
type="button"
id="mark-all-notifications-read"
class="inline-flex items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/70 transition-colors hover:bg-white/[0.08] hover:text-white"
>
<i class="fa-solid fa-check-double text-xs"></i>
Mark all read
</button>
</form>
</x-slot>
</x-nova-page-header>
<div class="px-6 pb-16 pt-8 md:px-10">
<div class="mb-6 flex flex-wrap items-center gap-2 rounded-2xl border border-white/[0.06] bg-white/[0.025] p-2 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
<a href="{{ route('dashboard.comments.received') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
<i class="fa-solid fa-inbox"></i>
Received
</a>
<a href="{{ route('messages.index') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium text-white/60 transition hover:bg-white/[0.05] hover:text-white">
<i class="fa-solid fa-envelope"></i>
Messages
</a>
<a href="{{ route('dashboard.notifications') }}"
class="inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition {{ request()->routeIs('dashboard.notifications') ? 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-400/20' : 'text-white/60 hover:bg-white/[0.05] hover:text-white' }}">
<i class="fa-solid fa-bell"></i>
Notifications
</a>
</div>
<div class="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<div class="rounded-2xl border border-sky-400/20 bg-sky-500/10 p-5 shadow-[0_18px_45px_rgba(10,132,255,0.10)]">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-200/75">Unread</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($unreadCount) }}</p>
<p class="mt-2 text-sm text-sky-100/70">Notifications that still need attention.</p>
</div>
<div class="rounded-2xl border border-violet-400/20 bg-violet-500/10 p-5">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-200/75">Loaded</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format($notifications->count()) }}</p>
<p class="mt-2 text-sm text-violet-100/70">Items shown on this page.</p>
</div>
<div class="rounded-2xl border border-amber-400/20 bg-amber-500/10 p-5">
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-amber-200/75">Total feed</p>
<p class="mt-3 text-3xl font-semibold text-white">{{ number_format((int) ($notificationsMeta['total'] ?? 0)) }}</p>
<p class="mt-2 text-sm text-amber-100/70">All notifications stored for your account.</p>
</div>
</div>
@if($notifications->isEmpty())
<div class="rounded-[28px] border border-white/[0.07] bg-white/[0.025] px-8 py-16 text-center shadow-[0_18px_45px_rgba(0,0,0,0.18)]">
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35">
<i class="fa-regular fa-bell text-2xl"></i>
</div>
<h2 class="mt-5 text-2xl font-semibold text-white">No notifications yet</h2>
<p class="mx-auto mt-3 max-w-xl text-sm leading-6 text-white/45">
Follows, comments, likes, and system notices will appear here as your account becomes more active.
</p>
</div>
@else
<div class="space-y-4">
@foreach($notifications as $item)
@php
$isUnread = !($item['read'] ?? false);
$actor = $item['actor'] ?? null;
$destination = $item['url'] ?: route('dashboard.comments.received');
@endphp
<a
href="{{ $destination }}"
class="block rounded-[24px] border p-5 transition hover:bg-white/[0.04] {{ $isUnread ? 'border-sky-400/20 bg-sky-500/[0.08]' : 'border-white/[0.07] bg-white/[0.025]' }}"
data-notification-link
data-notification-id="{{ $item['id'] }}"
data-notification-read="{{ ($item['read'] ?? false) ? '1' : '0' }}"
>
<div class="flex items-start gap-4">
<img
src="{{ $actor['avatar_url'] ?? 'https://files.skinbase.org/default/avatar_default.webp' }}"
alt="{{ $actor['name'] ?? 'Notification' }}"
class="h-12 w-12 rounded-full object-cover ring-1 ring-white/10"
>
<div class="min-w-0 flex-1">
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div class="min-w-0">
<p class="text-sm font-semibold text-white/90">{{ $item['message'] ?? 'New activity' }}</p>
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs uppercase tracking-[0.16em] text-white/30">
<span>{{ $item['type'] ?? 'notification' }}</span>
<span>{{ $item['time_ago'] ?? '' }}</span>
@if(!empty($actor['username']))
<span>{{ '@' . $actor['username'] }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
@if($isUnread)
<span class="inline-flex items-center gap-1.5 rounded-full border border-sky-400/25 bg-sky-500/12 px-3 py-1 text-xs font-semibold text-sky-200">
<i class="fa-solid fa-circle text-[8px]"></i>
Unread
</span>
@else
<span class="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-xs font-semibold text-white/50">
Read
</span>
@endif
</div>
</div>
</div>
</div>
</a>
@endforeach
</div>
@if(($notificationsMeta['last_page'] ?? 1) > 1)
<div class="mt-8 flex flex-wrap justify-center gap-2">
@for($page = 1; $page <= (int) ($notificationsMeta['last_page'] ?? 1); $page++)
<a
href="{{ route('dashboard.notifications', ['page' => $page]) }}"
class="inline-flex h-10 min-w-10 items-center justify-center rounded-xl border px-3 text-sm font-medium transition {{ (int) ($notificationsMeta['current_page'] ?? 1) === $page ? 'border-sky-400/30 bg-sky-500/15 text-sky-200' : 'border-white/[0.08] bg-white/[0.03] text-white/60 hover:bg-white/[0.06] hover:text-white' }}"
>
{{ $page }}
</a>
@endfor
</div>
@endif
@endif
</div>
@push('scripts')
<script>
(function () {
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
var markAllButton = document.getElementById('mark-all-notifications-read');
async function postJson(url) {
return fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
credentials: 'same-origin',
});
}
if (markAllButton) {
markAllButton.addEventListener('click', async function () {
markAllButton.disabled = true;
try {
await postJson('/api/notifications/read-all');
window.location.reload();
} catch (_error) {
markAllButton.disabled = false;
}
});
}
document.querySelectorAll('[data-notification-link]').forEach(function (link) {
link.addEventListener('click', async function (event) {
if (link.dataset.notificationRead === '1') return;
event.preventDefault();
try {
await postJson('/api/notifications/' + link.dataset.notificationId + '/read');
} catch (_error) {
// Navigate even if mark-read fails.
}
window.location.assign(link.href);
});
});
})();
</script>
@endpush
@endsection

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skinbase Email Verification</title>
</head>
<body style="margin:0;padding:0;background:#0f172a;color:#e2e8f0;font-family:Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#0f172a;padding:24px;">
<tr>
<td align="center">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:#111827;border:1px solid #1f2937;border-radius:12px;overflow:hidden;">
<tr>
<td style="padding:24px;">
<h1 style="margin:0 0 12px 0;font-size:20px;line-height:1.2;color:#f8fafc;">Verify your new email address</h1>
<p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#cbd5e1;">Use this code to confirm your email change on Skinbase:</p>
<p style="margin:0 0 16px 0;font-size:32px;line-height:1.2;font-weight:700;letter-spacing:4px;color:#f8fafc;">{{ $code }}</p>
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;color:#cbd5e1;">This code expires in {{ $expiresInMinutes }} minutes.</p>
<p style="margin:0;font-size:13px;line-height:1.5;color:#94a3b8;">If you did not request this change, you can ignore this email.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skinbase Security Alert</title>
</head>
<body style="margin:0;padding:0;background:#0f172a;color:#e2e8f0;font-family:Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#0f172a;padding:24px;">
<tr>
<td align="center">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:#111827;border:1px solid #1f2937;border-radius:12px;overflow:hidden;">
<tr>
<td style="padding:24px;">
<h1 style="margin:0 0 12px 0;font-size:20px;line-height:1.2;color:#f8fafc;">Your Skinbase email address was changed</h1>
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;color:#cbd5e1;">Your account email is now set to <strong>{{ $newEmail }}</strong>.</p>
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;color:#cbd5e1;">If you did not perform this action, contact support immediately.</p>
<p style="margin:0;font-size:13px;line-height:1.5;color:#94a3b8;">Support: {{ $supportEmail }}</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify your email</title>
</head>
<body style="margin:0;padding:20px;background:#0b0f14;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial;color:#e6eef6;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;margin:24px auto;border-radius:12px;border:1px solid #1f2937;background:#081016;overflow:hidden;">
<tr>
<td style="padding:20px 24px;border-bottom:1px solid #111827;background:#071018;">
<h2 style="margin:0;font-size:18px;color:#fff;font-weight:600;">{{ config('app.name', 'Skinbase') }}</h2>
</td>
</tr>
<tr>
<td style="padding:24px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0));">
<p style="margin:0 0 12px;color:#cbd5e1;">Welcome to {{ config('app.name', 'Skinbase') }} thanks for signing up.</p>
<p style="margin:0 0 18px;color:#cbd5e1;">Please verify your email to continue account setup.</p>
<div style="text-align:center;margin:20px 0;">
<a href="{{ $verificationUrl }}" style="display:inline-block;padding:12px 20px;background:#0ea5a9;color:#06121a;text-decoration:none;border-radius:8px;font-weight:600;">Verify Email</a>
</div>
<p style="margin:0 0 8px;color:#9fb0c8;font-size:13px;">This link expires in {{ $expiresInHours }} hours.</p>
<p style="margin:12px 0 0;color:#9fb0c8;font-size:13px;">Need help? Contact support: <a href="{{ $supportUrl }}" style="color:#8bd0d3;">{{ $supportUrl }}</a></p>
</td>
</tr>
<tr>
<td style="padding:12px 24px;background:#040607;border-top:1px solid #0e1113;text-align:center;color:#6b7280;font-size:12px;">© {{ date('Y') }} {{ config('app.name', 'Skinbase') }}. All rights reserved.</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; color:#0f172a; }
.container { max-width:700px; margin:24px auto; background:#ffffff; border-radius:8px; padding:20px; box-shadow:0 6px 18px rgba(2,6,23,0.08); }
.brand { display:flex; align-items:center; gap:12px; }
.brand img { height:40px; }
.title { margin-top:16px; font-size:20px; font-weight:700; color:#0b1220; }
.meta { margin-top:8px; color:#475569; font-size:13px; }
.section { margin-top:18px; }
.label { font-weight:600; color:#0b1220; font-size:13px; }
.value { margin-top:6px; color:#0f172a; }
.footer { margin-top:22px; color:#64748b; font-size:12px; }
a { color:#0ea5e9; text-decoration:none; }
.pill { display:inline-block; padding:4px 8px; border-radius:999px; background:#f1f5f9; color:#0b1220; font-size:12px; }
pre { white-space:pre-wrap; font-family:inherit; font-size:13px; color:#0f172a; }
</style>
</head>
<body>
<div class="container">
<div class="brand">
<img src="https://cdn.skinbase.org/images/skinbase_logo_64.webp" alt="Skinbase">
<div>
<div style="font-weight:700;">Skinbase</div>
<div style="font-size:12px;color:#64748b;">New staff application / contact form submission</div>
</div>
</div>
<div class="title">New {{ $topicLabel }}: {{ $application->name }}</div>
<div class="meta">Received {{ $application->created_at->toDayDateTimeString() }}</div>
<div class="section">
<div class="label">Details</div>
<div class="value">
<div><span class="pill">Topic</span> {{ $topicLabel }}</div>
<div><strong>Name:</strong> {{ $application->name }}</div>
<div><strong>Email:</strong> <a href="mailto:{{ $application->email }}">{{ $application->email }}</a></div>
@if($application->role)<div><strong>Role:</strong> {{ $application->role }}</div>@endif
@if($application->portfolio)<div><strong>Portfolio:</strong> <a href="{{ $application->portfolio }}">{{ $application->portfolio }}</a></div>@endif
</div>
</div>
@if($application->topic === 'bug')
<div class="section">
<div class="label">Bug report</div>
<div class="value">
@if($application->payload['data']['affected_url'] ?? false)
<div><strong>Affected URL:</strong> <a href="{{ $application->payload['data']['affected_url'] }}">{{ $application->payload['data']['affected_url'] }}</a></div>
@endif
@if($application->payload['data']['steps'] ?? false)
<div style="margin-top:8px"><strong>Steps to reproduce:</strong>
<pre>{{ $application->payload['data']['steps'] }}</pre>
</div>
@endif
</div>
</div>
@endif
<div class="section">
<div class="label">Message</div>
<div class="value"><pre>{{ $application->message }}</pre></div>
</div>
<div class="section">
<div class="label">Technical</div>
<div class="value">
<div><strong>IP:</strong> {{ $application->ip }}</div>
<div style="margin-top:6px"><strong>User agent:</strong> {{ $application->user_agent }}</div>
</div>
</div>
<div class="footer">
<div>If you prefer to manage submissions in the admin UI, open: <a href="{{ url('/admin/applications') }}">/admin/applications</a></div>
<div style="margin-top:8px"> Skinbase</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
New {{ $topicLabel }}: {{ $application->name }}
Received: {{ $application->created_at->toDayDateTimeString() }}
Topic: {{ $topicLabel }}
Name: {{ $application->name }}
Email: {{ $application->email }}
@if($application->role)
Role: {{ $application->role }}
@endif
@if($application->portfolio)
Portfolio: {{ $application->portfolio }}
@endif
Message:
{{ $application->message }}
Technical:
IP: {{ $application->ip }}
User agent: {{ $application->user_agent }}
Manage submissions: {{ url('/admin/applications') }}

View File

@@ -0,0 +1,28 @@
{{--
401 Unauthorized
Use for: routes that require authentication when user is not logged in.
--}}
@extends('errors._layout', [
'error_code' => 401,
'error_title' => 'Sign In Required',
'error_message' => 'Please sign in to access this page.',
])
@section('badge', 'Unauthorized')
@section('primary-cta')
<a href="/login"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-sign-in-alt" aria-hidden="true"></i>
Sign In
</a>
@endsection
@section('secondary-ctas')
<a href="/register" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Create Account
</a>
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection

View File

@@ -0,0 +1,43 @@
{{--
403 Forbidden
Use for: private artwork, banned content, region restrictions.
Shows login button if user is a guest; profile/discover if logged in.
--}}
@extends('errors._layout', [
'error_code' => 403,
'error_title' => 'Access Denied',
'error_message' => $message ?? 'You do not have permission to view this content.',
])
@section('badge', 'Forbidden')
@section('primary-cta')
@guest
<a href="/login"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-sign-in-alt" aria-hidden="true"></i>
Sign In
</a>
@else
<a href="/discover/trending"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-compass" aria-hidden="true"></i>
Back to Discover
</a>
@endguest
@endsection
@section('secondary-ctas')
@guest
<a href="/register" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Create Account
</a>
@else
<a href="/@{{ Auth::user()->username }}" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
My Profile
</a>
@endguest
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection

View File

@@ -0,0 +1,90 @@
{{--
404 Generic Not Found
Returned when a route has no match.
Includes: trending artworks (6), top tags (10), discover CTA.
--}}
@extends('errors._layout', [
'error_code' => 404,
'error_title' => '404 — Lost in the Nova',
'error_message' => 'This page drifted into deep space. Let\'s get you back on track.',
])
@section('badge', 'Page Not Found')
@section('primary-cta')
<a href="/discover/trending"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-compass" aria-hidden="true"></i>
Explore Discover
</a>
@endsection
@section('secondary-ctas')
<a href="/explore/wallpapers" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Browse Wallpapers
</a>
<a href="/search" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-search mr-1.5" aria-hidden="true"></i> Search
</a>
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection
@section('recovery')
{{-- Trending artworks --}}
@if(isset($trendingArtworks) && $trendingArtworks->count())
<div class="mb-12">
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Trending Right Now</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
@foreach($trendingArtworks->take(6) as $artwork)
@include('errors._artwork-card', ['artwork' => $artwork])
@endforeach
</div>
</div>
@endif
{{-- Top tags --}}
@if(isset($trendingTags) && $trendingTags->count())
<div class="mb-12">
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Popular Tags</h2>
<div class="flex flex-wrap gap-2">
@foreach($trendingTags->take(10) as $tag)
<a href="/tag/{{ $tag->slug }}"
class="rounded-full bg-white/5 hover:bg-sky-500/20 border border-white/8 hover:border-sky-500/30 text-white/70 hover:text-sky-300 px-3 py-1 text-xs font-medium transition-colors">
#{{ $tag->name }}
</a>
@endforeach
</div>
</div>
@endif
{{-- Explore categories --}}
<div>
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Explore Categories</h2>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<a href="/explore/wallpapers"
class="rounded-xl bg-white/3 hover:bg-sky-500/10 border border-white/5 hover:border-sky-500/20 px-4 py-3 flex items-center gap-3 transition-all group">
<i class="fas fa-image text-sky-400/60 group-hover:text-sky-400 text-lg" aria-hidden="true"></i>
<span class="text-sm font-medium text-white/80 group-hover:text-white">Wallpapers</span>
</a>
<a href="/explore/skins"
class="rounded-xl bg-white/3 hover:bg-sky-500/10 border border-white/5 hover:border-sky-500/20 px-4 py-3 flex items-center gap-3 transition-all group">
<i class="fas fa-paint-brush text-sky-400/60 group-hover:text-sky-400 text-lg" aria-hidden="true"></i>
<span class="text-sm font-medium text-white/80 group-hover:text-white">Skins</span>
</a>
<a href="/explore/photography"
class="rounded-xl bg-white/3 hover:bg-sky-500/10 border border-white/5 hover:border-sky-500/20 px-4 py-3 flex items-center gap-3 transition-all group">
<i class="fas fa-camera text-sky-400/60 group-hover:text-sky-400 text-lg" aria-hidden="true"></i>
<span class="text-sm font-medium text-white/80 group-hover:text-white">Photography</span>
</a>
<a href="/explore/other"
class="rounded-xl bg-white/3 hover:bg-sky-500/10 border border-white/5 hover:border-sky-500/20 px-4 py-3 flex items-center gap-3 transition-all group">
<i class="fas fa-layer-group text-sky-400/60 group-hover:text-sky-400 text-lg" aria-hidden="true"></i>
<span class="text-sm font-medium text-white/80 group-hover:text-white">Other</span>
</a>
</div>
</div>
@endsection

View File

@@ -0,0 +1,29 @@
{{--
410 Gone
Use for permanently deleted artworks, DMCA removed content, deleted blog posts.
Minimal content no heavy suggestions needed by spec.
--}}
@extends('errors._layout', [
'error_code' => 410,
'error_title' => 'Content Permanently Removed',
'error_message' => 'This content has been permanently removed and is no longer available.',
])
@section('badge', 'Gone')
@section('primary-cta')
<a href="/discover/trending"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-compass" aria-hidden="true"></i>
Explore Discover
</a>
@endsection
@section('secondary-ctas')
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Return Home
</a>
<a href="/search" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-search mr-1.5" aria-hidden="true"></i> Search
</a>
@endsection

View File

@@ -0,0 +1,24 @@
{{--
419 Page Expired (CSRF token mismatch)
--}}
@extends('errors._layout', [
'error_code' => 419,
'error_title' => 'Page Expired',
'error_message' => 'Your session has expired. Please refresh the page and try again.',
])
@section('badge', 'Session Expired')
@section('primary-cta')
<button onclick="window.location.reload()"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors cursor-pointer">
<i class="fas fa-redo" aria-hidden="true"></i>
Refresh Page
</button>
@endsection
@section('secondary-ctas')
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Return Home
</a>
@endsection

View File

@@ -0,0 +1,46 @@
{{--
500 Server Error
Shows a user-friendly message and a reference/correlation ID.
Never shows a stack trace in production.
--}}
@extends('errors._layout', [
'error_code' => 500,
'error_title' => 'Something Went Wrong in the Nova',
'error_message' => 'An unexpected error occurred. Our team has been notified and is on it.',
])
@section('badge', 'Server Error')
@section('primary-cta')
<button onclick="window.location.reload()"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors cursor-pointer">
<i class="fas fa-redo" aria-hidden="true"></i>
Try Again
</button>
@endsection
@section('secondary-ctas')
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Return Home
</a>
<a href="/contact" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Report Issue
</a>
@endsection
@section('recovery')
@if(isset($correlationId))
<div class="flex justify-center">
<div class="inline-flex items-center gap-2 rounded-xl bg-white/4 border border-white/8 px-5 py-3 text-xs text-white/40">
<i class="fas fa-fingerprint text-white/25" aria-hidden="true"></i>
Reference ID: <span class="font-mono font-semibold text-white/60 select-all">{{ $correlationId }}</span>
</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

View File

@@ -0,0 +1,18 @@
{{--
503 Service Unavailable / Maintenance Mode
--}}
@extends('errors._layout', [
'error_code' => 503,
'error_title' => 'We\'ll Be Right Back',
'error_message' => 'Skinbase is under scheduled maintenance. Check back soon.',
])
@section('badge', 'Maintenance')
@section('primary-cta')
<button onclick="window.location.reload()"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors cursor-pointer">
<i class="fas fa-redo" aria-hidden="true"></i>
Check Again
</button>
@endsection

View File

@@ -0,0 +1,24 @@
{{--
Shared: artwork suggestion card for error pages.
Expects $artwork array: [id, title, author, url, thumb]
--}}
<a href="{{ $artwork['url'] }}" class="group relative rounded-xl overflow-hidden bg-nova-800 border border-white/5 hover:border-sky-500/30 transition-all duration-200 block">
@if($artwork['thumb'])
<div class="aspect-video w-full overflow-hidden bg-nova-700">
<img src="{{ $artwork['thumb'] }}"
@if(!empty($artwork['thumb_srcset'])) srcset="{{ $artwork['thumb_srcset'] }}" @endif
sizes="(max-width: 768px) 50vw, 240px"
alt="{{ $artwork['title'] }}"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 opacity-80 group-hover:opacity-100" />
</div>
@else
<div class="aspect-video w-full bg-gradient-to-br from-nova-700 to-nova-800 flex items-center justify-center">
<i class="fas fa-image text-white/20 text-3xl" aria-hidden="true"></i>
</div>
@endif
<div class="p-3">
<p class="text-sm font-semibold text-white truncate">{{ $artwork['title'] }}</p>
<p class="text-xs text-white/50 truncate mt-0.5">by {{ $artwork['author'] }}</p>
</div>
</a>

View File

@@ -0,0 +1,71 @@
{{--
Error Layout extends nova.blade.php
Shared structure for all error pages (404, 410, 403, 401, 500, contextual variants).
Enforces:
noindex
No canonical link
Dark Nova design
Full navigation visible
Recovery CTAs
Variables:
$error_code int HTTP status code
$error_title string Short headline
$error_message string Friendly sentence
--}}
@extends('layouts.nova')
@php
$code = $error_code ?? 404;
$title = $error_title ?? 'Page Not Found';
$message = $error_message ?? 'This page drifted into deep space.';
@endphp
@push('head')
{{-- SEO: never index error pages --}}
<meta name="robots" content="noindex, nofollow" />
@endpush
@section('content')
<div class="min-h-[70vh] flex flex-col items-center justify-center px-4 py-16">
{{-- Hero block --}}
<div class="text-center max-w-xl mx-auto">
{{-- Code glow --}}
<div class="text-8xl font-extrabold text-sky-500/20 select-none leading-none mb-2">{{ $code }}</div>
{{-- Gradient badge --}}
<div class="inline-flex items-center gap-2 rounded-full bg-sky-500/10 border border-sky-500/20 px-4 py-1 text-xs font-semibold text-sky-400 uppercase tracking-widest mb-6">
@yield('badge', 'Error')
</div>
<h1 class="text-3xl sm:text-4xl font-extrabold text-white leading-tight mb-4">
{{ $title }}
</h1>
<p class="text-white/60 text-base sm:text-lg leading-relaxed mb-8">
{{ $message }}
</p>
{{-- Primary CTA --}}
@yield('primary-cta')
{{-- Secondary CTAs --}}
@hasSection('secondary-ctas')
<div class="flex flex-wrap justify-center gap-3 mt-4">
@yield('secondary-ctas')
</div>
@endif
</div>
{{-- Contextual recovery section --}}
@hasSection('recovery')
<div class="w-full max-w-5xl mx-auto mt-16">
@yield('recovery')
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,99 @@
{{--
Artwork Not Found (contextual) HTTP 404 or 403
Shown when:
- Artwork ID not found at all HTTP 404
- Artwork exists but is private/unapproved HTTP 403 ($isForbidden=true)
Separate view for permanently deleted errors/410.blade.php
Variables:
$isForbidden bool true when private/403
$trendingArtworks Collection (max 6)
$creatorArtworks Collection (max 6, optional)
$creatorUsername string|null
--}}
@php
$isForbidden = $isForbidden ?? false;
$errorCode = $isForbidden ? 403 : 404;
$errorTitle = $isForbidden ? 'Access Denied' : 'Artwork Not Found';
$errorMessage = $isForbidden
? 'This artwork is private and not publicly available.'
: 'This artwork is no longer available, or the link may be broken.';
$badgeLabel = $isForbidden ? 'Private Artwork' : 'Artwork Not Found';
@endphp
@extends('errors._layout', [
'error_code' => $errorCode,
'error_title' => $errorTitle,
'error_message' => $errorMessage,
])
@section('badge', $badgeLabel)
@section('primary-cta')
@if($isForbidden)
@guest
<a href="/login"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-sign-in-alt" aria-hidden="true"></i>
Sign In to View
</a>
@else
<a href="/discover/trending"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-compass" aria-hidden="true"></i>
Explore Discover
</a>
@endguest
@else
<a href="/discover/trending"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-compass" aria-hidden="true"></i>
Explore Discover
</a>
@endif
@endsection
@section('secondary-ctas')
<a href="/explore/wallpapers" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Browser Wallpapers
</a>
<a href="/search" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-search mr-1.5" aria-hidden="true"></i> Search
</a>
@endsection
@section('recovery')
{{-- Creator's other artworks (if we have a hint about the creator) --}}
@if(isset($creatorArtworks) && $creatorArtworks->count())
<div class="mb-12">
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">
More from this Creator
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
@foreach($creatorArtworks->take(6) as $artwork)
@include('errors._artwork-card', ['artwork' => $artwork])
@endforeach
</div>
@if(isset($creatorUsername))
<div class="mt-3">
<a href="/@{{ $creatorUsername }}" class="text-xs text-sky-400 hover:text-sky-300 transition-colors">
View full gallery
</a>
</div>
@endif
</div>
@endif
{{-- Trending artworks --}}
@if(isset($trendingArtworks) && $trendingArtworks->count())
<div>
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Trending Wallpapers</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
@foreach($trendingArtworks->take(6) as $artwork)
@include('errors._artwork-card', ['artwork' => $artwork])
@endforeach
</div>
</div>
@endif
@endsection

View File

@@ -0,0 +1,52 @@
{{--
Blog Post Not Found Contextual 404
Shown at /blog/:slug when post doesn't exist or is unpublished.
Variables:
$latestPosts Collection (max 6)
--}}
@extends('errors._layout', [
'error_code' => 404,
'error_title' => 'Article Not Found',
'error_message' => 'This article is no longer available or the link has changed.',
])
@section('badge', 'Article Not Found')
@section('primary-cta')
<a href="/blog"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-newspaper" aria-hidden="true"></i>
Visit Blog
</a>
@endsection
@section('secondary-ctas')
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection
@section('recovery')
@if(isset($latestPosts) && $latestPosts->count())
<div>
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Latest Articles</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach($latestPosts->take(6) as $post)
<a href="{{ $post['url'] }}"
class="flex flex-col gap-2 rounded-xl p-4 bg-white/3 hover:bg-white/7 border border-white/5 hover:border-sky-500/20 transition-all">
<p class="text-sm font-semibold text-white leading-snug">{{ $post['title'] }}</p>
@if(!empty($post['excerpt']))
<p class="text-xs text-white/50 leading-relaxed flex-1">{{ $post['excerpt'] }}</p>
@endif
@if(!empty($post['published_at']))
<p class="text-xs text-white/30 mt-auto">{{ $post['published_at'] }}</p>
@endif
</a>
@endforeach
</div>
</div>
@endif
@endsection

View File

@@ -0,0 +1,94 @@
{{--
Creator Not Found Contextual 404
Shown at /@:username when user doesn't exist.
Variables:
$requestedUsername string|null
$trendingCreators Collection (max 6)
$recentCreators Collection (max 6)
--}}
@extends('errors._layout', [
'error_code' => 404,
'error_title' => 'Creator Not Found',
'error_message' => isset($requestedUsername)
? 'The creator "@' . $requestedUsername . '" does not exist on Skinbase.'
: 'This creator profile does not exist.',
])
@section('badge', 'Creator Not Found')
@section('primary-cta')
{{-- Inline creator search --}}
<form action="/search" method="GET" class="flex items-center gap-2 w-full max-w-sm mx-auto mb-2">
<input
type="text"
name="q"
placeholder="Search for a creator…"
value="{{ isset($requestedUsername) ? '@'.$requestedUsername : '' }}"
class="flex-1 rounded-xl bg-white/8 border border-white/12 focus:border-sky-500/50 focus:ring-0 text-sm text-white placeholder-white/30 px-4 py-2.5 outline-none transition-colors"
/>
<button type="submit"
class="rounded-xl bg-sky-500 hover:bg-sky-400 text-white px-4 py-2.5 text-sm font-semibold transition-colors shrink-0">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</form>
@endsection
@section('secondary-ctas')
<a href="/creators/top" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-trophy mr-1.5" aria-hidden="true"></i> Top Creators
</a>
<a href="/register" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-star mr-1.5" aria-hidden="true"></i> Join Skinbase
</a>
@endsection
@section('recovery')
{{-- Trending creators --}}
@if(isset($trendingCreators) && $trendingCreators->count())
<div class="mb-12">
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Top Creators</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4">
@foreach($trendingCreators->take(6) as $creator)
<a href="{{ $creator['url'] }}"
class="flex flex-col items-center gap-2 rounded-xl p-4 bg-white/3 hover:bg-white/7 border border-white/5 hover:border-sky-500/20 transition-all text-center group">
<img src="{{ $creator['avatar_url'] }}"
alt="{{ $creator['name'] }}"
loading="lazy"
class="w-14 h-14 rounded-full object-cover ring-2 ring-white/10 group-hover:ring-sky-500/30 transition-all"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'" />
<div>
<p class="text-sm font-semibold text-white truncate max-w-[100px]">{{ $creator['name'] }}</p>
<p class="text-xs text-white/40">{{ $creator['artworks_count'] }} uploads</p>
</div>
</a>
@endforeach
</div>
</div>
@endif
{{-- Recently joined creators --}}
@if(isset($recentCreators) && $recentCreators->count())
<div>
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Recently Joined</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4">
@foreach($recentCreators->take(6) as $creator)
<a href="{{ $creator['url'] }}"
class="flex flex-col items-center gap-2 rounded-xl p-4 bg-white/3 hover:bg-white/7 border border-white/5 hover:border-sky-500/20 transition-all text-center group">
<img src="{{ $creator['avatar_url'] }}"
alt="{{ $creator['name'] }}"
loading="lazy"
class="w-14 h-14 rounded-full object-cover ring-2 ring-white/10 group-hover:ring-sky-500/30 transition-all"
onerror="this.src='https://files.skinbase.org/default/avatar_default.webp'" />
<div>
<p class="text-sm font-semibold text-white truncate max-w-[100px]">{{ $creator['name'] }}</p>
<p class="text-xs text-white/40">{{ $creator['artworks_count'] }} uploads</p>
</div>
</a>
@endforeach
</div>
</div>
@endif
@endsection

View File

@@ -0,0 +1,31 @@
{{--
Static Page Not Found Contextual 404
Shown at /pages/:slug or /about|/help|/contact when page not in DB.
--}}
@extends('errors._layout', [
'error_code' => 404,
'error_title' => 'Page Not Found',
'error_message' => 'This page was removed or renamed. Try one of the links below.',
])
@section('badge', 'Page Not Found')
@section('primary-cta')
<a href="/help"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-question-circle" aria-hidden="true"></i>
Help Center
</a>
@endsection
@section('secondary-ctas')
<a href="/about" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
About
</a>
<a href="/contact" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Contact
</a>
<a href="/" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Home
</a>
@endsection

View File

@@ -0,0 +1,70 @@
{{--
Tag Not Found Contextual 404
Shown at /tag/:slug when slug not in DB.
Variables:
$requestedSlug string
$similarTags Collection (max 10)
$trendingTags Collection (max 10)
--}}
@extends('errors._layout', [
'error_code' => 404,
'error_title' => 'Tag Not Found',
'error_message' => 'The tag "' . ($requestedSlug ?? '') . '" doesn\'t exist yet.',
])
@section('badge', 'Tag Not Found')
@section('primary-cta')
<a href="/tags"
class="inline-flex items-center gap-2 rounded-xl bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 text-sm shadow-lg shadow-sky-900/30 transition-colors">
<i class="fas fa-tags" aria-hidden="true"></i>
Browse All Tags
</a>
@endsection
@section('secondary-ctas')
<a href="/discover/trending" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
Trending
</a>
<a href="/search" class="rounded-xl border border-white/10 hover:border-white/25 text-white/70 hover:text-white px-4 py-2 text-sm transition-colors">
<i class="fas fa-search mr-1.5" aria-hidden="true"></i> Search
</a>
@endsection
@section('recovery')
{{-- Similar tags --}}
@if(isset($similarTags) && $similarTags->count())
<div class="mb-10">
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Similar Tags</h2>
<div class="flex flex-wrap gap-2">
@foreach($similarTags->take(10) as $tag)
<a href="/tag/{{ $tag->slug }}"
class="rounded-full bg-sky-500/10 hover:bg-sky-500/20 border border-sky-500/20 hover:border-sky-500/40 text-sky-300 hover:text-sky-200 px-3 py-1 text-xs font-medium transition-colors">
#{{ $tag->name }}
@if($tag->artworks_count ?? null)
<span class="text-sky-400/60 ml-1">{{ number_format($tag->artworks_count) }}</span>
@endif
</a>
@endforeach
</div>
</div>
@endif
{{-- Trending tags --}}
@if(isset($trendingTags) && $trendingTags->count())
<div>
<h2 class="text-sm font-semibold text-white/40 uppercase tracking-widest mb-4">Popular Tags</h2>
<div class="flex flex-wrap gap-2">
@foreach($trendingTags->take(10) as $tag)
<a href="/tag/{{ $tag->slug }}"
class="rounded-full bg-white/5 hover:bg-sky-500/20 border border-white/8 hover:border-sky-500/30 text-white/70 hover:text-sky-300 px-3 py-1 text-xs font-medium transition-colors">
#{{ $tag->name }}
</a>
@endforeach
</div>
</div>
@endif
@endsection

Some files were not shown because too many files have changed in this diff Show More