Files
SkinbaseNova/resources/js/components/profile/tabs/TabFavourites.jsx
2026-03-20 21:17:26 +01:00

127 lines
4.0 KiB
JavaScript

import React, { useCallback, useEffect, useRef, useState } from 'react'
import ArtworkGallery from '../../artwork/ArtworkGallery'
function FavSkeleton() {
return (
<div className="rounded-2xl overflow-hidden bg-white/5 animate-pulse">
<div className="aspect-square bg-white/8" />
</div>
)
}
/**
* TabFavourites
* Shows artworks the user has favourited.
*/
export default function TabFavourites({ favourites, isOwner, username }) {
const initialItems = Array.isArray(favourites)
? favourites
: (favourites?.data ?? [])
const [items, setItems] = useState(initialItems)
const [nextCursor, setNextCursor] = useState(favourites?.next_cursor ?? null)
const [loadingMore, setLoadingMore] = useState(false)
const loadMoreRef = useRef(null)
useEffect(() => {
setItems(initialItems)
setNextCursor(favourites?.next_cursor ?? null)
}, [favourites, initialItems])
const loadMore = useCallback(async () => {
if (!nextCursor || loadingMore) return
setLoadingMore(true)
try {
const res = await fetch(
`/api/profile/${encodeURIComponent(username)}/favourites?cursor=${encodeURIComponent(nextCursor)}`,
{ headers: { Accept: 'application/json' } }
)
if (res.ok) {
const data = await res.json()
setItems((prev) => [...prev, ...(data.data ?? data)])
setNextCursor(data.next_cursor ?? null)
}
} catch (_) {}
setLoadingMore(false)
}, [loadingMore, nextCursor, username])
useEffect(() => {
const node = loadMoreRef.current
if (!node || !nextCursor) {
return undefined
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
loadMore()
}
},
{
rootMargin: '320px 0px',
}
)
observer.observe(node)
return () => observer.disconnect()
}, [loadMore, nextCursor])
return (
<div
id="tabpanel-favourites"
role="tabpanel"
aria-labelledby="tab-favourites"
className="pt-6"
>
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-4 flex items-center gap-2">
<i className="fa-solid fa-heart text-pink-400 fa-fw" />
{isOwner ? 'Your Favourites' : 'Favourites'}
</h2>
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-20 h-20 rounded-2xl bg-white/5 flex items-center justify-center mb-5 text-slate-500">
<i className="fa-solid fa-heart text-3xl" />
</div>
<p className="text-slate-400 font-medium">No favourites yet</p>
<p className="text-slate-600 text-sm mt-1">Artworks added to favourites will appear here.</p>
</div>
) : (
<>
<ArtworkGallery
items={items}
layout="grid"
className="grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4"
resolveCardProps={(_, index) => ({
loading: index < 8 ? 'eager' : 'lazy',
})}
>
{loadingMore && Array.from({ length: 4 }).map((_, i) => <FavSkeleton key={`sk-${i}`} />)}
</ArtworkGallery>
{nextCursor && (
<div ref={loadMoreRef} className="h-6 w-full" aria-hidden="true" />
)}
{nextCursor && (
<div className="mt-8 text-center">
<button
onClick={loadMore}
disabled={loadingMore}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-sm font-medium border border-white/10 transition-all"
>
{loadingMore
? <><i className="fa-solid fa-circle-notch fa-spin fa-fw" /> Loading</>
: <><i className="fa-solid fa-chevron-down fa-fw" /> Load more</>
}
</button>
</div>
)}
</>
)}
</div>
)
}