import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { usePage, Link } from '@inertiajs/react' import StudioLayout from '../../Layouts/StudioLayout' import MarkdownEditor from '../../components/ui/MarkdownEditor' 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.categories || 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, }) const [versionCount, setVersionCount] = useState(artwork?.version_count ?? 1) const [requiresReapproval, setRequiresReapproval] = useState(artwork?.requires_reapproval ?? false) const [changeNote, setChangeNote] = useState('') const [showChangeNote, setShowChangeNote] = useState(false) // Version history modal state const [showHistory, setShowHistory] = useState(false) const [historyData, setHistoryData] = useState(null) const [historyLoading, setHistoryLoading] = useState(false) const [restoring, setRestoring] = useState(null) // version id being restored // --- 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) if (changeNote.trim()) fd.append('change_note', changeNote.trim()) 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 }) if (data.version_number) setVersionCount(data.version_number) if (typeof data.requires_reapproval !== 'undefined') setRequiresReapproval(data.requires_reapproval) setChangeNote('') setShowChangeNote(false) } else { alert(data.error || 'File replacement failed.') } } catch (err) { console.error('File replace failed:', err) } finally { setReplacing(false) if (fileInputRef.current) fileInputRef.current.value = '' } } const loadVersionHistory = async () => { setHistoryLoading(true) setShowHistory(true) try { const res = await fetch(`/api/studio/artworks/${artwork.id}/versions`, { headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, credentials: 'same-origin', }) const data = await res.json() setHistoryData(data) } catch (err) { console.error('Failed to load version history:', err) } finally { setHistoryLoading(false) } } const handleRestoreVersion = async (versionId) => { if (!window.confirm('Restore this version? It will be cloned as the new current version.')) return setRestoring(versionId) try { const res = await fetch(`/api/studio/artworks/${artwork.id}/restore/${versionId}`, { method: 'POST', headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, credentials: 'same-origin', }) const data = await res.json() if (res.ok && data.success) { alert(data.message) setVersionCount((n) => n + 1) setShowHistory(false) } else { alert(data.error || 'Restore failed.') } } catch (err) { console.error('Restore failed:', err) } finally { setRestoring(null) } } // --- Render --- return ( Back to Artworks
{/* ── Uploaded Asset ── */}

Uploaded Asset

{requiresReapproval && ( Under Review )} v{versionCount} {versionCount > 1 && ( )}
{thumbUrl ? ( {title} ) : (
)}

{fileMeta.name}

{formatBytes(fileMeta.size)}

{fileMeta.width > 0 && (

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

)} {showChangeNote && (