import React, { useState, useMemo, useRef, useCallback } from 'react'
import { usePage, Link } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
import MarkdownEditor from '../../components/ui/MarkdownEditor'
import TextInput from '../../components/ui/TextInput'
import Button from '../../components/ui/Button'
import Toggle from '../../components/ui/Toggle'
import Modal from '../../components/ui/Modal'
import FormField from '../../components/ui/FormField'
import TagPicker from '../../components/tags/TagPicker'
// ─── Helpers ─────────────────────────────────────────────────────────────────
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 || [],
})),
}))
}
// ─── Sub-components ──────────────────────────────────────────────────────────
/** Glass-morphism section card (Nova theme) */
function Section({ children, className = '' }) {
return (
)
}
/** Section heading */
function SectionTitle({ icon, children }) {
return (
{icon && }
{children}
)
}
// ─── Main Component ──────────────────────────────────────────────────────────
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 [tagSlugs, setTagSlugs] = useState(() => (artwork?.tags || []).map((t) => 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({})
// File replace
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
const [showHistory, setShowHistory] = useState(false)
const [historyData, setHistoryData] = useState(null)
const [historyLoading, setHistoryLoading] = useState(false)
const [restoring, setRestoring] = useState(null)
// ── Derived ────────────────────────────────────────────────────────────────
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 = useCallback(async () => {
setSaving(true)
setSaved(false)
setErrors({})
try {
const payload = {
title,
description,
is_public: isPublic,
category_id: subCategoryId || categoryId || null,
tags: tagSlugs,
}
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)
}
} catch (err) {
console.error('Save failed:', err)
} finally {
setSaving(false)
}
}, [title, description, isPublic, subCategoryId, categoryId, tagSlugs, artwork?.id])
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',
})
setHistoryData(await res.json())
} catch (err) {
console.error('Failed to load version history:', err)
} finally {
setHistoryLoading(false)
}
}
const handleRestoreVersion = async (versionId) => {
if (!window.confirm('Restore this version? A copy will become 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) {
setVersionCount((n) => n + 1)
setShowHistory(false)
} else {
alert(data.error || 'Restore failed.')
}
} catch (err) {
console.error('Restore failed:', err)
} finally {
setRestoring(null)
}
}
// ── Render ─────────────────────────────────────────────────────────────────
return (
{/* ── Page Header ── */}
{title || 'Untitled artwork'}
Editing ·{' '}
{isPublic ? 'Published' : 'Draft'}
{saved && (
Saved
)}
{/* ── Two-column Layout ── */}
{/* ─────────── LEFT SIDEBAR ─────────── */}
{/* Preview Card */}
Preview
{/* Thumbnail */}
{thumbUrl ? (

) : (
)}
{replacing && (
)}
{/* File Metadata */}
{fileMeta.name}
{fileMeta.width > 0 && (
{fileMeta.width} × {fileMeta.height}
)}
{formatBytes(fileMeta.size)}
{/* Version + History */}
{requiresReapproval && (
Requires re-approval after replace
)}
{/* Replace File */}
{/* Quick Links */}
{/* ─────────── RIGHT MAIN FORM ─────────── */}
{/* ── Content Type ── */}
Content Type
{contentTypes.map((ct) => {
const isActive = contentTypeId === ct.id
const visualKey = getContentTypeVisualKey(ct.slug)
return (
)
})}
{/* ── Category ── */}
{rootCategories.length > 0 && (
Category
{rootCategories.map((cat) => {
const isActive = categoryId === cat.id
return (
)
})}
{subCategories.length > 0 && (
Subcategory
{subCategories.map((sub) => {
const isActive = subCategoryId === sub.id
return (
)
})}
)}
{errors.category_id && {errors.category_id[0]}
}
)}
{/* ── Details (Title + Description) ── */}
Details
setTitle(e.target.value)}
placeholder="Give your artwork a title"
error={errors.title?.[0]}
required
/>
{/* ── Tags ── */}
{/* ── Visibility ── */}
Visibility
{isPublic
? 'Your artwork is visible to everyone'
: 'Your artwork is only visible to you'}
setIsPublic(e.target.checked)}
label={isPublic ? 'Published' : 'Draft'}
variant={isPublic ? 'emerald' : 'accent'}
size="md"
/>
{/* ── Bottom Save Bar (mobile) ── */}
{saved && (
Saved
)}
{/* ── Version History Modal ── */}
setShowHistory(false)}
title="Version History"
size="lg"
footer={
Restoring creates a new version — nothing is deleted.
}
>
{historyLoading && (
)}
{!historyLoading && historyData && (
{historyData.versions.map((v) => (
v{v.version_number}
{v.is_current && (
Current
)}
{v.created_at ? new Date(v.created_at).toLocaleString() : ''}
{v.width && (
{v.width} × {v.height} px · {formatBytes(v.file_size)}
)}
{v.change_note && (
“{v.change_note}”
)}
{!v.is_current && (
)}
))}
{historyData.versions.length === 0 && (
No version history yet.
)}
)}
)
}