Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -0,0 +1,146 @@
import React, { useEffect, useMemo, useState } from 'react'
function getScreenshotName(item, fallbackIndex) {
if (item && typeof item === 'object' && typeof item.name === 'string' && item.name.trim()) {
return item.name.trim()
}
return `Screenshot ${fallbackIndex + 1}`
}
function resolveScreenshotSource(item) {
if (!item) return { src: null, revoke: null }
if (typeof item === 'string') {
return { src: item, revoke: null }
}
if (typeof item === 'object') {
if (typeof item.preview === 'string' && item.preview) {
return { src: item.preview, revoke: null }
}
if (typeof item.src === 'string' && item.src) {
return { src: item.src, revoke: null }
}
if (typeof item.url === 'string' && item.url) {
return { src: item.url, revoke: null }
}
if (typeof File !== 'undefined' && item instanceof File) {
const objectUrl = URL.createObjectURL(item)
return {
src: objectUrl,
revoke: () => URL.revokeObjectURL(objectUrl),
}
}
}
return { src: null, revoke: null }
}
export default function ArchiveScreenshotPicker({
screenshots = [],
selectedIndex = 0,
onSelect,
compact = false,
title = 'Screenshots',
description = 'Choose which screenshot should be used as the default preview.',
}) {
const [resolvedScreenshots, setResolvedScreenshots] = useState([])
useEffect(() => {
const cleanup = []
const next = (Array.isArray(screenshots) ? screenshots : []).map((item, index) => {
const { src, revoke } = resolveScreenshotSource(item)
if (revoke) cleanup.push(revoke)
return {
src,
alt: getScreenshotName(item, index),
}
}).filter((item) => Boolean(item.src))
setResolvedScreenshots(next)
return () => {
cleanup.forEach((revoke) => revoke())
}
}, [screenshots])
const normalizedIndex = useMemo(() => {
if (resolvedScreenshots.length === 0) return 0
if (!Number.isFinite(selectedIndex)) return 0
return Math.min(Math.max(0, Math.floor(selectedIndex)), resolvedScreenshots.length - 1)
}, [resolvedScreenshots.length, selectedIndex])
const selectedScreenshot = resolvedScreenshots[normalizedIndex] ?? null
if (!selectedScreenshot) {
return null
}
return (
<div className={compact ? 'space-y-3' : 'space-y-4'}>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-white/45">{title}</p>
<p className="mt-1 text-xs text-white/55">{description}</p>
</div>
<span className="rounded-full border border-emerald-300/30 bg-emerald-400/10 px-2.5 py-1 text-[11px] text-emerald-100">
Default preview
</span>
</div>
<div className={compact ? 'overflow-hidden rounded-2xl border border-white/10 bg-black/25' : 'overflow-hidden rounded-3xl border border-white/10 bg-black/25'}>
<img
src={selectedScreenshot.src}
alt={selectedScreenshot.alt}
className={compact ? 'h-40 w-full object-cover' : 'h-56 w-full object-cover sm:h-72'}
loading="lazy"
decoding="async"
/>
</div>
<div className={compact ? 'grid grid-cols-4 gap-2' : 'grid grid-cols-2 gap-3 sm:grid-cols-4'}>
{resolvedScreenshots.map((item, index) => {
const isSelected = index === normalizedIndex
return (
<button
key={`${item.src}-${index}`}
type="button"
onClick={() => onSelect?.(index)}
aria-label={`Use ${item.alt} as default screenshot`}
className={[
'group relative overflow-hidden rounded-2xl border text-left transition',
isSelected
? 'border-emerald-300/45 bg-emerald-400/10 shadow-[0_0_0_1px_rgba(52,211,153,0.22)]'
: 'border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]',
].join(' ')}
aria-pressed={isSelected}
>
<img
src={item.src}
alt={item.alt}
className={compact ? 'h-16 w-full object-cover' : 'h-20 w-full object-cover'}
loading="lazy"
decoding="async"
/>
<div className="flex items-center justify-between gap-2 px-2.5 py-2">
<span className="truncate text-[11px] text-white/70">{item.alt}</span>
<span className={[
'shrink-0 rounded-full px-2 py-0.5 text-[10px]',
isSelected ? 'bg-emerald-300/20 text-emerald-100' : 'bg-white/10 text-white/45',
].join(' ')}>
{isSelected ? 'Default' : 'Use'}
</span>
</div>
</button>
)
})}
</div>
</div>
)
}