import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { usePage, Link } from '@inertiajs/react' import StudioLayout from '../../Layouts/StudioLayout' function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } function formatBytes(bytes) { if (!bytes) return '—' if (bytes < 1024) return bytes + ' B' if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / 1048576).toFixed(1) + ' MB' } function getContentTypeVisualKey(slug) { const map = { skins: 'skins', wallpapers: 'wallpapers', photography: 'photography', other: 'other', members: 'members' } return map[slug] || 'other' } function buildCategoryTree(contentTypes) { return (contentTypes || []).map((ct) => ({ ...ct, rootCategories: (ct.root_categories || []).map((rc) => ({ ...rc, children: rc.children || [], })), })) } export default function StudioArtworkEdit() { const { props } = usePage() const { artwork, contentTypes: rawContentTypes } = props const contentTypes = useMemo(() => buildCategoryTree(rawContentTypes || []), [rawContentTypes]) // --- State --- const [contentTypeId, setContentTypeId] = useState(artwork?.content_type_id || null) const [categoryId, setCategoryId] = useState(artwork?.parent_category_id || null) const [subCategoryId, setSubCategoryId] = useState(artwork?.sub_category_id || null) const [title, setTitle] = useState(artwork?.title || '') const [description, setDescription] = useState(artwork?.description || '') const [tags, setTags] = useState(() => (artwork?.tags || []).map((t) => ({ id: t.id, name: t.name, slug: t.slug || t.name }))) const [isPublic, setIsPublic] = useState(artwork?.is_public ?? true) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [errors, setErrors] = useState({}) // Tag picker state const [tagQuery, setTagQuery] = useState('') const [tagResults, setTagResults] = useState([]) const [tagLoading, setTagLoading] = useState(false) const tagInputRef = useRef(null) const tagSearchTimer = useRef(null) // File replace state const fileInputRef = useRef(null) const [replacing, setReplacing] = useState(false) const [thumbUrl, setThumbUrl] = useState(artwork?.thumb_url_lg || artwork?.thumb_url || null) const [fileMeta, setFileMeta] = useState({ name: artwork?.file_name || '—', size: artwork?.file_size || 0, width: artwork?.width || 0, height: artwork?.height || 0, }) // --- Tag search --- const searchTags = useCallback(async (q) => { setTagLoading(true) try { const params = new URLSearchParams() if (q) params.set('q', q) const res = await fetch(`/api/studio/tags/search?${params.toString()}`, { headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, credentials: 'same-origin', }) const data = await res.json() setTagResults(data || []) } catch { setTagResults([]) } finally { setTagLoading(false) } }, []) useEffect(() => { clearTimeout(tagSearchTimer.current) tagSearchTimer.current = setTimeout(() => searchTags(tagQuery), 250) return () => clearTimeout(tagSearchTimer.current) }, [tagQuery, searchTags]) const toggleTag = (tag) => { setTags((prev) => { const exists = prev.find((t) => t.id === tag.id) return exists ? prev.filter((t) => t.id !== tag.id) : [...prev, { id: tag.id, name: tag.name, slug: tag.slug }] }) } const removeTag = (id) => { setTags((prev) => prev.filter((t) => t.id !== id)) } // --- Derived data --- const selectedCT = contentTypes.find((ct) => ct.id === contentTypeId) || null const rootCategories = selectedCT?.rootCategories || [] const selectedRoot = rootCategories.find((c) => c.id === categoryId) || null const subCategories = selectedRoot?.children || [] // --- Handlers --- const handleContentTypeChange = (id) => { setContentTypeId(id) setCategoryId(null) setSubCategoryId(null) } const handleCategoryChange = (id) => { setCategoryId(id) setSubCategoryId(null) } const handleSave = async () => { setSaving(true) setSaved(false) setErrors({}) try { const payload = { title, description, is_public: isPublic, category_id: subCategoryId || categoryId || null, tags: tags.map((t) => t.slug || t.name), } const res = await fetch(`/api/studio/artworks/${artwork.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, credentials: 'same-origin', body: JSON.stringify(payload), }) if (res.ok) { setSaved(true) setTimeout(() => setSaved(false), 3000) } else { const data = await res.json() if (data.errors) setErrors(data.errors) console.error('Save failed:', data) } } catch (err) { console.error('Save failed:', err) } finally { setSaving(false) } } const handleFileReplace = async (e) => { const file = e.target.files?.[0] if (!file) return setReplacing(true) try { const fd = new FormData() fd.append('file', file) const res = await fetch(`/api/studio/artworks/${artwork.id}/replace-file`, { method: 'POST', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, credentials: 'same-origin', body: fd, }) const data = await res.json() if (res.ok && data.thumb_url) { setThumbUrl(data.thumb_url) setFileMeta({ name: file.name, size: file.size, width: data.width || 0, height: data.height || 0 }) } else { console.error('File replace failed:', data) } } catch (err) { console.error('File replace failed:', err) } finally { setReplacing(false) if (fileInputRef.current) fileInputRef.current.value = '' } } // --- Render --- return ( Back to Artworks
{/* ── Uploaded Asset ── */}

Uploaded Asset

{thumbUrl ? ( {title} ) : (
)}

{fileMeta.name}

{formatBytes(fileMeta.size)}

{fileMeta.width > 0 && (

{fileMeta.width} × {fileMeta.height} px

)}
{/* ── Content Type ── */}

Content Type

{contentTypes.map((ct) => { const active = ct.id === contentTypeId const vk = getContentTypeVisualKey(ct.slug) return ( ) })}
{/* ── Category ── */} {rootCategories.length > 0 && (

Category

{rootCategories.map((cat) => { const active = cat.id === categoryId return ( ) })}
{/* Subcategory */} {subCategories.length > 0 && (

Subcategory

{subCategories.map((sub) => { const active = sub.id === subCategoryId return ( ) })}
)}
)} {/* ── Basics ── */}

Basics

setTitle(e.target.value)} maxLength={120} className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" /> {errors.title &&

{errors.title[0]}

}