Remove legacy frontend assets and update gallery routes
This commit is contained in:
@@ -65,4 +65,4 @@ export default function ForumSection({ category, boards = [] }) {
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function ProfileShow() {
|
||||
/>
|
||||
|
||||
{/* Tab content area */}
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className={activeTab === 'artworks' ? 'w-full px-4 md:px-6' : 'max-w-6xl mx-auto px-4'}>
|
||||
{activeTab === 'artworks' && (
|
||||
<TabArtworks
|
||||
artworks={{ data: artworkList, next_cursor: artworkNextCursor }}
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
|
||||
// ── widths / opacities ───────────────────────────────────────────────────
|
||||
const pillOpacity = phase === 'idle' ? 1 : 0
|
||||
const formOpacity = phase === 'open' ? 1 : 0
|
||||
const formOpacity = (phase === 'opening' || phase === 'open' || phase === 'closing') ? 1 : 0
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -280,7 +280,7 @@ export default function SearchBar({ placeholder = 'Search artworks, artists, tag
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
role="search"
|
||||
style={{ position: 'absolute', inset: 0, opacity: formOpacity, pointerEvents: phase === 'open' ? 'auto' : 'none', transition: 'opacity 180ms ease 60ms' }}
|
||||
style={{ position: 'absolute', inset: 0, opacity: formOpacity, pointerEvents: phase === 'open' ? 'auto' : 'none', transition: 'opacity 160ms ease' }}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
<svg className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-soft pointer-events-none"
|
||||
|
||||
@@ -14,6 +14,11 @@ export default function ArtworkAuthor({ artwork, presentSq }) {
|
||||
: null
|
||||
|
||||
const onToggleFollow = async () => {
|
||||
if (following) {
|
||||
const confirmed = window.confirm(`Unfollow @${user.username || user.name || 'this creator'}?`)
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const nextState = !following
|
||||
setFollowing(nextState)
|
||||
try {
|
||||
|
||||
@@ -124,6 +124,10 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
const scrollRef = useRef(null)
|
||||
const isResettingRef = useRef(false)
|
||||
const scrollEndTimer = useRef(null)
|
||||
const suppressClickTimerRef = useRef(null)
|
||||
const touchStartRef = useRef({ x: 0, y: 0 })
|
||||
const draggedRef = useRef(false)
|
||||
const suppressClickRef = useRef(false)
|
||||
const itemCount = items.length
|
||||
|
||||
/* Triple items so we can loop seamlessly: [clone|original|clone] */
|
||||
@@ -139,6 +143,13 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
return el.children[itemCount].offsetLeft - el.children[0].offsetLeft
|
||||
}, [itemCount])
|
||||
|
||||
/* Scroll step based on rendered card width + gap for predictable smooth motion */
|
||||
const getStepWidth = useCallback(() => {
|
||||
const el = scrollRef.current
|
||||
if (!el || el.children.length < 2) return el ? el.clientWidth * 0.75 : 0
|
||||
return el.children[1].offsetLeft - el.children[0].offsetLeft
|
||||
}, [])
|
||||
|
||||
/* Centre on the middle (real) set after mount / data change */
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current
|
||||
@@ -176,6 +187,19 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
}
|
||||
}, [getSetWidth, itemCount])
|
||||
|
||||
/* Keep user in the centre segment before scripted smooth scroll starts */
|
||||
const normalizeToMiddle = useCallback(() => {
|
||||
const el = scrollRef.current
|
||||
if (!el || !itemCount) return
|
||||
const setW = getSetWidth()
|
||||
if (setW === 0) return
|
||||
if (el.scrollLeft < setW || el.scrollLeft >= setW * 2) {
|
||||
el.style.scrollBehavior = 'auto'
|
||||
el.scrollLeft = ((el.scrollLeft % setW) + setW) % setW + setW
|
||||
el.style.scrollBehavior = ''
|
||||
}
|
||||
}, [getSetWidth, itemCount])
|
||||
|
||||
/* Scroll listener: debounced boundary check + resize re-centre */
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current
|
||||
@@ -199,6 +223,7 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
el.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('resize', onResize)
|
||||
clearTimeout(scrollEndTimer.current)
|
||||
clearTimeout(suppressClickTimerRef.current)
|
||||
}
|
||||
}, [loopItems, resetIfNeeded, getSetWidth])
|
||||
|
||||
@@ -219,8 +244,48 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
const scroll = useCallback((dir) => {
|
||||
const el = scrollRef.current
|
||||
if (!el) return
|
||||
const amount = el.clientWidth * 0.75
|
||||
normalizeToMiddle()
|
||||
const step = getStepWidth()
|
||||
const amount = step > 0 ? step * 2 : el.clientWidth * 0.75
|
||||
el.scrollBy({ left: dir === 'left' ? -amount : amount, behavior: 'smooth' })
|
||||
clearTimeout(scrollEndTimer.current)
|
||||
scrollEndTimer.current = setTimeout(resetIfNeeded, 260)
|
||||
}, [getStepWidth, normalizeToMiddle, resetIfNeeded])
|
||||
|
||||
/* Prevent accidental link activation after horizontal swipe on touch devices */
|
||||
const onTouchStart = useCallback((e) => {
|
||||
if (!e.touches?.length) return
|
||||
const t = e.touches[0]
|
||||
touchStartRef.current = { x: t.clientX, y: t.clientY }
|
||||
draggedRef.current = false
|
||||
}, [])
|
||||
|
||||
const onTouchMove = useCallback((e) => {
|
||||
if (!e.touches?.length) return
|
||||
const t = e.touches[0]
|
||||
const dx = Math.abs(t.clientX - touchStartRef.current.x)
|
||||
const dy = Math.abs(t.clientY - touchStartRef.current.y)
|
||||
if (dx > 10 && dx > dy) {
|
||||
draggedRef.current = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onTouchEnd = useCallback(() => {
|
||||
if (!draggedRef.current) return
|
||||
suppressClickRef.current = true
|
||||
clearTimeout(suppressClickTimerRef.current)
|
||||
suppressClickTimerRef.current = setTimeout(() => {
|
||||
suppressClickRef.current = false
|
||||
}, 260)
|
||||
}, [])
|
||||
|
||||
const onClickCapture = useCallback((e) => {
|
||||
if (!suppressClickRef.current) return
|
||||
const link = e.target?.closest?.('a')
|
||||
if (link) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!items.length) return null
|
||||
@@ -238,7 +303,7 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="relative" data-nav-swipe-ignore="1">
|
||||
{/* Permanent edge fades for infinite illusion */}
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 z-20 w-24 bg-gradient-to-r from-[#0F1724] to-transparent" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-20 w-24 bg-gradient-to-l from-[#0F1724] to-transparent" />
|
||||
@@ -248,7 +313,11 @@ function Rail({ title, emoji, items, seeAllHref }) {
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex gap-4 overflow-x-auto px-4 pb-3 sm:px-6 lg:px-8 scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onClickCapture={onClickCapture}
|
||||
className="flex gap-4 overflow-x-auto px-4 pb-3 sm:px-6 lg:px-8 snap-x snap-mandatory scroll-smooth scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{loopItems.map((item, idx) => (
|
||||
<RailCard key={`${item.id || item.url}-${idx}`} item={item} />
|
||||
@@ -356,9 +425,13 @@ export default function ArtworkRecommendationsRails({ artwork, related = [] }) {
|
||||
? `/discover/trending`
|
||||
: '/discover/trending'
|
||||
|
||||
const similarHref = artwork?.name
|
||||
? `/search?q=${encodeURIComponent(artwork.name)}`
|
||||
: '/search'
|
||||
|
||||
return (
|
||||
<div className="space-y-14">
|
||||
<Rail title="Similar Artworks" emoji="✨" items={similarItems} />
|
||||
<Rail title="Similar Artworks" emoji="✨" items={similarItems} seeAllHref={similarHref} />
|
||||
<Rail title={trendingLabel} emoji="🔥" items={trendingRailItems} seeAllHref={trendingHref} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -44,6 +44,11 @@ export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
|
||||
}, [related, authorName, artwork?.canonical_url])
|
||||
|
||||
const onToggleFollow = async () => {
|
||||
if (following) {
|
||||
const confirmed = window.confirm(`Unfollow @${user.username || authorName}?`)
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const nextState = !following
|
||||
setFollowing(nextState)
|
||||
try {
|
||||
@@ -93,16 +98,17 @@ export default function CreatorSpotlight({ artwork, presentSq, related = [] }) {
|
||||
{followersCount.toLocaleString()} Followers
|
||||
</p>
|
||||
|
||||
{/* Follow + Profile buttons */}
|
||||
{/* Profile + Follow buttons */}
|
||||
<div className="mt-4 flex w-full gap-2">
|
||||
<a
|
||||
href={profileUrl}
|
||||
title="View profile"
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-white/[0.08] bg-white/[0.04] px-3 py-2.5 text-sm font-medium text-white/70 transition-all hover:border-white/[0.15] hover:bg-white/[0.07] hover:text-white"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-4 w-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
Follow
|
||||
Profile
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -116,7 +116,6 @@ export default function TabArtworks({ artworks, featuredArtworks, username, isAc
|
||||
cursorEndpoint={`/api/profile/${encodeURIComponent(username)}/artworks?sort=${encodeURIComponent(sort)}`}
|
||||
initialNextCursor={nextCursor}
|
||||
limit={24}
|
||||
gridClassName="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function ArtworkNavigator({ artworkId, onNavigate, onOpenViewer,
|
||||
|
||||
const touchStartX = useRef(null);
|
||||
const touchStartY = useRef(null);
|
||||
const touchIgnoreSwipe = useRef(false);
|
||||
|
||||
// Resolve neighbors on mount / artworkId change
|
||||
useEffect(() => {
|
||||
@@ -133,14 +134,25 @@ export default function ArtworkNavigator({ artworkId, onNavigate, onOpenViewer,
|
||||
// Touch swipe
|
||||
useEffect(() => {
|
||||
function onTouchStart(e) {
|
||||
touchIgnoreSwipe.current = Boolean(e.target?.closest?.('[data-nav-swipe-ignore="1"]'));
|
||||
if (touchIgnoreSwipe.current) {
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
return;
|
||||
}
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
touchStartY.current = e.touches[0].clientY;
|
||||
}
|
||||
function onTouchEnd(e) {
|
||||
if (touchIgnoreSwipe.current) {
|
||||
touchIgnoreSwipe.current = false;
|
||||
return;
|
||||
}
|
||||
if (touchStartX.current === null) return;
|
||||
const dx = e.changedTouches[0].clientX - touchStartX.current;
|
||||
const dy = e.changedTouches[0].clientY - touchStartY.current;
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
if (Math.abs(dx) > 50 && Math.abs(dy) < 80) {
|
||||
const n = neighborsRef.current;
|
||||
if (dx > 0) navigate(n.prevId, n.prevUrl);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-6xl px-4 py-8">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-100">Stories Management</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('admin.stories.review') }}" class="rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-sm text-amber-200">Review queue</a>
|
||||
<a href="{{ route('admin.stories.create') }}" class="rounded-lg border border-sky-500/40 bg-sky-500/10 px-3 py-2 text-sm text-sky-200">Create story</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-gray-700 bg-gray-900">
|
||||
<table class="min-w-full divide-y divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-800 text-gray-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Title</th>
|
||||
<th class="px-4 py-3 text-left">Creator</th>
|
||||
<th class="px-4 py-3 text-left">Status</th>
|
||||
<th class="px-4 py-3 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 text-gray-200">
|
||||
@foreach($stories as $story)
|
||||
<tr>
|
||||
<td class="px-4 py-3">{{ $story->title }}</td>
|
||||
<td class="px-4 py-3">{{ $story->creator?->username ?? 'n/a' }}</td>
|
||||
<td class="px-4 py-3">{{ $story->status }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="{{ route('admin.stories.show', ['story' => $story->id]) }}" class="text-amber-300">View</a>
|
||||
<span class="mx-1 text-gray-500">|</span>
|
||||
<a href="{{ route('admin.stories.edit', ['story' => $story->id]) }}" class="text-sky-300">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $stories->links() }}</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-6xl px-4 py-8">
|
||||
<div class="mb-5 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-100">Stories Review Queue</h1>
|
||||
<p class="text-sm text-gray-300">Pending creator stories waiting for moderation.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.stories.index') }}" class="rounded-lg border border-gray-600 px-3 py-2 text-sm text-gray-200">All stories</a>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-gray-700 bg-gray-900">
|
||||
<table class="min-w-full divide-y divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-800 text-gray-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Story</th>
|
||||
<th class="px-4 py-3 text-left">Creator</th>
|
||||
<th class="px-4 py-3 text-left">Submitted</th>
|
||||
<th class="px-4 py-3 text-left">Status</th>
|
||||
<th class="px-4 py-3 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 text-gray-200">
|
||||
@forelse($stories as $story)
|
||||
<tr>
|
||||
<td class="px-4 py-3">{{ $story->title }}</td>
|
||||
<td class="px-4 py-3">{{ $story->creator?->username ?? 'n/a' }}</td>
|
||||
<td class="px-4 py-3">{{ optional($story->submitted_for_review_at)->diffForHumans() ?? optional($story->updated_at)->diffForHumans() }}</td>
|
||||
<td class="px-4 py-3"><span class="rounded-full border border-amber-500/40 px-2 py-1 text-xs text-amber-200">{{ $story->status }}</span></td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="{{ route('admin.stories.show', ['story' => $story->id]) }}" class="text-sky-300 hover:text-sky-200">Review</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-6 text-center text-gray-400">No stories pending review.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $stories->links() }}</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-6xl px-4 py-8">
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<h1 class="text-xl font-semibold text-gray-100">Review Story</h1>
|
||||
<a href="{{ route('admin.stories.review') }}" class="rounded-lg border border-gray-600 px-3 py-2 text-sm text-gray-200">Back to queue</a>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-12">
|
||||
<article class="lg:col-span-8 rounded-xl border border-gray-700 bg-gray-900/80 p-5">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-400">{{ $story->story_type }}</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold text-white">{{ $story->title }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-300">Creator: @{{ $story->creator?->username ?? 'unknown' }}</p>
|
||||
@if($story->excerpt)
|
||||
<p class="mt-3 text-sm text-gray-200">{{ $story->excerpt }}</p>
|
||||
@endif
|
||||
<div class="prose prose-invert mt-5 max-w-none prose-a:text-sky-300">
|
||||
{!! preg_replace('/<(script|style)\\b[^>]*>.*?<\\/\\1>/is', '', (string) $story->content) !!}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="space-y-4 lg:col-span-4">
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Moderation Actions</h3>
|
||||
<form method="POST" action="{{ route('admin.stories.approve', ['story' => $story->id]) }}" class="mt-3">
|
||||
@csrf
|
||||
<button class="w-full rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-200 transition hover:scale-[1.02]">Approve & Publish</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('admin.stories.reject', ['story' => $story->id]) }}" class="mt-3 space-y-2">
|
||||
@csrf
|
||||
<label class="block text-xs uppercase tracking-wide text-gray-400">Rejection feedback</label>
|
||||
<textarea name="reason" rows="4" required class="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-sm text-white" placeholder="Explain what needs to change..."></textarea>
|
||||
<button class="w-full rounded-lg border border-rose-500/40 bg-rose-500/10 px-3 py-2 text-sm text-rose-200 transition hover:scale-[1.02]">Reject Story</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/70 p-4">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-300">Quick Links</h3>
|
||||
<div class="mt-3 flex flex-col gap-2 text-sm">
|
||||
<a href="{{ route('admin.stories.edit', ['story' => $story->id]) }}" class="rounded-lg border border-gray-600 px-3 py-2 text-gray-200">Edit in admin form</a>
|
||||
<a href="{{ route('creator.stories.preview', ['story' => $story->id]) }}" class="rounded-lg border border-gray-600 px-3 py-2 text-gray-200">Open creator preview</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
31
resources/views/gallery/_browse_nav.blade.php
Normal file
31
resources/views/gallery/_browse_nav.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
{{--
|
||||
Browse section-switcher pills.
|
||||
|
||||
Expected variable: $section (string) — one of: artworks, photography, wallpapers, skins, other
|
||||
--}}
|
||||
|
||||
@php
|
||||
$active = $section ?? 'artworks';
|
||||
|
||||
$sections = collect([
|
||||
'artworks' => ['label' => 'All Artworks', 'icon' => 'fa-border-all', 'href' => '/browse'],
|
||||
'photography' => ['label' => 'Photography', 'icon' => 'fa-camera', 'href' => '/photography'],
|
||||
'wallpapers' => ['label' => 'Wallpapers', 'icon' => 'fa-desktop', 'href' => '/wallpapers'],
|
||||
'skins' => ['label' => 'Skins', 'icon' => 'fa-layer-group', 'href' => '/skins'],
|
||||
'other' => ['label' => 'Other', 'icon' => 'fa-folder-open', 'href' => '/other'],
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<nav class="flex flex-wrap items-center gap-2 text-sm" aria-label="Browse sections">
|
||||
@foreach($sections as $slug => $meta)
|
||||
<a href="{{ $meta['href'] }}"
|
||||
@if($active === $slug) aria-current="page" @endif
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
||||
{{ $active === $slug
|
||||
? 'bg-sky-600 text-white'
|
||||
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white' }}">
|
||||
<i class="fa-solid {{ $meta['icon'] }} text-xs"></i>
|
||||
{{ $meta['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
@@ -75,55 +75,58 @@
|
||||
<div class="container-fluid legacy-page">
|
||||
@php Banner::ShowResponsiveAd(); @endphp
|
||||
|
||||
@php
|
||||
$browseSection = isset($contentType) && $contentType ? strtolower((string) $contentType->slug) : 'artworks';
|
||||
$browseIconMap = [
|
||||
'artworks' => 'fa-border-all',
|
||||
'photography' => 'fa-camera',
|
||||
'wallpapers' => 'fa-desktop',
|
||||
'skins' => 'fa-layer-group',
|
||||
'other' => 'fa-folder-open',
|
||||
];
|
||||
$browseIcon = $browseIconMap[$browseSection] ?? 'fa-border-all';
|
||||
@endphp
|
||||
|
||||
<div class="pt-0">
|
||||
<div class="mx-auto w-full">
|
||||
<div class="relative min-h-[calc(120vh-64px)] md:min-h-[calc(100vh-64px)]">
|
||||
|
||||
<main class="w-full">
|
||||
|
||||
{{-- ═══════════════════════════════════════════════════════════════ --}}
|
||||
{{-- HERO HEADER --}}
|
||||
{{-- ═══════════════════════════════════════════════════════════════ --}}
|
||||
<div class="relative overflow-hidden nb-hero-radial">
|
||||
{{-- Animated gradient overlays --}}
|
||||
<div class="absolute inset-0 nb-hero-gradient" aria-hidden="true"></div>
|
||||
<div class="absolute inset-0 opacity-20 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,#E07A2130,transparent)]" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative px-6 py-10 md:px-10 md:py-14">
|
||||
|
||||
{{-- Breadcrumb --}}
|
||||
@include('components.breadcrumbs', ['breadcrumbs' => collect(array_filter([
|
||||
{{-- ── Hero header (discover-style) ── --}}
|
||||
<header class="px-6 pt-10 pb-7 md:px-10 border-b border-white/[0.06] bg-gradient-to-b from-sky-500/[0.04] to-transparent">
|
||||
@php
|
||||
$headerBreadcrumbs = collect(array_filter([
|
||||
isset($contentType) && $contentType ? (object) ['name' => 'Explore', 'url' => '/explore'] : null,
|
||||
isset($contentType) && $contentType ? (object) ['name' => $contentType->name, 'url' => '/explore/' . strtolower($contentType->slug)] : (object) ['name' => 'Explore', 'url' => '/explore'],
|
||||
...(($gallery_type ?? null) === 'category' && isset($breadcrumbs) ? $breadcrumbs->all() : []),
|
||||
]))])
|
||||
]));
|
||||
@endphp
|
||||
|
||||
{{-- Glass title panel --}}
|
||||
<div class="mt-4 py-5">
|
||||
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-white/95 leading-tight">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="max-w-3xl">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-white/30 mb-1">Browse</p>
|
||||
<h1 class="text-3xl font-bold text-white leading-tight flex items-center gap-3">
|
||||
<i class="fa-solid {{ $browseIcon }} text-sky-400 text-2xl"></i>
|
||||
{{ $hero_title ?? 'Browse Artworks' }}
|
||||
</h1>
|
||||
|
||||
@if(!empty($hero_description))
|
||||
<p class="mt-2 text-sm leading-6 text-neutral-400 max-w-xl">
|
||||
{!! $hero_description !!}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if(is_object($artworks) && method_exists($artworks, 'total') && $artworks->total() > 0)
|
||||
<div class="mt-3 flex items-center gap-1.5 text-xs text-neutral-500">
|
||||
<svg class="h-3.5 w-3.5 text-accent/70" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>{{ number_format($artworks->total()) }} artworks</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-white/50">{!! $hero_description !!}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-3 lg:items-end">
|
||||
<div class="hidden lg:flex lg:justify-end">
|
||||
@include('components.breadcrumbs', ['breadcrumbs' => $headerBreadcrumbs])
|
||||
</div>
|
||||
@include('gallery._browse_nav', ['section' => $browseSection])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute left-0 right-0 bottom-0 h-16 nb-hero-fade pointer-events-none" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="mt-4 lg:hidden">
|
||||
@include('components.breadcrumbs', ['breadcrumbs' => $headerBreadcrumbs])
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ═══════════════════════════════════════════════════════════════ --}}
|
||||
{{-- RANKING TABS --}}
|
||||
|
||||
@@ -119,12 +119,6 @@
|
||||
<i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories
|
||||
</a>
|
||||
@auth
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('creator.stories.index') }}">
|
||||
<i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('creator.stories.create') }}">
|
||||
<i class="fa-solid fa-pen-to-square w-4 text-center text-sb-muted"></i>Write Story
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.following') }}">
|
||||
<i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following
|
||||
</a>
|
||||
@@ -233,8 +227,9 @@
|
||||
$toolbarUsername = strtolower((string) (Auth::user()->username ?? ''));
|
||||
$routeUpload = Route::has('upload') ? route('upload') : '/upload';
|
||||
$routeDashboard = Route::has('dashboard') ? route('dashboard') : '/dashboard';
|
||||
$routeMyArtworks = Route::has('creator.artworks') ? route('creator.artworks') : '/creator/artworks';
|
||||
$routeMyArtworks = Route::has('studio.artworks') ? route('studio.artworks') : '/studio/artworks';
|
||||
$routeMyStories = Route::has('creator.stories.index') ? route('creator.stories.index') : '/creator/stories';
|
||||
$routeWriteStory = Route::has('creator.stories.create') ? route('creator.stories.create') : '/creator/stories/create';
|
||||
$routeDashboardFavorites = Route::has('dashboard.favorites') ? route('dashboard.favorites') : '/dashboard/favorites';
|
||||
$routeEditProfile = Route::has('dashboard.profile')
|
||||
? route('dashboard.profile')
|
||||
@@ -253,18 +248,14 @@
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-table-columns text-xs text-sb-muted"></i></span>
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeMyArtworks }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-image text-xs text-sb-muted"></i></span>
|
||||
My Artworks
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/studio/artworks">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-palette text-xs text-sb-muted"></i></span>
|
||||
Studio
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeMyStories }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-book-open text-xs text-sb-muted"></i></span>
|
||||
My Stories
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/studio/artworks">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-palette text-xs text-sb-muted"></i></span>
|
||||
Studio
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ $routeDashboardFavorites }}">
|
||||
<span class="w-6 h-6 rounded-md bg-white/5 inline-flex items-center justify-center shrink-0"><i class="fa-solid fa-heart text-xs text-sb-muted"></i></span>
|
||||
My Favorites
|
||||
@@ -392,7 +383,6 @@
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
||||
@auth
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.create') }}"><i class="fa-solid fa-pen-to-square w-4 text-center text-sb-muted"></i>Write Story</a>
|
||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,67 @@
|
||||
@extends('layouts.nova.content-layout')
|
||||
@extends('layouts.nova')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$hero_title = 'Creator Stories';
|
||||
$hero_description = 'Articles, tutorials, interviews, and project breakdowns from the Skinbase creator community.';
|
||||
$currentCategory = (string) (request()->route('category') ?? request()->query('category', ''));
|
||||
$storyTabs = [
|
||||
['label' => '🔥 Trending', 'href' => '#trending'],
|
||||
['label' => '🚀 New & Hot', 'href' => '#featured'],
|
||||
['label' => '⭐ Best', 'href' => '#latest'],
|
||||
['label' => '🕐 Latest', 'href' => '#latest'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@section('page-content')
|
||||
<div class="relative overflow-hidden nb-hero-radial border-b border-white/10">
|
||||
<div class="absolute inset-0 nb-hero-gradient" aria-hidden="true"></div>
|
||||
<div class="absolute inset-0 opacity-20 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,#E07A2130,transparent)]" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative px-6 py-12 md:px-10 md:py-14">
|
||||
<div class="mt-2 py-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-white/30 mb-4">Browse</p>
|
||||
<h1 class="text-4xl md:text-5xl font-bold tracking-tight text-white/95 leading-tight flex items-center gap-3">
|
||||
<i class="fa-solid fa-newspaper text-sky-400 text-3xl"></i>
|
||||
Browse Stories
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-neutral-400 max-w-3xl">List of all published stories across tutorials, creator journals, interviews, and project breakdowns.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-white/10 bg-nova-900/90 backdrop-blur-md">
|
||||
<div class="px-6 md:px-10">
|
||||
<nav data-stories-tabs class="flex items-center gap-0 -mb-px nb-scrollbar-none overflow-x-auto" role="tablist" aria-label="Stories sections">
|
||||
@foreach($storyTabs as $index => $tab)
|
||||
<a href="{{ $tab['href'] }}"
|
||||
data-stories-tab
|
||||
data-target="{{ ltrim($tab['href'], '#') }}"
|
||||
role="tab"
|
||||
aria-selected="{{ $index === 0 ? 'true' : 'false' }}"
|
||||
class="relative whitespace-nowrap px-5 py-4 text-sm font-medium {{ $index === 0 ? 'text-white' : 'text-neutral-400 hover:text-white' }}">
|
||||
{{ $tab['label'] }}
|
||||
<span data-tab-indicator class="absolute bottom-0 left-0 right-0 h-0.5 {{ $index === 0 ? 'bg-accent scale-x-100' : 'bg-transparent scale-x-0' }} transition-transform duration-300 origin-left rounded-full"></span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-white/10 bg-nova-900/70">
|
||||
<div class="px-6 md:px-10 py-6">
|
||||
<div class="flex gap-3 overflow-x-auto nb-scrollbar-none pb-1">
|
||||
<a href="{{ route('stories.index') }}" class="whitespace-nowrap rounded-full px-3 py-1.5 text-sm font-semibold transition-colors {{ $currentCategory === '' ? 'bg-orange-500 text-white' : 'border border-white/10 bg-white/[0.05] text-white/70 hover:bg-white/[0.1] hover:text-white' }}">All</a>
|
||||
@foreach($categories as $category)
|
||||
<a href="{{ route('stories.category', $category['slug']) }}" class="whitespace-nowrap rounded-full px-3 py-1.5 text-sm transition-colors {{ $currentCategory === $category['slug'] ? 'bg-orange-500 text-white font-semibold' : 'border border-white/10 bg-white/[0.05] text-white/70 hover:bg-white/[0.1] hover:text-white' }}">
|
||||
{{ $category['name'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 pb-16 md:px-10">
|
||||
<div class="space-y-10">
|
||||
@if($featured)
|
||||
<section class="overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70 shadow-lg">
|
||||
<section id="featured" class="overflow-hidden rounded-xl border border-gray-700 bg-gray-800/70 shadow-lg">
|
||||
<a href="{{ route('stories.show', $featured->slug) }}" class="grid gap-0 lg:grid-cols-2">
|
||||
<div class="aspect-video overflow-hidden bg-gray-900">
|
||||
@if($featured->cover_url)
|
||||
@@ -25,7 +78,7 @@
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<section>
|
||||
<section id="trending">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h3 class="text-2xl font-semibold tracking-tight text-white">Trending Stories</h3>
|
||||
</div>
|
||||
@@ -47,7 +100,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section id="latest">
|
||||
<h3 class="mb-5 text-2xl font-semibold tracking-tight text-white">Latest Stories</h3>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
@foreach($latestStories as $story)
|
||||
@@ -59,4 +112,61 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tabLinks = Array.from(document.querySelectorAll('[data-stories-tab]'));
|
||||
if (!tabLinks.length) return;
|
||||
|
||||
function setActiveTab(targetId) {
|
||||
tabLinks.forEach(function (link) {
|
||||
var isActive = link.getAttribute('data-target') === targetId;
|
||||
link.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
link.classList.toggle('text-white', isActive);
|
||||
link.classList.toggle('text-neutral-400', !isActive);
|
||||
|
||||
var indicator = link.querySelector('[data-tab-indicator]');
|
||||
if (!indicator) return;
|
||||
indicator.classList.toggle('bg-accent', isActive);
|
||||
indicator.classList.toggle('bg-transparent', !isActive);
|
||||
indicator.classList.toggle('scale-x-100', isActive);
|
||||
indicator.classList.toggle('scale-x-0', !isActive);
|
||||
});
|
||||
}
|
||||
|
||||
tabLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
var targetId = link.getAttribute('data-target');
|
||||
if (targetId) setActiveTab(targetId);
|
||||
});
|
||||
});
|
||||
|
||||
var sectionIds = Array.from(new Set(tabLinks.map(function (link) {
|
||||
return link.getAttribute('data-target');
|
||||
}).filter(Boolean)));
|
||||
|
||||
var sections = sectionIds
|
||||
.map(function (id) { return document.getElementById(id); })
|
||||
.filter(Boolean);
|
||||
|
||||
if (!sections.length) return;
|
||||
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveTab(entry.target.id);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: '-35% 0px -55% 0px',
|
||||
threshold: 0.01
|
||||
});
|
||||
|
||||
sections.forEach(function (section) { observer.observe(section); });
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
Reference in New Issue
Block a user