'use client'; import { useEffect, useRef, useState } from 'react'; import QRCode from 'qrcode'; import { pb } from '~/lib/pocketbase'; import { ImageOff, Upload, Link, X } from 'lucide-react'; type InputMode = 'url' | 'file'; export default function Dashboard() { const [name, setName] = useState(''); // GLB const [glbMode, setGlbMode] = useState('file'); const [glbUrl, setGlbUrl] = useState(''); const [glbFile, setGlbFile] = useState(null); const glbInputRef = useRef(null); // USDZ const [usdzMode, setUsdzMode] = useState('file'); const [usdzUrl, setUsdzUrl] = useState(''); const [usdzFile, setUsdzFile] = useState(null); const usdzInputRef = useRef(null); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const fetchAssets = async () => { try { const records = await pb.collection('ar_assets').getFullList({ sort: '-created', }); setAssets(records); } catch (err) { console.error('Failed to fetch assets:', err); setAssets([]); } }; useEffect(() => { fetchAssets(); }, []); const createAsset = async () => { const hasGlb = glbMode === 'file' ? !!glbFile : !!glbUrl.trim(); if (!name.trim() || !hasGlb) { alert('Please provide a name and a GLB file or URL'); return; } setLoading(true); try { // Build FormData so we can send files + fields in one request const formData = new FormData(); formData.append('name', name.trim()); if (glbMode === 'file' && glbFile) { formData.append('glb_file', glbFile); } else { formData.append('glb_url', glbUrl.trim()); } if (usdzMode === 'file' && usdzFile) { formData.append('usdz_file', usdzFile); } else if (usdzMode === 'url' && usdzUrl.trim()) { formData.append('usdz_url', usdzUrl.trim()); } const record = await pb.collection('ar_assets').create(formData); const qrUrl = `${import.meta.env.VITE_FRONTEND_URL}/ar/${record.id}`; const qrImage = await QRCode.toDataURL(qrUrl, { width: 512, margin: 2, }); const qrBlob = await (await fetch(qrImage)).blob(); const qrFormData = new FormData(); qrFormData.append('qr_url', qrUrl); qrFormData.append('qr_image', new File([qrBlob], `${record.id}.png`, { type: 'image/png' })); await pb.collection('ar_assets').update(record.id, qrFormData); // Reset all fields setName(''); setGlbUrl(''); setGlbFile(null); setUsdzUrl(''); setUsdzFile(null); if (glbInputRef.current) glbInputRef.current.value = ''; if (usdzInputRef.current) usdzInputRef.current.value = ''; fetchAssets(); } catch (err: any) { alert(err?.message || 'Failed to create asset'); } finally { setLoading(false); } }; return (

AR Asset Dashboard

Manage 3D assets and generate AR QR codes

Create New Asset

{/* Asset Name */}
setName(e.target.value)} />
{/* GLB — Android / Web */} {/* USDZ — iOS */}

Generated Assets

{assets.length === 0 ? (

No assets created yet.

) : (
{assets.map((asset) => (

{asset?.name}

{asset?.qr_image ? ( {asset?.name} ) : ( asset?.qr_file ? ( {asset?.name} ) :
No QR Image
)}
))}
)}
); } // ─── Reusable File-or-URL field ─────────────────────────────────────────────── interface FileOrUrlFieldProps { label: string; required?: boolean; mode: InputMode; onModeChange: (m: InputMode) => void; accept: string; file: File | null; onFileChange: (f: File | null) => void; inputRef: React.RefObject; url: string; onUrlChange: (u: string) => void; urlPlaceholder: string; } function FileOrUrlField({ label, required, mode, onModeChange, accept, file, onFileChange, inputRef, url, onUrlChange, urlPlaceholder, }: FileOrUrlFieldProps) { return (
{/* Label + toggle */}
{mode === 'file' ? (
inputRef.current?.click()} className={`flex cursor-pointer items-center gap-3 rounded-lg border-2 border-dashed px-4 py-3 transition-colors ${ file ? 'border-black bg-gray-50' : 'border-gray-200 hover:border-gray-400' }`} > onFileChange(e.target.files?.[0] ?? null)} /> {file ? ( <>
{file.name} {(file.size / 1024 / 1024).toFixed(1)} MB
) : (
Click to choose a file ({accept})
)}
) : ( onUrlChange(e.target.value)} /> )}
); }