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);
|
||||
|
||||
Reference in New Issue
Block a user