Remove legacy frontend assets and update gallery routes

This commit is contained in:
2026-03-14 15:06:28 +01:00
parent 4f576ceb04
commit 78151aabfe
550 changed files with 826 additions and 108930 deletions

View File

@@ -65,4 +65,4 @@ export default function ForumSection({ category, boards = [] }) {
</section>
</div>
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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