Add tests for featured thumbnail generation; apply Pint formatting and related edits
This commit is contained in:
@@ -31,37 +31,34 @@
|
||||
<input type="text" name="homepage_url" value="" tabindex="-1" autocomplete="off" class="hidden" aria-hidden="true">
|
||||
<input type="hidden" name="_bot_fingerprint" value="">
|
||||
|
||||
@php
|
||||
$captchaProvider = $captcha['provider'] ?? 'turnstile';
|
||||
$captchaSiteKey = $captcha['siteKey'] ?? '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
|
||||
<input id="email" name="email" type="email" required placeholder="you@example.com" value="{{ old('email', $prefillEmail ?? '') }}" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
@if((($requiresCaptcha ?? false) || session('bot_captcha_required')) && $captchaSiteKey !== '')
|
||||
@if($captchaProvider === 'recaptcha')
|
||||
<div class="g-recaptcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
|
||||
@elseif($captchaProvider === 'hcaptcha')
|
||||
<div class="h-captcha" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
|
||||
@else
|
||||
<div class="cf-turnstile" data-sitekey="{{ $captchaSiteKey }}" data-theme="dark"></div>
|
||||
@endif
|
||||
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
|
||||
@if(($turnstile['enabled'] ?? false) && (($turnstile['siteKey'] ?? '') !== ''))
|
||||
<div class="space-y-2" data-turnstile-container>
|
||||
<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="{{ $turnstile['siteKey'] }}"
|
||||
data-theme="dark"
|
||||
></div>
|
||||
<p class="text-xs text-white/50" data-turnstile-status>Complete the security check before continuing.</p>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('turnstile_token')" class="mt-2" />
|
||||
@endif
|
||||
|
||||
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-cyan-500 to-sky-400 hover:from-cyan-400 hover:to-sky-300 text-slate-900 transition">Continue</button>
|
||||
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-cyan-500 to-sky-400 hover:from-cyan-400 hover:to-sky-300 text-slate-900 transition disabled:cursor-not-allowed disabled:opacity-60" data-turnstile-submit>Continue</button>
|
||||
|
||||
<p class="text-sm text-center text-white/60">Already registered? <a href="{{ route('login') }}" class="text-cyan-400 hover:underline">Sign in</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if((($requiresCaptcha ?? false) || session('bot_captcha_required')) && (($captcha['siteKey'] ?? '') !== '') && (($captcha['scriptUrl'] ?? '') !== ''))
|
||||
<script src="{{ $captcha['scriptUrl'] }}" async defer></script>
|
||||
@if(($turnstile['enabled'] ?? false) && (($turnstile['siteKey'] ?? '') !== '') && (($turnstile['scriptUrl'] ?? '') !== ''))
|
||||
<script src="{{ $turnstile['scriptUrl'] }}" @if(($turnstile['cspNonce'] ?? '') !== '') nonce="{{ $turnstile['cspNonce'] }}" @endif async defer></script>
|
||||
<script src="{{ asset('js/register-turnstile.js') }}" @if(($turnstile['cspNonce'] ?? '') !== '') nonce="{{ $turnstile['cspNonce'] }}" @endif defer></script>
|
||||
@endif
|
||||
@include('partials.bot-fingerprint-script')
|
||||
@endsection
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
]);
|
||||
$page_robots = $page_robots ?? ($isAuthSeoRoute ? 'noindex,nofollow' : null);
|
||||
$shouldRenderBladeSeo = ($useUnifiedSeo ?? ! $isInertiaPage) && (($renderBladeSeo ?? true) || ! $isInertiaPage);
|
||||
$novaCssEntries = [
|
||||
$novaCssEntries = $novaCssEntries ?? [
|
||||
'resources/css/app.css',
|
||||
'resources/css/nova-grid.css',
|
||||
'resources/scss/nova.scss',
|
||||
@@ -70,8 +70,27 @@
|
||||
@if(!$deferWebManifest)
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
@endif
|
||||
<style>
|
||||
html {
|
||||
background-color: rgb(14, 18, 27);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgb(14, 18, 27);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@foreach($novaCssEntries as $novaCssEntry)
|
||||
<link rel="stylesheet" href="{{ Vite::asset($novaCssEntry) }}">
|
||||
@php
|
||||
$novaCssHref = Vite::asset($novaCssEntry);
|
||||
@endphp
|
||||
<link rel="preload" href="{{ $novaCssHref }}" as="style" onload="this.rel='stylesheet'">
|
||||
<link rel="stylesheet" href="{{ $novaCssHref }}">
|
||||
@endforeach
|
||||
@vite($novaViteEntries)
|
||||
<script>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center gap-2 pr-2 shrink-0">
|
||||
<img src="https://cdn.skinbase.org/images/sb_logo.webp" alt="" width="289" height="100" class="h-9 w-auto rounded-sm shadow-sm object-contain">
|
||||
<img src="https://cdn.skinbase.org/images/sb_logo_full.webp" alt="" width="104" height="36" class="h-9 w-auto rounded-sm shadow-sm object-contain">
|
||||
<span class="sr-only">Skinbase.org</span>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a href="{{ route('news.show', $article->slug) }}" class="block">
|
||||
<div class="relative aspect-[16/9] overflow-hidden bg-black/20">
|
||||
@if($article->cover_url)
|
||||
<img src="{{ $article->cover_url }}" alt="{{ $article->title }}" class="h-full w-full object-cover transition duration-300 group-hover:scale-[1.04]">
|
||||
<img src="{{ $article->cover_url }}" @if($article->cover_srcset) srcset="{{ $article->cover_srcset }}" sizes="(max-width: 767px) 100vw, (max-width: 1279px) 50vw, 390px" @endif alt="{{ $article->title }}" class="h-full w-full object-cover transition duration-300 group-hover:scale-[1.04]">
|
||||
@else
|
||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_45%),linear-gradient(180deg,rgba(15,23,42,0.92),rgba(2,6,23,0.98))]"></div>
|
||||
@endif
|
||||
|
||||
@@ -37,9 +37,14 @@
|
||||
|
||||
@if(!empty($tags) && $tags->isNotEmpty())
|
||||
<section class="rounded-[24px] border border-white/[0.06] bg-white/[0.025] p-5">
|
||||
<div class="mb-4 flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.18em] text-white/45">
|
||||
<i class="fa-solid fa-tags text-[11px] text-sky-300"></i>
|
||||
Topics
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.18em] text-white/45">
|
||||
<i class="fa-solid fa-tags text-[11px] text-sky-300"></i>
|
||||
Popular Topics
|
||||
</div>
|
||||
<a href="{{ route('tags.index') }}" class="text-[11px] font-semibold uppercase tracking-[0.14em] text-sky-200/75 transition hover:text-sky-100">
|
||||
All Tags
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($tags as $tag)
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => $archiveDate->format('F Y'), 'url' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month])],
|
||||
]);
|
||||
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => $archiveDate->format('F Y') . ' — News Archive',
|
||||
'description' => 'News archive for ' . $archiveDate->format('F Y') . ' on Skinbase.',
|
||||
'canonical' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month]),
|
||||
'breadcrumbs' => $headerBreadcrumbs,
|
||||
'structured_data' => [
|
||||
[
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $archiveDate->format('F Y') . ' — News Archive',
|
||||
'description' => 'Published News stories from ' . $archiveDate->format('F Y') . '.',
|
||||
'url' => route('news.archive', ['year' => $archiveDate->year, 'month' => $archiveDate->month]),
|
||||
],
|
||||
],
|
||||
])->build();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => $authorLabel, 'url' => route('news.author', ['username' => $author->username])],
|
||||
]);
|
||||
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => $authorLabel . ' — News Author',
|
||||
'description' => 'News stories and announcements by ' . $authorLabel . '.',
|
||||
'canonical' => route('news.author', ['username' => $author->username]),
|
||||
'breadcrumbs' => $headerBreadcrumbs,
|
||||
'structured_data' => [
|
||||
[
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $authorLabel . ' — News Author',
|
||||
'description' => 'Editorial stories and updates by ' . $authorLabel . '.',
|
||||
'url' => route('news.author', ['username' => $author->username]),
|
||||
],
|
||||
],
|
||||
])->build();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
|
||||
@@ -6,15 +6,50 @@
|
||||
|
||||
@section('news_content')
|
||||
@php
|
||||
$articleItems = collect($articles->items());
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => 'Community', 'url' => route('community.activity')],
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => 'News', 'url' => route('news.index')],
|
||||
(object) ['name' => $category->name, 'url' => route('news.category', $category->slug)],
|
||||
]);
|
||||
|
||||
$structuredData = [
|
||||
[
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $category->name . ' — News',
|
||||
'description' => $category->description ?: ('Announcements filed under ' . $category->name . '.'),
|
||||
'url' => route('news.category', $category->slug),
|
||||
],
|
||||
];
|
||||
|
||||
if ($articleItems->isNotEmpty()) {
|
||||
$structuredData[] = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ItemList',
|
||||
'name' => $category->name . ' — News Articles',
|
||||
'description' => 'Published News stories in the ' . $category->name . ' category.',
|
||||
'url' => route('news.category', $category->slug),
|
||||
'numberOfItems' => $articleItems->count(),
|
||||
'itemListElement' => $articleItems->values()->map(fn ($article, int $index): array => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'name' => $article->title,
|
||||
'url' => route('news.show', ['slug' => $article->slug]),
|
||||
])->all(),
|
||||
];
|
||||
}
|
||||
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => $category->name . ' — News',
|
||||
'description' => $category->description ?: ('Announcements in the ' . $category->name . ' category.'),
|
||||
'canonical' => route('news.category', $category->slug),
|
||||
'breadcrumbs' => $headerBreadcrumbs,
|
||||
'structured_data' => $structuredData,
|
||||
])->build();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
section="Community"
|
||||
section="News"
|
||||
:title="$category->name"
|
||||
icon="fa-folder-open"
|
||||
:breadcrumbs="$headerBreadcrumbs"
|
||||
|
||||
@@ -6,14 +6,55 @@
|
||||
|
||||
@section('news_content')
|
||||
@php
|
||||
$articleItems = collect([$featured])
|
||||
->merge($highlights)
|
||||
->merge($articles->items())
|
||||
->filter(fn ($article) => $article !== null)
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => 'Community', 'url' => route('community.activity')],
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => 'News', 'url' => route('news.index')],
|
||||
]);
|
||||
|
||||
$structuredData = [
|
||||
[
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => config('news.rss_title', 'News'),
|
||||
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
|
||||
'url' => route('news.index'),
|
||||
],
|
||||
];
|
||||
|
||||
if ($articleItems->isNotEmpty()) {
|
||||
$structuredData[] = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ItemList',
|
||||
'name' => config('news.rss_title', 'News') . ' Articles',
|
||||
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
|
||||
'url' => route('news.index'),
|
||||
'numberOfItems' => $articleItems->count(),
|
||||
'itemListElement' => $articleItems->map(fn ($article, int $index): array => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'name' => $article->title,
|
||||
'url' => route('news.show', $article->slug),
|
||||
])->all(),
|
||||
];
|
||||
}
|
||||
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => config('news.rss_title', 'News'),
|
||||
'description' => config('news.rss_description', 'Latest news, feature rollouts, and team updates from Skinbase.'),
|
||||
'canonical' => route('news.index'),
|
||||
'breadcrumbs' => $headerBreadcrumbs,
|
||||
'structured_data' => $structuredData,
|
||||
])->build();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
section="Community"
|
||||
section="News"
|
||||
title="News"
|
||||
icon="fa-newspaper"
|
||||
:breadcrumbs="$headerBreadcrumbs"
|
||||
@@ -44,7 +85,7 @@
|
||||
<div class="grid lg:grid-cols-[1.25fr_0.95fr]">
|
||||
<div class="relative min-h-[280px] overflow-hidden bg-black/20">
|
||||
@if($featured->cover_url)
|
||||
<img src="{{ $featured->cover_url }}" alt="{{ $featured->title }}" class="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]">
|
||||
<img src="{{ $featured->cover_url }}" @if($featured->cover_srcset) srcset="{{ $featured->cover_srcset }}" sizes="(max-width: 1023px) 100vw, 768px" @endif alt="{{ $featured->title }}" class="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]">
|
||||
@else
|
||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_45%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.98))]"></div>
|
||||
@endif
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@php
|
||||
$useUnifiedSeo = true;
|
||||
$novaCssEntries = [
|
||||
'resources/css/app.css',
|
||||
'resources/scss/nova.scss',
|
||||
];
|
||||
@endphp
|
||||
@extends('layouts.nova')
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@php
|
||||
$isPreview = (bool) ($previewMode ?? false);
|
||||
$articleUrl = $isPreview ? ($previewCanonical ?? url()->current()) : route('news.show', $article->slug);
|
||||
$articleSchemaImage = $article->effective_og_image
|
||||
? url($article->effective_og_image)
|
||||
: url((string) config('seo.fallback_image_path', '/gfx/skinbase_back_001.webp'));
|
||||
$articleCoverSizes = '(max-width: 767px) calc(100vw - 3rem), (max-width: 1279px) calc(100vw - 5rem), 768px';
|
||||
$articleCoverPreloadHref = $article->cover_desktop_url ?: $article->cover_url;
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => $article->meta_title ?: $article->title,
|
||||
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
|
||||
@@ -12,31 +17,55 @@
|
||||
'og_description' => $article->effective_og_description,
|
||||
'og_image' => $article->effective_og_image,
|
||||
'breadcrumbs' => collect([
|
||||
(object) ['name' => 'Community', 'url' => route('community.activity')],
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => 'Home', 'url' => url('/')],
|
||||
(object) ['name' => 'News', 'url' => route('news.index')],
|
||||
$article->category
|
||||
? (object) ['name' => $article->category->name, 'url' => route('news.category', $article->category->slug)]
|
||||
: null,
|
||||
(object) ['name' => $article->title, 'url' => route('news.show', $article->slug)],
|
||||
])->filter()->values(),
|
||||
])
|
||||
->addJsonLd(array_filter([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Article',
|
||||
'@type' => 'NewsArticle',
|
||||
'headline' => $article->title,
|
||||
'description' => $article->meta_description ?: Str::limit(strip_tags((string) $article->excerpt), 160),
|
||||
'image' => $article->effective_og_image,
|
||||
'image' => $articleSchemaImage
|
||||
? array_filter([
|
||||
'@type' => 'ImageObject',
|
||||
'url' => $articleSchemaImage,
|
||||
'contentUrl' => $articleSchemaImage,
|
||||
'thumbnailUrl' => $article->cover_mobile_url,
|
||||
'caption' => $article->title,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== '')
|
||||
: null,
|
||||
'datePublished' => $article->published_at?->toIso8601String(),
|
||||
'dateModified' => $article->updated_at?->toIso8601String(),
|
||||
'articleSection' => $article->category?->name,
|
||||
'author' => array_filter([
|
||||
'@type' => 'Person',
|
||||
'name' => $article->author?->name,
|
||||
]),
|
||||
'publisher' => [
|
||||
'@type' => 'Organization',
|
||||
'name' => config('seo.site_name', 'Skinbase'),
|
||||
],
|
||||
'mainEntityOfPage' => $articleUrl,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== ''))
|
||||
->build();
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
@if($articleCoverPreloadHref)
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="{{ $articleCoverPreloadHref }}"
|
||||
@if($article->cover_srcset) imagesrcset="{{ $article->cover_srcset }}" imagesizes="{{ $articleCoverSizes }}" @endif
|
||||
fetchpriority="high"
|
||||
>
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@extends('news.layout', [
|
||||
'metaTitle' => $article->meta_title ?: $article->title,
|
||||
'metaDescription' => $article->meta_description ?: Str::limit(strip_tags((string)$article->excerpt), 160),
|
||||
@@ -48,17 +77,16 @@
|
||||
|
||||
@php
|
||||
$headerBreadcrumbs = collect([
|
||||
(object) ['name' => 'Community', 'url' => route('community.activity')],
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => 'Home', 'url' => url('/')],
|
||||
(object) ['name' => 'News', 'url' => route('news.index')],
|
||||
$article->category
|
||||
? (object) ['name' => $article->category->name, 'url' => route('news.category', $article->category->slug)]
|
||||
: null,
|
||||
(object) ['name' => $article->title, 'url' => $articleUrl],
|
||||
])->filter()->values();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
section="Community"
|
||||
section="News"
|
||||
:title="$article->title"
|
||||
icon="fa-newspaper"
|
||||
:breadcrumbs="$headerBreadcrumbs"
|
||||
@@ -105,7 +133,18 @@
|
||||
<article class="min-w-0">
|
||||
@if($article->cover_url)
|
||||
<div class="overflow-hidden rounded-[32px] border border-white/[0.06] bg-black/20 shadow-[0_24px_60px_rgba(0,0,0,0.24)]">
|
||||
<img src="{{ $article->cover_url }}" alt="{{ $article->title }}" class="h-auto max-h-[520px] w-full object-cover">
|
||||
<a href="{{ $articleCoverPreloadHref }}" class="group block focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950" aria-label="Open full cover image">
|
||||
<div class="relative">
|
||||
<img src="{{ $article->cover_url }}" @if($article->cover_srcset) srcset="{{ $article->cover_srcset }}" sizes="{{ $articleCoverSizes }}" @endif alt="{{ $article->title }}" fetchpriority="high" loading="eager" decoding="async" class="h-auto max-h-[520px] w-full object-cover transition duration-300 group-hover:scale-[1.01]">
|
||||
<div class="pointer-events-none absolute inset-x-4 bottom-4 flex items-center justify-between gap-3 rounded-full border border-white/10 bg-slate-950/72 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/82 backdrop-blur-sm">
|
||||
<span>Open Image</span>
|
||||
<span class="inline-flex items-center gap-2 text-sky-200/90">
|
||||
<i class="fa-solid fa-magnifying-glass-plus text-[11px]"></i>
|
||||
Full Image
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
(object) ['name' => 'Announcements', 'url' => route('news.index')],
|
||||
(object) ['name' => '#' . $tag->name, 'url' => route('news.tag', $tag->slug)],
|
||||
]);
|
||||
|
||||
$seo = \App\Support\Seo\SeoDataBuilder::fromArray([
|
||||
'title' => '#' . $tag->name . ' — News',
|
||||
'description' => 'Announcements tagged with ' . $tag->name . '.',
|
||||
'canonical' => route('news.tag', $tag->slug),
|
||||
'breadcrumbs' => $headerBreadcrumbs,
|
||||
'structured_data' => [
|
||||
[
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => '#' . $tag->name . ' — News',
|
||||
'description' => 'Stories and announcements tagged with #' . $tag->name . '.',
|
||||
'url' => route('news.tag', $tag->slug),
|
||||
],
|
||||
],
|
||||
])->build();
|
||||
@endphp
|
||||
|
||||
<x-nova-page-header
|
||||
|
||||
@@ -16,97 +16,78 @@
|
||||
$shouldBlur = (bool) ($maturity['should_blur'] ?? false);
|
||||
$cardImageId = ($idPrefix ?? 'artwork') . '-image-' . ($index ?? 0);
|
||||
$medalScore = (int) data_get($artwork, 'medals.score_30d', data_get($artwork, 'medals.score', 0));
|
||||
$cardFrameClass = ($layout ?? 'grid') === 'rail'
|
||||
? 'aspect-video'
|
||||
: 'aspect-[4/5] sm:aspect-[5/4] lg:aspect-video';
|
||||
@endphp
|
||||
|
||||
<article class="{{ ($layout ?? 'grid') === 'rail' ? 'min-w-[72%] snap-start sm:min-w-[44%] lg:min-w-0' : 'min-w-0' }}">
|
||||
<div class="group overflow-hidden rounded-2xl bg-black/20 shadow-lg shadow-black/40 ring-1 ring-white/5 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-within:ring-2 focus-within:ring-sky-300/70">
|
||||
<a href="{{ $artworkUrl }}" class="relative block overflow-hidden">
|
||||
<div class="relative aspect-video overflow-hidden bg-neutral-900">
|
||||
<div class="pointer-events-none absolute inset-0 z-10 bg-gradient-to-br from-white/10 via-white/5 to-transparent"></div>
|
||||
<img
|
||||
id="{{ $cardImageId }}"
|
||||
src="{{ $thumbUrl }}"
|
||||
@if (!empty($artwork['thumb_srcset']))
|
||||
srcset="{{ $artwork['thumb_srcset'] }}"
|
||||
sizes="{{ $sizes ?? '100vw' }}"
|
||||
@endif
|
||||
alt="{{ $titleText }}"
|
||||
width="{{ max(1, (int) ($artwork['width'] ?? 1600)) }}"
|
||||
height="{{ max(1, (int) ($artwork['height'] ?? 900)) }}"
|
||||
class="h-full w-full object-cover transition-[transform,filter] duration-300 ease-out group-hover:scale-[1.04] {{ $shouldBlur ? 'blur-2xl scale-[1.03]' : '' }}"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
>
|
||||
|
||||
@if (!empty($badge))
|
||||
<div class="absolute left-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-bold ring-1 ring-white/10 backdrop-blur-sm {{ $badgeClass ?? 'bg-sky-500/80 text-white' }}">
|
||||
{{ $badge }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif ($metricBadge && !empty($metricBadge['label']))
|
||||
<div class="absolute left-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-full border border-sky-300/30 bg-sky-500/14 px-2.5 py-1 text-[11px] font-semibold text-sky-100 ring-1 ring-sky-300/20 backdrop-blur-sm">
|
||||
{{ $metricBadge['label'] }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ $artworkUrl }}" class="group relative block overflow-hidden rounded-2xl bg-black/20 shadow-lg shadow-black/40 ring-1 ring-white/5 transition-all duration-200 ease-out hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/70">
|
||||
<div class="relative {{ $cardFrameClass }} overflow-hidden bg-neutral-900">
|
||||
<div class="pointer-events-none absolute inset-0 z-10 bg-gradient-to-br from-white/10 via-white/5 to-transparent"></div>
|
||||
<img
|
||||
id="{{ $cardImageId }}"
|
||||
src="{{ $thumbUrl }}"
|
||||
@if (!empty($artwork['thumb_srcset']))
|
||||
srcset="{{ $artwork['thumb_srcset'] }}"
|
||||
sizes="{{ $sizes ?? '100vw' }}"
|
||||
@endif
|
||||
alt="{{ $titleText }}"
|
||||
width="{{ max(1, (int) ($artwork['width'] ?? 1600)) }}"
|
||||
height="{{ max(1, (int) ($artwork['height'] ?? 900)) }}"
|
||||
class="h-full w-full object-cover transition-[transform,filter] duration-300 ease-out group-hover:scale-[1.04] {{ $shouldBlur ? 'blur-2xl scale-[1.03]' : '' }}"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
>
|
||||
|
||||
@if ($medalScore > 0)
|
||||
<div class="absolute right-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-full border border-amber-300/20 bg-amber-300/12 px-2.5 py-1 text-[11px] font-semibold text-amber-100 ring-1 ring-amber-300/20 backdrop-blur-sm">
|
||||
Medal {{ number_format($medalScore) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if (!empty($badge))
|
||||
<div class="absolute left-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-bold ring-1 ring-white/10 backdrop-blur-sm {{ $badgeClass ?? 'bg-sky-500/80 text-white' }}">
|
||||
{{ $badge }}
|
||||
</span>
|
||||
</div>
|
||||
@elseif ($metricBadge && !empty($metricBadge['label']))
|
||||
<div class="absolute left-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-full border border-sky-300/30 bg-sky-500/14 px-2.5 py-1 text-[11px] font-semibold text-sky-100 ring-1 ring-sky-300/20 backdrop-blur-sm">
|
||||
{{ $metricBadge['label'] }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($shouldBlur)
|
||||
<div class="absolute inset-0 z-20 flex items-center justify-center bg-slate-950/55 p-4" data-home-mature-overlay>
|
||||
<div class="rounded-2xl border border-white/10 bg-black/45 px-4 py-4 text-center shadow-2xl backdrop-blur-md">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-white/70">Mature content</p>
|
||||
<p class="mt-2 max-w-[16rem] text-sm text-white/90">This artwork may contain mature material.</p>
|
||||
<button
|
||||
type="button"
|
||||
data-home-mature-toggle="{{ $cardImageId }}"
|
||||
class="mt-4 inline-flex items-center rounded-full border border-white/15 bg-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/20"
|
||||
>
|
||||
Reveal image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if ($medalScore > 0)
|
||||
<div class="absolute right-3 top-3 z-30">
|
||||
<span class="inline-flex items-center rounded-full border border-amber-300/20 bg-amber-300/12 px-2.5 py-1 text-[11px] font-semibold text-amber-100 ring-1 ring-amber-300/20 backdrop-blur-sm">
|
||||
Medal {{ number_format($medalScore) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/85 via-black/45 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
|
||||
<div class="truncate text-sm font-semibold text-white">{{ $titleText }}</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-white/80">
|
||||
<img src="{{ $authorAvatar }}" alt="{{ $authorName }}" class="h-6 w-6 shrink-0 rounded-full object-cover" loading="lazy" decoding="async">
|
||||
<span class="truncate">{{ $authorName }}</span>
|
||||
@if ($authorUsername !== '')
|
||||
<span class="shrink-0 text-white/50">@{{ $authorUsername }}</span>
|
||||
@endif
|
||||
@if ($shouldBlur)
|
||||
<div class="absolute inset-0 z-20 flex items-center justify-center bg-slate-950/55 p-4" data-home-mature-overlay>
|
||||
<div class="rounded-2xl border border-white/10 bg-black/45 px-4 py-4 text-center shadow-2xl backdrop-blur-md">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-white/70">Mature content</p>
|
||||
<p class="mt-2 max-w-[16rem] text-sm text-white/90">This artwork may contain mature material.</p>
|
||||
<button
|
||||
type="button"
|
||||
data-home-mature-toggle="{{ $cardImageId }}"
|
||||
class="mt-4 inline-flex items-center rounded-full border border-white/15 bg-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/20"
|
||||
>
|
||||
Reveal image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<div class="flex items-start justify-between gap-3 border-t border-white/5 bg-slate-950/40 px-3 py-3">
|
||||
<div class="min-w-0">
|
||||
<a href="{{ $artworkUrl }}" class="block truncate text-sm font-semibold text-white transition hover:text-sky-100">{{ $titleText }}</a>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-soft">
|
||||
@if ($authorUrl)
|
||||
<a href="{{ $authorUrl }}" class="truncate text-nova-200 transition hover:text-white">{{ $authorName }}</a>
|
||||
@else
|
||||
<span class="truncate">{{ $authorName }}</span>
|
||||
@endif
|
||||
@if (!empty($artwork['category_name']))
|
||||
<span class="shrink-0 text-white/35">•</span>
|
||||
<span class="truncate">{{ $artwork['category_name'] }}</span>
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/85 via-black/45 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
|
||||
<div class="truncate text-sm font-semibold text-white">{{ $titleText }}</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-xs text-white/80">
|
||||
<img src="{{ $authorAvatar }}" alt="{{ $authorName }}" class="h-6 w-6 shrink-0 rounded-full object-cover" loading="lazy" decoding="async">
|
||||
<span class="truncate">{{ $authorName }}</span>
|
||||
@if ($authorUsername !== '')
|
||||
<span class="shrink-0 text-white/50">{{ $authorUsername }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ $artworkUrl }}" class="shrink-0 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-white/80 transition hover:border-white/20 hover:bg-white/[0.08] hover:text-white">
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
@if ($sectionItems->isNotEmpty())
|
||||
<div class="{{ $sectionLayout === 'rail' ? 'flex snap-x snap-mandatory gap-4 overflow-x-auto pb-3 ' . $sectionColumns . ' lg:grid lg:overflow-visible' : 'grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 ' . $sectionColumns }}">
|
||||
<div class="{{ $sectionLayout === 'rail' ? 'flex snap-x snap-mandatory gap-4 overflow-x-auto pb-3 ' . $sectionColumns . ' lg:grid lg:overflow-visible' : 'grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 ' . $sectionColumns }}">
|
||||
@foreach ($sectionItems as $index => $item)
|
||||
@include('web.home.sections.artwork-card', [
|
||||
'item' => $item,
|
||||
@@ -36,7 +36,7 @@
|
||||
'badgeClass' => $badge_class ?? null,
|
||||
'sizes' => $sectionLayout === 'rail'
|
||||
? '(max-width: 640px) 72vw, (max-width: 1024px) 44vw, 240px'
|
||||
: '(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw',
|
||||
: '(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw',
|
||||
'idPrefix' => Str::slug((string) $title, '-'),
|
||||
'index' => $index,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user