feat: catalog style toggle (classic/new) with admin mode & favorites

This commit is contained in:
Life
2026-03-22 16:54:40 +01:00
parent ccaec9185e
commit a5ea88010e
34 changed files with 2799 additions and 575 deletions
@@ -9,8 +9,8 @@ interface FurniEditorEditViewProps
furniDataEntry: Record<string, unknown> | null;
interactions: string[];
loading: boolean;
onUpdate: (id: number, fields: Record<string, unknown>) => Promise<boolean>;
onDelete: (id: number) => Promise<boolean>;
onUpdate: (id: number, fields: Record<string, unknown>) => void;
onDelete: (id: number) => void;
onBack: () => void;
onRefresh: (id: number) => void;
}
@@ -39,6 +39,19 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
interactionType: '',
interactionModesCount: 0,
customparams: '',
description: '',
revision: 0,
category: '',
defaultdir: 0,
offerid: 0,
buyout: false,
rentofferid: 0,
rentbuyout: false,
bc: false,
excludeddynamic: false,
furniline: '',
environment: '',
rare: false,
});
const [ confirmDelete, setConfirmDelete ] = useState(false);
@@ -67,6 +80,19 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
interactionType: item.interactionType || '',
interactionModesCount: item.interactionModesCount || 0,
customparams: item.customparams || '',
description: item.description || '',
revision: item.revision || 0,
category: item.category || '',
defaultdir: item.defaultdir || 0,
offerid: item.offerid || 0,
buyout: !!item.buyout,
rentofferid: item.rentofferid || 0,
rentbuyout: !!item.rentbuyout,
bc: !!item.bc,
excludeddynamic: !!item.excludeddynamic,
furniline: item.furniline || '',
environment: item.environment || '',
rare: !!item.rare,
});
setConfirmDelete(false);
@@ -77,20 +103,17 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
setForm(prev => ({ ...prev, [key]: value }));
}, []);
const handleSave = useCallback(async () =>
const handleSave = useCallback(() =>
{
const ok = await onUpdate(item.id, form);
onUpdate(item.id, form);
}, [ item, form, onUpdate ]);
if(ok) onRefresh(item.id);
}, [ item, form, onUpdate, onRefresh ]);
const handleDelete = useCallback(async () =>
const handleDelete = useCallback(() =>
{
if(!confirmDelete) return setConfirmDelete(true);
const ok = await onDelete(item.id);
if(ok) onBack();
onDelete(item.id);
onBack();
}, [ confirmDelete, item, onDelete, onBack ]);
const inputClass = 'form-control form-control-sm';
@@ -101,15 +124,9 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
<Flex gap={ 1 } alignItems="center" className="mb-1">
<Button variant="secondary" onClick={ onBack }>Back</Button>
<Flex alignItems="center" gap={ 1 } className="bg-[#e9ecef] px-2 py-0.5 rounded">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#1e7295]">
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
</svg>
<Text bold className="text-[12px]">{ item.id }</Text>
<Text bold className="text-[12px]">ID: { item.id }</Text>
<span className="text-[#999] mx-0.5">|</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#1e7295]">
<path d="M12.586 2.586a2 2 0 1 1 2.828 2.828l-3 3a2 2 0 0 1-2.828 0 1 1 0 0 0-1.414 1.414 4 4 0 0 0 5.656 0l3-3a4 4 0 0 0-5.656-5.656l-1.5 1.5a1 1 0 1 0 1.414 1.414l1.5-1.5ZM7.414 17.414a2 2 0 1 1-2.828-2.828l3-3a2 2 0 0 1 2.828 0 1 1 0 0 0 1.414-1.414 4 4 0 0 0-5.656 0l-3 3a4 4 0 0 0 5.656 5.656l1.5-1.5a1 1 0 1 0-1.414-1.414l-1.5 1.5Z" />
</svg>
<Text bold className="text-[12px]">{ item.spriteId }</Text>
<Text bold className="text-[12px]">Sprite: { item.spriteId }</Text>
</Flex>
<Text small variant="gray">({ item.usageCount } in use)</Text>
</Flex>
@@ -126,6 +143,10 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
<label className={ labelClass }>Public Name</label>
<input className={ inputClass } value={ form.publicName } onChange={ e => setField('publicName', e.target.value) } />
</div>
<div className="col-span-2">
<label className={ labelClass }>Description</label>
<textarea className={ inputClass } rows={ 2 } value={ form.description } onChange={ e => setField('description', e.target.value) } />
</div>
<div>
<label className={ labelClass }>Sprite ID</label>
<input type="number" className={ inputClass } value={ form.spriteId } onChange={ e => setField('spriteId', Number(e.target.value)) } />
@@ -143,7 +164,7 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
{ /* Dimensions */ }
<div className="bg-white rounded border border-[#ccc] p-2">
<Text small bold variant="primary" className="mb-1 block">Dimensions</Text>
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-4 gap-2">
<div>
<label className={ labelClass }>Width</label>
<input type="number" className={ inputClass } value={ form.width } onChange={ e => setField('width', Number(e.target.value)) } />
@@ -156,6 +177,10 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
<label className={ labelClass }>Stack Height</label>
<input type="number" step="0.01" className={ inputClass } value={ form.stackHeight } onChange={ e => setField('stackHeight', Number(e.target.value)) } />
</div>
<div>
<label className={ labelClass }>Default Dir</label>
<input type="number" className={ inputClass } value={ form.defaultdir } onChange={ e => setField('defaultdir', Number(e.target.value)) } />
</div>
</div>
</div>
@@ -201,6 +226,56 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
</div>
</div>
{ /* FurniData JSON */ }
<div className="bg-white rounded border border-[#ccc] p-2">
<Text small bold variant="primary" className="mb-1 block">FurniData.json</Text>
<div className="grid grid-cols-3 gap-2">
<div>
<label className={ labelClass }>Revision</label>
<input type="number" className={ inputClass } value={ form.revision } onChange={ e => setField('revision', Number(e.target.value)) } />
</div>
<div>
<label className={ labelClass }>Category</label>
<input className={ inputClass } value={ form.category } onChange={ e => setField('category', e.target.value) } />
</div>
<div>
<label className={ labelClass }>Offer ID</label>
<input type="number" className={ inputClass } value={ form.offerid } onChange={ e => setField('offerid', Number(e.target.value)) } />
</div>
<div>
<label className={ labelClass }>Rent Offer ID</label>
<input type="number" className={ inputClass } value={ form.rentofferid } onChange={ e => setField('rentofferid', Number(e.target.value)) } />
</div>
<div>
<label className={ labelClass }>Furniline</label>
<input className={ inputClass } value={ form.furniline } onChange={ e => setField('furniline', e.target.value) } />
</div>
<div>
<label className={ labelClass }>Environment</label>
<input className={ inputClass } value={ form.environment } onChange={ e => setField('environment', e.target.value) } />
</div>
</div>
<div className="grid grid-cols-4 gap-x-3 gap-y-1 mt-1">
{ [
['buyout', 'Buyout'],
['rentbuyout', 'Rent Buyout'],
['bc', 'BC'],
['excludeddynamic', 'Excl. Dynamic'],
['rare', 'Rare']
].map(([ key, label ]) => (
<label key={ key } className="flex items-center gap-1 text-[11px] cursor-pointer">
<input
type="checkbox"
className="form-check-input"
checked={ (form as any)[key] }
onChange={ e => setField(key, e.target.checked) }
/>
{ label }
</label>
)) }
</div>
</div>
{ /* Catalog References */ }
{ catalogItems.length > 0 &&
<div className="bg-white rounded border border-[#ccc] p-2">
@@ -216,21 +291,6 @@ export const FurniEditorEditView: FC<FurniEditorEditViewProps> = props =>
</div>
}
{ /* FurniData.json Entry */ }
{ furniDataEntry &&
<div className="bg-white rounded border border-[#ccc] p-2">
<Text small bold variant="primary" className="mb-1 block">FurniData.json</Text>
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[10px]">
{ Object.entries(furniDataEntry).map(([ key, value ]) => (
<div key={ key } className="flex justify-between bg-[#f5f5f5] px-2 py-0.5 rounded">
<span className="font-bold text-[#555]">{ key }</span>
<span className="text-[#333] truncate ml-1 max-w-[120px] text-right">{ String(value ?? '') }</span>
</div>
)) }
</div>
</div>
}
{ /* Actions */ }
<Flex gap={ 1 } justifyContent="between" className="mt-1">
<Button variant="success" disabled={ loading } onClick={ handleSave }>