minor fixes
This commit is contained in:
@@ -119,6 +119,24 @@ function getCsrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
|
||||
function swapImageToFallbackOnce(event, fallbackSrc, { clearResponsive = false } = {}) {
|
||||
const image = event.currentTarget
|
||||
|
||||
if (!image || image.dataset.fallbackApplied === '1') {
|
||||
return
|
||||
}
|
||||
|
||||
image.dataset.fallbackApplied = '1'
|
||||
image.onerror = null
|
||||
|
||||
if (clearResponsive) {
|
||||
image.removeAttribute('srcset')
|
||||
image.removeAttribute('sizes')
|
||||
}
|
||||
|
||||
image.src = fallbackSrc
|
||||
}
|
||||
|
||||
function sendDiscoveryEvent(endpoint, payload) {
|
||||
if (!endpoint) return
|
||||
|
||||
@@ -437,18 +455,27 @@ export default function ArtworkCard({
|
||||
|
||||
const item = artwork || {}
|
||||
const rawAuthor = item.author || item.creator
|
||||
const publisher = item.publisher && typeof item.publisher === 'object' ? item.publisher : null
|
||||
const isGroupPublisher = (publisher?.type === 'group') || item.published_as_type === 'group'
|
||||
const title = decodeHtml(item.title || item.name || 'Untitled artwork')
|
||||
const author = decodeHtml(
|
||||
(typeof rawAuthor === 'string' ? rawAuthor : rawAuthor?.name)
|
||||
(isGroupPublisher ? publisher?.name : null)
|
||||
|| (typeof rawAuthor === 'string' ? rawAuthor : rawAuthor?.name)
|
||||
|| item.author_name
|
||||
|| item.uname
|
||||
|| 'Skinbase Artist'
|
||||
)
|
||||
const username = rawAuthor?.username || item.author_username || item.username || null
|
||||
const authorLevel = Number(rawAuthor?.level ?? item.author_level ?? item.creator?.level ?? 0)
|
||||
const authorRank = rawAuthor?.rank || item.author_rank || item.creator?.rank || ''
|
||||
const username = isGroupPublisher ? null : (rawAuthor?.username || item.author_username || item.username || null)
|
||||
const authorLevel = isGroupPublisher ? 0 : Number(rawAuthor?.level ?? item.author_level ?? item.creator?.level ?? 0)
|
||||
const authorRank = isGroupPublisher ? '' : (rawAuthor?.rank || item.author_rank || item.creator?.rank || '')
|
||||
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 avatar = (isGroupPublisher ? publisher?.avatar_url : null)
|
||||
|| 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
|
||||
@@ -470,7 +497,7 @@ export default function ArtworkCard({
|
||||
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 authorHref = publisher?.profile_url || rawAuthor?.profile_url || item.profile_url || item.author_url || (username ? `/@${username}` : null)
|
||||
const resolvedMetricBadge = metricBadge || item.metric_badge || null
|
||||
const relativePublishedAt = useMemo(
|
||||
() => formatRelativeTime(item.published_at || item.publishedAt || null),
|
||||
@@ -750,7 +777,7 @@ export default function ArtworkCard({
|
||||
decoding={decoding}
|
||||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = IMAGE_FALLBACK
|
||||
swapImageToFallbackOnce(event, IMAGE_FALLBACK)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -761,7 +788,7 @@ export default function ArtworkCard({
|
||||
<div className="mt-0.5 flex items-center gap-2 text-xs text-slate-400">
|
||||
{authorHref ? (
|
||||
<span>
|
||||
by {author} <span className="text-slate-500">@{username}</span>
|
||||
by {author} {username ? <span className="text-slate-500">@{username}</span> : null}
|
||||
</span>
|
||||
) : (
|
||||
<span>by {author}</span>
|
||||
@@ -810,7 +837,7 @@ export default function ArtworkCard({
|
||||
fetchPriority={fetchPriority || undefined}
|
||||
className={cx('h-full w-full object-cover transition duration-500 group-hover:scale-110 group-focus-within:scale-110', imageClassName)}
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = IMAGE_FALLBACK
|
||||
swapImageToFallbackOnce(event, IMAGE_FALLBACK, { clearResponsive: true })
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -880,14 +907,14 @@ export default function ArtworkCard({
|
||||
decoding="async"
|
||||
className="h-9 w-9 shrink-0 rounded-full object-cover"
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = AVATAR_FALLBACK
|
||||
swapImageToFallbackOnce(event, AVATAR_FALLBACK)
|
||||
}}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="block min-w-0 truncate text-sm font-medium text-white/90">
|
||||
{author}
|
||||
{username && <span className="text-[11px] text-white/60"> @{username}</span>}
|
||||
{username ? <span className="text-[11px] text-white/60"> @{username}</span> : null}
|
||||
</span>
|
||||
{authorLevel > 0 ? <LevelBadge level={authorLevel} rank={authorRank} compact className="shrink-0" /> : null}
|
||||
</span>
|
||||
|
||||
@@ -6,6 +6,9 @@ const FALLBACK_XL = 'https://files.skinbase.org/default/missing_xl.webp'
|
||||
|
||||
export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl, mediaWidth = null, mediaHeight = null, mediaKey = 'cover', onOpenViewer, hasPrev, hasNext, onPrev, onNext }) {
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [mainImageMode, setMainImageMode] = useState('primary')
|
||||
const [previewImageMode, setPreviewImageMode] = useState('primary')
|
||||
const [showBackdrop, setShowBackdrop] = useState(true)
|
||||
|
||||
const mdSource = presentMd?.url || artwork?.thumbs?.md?.url || null
|
||||
const lgSource = presentLg?.url || artwork?.thumbs?.lg?.url || null
|
||||
@@ -17,6 +20,19 @@ export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl,
|
||||
|
||||
const hasRealArtworkImage = Boolean(mdSource || lgSource || xlSource)
|
||||
const blurBackdropSrc = mdSource || lgSource || xlSource || null
|
||||
const primaryMainSrc = lgSource || xlSource || mdSource || FALLBACK_LG
|
||||
const primaryPreviewSrc = mdSource || lgSource || xlSource || FALLBACK_MD
|
||||
const srcSet = [
|
||||
mdSource ? `${mdSource} 640w` : null,
|
||||
lgSource ? `${lgSource} 1280w` : null,
|
||||
xlSource ? `${xlSource} 1920w` : null,
|
||||
].filter(Boolean).join(', ')
|
||||
const resolvedMainSrc = mainImageMode === 'fallback'
|
||||
? FALLBACK_LG
|
||||
: (mainImageMode === 'hidden' ? null : primaryMainSrc)
|
||||
const resolvedPreviewSrc = previewImageMode === 'fallback'
|
||||
? FALLBACK_MD
|
||||
: (previewImageMode === 'hidden' ? null : primaryPreviewSrc)
|
||||
|
||||
const dbWidth = Number(mediaWidth ?? artwork?.width)
|
||||
const dbHeight = Number(mediaHeight ?? artwork?.height)
|
||||
@@ -30,6 +46,10 @@ export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl,
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoaded(false)
|
||||
setMainImageMode('primary')
|
||||
setPreviewImageMode('primary')
|
||||
setShowBackdrop(true)
|
||||
|
||||
if (hasDbDims) {
|
||||
setNaturalDims({ w: dbWidth, h: dbHeight })
|
||||
return
|
||||
@@ -47,16 +67,15 @@ export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl,
|
||||
setNaturalDims({ w: img.naturalWidth, h: img.naturalHeight })
|
||||
}
|
||||
}
|
||||
img.onerror = null
|
||||
img.src = xlSource
|
||||
}, [xlSource, naturalDims])
|
||||
|
||||
const aspectRatio = naturalDims ? `${naturalDims.w} / ${naturalDims.h}` : '16 / 9'
|
||||
|
||||
const srcSet = `${md} 640w, ${lg} 1280w, ${xl} 1920w`
|
||||
|
||||
return (
|
||||
<figure className="relative w-full overflow-hidden rounded-[2rem] border border-white/10 bg-gradient-to-b from-nova-950 via-nova-900 to-nova-900 p-2 shadow-[0_35px_90px_-35px_rgba(15,23,36,0.9)] sm:p-4">
|
||||
{blurBackdropSrc && (
|
||||
{blurBackdropSrc && showBackdrop && (
|
||||
<>
|
||||
<img
|
||||
src={blurBackdropSrc}
|
||||
@@ -65,6 +84,10 @@ export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl,
|
||||
className="pointer-events-none absolute inset-0 h-full w-full scale-110 object-cover opacity-30 blur-3xl"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
event.currentTarget.onerror = null
|
||||
setShowBackdrop(false)
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-nova-950/55 via-nova-900/40 to-nova-950/70" />
|
||||
<div className="pointer-events-none absolute inset-0 backdrop-blur-sm" />
|
||||
@@ -102,29 +125,52 @@ export default function ArtworkHero({ artwork, presentMd, presentLg, presentXl,
|
||||
}
|
||||
} : undefined}
|
||||
>
|
||||
<img
|
||||
src={md}
|
||||
alt={artwork?.title ?? 'Artwork'}
|
||||
className="absolute inset-0 h-full w-full object-contain rounded-xl"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
{resolvedPreviewSrc ? (
|
||||
<img
|
||||
src={resolvedPreviewSrc}
|
||||
alt={artwork?.title ?? 'Artwork'}
|
||||
className="absolute inset-0 h-full w-full object-contain rounded-xl"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
onError={(event) => {
|
||||
event.currentTarget.onerror = null
|
||||
|
||||
<img
|
||||
src={lg}
|
||||
srcSet={srcSet}
|
||||
sizes="(min-width: 1536px) 1400px, (min-width: 1024px) 92vw, 100vw"
|
||||
alt={artwork?.title ?? 'Artwork'}
|
||||
className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-500 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = FALLBACK_LG
|
||||
}}
|
||||
/>
|
||||
if (previewImageMode === 'primary') {
|
||||
setPreviewImageMode('fallback')
|
||||
return
|
||||
}
|
||||
|
||||
setPreviewImageMode('hidden')
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{resolvedMainSrc ? (
|
||||
<img
|
||||
src={resolvedMainSrc}
|
||||
srcSet={mainImageMode === 'primary' && srcSet !== '' ? srcSet : undefined}
|
||||
sizes={mainImageMode === 'primary' && srcSet !== '' ? '(min-width: 1536px) 1400px, (min-width: 1024px) 92vw, 100vw' : undefined}
|
||||
alt={artwork?.title ?? 'Artwork'}
|
||||
className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-500 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={(event) => {
|
||||
event.currentTarget.onerror = null
|
||||
|
||||
if (mainImageMode === 'primary') {
|
||||
setMainImageMode('fallback')
|
||||
setIsLoaded(false)
|
||||
return
|
||||
}
|
||||
|
||||
setMainImageMode('hidden')
|
||||
setIsLoaded(true)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{onOpenViewer && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user