minor fixes

This commit is contained in:
2026-04-09 08:50:36 +02:00
parent 23d363a50c
commit a2457f4e49
75 changed files with 3848 additions and 387 deletions

View File

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

View File

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