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

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