forked from hazem/yslootahrobotics
feat: implement GLB file synchronization and add API for listing models
This commit is contained in:
parent
51671b85b8
commit
a501d94645
BIN
public/models/robot-doctor.glb
Normal file
BIN
public/models/robot-doctor.glb
Normal file
Binary file not shown.
@ -35,6 +35,8 @@ export default function AdminPage() {
|
|||||||
const [rowGlbFiles, setRowGlbFiles] = useState<Record<string, File | null>>({});
|
const [rowGlbFiles, setRowGlbFiles] = useState<Record<string, File | null>>({});
|
||||||
const [rowGlbUploading, setRowGlbUploading] = useState<Record<string, boolean>>({});
|
const [rowGlbUploading, setRowGlbUploading] = useState<Record<string, boolean>>({});
|
||||||
const [rowGlbError, setRowGlbError] = useState<Record<string, string>>({});
|
const [rowGlbError, setRowGlbError] = useState<Record<string, string>>({});
|
||||||
|
const [syncingGlbs, setSyncingGlbs] = useState(false);
|
||||||
|
const [syncResult, setSyncResult] = useState('');
|
||||||
|
|
||||||
useEffect(() => { pricingStore.getState().hydrate(); }, []);
|
useEffect(() => { pricingStore.getState().hydrate(); }, []);
|
||||||
|
|
||||||
@ -99,6 +101,49 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Reads all .glb files already on the server and links them to matching personas/items */
|
||||||
|
const handleSyncGlbs = async () => {
|
||||||
|
setSyncingGlbs(true);
|
||||||
|
setSyncResult('');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/list-models/');
|
||||||
|
const data = await res.json();
|
||||||
|
const models: { id: string; modelPath: string; filename: string }[] = data.models ?? [];
|
||||||
|
let count = 0;
|
||||||
|
models.forEach(({ id, modelPath }) => {
|
||||||
|
// Update pricing item if it exists
|
||||||
|
const item = pricingStore.getState().items.find((i) => i.id === id);
|
||||||
|
if (item && item.modelPath !== modelPath) {
|
||||||
|
pricingStore.getState().updateItem(id, { modelPath });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
// Update or create persona
|
||||||
|
const existing = personaStore.getState().personas.find((p) => p.id === id);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.modelPath !== modelPath) {
|
||||||
|
personaStore.getState().updatePersona(id, { modelPath });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
} else if (item) {
|
||||||
|
personaStore.getState().addPersona({
|
||||||
|
id,
|
||||||
|
label: item.label,
|
||||||
|
description: item.label,
|
||||||
|
colors: { torso: '#3b82f6', legs: '#3b82f6' },
|
||||||
|
modelPath,
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setSyncResult(count > 0 ? `✓ Synced ${models.length} model(s)` : `✓ Already up to date (${models.length} model(s))`);
|
||||||
|
setTimeout(() => setSyncResult(''), 3000);
|
||||||
|
} catch {
|
||||||
|
setSyncResult('Sync failed');
|
||||||
|
} finally {
|
||||||
|
setSyncingGlbs(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddItem = async () => {
|
const handleAddItem = async () => {
|
||||||
setAddItemError('');
|
setAddItemError('');
|
||||||
const id = newItem.id.trim().toLowerCase().replace(/_/g, '-').replace(/\s+/g, '-');
|
const id = newItem.id.trim().toLowerCase().replace(/_/g, '-').replace(/\s+/g, '-');
|
||||||
@ -480,7 +525,11 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{addItemError && <p style={errorTextStyle}>{addItemError}</p>}
|
{addItemError && <p style={errorTextStyle}>{addItemError}</p>}
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', justifyContent: 'flex-end', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
{syncResult && <span style={{ fontSize: '0.75rem', color: syncResult.startsWith('✓') ? '#16a34a' : '#dc2626', marginRight: 'auto' }}>{syncResult}</span>}
|
||||||
|
<button onClick={handleSyncGlbs} disabled={syncingGlbs} style={{ ...ghostBtnStyle, fontSize: '0.75rem' }} title="Link all .glb files in public/models/ to their matching items">
|
||||||
|
{syncingGlbs ? 'Syncing…' : '⟳ Sync GLBs'}
|
||||||
|
</button>
|
||||||
<button onClick={() => pricingStore.getState().resetPrices()} style={dangerBtnStyle}>Reset to Defaults</button>
|
<button onClick={() => pricingStore.getState().resetPrices()} style={dangerBtnStyle}>Reset to Defaults</button>
|
||||||
<button onClick={handleAddItem} disabled={addItemUploading} style={secondaryBtnStyle}>
|
<button onClick={handleAddItem} disabled={addItemUploading} style={secondaryBtnStyle}>
|
||||||
{addItemUploading ? 'Uploading…' : 'Add Item'}
|
{addItemUploading ? 'Uploading…' : 'Add Item'}
|
||||||
|
|||||||
39
src/app/api/admin/list-models/route.ts
Normal file
39
src/app/api/admin/list-models/route.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { jwtVerify } from 'jose';
|
||||||
|
import { readdir } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
async function verifyAdmin() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('admin_token')?.value;
|
||||||
|
if (!token) return false;
|
||||||
|
const jwtSecret = process.env.ADMIN_JWT_SECRET;
|
||||||
|
if (!jwtSecret) return false;
|
||||||
|
try {
|
||||||
|
await jwtVerify(token, new TextEncoder().encode(jwtSecret));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/admin/list-models/ → returns all .glb files in public/models/
|
||||||
|
export async function GET() {
|
||||||
|
if (!(await verifyAdmin())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
const modelsDir = path.resolve(process.cwd(), 'public', 'models');
|
||||||
|
try {
|
||||||
|
const files = await readdir(modelsDir);
|
||||||
|
const glbFiles = files
|
||||||
|
.filter((f) => f.toLowerCase().endsWith('.glb'))
|
||||||
|
.map((f) => ({
|
||||||
|
filename: f,
|
||||||
|
id: f.replace(/\.glb$/i, ''),
|
||||||
|
modelPath: `/models/${f}`,
|
||||||
|
}));
|
||||||
|
return NextResponse.json({ models: glbFiles });
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ models: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user