Implement creator studio and upload updates
This commit is contained in:
146
resources/js/components/upload/ArchiveScreenshotPicker.jsx
Normal file
146
resources/js/components/upload/ArchiveScreenshotPicker.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user