import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Column, Flex, LayoutFurniIconImageView, Text } from '../../../common'; import { FurniDetail } from '../../../hooks/furni-editor'; interface FurniEditorEditViewProps { item: FurniDetail; furniDataEntry: Record | null; interactions: string[]; loading: boolean; onUpdate: (id: number, fields: Record) => void; onDelete: (id: number) => void; onBack: () => void; onUpdateFurnidata: (id: number, name: string, description: string) => void; onRevertFurnidata: (id: number) => void; } const FIELD_TIPS: Record = { stackHeight: 'Visual height when items are stacked on top of this furniture', interactionType: 'Defines behavior when user interacts (e.g. default, gate, teleport, vendingmachine)', customparams: 'Extra parameters for the interaction type (format depends on interaction)', interactionModesCount: 'Number of visual states/animations this furniture has', }; const PERM_GROUPS = [ { label: 'Gameplay', keys: [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay' ] }, { label: 'Trading', keys: [ 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell' ] }, { label: 'Inventory', keys: [ 'allowInventoryStack' ] }, ]; interface SectionProps { title: string; children: React.ReactNode; defaultOpen?: boolean } const Section: FC = ({ title, children, defaultOpen = true }) => { const [ open, setOpen ] = useState(defaultOpen); return (
{ open &&
{ children }
}
); }; const Tip: FC<{ field: string }> = ({ field }) => { const tip = FIELD_TIPS[field]; if(!tip) return null; return ( ? { tip } ); }; const CopyValue: FC<{ value: string | number }> = ({ value }) => { const [ copied, setCopied ] = useState(false); const copy = useCallback(() => { const text = String(value); const done = () => { setCopied(true); window.setTimeout(() => setCopied(false), 1000); }; if(navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(done).catch(() => done()); else done(); }, [ value ]); return (
{ String(value) } { copied ? 'copied!' : 'copy' }
); }; export const FurniEditorEditView: FC = props => { const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata } = props; const saveRef = useRef<() => void>(null); const [ form, setForm ] = useState({ itemName: '', publicName: '', spriteId: 0, type: 's', width: 1, length: 1, stackHeight: 0, allowStack: true, allowWalk: false, allowSit: false, allowLay: false, allowGift: true, allowTrade: true, allowRecycle: true, allowMarketplaceSell: true, allowInventoryStack: true, interactionType: '', interactionModesCount: 0, customparams: '', }); const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); const [ furniName, setFurniName ] = useState(''); const [ furniDescription, setFurniDescription ] = useState(''); const [ confirmFurnidata, setConfirmFurnidata ] = useState(false); useEffect(() => { if(!item) return; setForm({ itemName: item.itemName || '', publicName: item.publicName || '', spriteId: item.spriteId || 0, type: item.type || 's', width: item.width || 1, length: item.length || 1, stackHeight: item.stackHeight || 0, allowStack: !!item.allowStack, allowWalk: !!item.allowWalk, allowSit: !!item.allowSit, allowLay: !!item.allowLay, allowGift: !!item.allowGift, allowTrade: !!item.allowTrade, allowRecycle: !!item.allowRecycle, allowMarketplaceSell: !!item.allowMarketplaceSell, allowInventoryStack: !!item.allowInventoryStack, interactionType: item.interactionType || '', interactionModesCount: item.interactionModesCount || 0, customparams: item.customparams || '', }); setShowDeleteDialog(false); setFurniName(String(furniDataEntry?.name ?? '')); setFurniDescription(String(furniDataEntry?.description ?? '')); setConfirmFurnidata(false); }, [ item, furniDataEntry ]); const setField = useCallback((key: string, value: unknown) => { setForm(prev => ({ ...prev, [key]: value })); }, []); const isDirty = useMemo(() => { if(!item) return false; return form.itemName !== (item.itemName || '') || form.publicName !== (item.publicName || '') || form.spriteId !== (item.spriteId || 0) || form.type !== (item.type || 's') || form.width !== (item.width || 1) || form.length !== (item.length || 1) || form.stackHeight !== (item.stackHeight || 0) || form.allowStack !== !!item.allowStack || form.allowWalk !== !!item.allowWalk || form.allowSit !== !!item.allowSit || form.allowLay !== !!item.allowLay || form.allowGift !== !!item.allowGift || form.allowTrade !== !!item.allowTrade || form.allowRecycle !== !!item.allowRecycle || form.allowMarketplaceSell !== !!item.allowMarketplaceSell || form.allowInventoryStack !== !!item.allowInventoryStack || form.interactionType !== (item.interactionType || '') || form.interactionModesCount !== (item.interactionModesCount || 0) || form.customparams !== (item.customparams || ''); }, [ form, item ]); const validation = useMemo(() => { const errors: Record = {}; if(!form.itemName.trim()) errors.itemName = 'Required'; if(!form.publicName.trim()) errors.publicName = 'Required'; if(form.width < 1) errors.width = 'Min 1'; if(form.length < 1) errors.length = 'Min 1'; if(form.stackHeight < 0) errors.stackHeight = 'Min 0'; return errors; }, [ form ]); const isValid = useMemo(() => Object.keys(validation).length === 0, [ validation ]); const handleSave = useCallback(() => { if(!isValid) return; onUpdate(item.id, form); }, [ item, form, isValid, onUpdate ]); // Expose save for keyboard shortcut saveRef.current = handleSave; const handleBack = useCallback(() => { if(isDirty && !window.confirm('You have unsaved changes. Discard and go back?')) return; onBack(); }, [ isDirty, onBack ]); const handleDeleteConfirm = useCallback(() => { onDelete(item.id); setShowDeleteDialog(false); }, [ item, onDelete ]); // Keyboard shortcuts useEffect(() => { const handler = (e: KeyboardEvent) => { if(e.ctrlKey && e.key === 's') { e.preventDefault(); saveRef.current?.(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const inputClass = (field?: string) => `w-full px-3 py-1.5 text-sm leading-normal rounded-lg border border-slate-300 bg-white focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/15 transition${ field && validation[field] ? ' border-red-400 bg-red-50' : '' }`; const labelClass = 'text-[11px] font-medium text-slate-500 mb-1 flex items-center gap-0.5'; return ( { /* Header */ }
{ furniName || form.publicName || form.itemName } { form.itemName } ID { item.id } Sprite { item.spriteId } { item.usageCount } in use { isDirty && Unsaved }
{ /* Primary edit surface: furnidata display name + description (server-authoritative, live) */ }
Display name & description LIVE { (furniName !== String(furniDataEntry?.name ?? '') || furniDescription !== String(furniDataEntry?.description ?? '')) && Unsaved }
setFurniName(e.target.value) } maxLength={ 256 } />
setFurniDescription(e.target.value) } maxLength={ 256 } />
setField('width', Number(e.target.value)) } /> { validation.width && { validation.width } }
setField('length', Number(e.target.value)) } /> { validation.length && { validation.length } }
setField('stackHeight', Number(e.target.value)) } /> { validation.stackHeight && { validation.stackHeight } }
{ PERM_GROUPS.map(group => (
{ group.label }
{ group.keys.map(key => { const on = (form as any)[key]; return ( ); }) }
)) }
setField('interactionModesCount', Number(e.target.value)) } />
setField('customparams', e.target.value) } />
{ /* Actions */ } Ctrl+S { /* Delete Confirmation Dialog */ } { showDeleteDialog &&
setShowDeleteDialog(false) }>
e.stopPropagation() }> Delete Item? Are you sure you want to delete { item.publicName || item.itemName } (ID: { item.id })? This action cannot be undone.
} { /* Furnidata Confirmation Dialog */ } { confirmFurnidata &&
setConfirmFurnidata(false) }>
e.stopPropagation() }> Apply furnidata change to ALL clients?
Name: { String(furniDataEntry?.name ?? '') } → { furniName }
Desc: { String(furniDataEntry?.description ?? '') } → { furniDescription }
}
); };