'use client' import { Box, Check, Compass, Copy, Eye, Layers, Loader2, MousePointerClick, Palette, RotateCcw, Ruler, Save, Sparkles, } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Textarea } from '@/components/ui/textarea' interface NitroEditorDialogProps { classname: string open: boolean onOpenChange: (open: boolean) => void } // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code function updateNested(obj: any, path: string, value: unknown): any { const clone = JSON.parse(JSON.stringify(obj)) const keys = path.split('.') let current = clone for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) current[keys[i]] = {} current = current[keys[i]] } current[keys[keys.length - 1]] = value return clone } // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code function getNested(obj: any, path: string, fallback?: unknown): any { const keys = path.split('.') let current = obj for (const key of keys) { if (current == null || typeof current !== 'object') return fallback current = current[key] } return current ?? fallback } const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7] const DIR_LABELS = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'] function hexToColor(hex: string): string { return `#${hex.padStart(6, '0')}` } function CopyClassname({ value }: { value: string }) { const [copied, setCopied] = useState(false) return ( ) } export function NitroEditorDialog({ classname, open, onOpenChange }: NitroEditorDialogProps) { // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code const [metadata, setMetadata] = useState(null) // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code const [originalMetadata, setOriginalMetadata] = useState(null) const [jsonText, setJsonText] = useState('') const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [activeTab, setActiveTab] = useState('visual') const [flags, setFlags] = useState({ canstandon: false, cansiton: false, canlayon: false }) const [originalFlags, setOriginalFlags] = useState({ canstandon: false, cansiton: false, canlayon: false, }) const [jsonError, setJsonError] = useState(null) const fetchMetadata = useCallback(async () => { if (!classname) return setLoading(true) setJsonError(null) try { const res = await fetch( `/api/admin/import/furni/nitro-editor?classname=${encodeURIComponent(classname)}`, ) if (!res.ok) { const err = await res.json().catch(() => ({})) toast.error(err.error || 'Failed to load .nitro metadata') onOpenChange(false) return } const data = await res.json() setMetadata(data.metadata) setOriginalMetadata(JSON.parse(JSON.stringify(data.metadata))) setJsonText(JSON.stringify(data.metadata, null, 2)) setFlags(data.flags) setOriginalFlags({ ...data.flags }) } catch { toast.error('Network error loading .nitro metadata') onOpenChange(false) } finally { setLoading(false) } }, [classname, onOpenChange]) useEffect(() => { if (open && classname) { setActiveTab('visual') fetchMetadata() } }, [open, classname, fetchMetadata]) const handleTabChange = (newTab: string) => { if (activeTab === 'json' && newTab === 'visual') { try { const parsed = JSON.parse(jsonText) setMetadata(parsed) setJsonError(null) } catch { toast.error('Invalid JSON — fix it before switching to Visual tab') return } } else if (activeTab === 'visual' && newTab === 'json') { setJsonText(JSON.stringify(metadata, null, 2)) } setActiveTab(newTab) } const handleReset = () => { if (!originalMetadata) return setMetadata(JSON.parse(JSON.stringify(originalMetadata))) setFlags({ ...originalFlags }) setJsonText(JSON.stringify(originalMetadata, null, 2)) toast.info('Reset to original values') } const handleSave = async () => { let metadataToSave = metadata if (activeTab === 'json') { try { metadataToSave = JSON.parse(jsonText) } catch { toast.error('Invalid JSON') return } } setSaving(true) try { const res = await fetch('/api/admin/import/furni/nitro-editor', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ classname, metadata: metadataToSave, ...flags, }), }) if (!res.ok) { const err = await res.json().catch(() => ({})) toast.error(err.error || 'Save failed') return } toast.success('Nitro metadata salvato') setMetadata(metadataToSave) setOriginalMetadata(JSON.parse(JSON.stringify(metadataToSave))) setOriginalFlags({ ...flags }) setJsonText(JSON.stringify(metadataToSave, null, 2)) } catch { toast.error('Network error') } finally { setSaving(false) } } // ── Visual tab helpers ── const dims = metadata ? getNested(metadata, 'logic.model.dimensions', { x: 1, y: 1, z: 0 }) : { x: 1, y: 1, z: 0 } const directions: number[] = metadata ? getNested(metadata, 'logic.model.directions', [0]) : [0] const setDim = (axis: 'x' | 'y' | 'z', value: string) => { const num = axis === 'z' ? parseFloat(value) || 0 : parseInt(value, 10) || 0 setMetadata((prev: unknown) => updateNested(prev, `logic.model.dimensions.${axis}`, num)) } const toggleDirection = (dir: number) => { const current: number[] = getNested(metadata, 'logic.model.directions', [0]) const next = current.includes(dir) ? current.filter((d: number) => d !== dir) : [...current, dir].sort((a: number, b: number) => a - b) if (next.length === 0) return setMetadata((prev: unknown) => updateNested(prev, 'logic.model.directions', next)) } // Get visualization with size=64 for layer/color editing // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code const viz64 = metadata?.visualizations?.find((v: any) => v.size === 64) const viz64Index = metadata?.visualizations?.findIndex((v: { size: number }) => v.size === 64) ?? -1 const layerCount = viz64?.layers ? Object.keys(viz64.layers).length : 0 const colorCount = viz64?.colors ? Object.keys(viz64.colors).length : 0 const animCount = viz64?.animations ? Object.keys(viz64.animations).length : 0 const handleJsonTextChange = (value: string) => { setJsonText(value) try { JSON.parse(value) setJsonError(null) } catch (e) { setJsonError((e as Error).message) } } // Check if there are unsaved changes const hasChanges = metadata && originalMetadata && (JSON.stringify(metadata) !== JSON.stringify(originalMetadata) || JSON.stringify(flags) !== JSON.stringify(originalFlags)) // Furni preview URL const starIdx = classname.indexOf('*') const baseClassname = starIdx !== -1 ? classname.substring(0, starIdx) : classname return ( {/* Header with preview */}
{/* Furni preview */}
{classname} { ;(e.target as HTMLImageElement).style.opacity = '0' }} />
Edit Nitro {/* Quick stats */} {metadata && (
{dims.x}×{dims.y}×{dims.z} {directions.length} dir {layerCount} layers {colorCount > 0 && ( {colorCount} colori )} {animCount > 0 && ( {animCount} anim )} {metadata.visualizationType || '?'}
)}
{loading ? (
) : metadata ? (
Visual {'{ }'} JSON
{/* ── Visual Tab ── */} {/* Dimensions + Interaction in 2-column layout */}
{/* Dimensions card */}

Dimensioni

setDim('x', e.target.value)} className="mt-1" />
setDim('y', e.target.value)} className="mt-1" />
setDim('z', e.target.value)} className="mt-1" />
{/* Interaction card */}

Interazione

Can Stand On setFlags((f) => ({ ...f, canstandon: !!v }))} />
Can Sit On setFlags((f) => ({ ...f, cansiton: !!v }))} />
Can Lay On setFlags((f) => ({ ...f, canlayon: !!v }))} />
{/* Directions - visual compass */}

Direzioni

{directions.length} attive
{/* Compass circle */}
{/* Direction buttons arranged in circle */} {ALL_DIRECTIONS.map((dir) => { const angle = (dir * 45 - 90) * (Math.PI / 180) const r = 80 const x = 96 + r * Math.cos(angle) - 18 const y = 96 + r * Math.sin(angle) - 18 const isActive = directions.includes(dir) return ( ) })} {/* Center label */}
{directions.length}/8
{/* Types */}

Tipo

setMetadata((prev: unknown) => updateNested(prev, 'visualizationType', e.target.value), ) } className="mt-1 font-mono text-sm" />
setMetadata((prev: unknown) => updateNested(prev, 'logicType', e.target.value), ) } className="mt-1 font-mono text-sm" />
{/* Layers */} {viz64?.layers && layerCount > 0 && (

Layer

{layerCount}
{/* Header */}
Layer Z-Index Alpha Ink
{Object.entries(viz64.layers).map(([layerId, layerData], idx) => { const layer = layerData as Record return (
#{layerId} { setMetadata((prev: unknown) => updateNested( prev, `visualizations.${viz64Index}.layers.${layerId}.z`, parseInt(e.target.value, 10) || 0, ), ) }} /> { setMetadata((prev: unknown) => updateNested( prev, `visualizations.${viz64Index}.layers.${layerId}.alpha`, parseInt(e.target.value, 10) || 0, ), ) }} /> { setMetadata((prev: unknown) => updateNested( prev, `visualizations.${viz64Index}.layers.${layerId}.ink`, e.target.value || undefined, ), ) }} />
) })}
)} {/* Colors */} {viz64?.colors && colorCount > 0 && (

Colori

{colorCount} varianti
{Object.entries(viz64.colors).map(([colorId, colorLayers]) => (
Variante #{colorId} {/* Color preview dots */}
{Object.values(colorLayers as Record).map( (lc, i) => (
), )}
{Object.entries(colorLayers as Record).map( ([layerId, layerColor]) => (
{ const hex = e.target.value .replace(/[^0-9a-fA-F]/g, '') .slice(0, 6) const num = parseInt(hex, 16) || 0 setMetadata((prev: unknown) => updateNested( prev, `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`, num, ), ) }} /> {/* Color swatch inside input */}
), )}
))}
)}
{/* ── JSON Tab ── */}