import React, { useEffect, useMemo, useState } from 'react' const IMAGE_FALLBACK = 'https://files.skinbase.org/default/missing_md.webp' const AVATAR_FALLBACK = 'https://files.skinbase.org/default/avatar_default.webp' const numberFormatter = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1, }) function cx(...parts) { return parts.filter(Boolean).join(' ') } function formatCount(value) { const numeric = Number(value ?? 0) if (!Number.isFinite(numeric)) return '0' return numberFormatter.format(numeric) } function slugify(value) { return String(value ?? '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') } function decodeHtml(value) { const text = String(value ?? '') if (!text.includes('&')) return text let decoded = text for (let index = 0; index < 3; index += 1) { decoded = decoded .replace(/&/gi, '&') .replace(/&(apos|#39);/gi, "'") .replace(/&(acute|#180|#x00B4);/gi, "'") .replace(/&(quot|#34);/gi, '"') .replace(/&(nbsp|#160);/gi, ' ') if (typeof document === 'undefined') { break } const textarea = document.createElement('textarea') textarea.innerHTML = decoded const nextValue = textarea.value if (nextValue === decoded) break decoded = nextValue } return decoded } function normalizeContentTypeLabel(value) { const raw = decodeHtml(value).trim() if (!raw) return '' const normalized = raw.toLowerCase() const knownLabels = { artworks: 'Artwork', artwork: 'Artwork', wallpapers: 'Wallpaper', wallpaper: 'Wallpaper', skins: 'Skin', skin: 'Skin', photography: 'Photography', photo: 'Photography', photos: 'Photography', other: 'Other', } if (knownLabels[normalized]) { return knownLabels[normalized] } return raw .replace(/[-_]+/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()) } function getCsrfToken() { if (typeof document === 'undefined') return '' return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } function HeartIcon(props) { return ( ) } function DownloadIcon(props) { return ( ) } function ViewIcon(props) { return ( ) } function ActionLink({ href, label, children, onClick }) { return ( {children} ) } function ActionButton({ label, children, onClick }) { return ( ) } export default function ArtworkCard({ artwork, variant = 'default', compact = false, showStats = true, showAuthor = true, className = '', articleClassName = '', frameClassName = '', mediaClassName = '', mediaStyle, articleStyle, imageClassName = '', imageSizes, imageSrcSet, imageWidth, imageHeight, loading = 'lazy', decoding = 'async', fetchPriority, onLike, showActions = true, }) { const item = artwork || {} const rawAuthor = item.author const title = decodeHtml(item.title || item.name || 'Untitled artwork') const author = decodeHtml( (typeof rawAuthor === 'string' ? rawAuthor : rawAuthor?.name) || item.author_name || item.uname || 'Skinbase Artist' ) const username = rawAuthor?.username || item.author_username || item.username || null const image = item.image || item.thumb || item.thumb_url || item.thumbnail_url || IMAGE_FALLBACK const avatar = rawAuthor?.avatar_url || rawAuthor?.avatar || item.avatar || item.author_avatar || item.avatar_url || AVATAR_FALLBACK const likes = item.likes ?? item.favourites ?? 0 const views = item.views ?? item.views_count ?? item.view_count ?? 0 const downloads = item.downloads ?? item.downloads_count ?? item.download_count ?? 0 const contentType = normalizeContentTypeLabel( item.content_type || item.content_type_name || item.contentType || item.contentTypeName || item.content_type_slug || '' ) const category = decodeHtml(item.category || item.category_name || '') const width = Number(item.width ?? 0) const height = Number(item.height ?? 0) const resolution = decodeHtml(item.resolution || ((width > 0 && height > 0) ? `${width}x${height}` : '')) const href = item.url || item.href || (item.id ? `/art/${item.id}/${slugify(title)}` : '#') const downloadHref = item.download_url || item.downloadHref || (item.id ? `/download/artwork/${item.id}` : href) const cardLabel = `${title} by ${author}` const aspectClass = compact ? 'aspect-square' : 'aspect-[4/5]' const titleClass = compact ? 'text-[0.96rem]' : 'text-[1rem] sm:text-[1.08rem]' const metadataLine = [contentType, category, resolution].filter(Boolean).join(' • ') const authorHref = username ? `/@${username}` : null const initialLiked = Boolean(item.viewer?.is_liked) const [liked, setLiked] = useState(initialLiked) const [likeCount, setLikeCount] = useState(Number(likes ?? 0) || 0) const [likeBusy, setLikeBusy] = useState(false) const [downloadBusy, setDownloadBusy] = useState(false) useEffect(() => { setLiked(Boolean(item.viewer?.is_liked)) setLikeCount(Number(item.likes ?? item.favourites ?? 0) || 0) }, [item.id, item.likes, item.favourites, item.viewer?.is_liked]) const articleData = useMemo(() => ({ 'data-art-id': item.id ?? undefined, 'data-art-url': href !== '#' ? href : undefined, 'data-art-title': title, 'data-art-img': image, }), [href, image, item.id, title]) const handleLike = async () => { if (!item.id || likeBusy) { onLike?.(item) return } const nextState = !liked setLikeBusy(true) setLiked(nextState) setLikeCount((current) => Math.max(0, current + (nextState ? 1 : -1))) try { const response = await fetch(`/api/artworks/${item.id}/like`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken(), }, credentials: 'same-origin', body: JSON.stringify({ state: nextState }), }) if (!response.ok) { throw new Error('like_request_failed') } onLike?.(item) } catch { setLiked(!nextState) setLikeCount((current) => Math.max(0, current + (nextState ? -1 : 1))) } finally { setLikeBusy(false) } } const handleDownload = async (event) => { event.preventDefault() if (!item.id || downloadBusy) return setDownloadBusy(true) try { const link = document.createElement('a') link.href = downloadHref link.rel = 'noopener noreferrer' document.body.appendChild(link) link.click() document.body.removeChild(link) } catch { window.open(downloadHref, '_blank', 'noopener,noreferrer') } finally { setDownloadBusy(false) } } if (variant === 'embed') { return (
{title} { event.currentTarget.src = IMAGE_FALLBACK }} />

{title}

{showAuthor && (

{authorHref ? ( by {author} @{username} ) : ( by {author} )}

)}

{contentType || 'Artwork'}

) } return (
{cardLabel}
{title} { event.currentTarget.src = IMAGE_FALLBACK }} />
{showActions && (
)}

{title}

{showAuthor ? (
{`Avatar { event.currentTarget.src = AVATAR_FALLBACK }} /> {author} {username && @{username}} {showStats && metadataLine && ( {metadataLine} )}
) : showStats && metadataLine ? (
{metadataLine}
) : null}
) }