mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat: catalog style toggle (classic/new) with admin mode & favorites
This commit is contained in:
@@ -5,14 +5,13 @@ interface FurniEditorCreateViewProps
|
||||
{
|
||||
interactions: string[];
|
||||
loading: boolean;
|
||||
onCreate: (fields: Record<string, unknown>) => Promise<number | null>;
|
||||
onCreated: (id: number) => void;
|
||||
onCreate: (fields: Record<string, unknown>) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
{
|
||||
const { interactions, loading, onCreate, onCreated } = props;
|
||||
const [ success, setSuccess ] = useState<number | null>(null);
|
||||
const { interactions, loading, onCreate, onBack } = props;
|
||||
|
||||
const [ form, setForm ] = useState({
|
||||
itemName: '',
|
||||
@@ -34,37 +33,42 @@ export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
interactionType: '',
|
||||
interactionModesCount: 1,
|
||||
customparams: '',
|
||||
description: '',
|
||||
revision: 0,
|
||||
category: '',
|
||||
defaultdir: 0,
|
||||
offerid: 0,
|
||||
buyout: false,
|
||||
rentofferid: 0,
|
||||
rentbuyout: false,
|
||||
bc: false,
|
||||
excludeddynamic: false,
|
||||
furniline: '',
|
||||
environment: '',
|
||||
rare: false,
|
||||
});
|
||||
|
||||
const setField = useCallback((key: string, value: unknown) =>
|
||||
{
|
||||
setForm(prev => ({ ...prev, [key]: value }));
|
||||
setSuccess(null);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(async () =>
|
||||
const handleCreate = useCallback(() =>
|
||||
{
|
||||
if(!form.itemName || !form.publicName) return;
|
||||
|
||||
const id = await onCreate(form);
|
||||
|
||||
if(id)
|
||||
{
|
||||
setSuccess(id);
|
||||
setTimeout(() => onCreated(id), 1000);
|
||||
}
|
||||
}, [ form, onCreate, onCreated ]);
|
||||
onCreate(form);
|
||||
}, [ form, onCreate ]);
|
||||
|
||||
const inputClass = 'form-control form-control-sm';
|
||||
const labelClass = 'text-[11px] font-bold text-[#333] mb-0';
|
||||
|
||||
return (
|
||||
<Column gap={ 1 } className="h-full overflow-auto">
|
||||
{ success &&
|
||||
<div className="bg-[#d4edda] border border-[#c3e6cb] rounded p-2 text-[#155724] text-xs">
|
||||
Item created with ID #{ success }!
|
||||
</div>
|
||||
}
|
||||
<Flex gap={ 1 } alignItems="center" className="mb-1">
|
||||
<Button variant="secondary" onClick={ onBack }>Back</Button>
|
||||
<Text bold className="text-[14px]">Create New Item</Text>
|
||||
</Flex>
|
||||
|
||||
<div className="bg-white rounded border border-[#ccc] p-2">
|
||||
<Text small bold variant="primary" className="mb-1 block">Basic Info</Text>
|
||||
@@ -77,6 +81,10 @@ export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
<label className={ labelClass }>Public Name *</label>
|
||||
<input className={ inputClass } value={ form.publicName } onChange={ e => setField('publicName', e.target.value) } placeholder="My Custom Furni" />
|
||||
</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)) } />
|
||||
@@ -93,7 +101,7 @@ export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
|
||||
<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)) } />
|
||||
@@ -106,6 +114,10 @@ export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = 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>
|
||||
|
||||
@@ -149,6 +161,55 @@ export const FurniEditorCreateView: FC<FurniEditorCreateViewProps> = props =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<Flex className="mt-1">
|
||||
<Button variant="success" disabled={ loading || !form.itemName || !form.publicName } onClick={ handleCreate }>
|
||||
{ loading ? 'Creating...' : 'Create Item' }
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
Reference in New Issue
Block a user