optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -1,5 +1,26 @@
@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 ?? [];
@endphp
@push('head')
<title>{{ $meta['title'] }}</title>
<meta name="description" content="{{ $meta['description'] }}">
@@ -30,7 +51,7 @@
@php
$authorName = $artwork->user?->name ?: $artwork->user?->username ?: null;
$keywords = $artwork->tags->pluck('name')->merge($artwork->categories->pluck('name'))->filter()->unique()->implode(', ');
$keywords = $artwork->tags->pluck('name')->filter()->unique()->values()->all();
$license = $artwork->license_url ?? null;
$imageObject = [
@@ -47,7 +68,7 @@
'author' => $authorName ? ['@type' => 'Person', 'name' => $authorName] : null,
'datePublished' => optional($artwork->published_at)->toAtomString(),
'license' => $license,
'keywords' => $keywords !== '' ? $keywords : null,
'keywords' => !empty($keywords) ? $keywords : null,
];
$creativeWork = [
@@ -59,7 +80,7 @@
'author' => $authorName ? ['@type' => 'Person', 'name' => $authorName] : null,
'datePublished' => optional($artwork->published_at)->toAtomString(),
'license' => $license,
'keywords' => $keywords !== '' ? $keywords : null,
'keywords' => !empty($keywords) ? $keywords : null,
'image' => $meta['og_image'] ?? null,
];

View File

@@ -0,0 +1,132 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? 'Card Challenges - Skinbase Nova' }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.challenges') }}" />
@endpush
@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,67 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? ($collection['name'] . ' - Nova Cards Collection - Skinbase Nova') }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? $collection['public_url'] }}" />
@if(!empty($meta['robots']))
<meta name="robots" content="{{ $meta['robots'] }}" />
@endif
<script type="application/ld+json">
{!! json_encode([
'@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(),
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
</script>
@endpush
@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,801 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? 'Nova Cards - Skinbase Nova' }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.index') }}" />
@if(!empty($meta['robots']))
<meta name="robots" content="{{ $meta['robots'] }}" />
@endif
<script type="application/ld+json">
{!! json_encode([
'@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(),
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
</script>
@endpush
@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-rectangle-history-circle-user 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,60 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? 'Nova Card Lineage - Skinbase Nova' }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.lineage', ['slug' => $card['slug'], 'id' => $card['id']]) }}" />
@if(!empty($meta['robots']))
<meta name="robots" content="{{ $meta['robots'] }}" />
@endif
@endpush
@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,58 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? 'Nova Cards Resources - Skinbase Nova' }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? route('cards.index') }}" />
@endpush
@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,393 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'] ?? '')
@push('head')
<title>{{ $meta['title'] ?? ($card['title'] . ' - Nova Cards - Skinbase Nova') }}</title>
<link rel="canonical" href="{{ $meta['canonical'] ?? $card['public_url'] }}" />
@if(!empty($meta['robots']))
<meta name="robots" content="{{ $meta['robots'] }}" />
@endif
@if(!empty($card['og_preview_url']))
<meta property="og:image" content="{{ $card['og_preview_url'] }}" />
@endif
<script type="application/ld+json">
{!! json_encode([
'@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('/'),
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
</script>
@endpush
@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

@@ -4,6 +4,7 @@
use App\Support\ForumPostContent;
use App\Support\AvatarUrl;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
$filesBaseUrl = rtrim((string) config('cdn.files_url', ''), '/');
@@ -26,6 +27,8 @@
'name' => $user->name,
'avatar_url' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null),
'role' => $user->role ?? 'member',
'level' => (int) ($user->level ?? 1),
'rank' => (string) ($user->rank ?? 'Newbie'),
] : null,
'attachments' => collect($post->attachments ?? [])->map(fn ($a) => [
'id' => $a->id,
@@ -40,6 +43,15 @@
$serializedOp = isset($opPost) && $opPost ? $serializePost($opPost) : null;
$serializedPosts = collect($posts->items())->map($serializePost)->values()->all();
$threadDescription = null;
$threadDescriptionSource = (string) ($serializedOp['rendered_content'] ?? $serializedOp['content'] ?? '');
if ($threadDescriptionSource !== '') {
$threadDescriptionSource = html_entity_decode($threadDescriptionSource, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$threadDescriptionSource = html_entity_decode($threadDescriptionSource, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$threadDescription = trim((string) preg_replace('/\s+/u', ' ', strip_tags($threadDescriptionSource)));
$threadDescription = Str::limit($threadDescription, 220);
}
$paginationData = [
'current_page' => $posts->currentPage(),
@@ -57,6 +69,7 @@
'id' => $thread->id,
'title' => $thread->title,
'slug' => $thread->slug,
'description'=> $threadDescription,
'views' => (int) ($thread->views ?? 0),
'is_pinned' => (bool) $thread->is_pinned,
'is_locked' => (bool) $thread->is_locked,

View File

@@ -47,6 +47,34 @@
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>
@endif
@if(($gallery_type ?? null) === 'tag' && !empty($tag_context))
<script type="application/ld+json">
{!! json_encode([
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $page_title ?? ($hero_title ?? ('#' . ($tag_context['name'] ?? 'Tag'))),
'description' => $page_meta_description ?? ($hero_description ?? null),
'url' => $page_canonical ?? $seoUrl($seoPage),
'mainEntity' => [
'@type' => 'ItemList',
'numberOfItems' => method_exists($artworks, 'total') ? $artworks->total() : count($artworks ?? []),
'itemListElement' => collect(method_exists($artworks, 'getCollection') ? $artworks->getCollection() : ($artworks ?? []))
->take(12)
->values()
->map(fn ($artwork, $index) => [
'@type' => 'ListItem',
'position' => $index + 1,
'url' => !empty($artwork->slug) ? url('/' . $artwork->slug) : null,
'name' => $artwork->name ?? null,
])
->filter(fn (array $item) => filled($item['url']) || filled($item['name']))
->values()
->all(),
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) !!}
</script>
@endif
@endpush
@php

View File

@@ -1,5 +1,18 @@
@php
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
$deferToolbarSearch = request()->routeIs('index');
$metaDescription = trim($__env->yieldContent('meta-description', $page_meta_description ?? ''));
$metaKeywords = trim($__env->yieldContent('meta-keywords', $page_meta_keywords ?? ''));
$novaViteEntries = [
'resources/css/app.css',
'resources/css/nova-grid.css',
'resources/scss/nova.scss',
'resources/js/nova.js',
];
if (!$deferToolbarSearch) {
$novaViteEntries[] = 'resources/js/entry-search.jsx';
}
@endphp
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}" data-grid-version="{{ $gridVersion }}">
@@ -9,8 +22,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="description" content="{{ $page_meta_description ?? '' }}">
<meta name="keywords" content="{{ $page_meta_keywords ?? '' }}">
<meta name="description" content="{{ $metaDescription }}">
<meta name="keywords" content="{{ $metaKeywords }}">
@isset($page_robots)
<meta name="robots" content="{{ $page_robots }}" />
@endisset
@@ -35,9 +48,7 @@
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
@vite(['resources/css/app.css','resources/css/nova-grid.css','resources/scss/nova.scss','resources/js/nova.js','resources/js/entry-search.jsx'])
@vite($novaViteEntries)
<style>
/* Card enter animation */
.nova-card-enter { opacity: 0; transform: translateY(10px) scale(0.995); }
@@ -66,6 +77,61 @@
[x-cloak] { display: none !important; }
</style>
@stack('head')
@if($deferToolbarSearch)
<script type="module">
(() => {
const searchEntryUrl = @js(Vite::asset('resources/js/entry-search.jsx'));
const triggerEvents = ['pointerdown', 'touchstart', 'focusin'];
let searchLoaded = false;
const loadSearch = () => {
if (searchLoaded) {
return;
}
searchLoaded = true;
cleanup();
import(searchEntryUrl);
};
const onReady = () => {
const searchRoot = document.getElementById('topbar-search-root');
if (!searchRoot) {
return;
}
triggerEvents.forEach((eventName) => {
searchRoot.addEventListener(eventName, loadSearch, { once: true, passive: true });
});
if ('requestIdleCallback' in window) {
window.requestIdleCallback(loadSearch, { timeout: 2000 });
} else {
window.setTimeout(loadSearch, 1500);
}
};
const cleanup = () => {
const searchRoot = document.getElementById('topbar-search-root');
if (!searchRoot) {
return;
}
triggerEvents.forEach((eventName) => {
searchRoot.removeEventListener(eventName, loadSearch);
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onReady, { once: true });
} else {
onReady();
}
})();
</script>
@endif
@if(isset($page) && is_array($page))
@inertiaHead
@endif

View File

@@ -2,7 +2,7 @@
<footer class="border-t border-neutral-800 bg-nova">
<div class="px-6 md:px-10 py-8 flex flex-col md:flex-row md:items-center md:justify-between gap-2">
<div class="text-xl font-semibold tracking-wide flex items-center gap-1">
<img src="/gfx/skinbase_logo.png" alt="Skinbase" class="h-16 w-auto object-contain">
<img src="/gfx/skinbase_logo.png" alt="Skinbase" width="320" height="64" class="h-16 w-auto object-contain">
<span class="sr-only">Skinbase</span>
</div>

View File

@@ -1,11 +1,11 @@
<header class="fixed inset-x-0 top-0 z-50 h-16 bg-black/40 backdrop-blur border-b border-white/10">
<div class="mx-auto w-full h-full px-4 flex items-center gap-3">
<div class="mx-auto w-full h-full px-3 sm:px-4 flex items-center gap-2 sm:gap-3">
<!-- Mobile hamburger -->
<button id="btnSidebar"
type="button"
data-mobile-toggle
class="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-lg hover:bg-white/5"
class="lg:hidden inline-flex items-center justify-center w-10 h-10 rounded-lg hover:bg-white/5"
aria-label="Open menu"
aria-controls="mobileMenu"
aria-expanded="false">
@@ -19,7 +19,7 @@
<!-- Logo -->
<a href="/" class="flex items-center gap-2 pr-2 shrink-0">
<img src="/gfx/sb_logo.png" alt="Skinbase.org" class="h-8 w-auto rounded-sm shadow-sm object-contain">
<img src="/gfx/sb_logo.webp" alt="Skinbase.org" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
<span class="sr-only">Skinbase.org</span>
</a>
@@ -169,36 +169,21 @@
</nav>
<!-- Search: collapsed pill expands on click -->
<div class="flex-1 flex items-center justify-center px-2 min-w-0">
<div class="flex-1 flex items-center justify-center px-1 sm:px-2 min-w-0">
<div id="topbar-search-root" class="w-full flex justify-center"></div>
</div>
@auth
<!-- Upload CTA -->
<a href="{{ route('upload') }}"
class="hidden md:inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium transition-colors shrink-0">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14" />
</svg>
Upload
</a>
<!-- Notification icons -->
<div class="hidden md:flex items-center gap-1 text-soft">
<a href="{{ route('discover.for-you') }}"
class="relative w-10 h-10 inline-flex items-center justify-center rounded-lg transition-colors {{ request()->routeIs('discover.for-you') ? 'bg-yellow-500/15 text-yellow-300' : 'hover:bg-white/5' }}"
title="For You">
<i class="fa-solid fa-wand-magic-sparkles w-5 h-5 text-[1.1rem] {{ request()->routeIs('discover.for-you') ? 'text-yellow-300' : 'text-sb-muted' }}"></i>
</a>
<div class="hidden md:flex items-center gap-0.5 lg:gap-1 text-soft shrink-0">
<a href="{{ route('dashboard.favorites') }}"
class="relative w-10 h-10 inline-flex items-center justify-center rounded-lg hover:bg-white/5"
class="relative inline-flex w-9 h-9 lg:w-10 lg:h-10 items-center justify-center rounded-lg hover:bg-white/5"
title="Favourites">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="w-[18px] h-[18px] lg:w-5 lg:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 21s-7-4.4-9-9a5.5 5.5 0 0 1 9-6 5.5 5.5 0 0 1 9 6c-2 4.6-9 9-9 9z" />
</svg>
@if(($favCount ?? 0) > 0)
<span class="absolute -bottom-1 right-0 text-[11px] tabular-nums px-1.5 py-0.5 rounded bg-red-700/70 text-white border border-sb-line">{{ $favCount }}</span>
<span class="absolute -bottom-1 right-0 text-[10px] lg:text-[11px] tabular-nums px-1 py-0 lg:px-1.5 lg:py-0.5 rounded bg-red-700/70 text-white border border-sb-line">{{ $favCount }}</span>
@endif
</a>
@@ -216,7 +201,7 @@
<!-- Profile dropdown -->
<div class="relative">
<button class="flex items-center gap-2 pl-2 pr-3 h-10 rounded-lg hover:bg-white/5 transition-colors" data-dd="user">
<button class="flex items-center gap-2 pl-1.5 sm:pl-2 pr-2 sm:pr-3 h-10 rounded-lg hover:bg-white/5 transition-colors shrink-0" data-dd="user">
@php
$toolbarUserId = (int) ($userId ?? Auth::id() ?? 0);
$toolbarAvatarHash = $avatarHash ?? optional(Auth::user())->profile->avatar_hash ?? null;
@@ -224,7 +209,7 @@
<img class="w-7 h-7 rounded-full object-cover ring-1 ring-white/10"
src="{{ \App\Support\AvatarUrl::forUser($toolbarUserId, $toolbarAvatarHash, 64) }}"
alt="{{ $displayName ?? 'User' }}" />
<span class="hidden xl:inline text-sm text-white/90">{{ $displayName ?? 'User' }}</span>
<span class="hidden min-[900px]:inline-block max-w-[8rem] truncate text-sm text-white/90">{{ $displayName ?? 'User' }}</span>
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6" />
</svg>
@@ -291,7 +276,7 @@
Settings
</a>
@if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true))
@if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true) && \Illuminate\Support\Facades\Route::has('admin.usernames.moderation'))
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('admin.usernames.moderation') }}">
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-user-shield text-xs text-sb-muted"></i></span>
Moderation
@@ -310,7 +295,7 @@
</div>
@else
<!-- Guest auth toolbar: desktop CTA + secondary sign-in. -->
<div class="hidden md:flex items-center gap-4">
<div class="hidden lg:flex items-center gap-4 shrink-0">
<a href="/register"
aria-label="Join Skinbase"
class="inline-flex items-center px-4 py-2 rounded-lg bg-gradient-to-r from-indigo-500 to-cyan-500 text-white text-sm font-semibold shadow-sm transition duration-200 ease-out hover:-translate-y-[1px] hover:shadow-[0_0_15px_rgba(99,102,241,0.7)] focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-black/40">
@@ -324,7 +309,7 @@
</div>
<!-- Guest auth on mobile: icon trigger with lightweight dropdown menu. -->
<details class="relative md:hidden">
<details class="relative lg:hidden shrink-0">
<summary
aria-label="Open authentication menu"
class="list-none inline-flex items-center justify-center w-10 h-10 rounded-lg text-gray-300 hover:text-white hover:bg-white/5 cursor-pointer focus:outline-none focus:ring-2 focus:ring-indigo-500">
@@ -371,9 +356,6 @@
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/discover/most-downloaded"><i class="fa-solid fa-download w-4 text-center text-sb-muted"></i>Most Downloaded</a>
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('downloads.today') }}"><i class="fa-solid fa-arrow-down-short-wide w-4 text-center text-sb-muted"></i>Today Downloads</a>
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/discover/on-this-day"><i class="fa-solid fa-calendar-day w-4 text-center text-sb-muted"></i>On This Day</a>
@auth
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('discover.for-you') }}"><i class="fa-solid fa-wand-magic-sparkles w-4 text-center text-yellow-400/70"></i>For You</a>
@endauth
</div>
</div>

View File

@@ -2,6 +2,7 @@
@push('head')
@vite(['resources/js/profile.jsx'])
<meta name="csrf-token" content="{{ csrf_token() }}" />
{{-- OG image (not in nova base layout) --}}
@if(!empty($og_image))
<meta property="og:image" content="{{ $og_image }}">

View File

@@ -4,7 +4,7 @@
<meta name="csrf-token" content="{{ csrf_token() }}" />
@vite(['resources/js/studio.jsx'])
<style>
body.page-studio main { padding-top: 4rem; }
body.page-studio main { padding-top: 2.3rem; }
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {

View File

@@ -24,6 +24,49 @@
</x-slot>
</x-nova-page-header>
@php
$cacheTone = match ($feed_meta['cache_status'] ?? null) {
'hit' => 'text-emerald-200 ring-emerald-400/30 bg-emerald-500/12',
'stale' => 'text-amber-200 ring-amber-400/30 bg-amber-500/12',
default => 'text-sky-100 ring-sky-300/30 bg-sky-500/12',
};
$generatedAt = !empty($feed_meta['generated_at']) ? \Illuminate\Support\Carbon::parse($feed_meta['generated_at'])->diffForHumans() : null;
@endphp
<section class="px-6 md:px-10">
<div class="grid gap-3 rounded-[1.6rem] border border-white/8 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_42%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.88))] p-5 shadow-[0_18px_60px_rgba(2,6,23,0.38)] md:grid-cols-[minmax(0,1fr)_auto] md:items-center">
<div class="space-y-2">
<p class="text-[0.7rem] font-semibold uppercase tracking-[0.28em] text-sky-200/70">Personalized discovery</p>
<p class="max-w-3xl text-sm leading-6 text-slate-300">
This feed now runs on the same recommendation engine as the API, so your views and clicks on this page can refine what shows up next.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-slate-200/85">
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Model</span>
<span>{{ $feed_meta['algo_version'] ?? 'n/a' }}</span>
</span>
<span class="inline-flex items-center gap-2 rounded-full ring-1 px-3 py-1.5 {{ $cacheTone }}">
<span class="text-slate-300/70">Cache</span>
<span>{{ str_replace(['-', '_'], ' ', $feed_meta['cache_status'] ?? 'unknown') }}</span>
</span>
@if (!empty($feed_meta['total_candidates']))
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Candidates</span>
<span>{{ number_format((int) $feed_meta['total_candidates']) }}</span>
</span>
@endif
@if ($generatedAt)
<span class="inline-flex items-center gap-2 rounded-full ring-1 ring-white/12 bg-white/6 px-3 py-1.5">
<span class="text-slate-400">Refreshed</span>
<span>{{ $generatedAt }}</span>
</span>
@endif
</div>
</div>
</section>
{{-- ── Artwork grid (React MasonryGallery) ── --}}
@php
$galleryArtworks = $artworks->map(fn ($art) => [
@@ -32,13 +75,20 @@
'thumb' => $art->thumb_url ?? null,
'thumb_srcset' => $art->thumb_srcset ?? null,
'uname' => $art->uname ?? '',
'username' => $art->uname ?? '',
'username' => $art->username ?? '',
'avatar_url' => $art->avatar_url ?? null,
'published_at' => $art->published_at ?? null,
'content_type_name' => $art->content_type_name ?? '',
'category_name' => $art->category_name ?? '',
'category_slug' => $art->category_slug ?? '',
'slug' => $art->slug ?? '',
'url' => $art->url ?? null,
'width' => $art->width ?? null,
'height' => $art->height ?? null,
'recommendation_source' => $art->recommendation_source ?? 'mixed',
'recommendation_reason' => $art->recommendation_reason ?? 'Picked for you',
'recommendation_score' => $art->recommendation_score,
'recommendation_algo_version' => $art->recommendation_algo_version ?? ($feed_meta['algo_version'] ?? null),
])->values();
@endphp
<section class="px-6 pt-8 md:px-10">
@@ -47,6 +97,8 @@
data-artworks="{{ json_encode($galleryArtworks) }}"
data-gallery-type="for-you"
data-cursor-endpoint="{{ route('discover.for-you') }}"
data-discovery-endpoint="{{ route('api.discovery.events.store') }}"
data-algo-version="{{ $feed_meta['algo_version'] ?? '' }}"
@if (!empty($next_cursor)) data-next-cursor="{{ $next_cursor }}" @endif
data-limit="40"
class="min-h-32"

View File

@@ -7,6 +7,13 @@
]);
@endphp
@php
$followingActivity = collect($following_activity ?? []);
$networkTrending = collect($network_trending ?? []);
$suggestedUsers = collect($suggested_users ?? $fallback_creators ?? []);
$fallbackTrending = collect($fallback_trending ?? []);
@endphp
@section('content')
<x-nova-page-header
@@ -23,9 +30,128 @@
</x-slot>
</x-nova-page-header>
@if (($section ?? null) === 'following')
<section class="px-6 pt-2 md:px-10">
@if (!empty($empty))
<div class="rounded-[32px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.08),rgba(255,255,255,0.03),rgba(249,115,22,0.06))] p-6 shadow-[0_20px_60px_rgba(2,6,23,0.24)]">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Personalized following feed</p>
<h2 class="mt-2 text-3xl font-semibold tracking-[-0.03em] text-white">Your network starts here</h2>
<p class="mt-3 max-w-2xl text-sm leading-relaxed text-slate-300">Follow a few creators to unlock a feed made of their newest art, social activity, and rising work from around your network.</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/discover/trending" class="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-medium text-slate-200 transition-colors hover:bg-white/[0.08]">
<i class="fa-solid fa-fire fa-fw"></i>
Explore trending
</a>
<a href="/feed/following" class="inline-flex items-center gap-2 rounded-2xl border border-sky-400/20 bg-sky-500/10 px-4 py-2.5 text-sm font-medium text-sky-200 transition-colors hover:bg-sky-500/15">
<i class="fa-solid fa-newspaper fa-fw"></i>
Open post feed
</a>
</div>
</div>
</div>
@endif
<div class="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4 flex items-center justify-between gap-4">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Activity from people you follow</p>
<h3 class="mt-1 text-xl font-semibold text-white">Network activity</h3>
</div>
<a href="/community/activity?filter=following" class="text-sm text-sky-300/80 transition-colors hover:text-sky-200">View all</a>
</div>
<div class="space-y-3">
@forelse ($followingActivity as $activity)
<div class="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-4 py-3">
<div class="flex items-start gap-3">
<img src="{{ data_get($activity, 'user.avatar_url') ?: '/images/avatar_default.webp' }}" alt="{{ data_get($activity, 'user.username') }}" class="h-10 w-10 rounded-full object-cover ring-1 ring-white/10" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-white">{{ data_get($activity, 'user.username') ? '@' . data_get($activity, 'user.username') : data_get($activity, 'user.name', 'Creator') }}</p>
<p class="mt-1 text-sm text-slate-300">
@if (data_get($activity, 'type') === 'follow')
started following {{ data_get($activity, 'target_user.username') ? '@' . data_get($activity, 'target_user.username') : 'another creator' }}
@elseif (data_get($activity, 'type') === 'upload')
published <a href="{{ data_get($activity, 'artwork.url') }}" class="text-sky-300 hover:text-sky-200">{{ data_get($activity, 'artwork.title', 'a new artwork') }}</a>
@elseif (in_array(data_get($activity, 'type'), ['comment', 'reply'], true))
{{ data_get($activity, 'type') === 'reply' ? 'replied on' : 'commented on' }} <a href="{{ data_get($activity, 'artwork.url') }}" class="text-sky-300 hover:text-sky-200">{{ data_get($activity, 'artwork.title', 'an artwork') }}</a>
@else
{{ ucfirst(str_replace('_', ' ', (string) data_get($activity, 'type', 'activity'))) }}
@endif
</p>
<p class="mt-1 text-xs text-slate-500">{{ data_get($activity, 'time_ago') ?: data_get($activity, 'created_at') }}</p>
</div>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] px-4 py-8 text-center text-sm text-slate-400">Follow activity will appear here as your network starts moving.</div>
@endforelse
</div>
</div>
<div class="space-y-6">
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Trending in your network</p>
<h3 class="mt-1 text-xl font-semibold text-white">Network highlights</h3>
</div>
<div class="space-y-3">
@foreach (($empty ?? false) ? $fallbackTrending->take(4) : $networkTrending->take(4) as $item)
@php
$itemId = (int) data_get($item, 'id', 0);
$itemSlug = (string) data_get($item, 'slug', '');
$itemUrl = $itemSlug !== '' && $itemId > 0
? route('art.show', ['id' => $itemId, 'slug' => $itemSlug])
: data_get($item, 'url', '#');
$itemThumb = data_get($item, 'thumb_url') ?: data_get($item, 'thumb') ?: data_get($item, 'thumbnail_url') ?: '/images/placeholder.jpg';
$itemTitle = data_get($item, 'title') ?: data_get($item, 'name', 'Artwork');
@endphp
<a href="{{ $itemUrl }}" class="flex items-center gap-3 rounded-2xl border border-white/[0.06] bg-white/[0.02] p-3 transition-colors hover:bg-white/[0.04]">
<img src="{{ $itemThumb }}" alt="{{ $itemTitle }}" class="h-16 w-16 rounded-xl object-cover" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-white">{{ $itemTitle ?: 'Untitled artwork' }}</p>
<p class="truncate text-xs text-slate-400">{{ data_get($item, 'author.username') ? '@' . data_get($item, 'author.username') : data_get($item, 'username', data_get($item, 'uname')) }}</p>
@if (is_array($item) && isset($item['stats']))
<p class="mt-1 text-[11px] text-slate-500">{{ number_format((int) data_get($item, 'stats.favorites', 0)) }} favourites · {{ number_format((int) data_get($item, 'stats.views', 0)) }} views</p>
@endif
</div>
</a>
@endforeach
</div>
</div>
<div class="rounded-[28px] border border-white/10 bg-white/[0.03] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div class="mb-4">
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">Suggested creators</p>
<h3 class="mt-1 text-xl font-semibold text-white">Who to follow next</h3>
</div>
<div class="space-y-3">
@foreach ($suggestedUsers->take(4) as $userCard)
<a href="{{ $userCard['profile_url'] ?? ('/@' . strtolower((string) ($userCard['username'] ?? ''))) }}" class="flex items-start gap-3 rounded-2xl border border-white/[0.06] bg-white/[0.02] p-3 transition-colors hover:bg-white/[0.04]">
<img src="{{ $userCard['avatar_url'] ?? '/images/avatar_default.webp' }}" alt="{{ $userCard['username'] ?? 'creator' }}" class="h-10 w-10 rounded-full object-cover ring-1 ring-white/10" loading="lazy" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-white">{{ $userCard['name'] ?? $userCard['username'] ?? 'Creator' }}</p>
<p class="truncate text-xs text-slate-500">@{{ $userCard['username'] ?? 'creator' }}</p>
<p class="mt-1 text-xs text-slate-400">{{ data_get($userCard, 'context.follower_overlap.label') ?: data_get($userCard, 'context.shared_following.label') ?: ($userCard['reason'] ?? 'Recommended for you') }}</p>
</div>
</a>
@endforeach
</div>
</div>
</div>
</div>
</section>
@endif
{{-- ── Artwork grid (React MasonryGallery) ── --}}
@php
$galleryArtworks = collect($artworks->items())->map(fn ($art) => [
$galleryItems = method_exists($artworks, 'items') ? $artworks->items() : (is_iterable($artworks) ? $artworks : []);
$galleryArtworks = collect($galleryItems)->map(fn ($art) => [
'id' => $art->id,
'name' => $art->name ?? null,
'thumb' => $art->thumb_url ?? $art->thumb ?? null,

View File

@@ -1,9 +1,10 @@
@extends('layouts.nova')
@section('meta-description', $meta['description'])
@section('meta-keywords', $meta['keywords'])
@push('head')
<title>{{ $meta['title'] }}</title>
<meta name="description" content="{{ $meta['description'] }}">
<meta name="keywords" content="{{ $meta['keywords'] }}">
<link rel="canonical" href="{{ $meta['canonical'] }}">
{{-- Open Graph --}}
@@ -53,15 +54,23 @@
@section('main-class', '')
@section('content')
@include('web.home.hero', ['artwork' => $props['hero'] ?? null])
{{-- Inline props for the React component (avoids data-attribute length limits) --}}
<script id="homepage-props" type="application/json">
{!! json_encode($props, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
</script>
<div id="homepage-root" class="min-h-screen">
<div id="homepage-root" class="min-h-[40vh]">
{{-- Loading skeleton (replaced by React on hydration) --}}
<div class="flex min-h-[60vh] items-center justify-center">
<div class="h-8 w-8 animate-spin rounded-full border-4 border-nova-500 border-t-transparent"></div>
<div class="space-y-10 px-4 pt-10 sm:px-6 lg:px-8">
<div class="h-14 rounded-2xl bg-nova-800/70"></div>
<div class="grid gap-4 lg:grid-cols-4">
<div class="aspect-video rounded-2xl bg-nova-800/70"></div>
<div class="aspect-video rounded-2xl bg-nova-800/70"></div>
<div class="aspect-video rounded-2xl bg-nova-800/70"></div>
<div class="aspect-video rounded-2xl bg-nova-800/70"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,63 @@
@php
$heroArtwork = $artwork ?? null;
$fallbackImage = 'https://files.skinbase.org/default/missing_lg.webp';
$heroImage = $heroArtwork['thumb_lg'] ?? $heroArtwork['thumb'] ?? $fallbackImage;
@endphp
@if (!$heroArtwork)
<section class="relative flex min-h-[62vh] max-h-[420px] w-full items-end overflow-hidden bg-nova-900 md:min-h-[38vh] md:max-h-[460px]">
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-nova-900 via-nova-900/60 to-transparent"></div>
<div class="relative z-10 w-full px-6 pb-7 sm:px-10 lg:px-16">
<h1 class="text-2xl font-bold tracking-tight text-white sm:text-4xl">
Skinbase Nova
</h1>
<p class="mt-2 max-w-xl text-sm text-soft">
Discover. Create. Inspire.
</p>
<div class="mt-4 flex flex-wrap gap-3">
<a href="/discover/trending" class="rounded-xl bg-accent px-5 py-2 text-sm font-semibold text-white shadow-lg transition hover:brightness-110">Explore Trending</a>
</div>
</div>
</section>
@else
<section class="group relative flex min-h-[62vh] max-h-[420px] w-full items-end overflow-hidden bg-nova-900 md:min-h-[38vh] md:max-h-[460px]">
<img
src="{{ $heroImage }}"
alt="{{ $heroArtwork['title'] ?? 'Featured artwork' }}"
class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
fetchpriority="high"
decoding="async"
/>
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-nova-900 via-nova-900/55 to-transparent"></div>
<div class="relative z-10 w-full px-6 pb-7 sm:px-10 lg:px-16">
<p class="mb-1.5 text-xs font-semibold uppercase tracking-widest text-accent">
Featured Artwork
</p>
<h1 class="text-2xl font-bold tracking-tight text-white drop-shadow sm:text-4xl lg:text-5xl">
{{ $heroArtwork['title'] ?? 'Untitled' }}
</h1>
<p class="mt-1.5 text-sm text-soft">
by
<a href="{{ $heroArtwork['url'] ?? '#' }}" class="text-nova-200 transition hover:text-white">
{{ $heroArtwork['author'] ?? 'Artist' }}
</a>
</p>
<div class="mt-4 flex flex-wrap gap-3">
<a
href="/discover/trending"
class="rounded-xl bg-accent px-5 py-2 text-sm font-semibold text-white shadow-lg transition hover:brightness-110"
>
Explore Trending
</a>
<a
href="{{ $heroArtwork['url'] ?? '#' }}"
class="rounded-xl border border-nova-600 px-5 py-2 text-sm font-semibold text-nova-200 shadow transition hover:border-nova-400 hover:text-white"
>
View Artwork
</a>
</div>
</div>
</section>
@endif