Compare commits

..

No commits in common. "721b9960c831b4f6cd732141f3e5275218a5908c" and "cb588b5caa621906a7cfc703bf6781f279ca20e2" have entirely different histories.

2 changed files with 36 additions and 197 deletions

View File

@ -1,7 +1,3 @@
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
const rawUrl = import.meta.env.VITE_POCKETBASE_URL ?? ''; export const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
// Ensure URL always has a protocol so PocketBase SDK treats it as absolute
const pbUrl = rawUrl.startsWith('http') ? rawUrl : `https://${rawUrl}`;
export const pb = new PocketBase(pbUrl);

View File

@ -1,27 +1,14 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import { pb } from '~/lib/pocketbase'; import { pb } from '~/lib/pocketbase';
import { ImageOff, Upload, Link, X } from 'lucide-react'; import { ImageOff } from 'lucide-react';
type InputMode = 'url' | 'file';
export default function Dashboard() { export default function Dashboard() {
const [name, setName] = useState(''); const [name, setName] = useState('');
// GLB
const [glbMode, setGlbMode] = useState<InputMode>('file');
const [glbUrl, setGlbUrl] = useState(''); const [glbUrl, setGlbUrl] = useState('');
const [glbFile, setGlbFile] = useState<File | null>(null);
const glbInputRef = useRef<HTMLInputElement>(null);
// USDZ
const [usdzMode, setUsdzMode] = useState<InputMode>('file');
const [usdzUrl, setUsdzUrl] = useState(''); const [usdzUrl, setUsdzUrl] = useState('');
const [usdzFile, setUsdzFile] = useState<File | null>(null);
const usdzInputRef = useRef<HTMLInputElement>(null);
const [assets, setAssets] = useState<any[]>([]); const [assets, setAssets] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -42,32 +29,19 @@ export default function Dashboard() {
}, []); }, []);
const createAsset = async () => { const createAsset = async () => {
const hasGlb = glbMode === 'file' ? !!glbFile : !!glbUrl.trim(); if (!name || !glbUrl) {
if (!name.trim() || !hasGlb) { alert('Please provide a name and a GLB URL');
alert('Please provide a name and a GLB file or URL');
return; return;
} }
setLoading(true); setLoading(true);
try { try {
// Build FormData so we can send files + fields in one request const record = await pb.collection('ar_assets').create({
const formData = new FormData(); name,
formData.append('name', name.trim()); glb_url: glbUrl,
usdz_url: usdzUrl || '',
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 qrUrl = `${import.meta.env.VITE_FRONTEND_URL}/ar/${record.id}`;
@ -84,14 +58,9 @@ export default function Dashboard() {
await pb.collection('ar_assets').update(record.id, qrFormData); await pb.collection('ar_assets').update(record.id, qrFormData);
// Reset all fields
setName(''); setName('');
setGlbUrl(''); setGlbUrl('');
setGlbFile(null);
setUsdzUrl(''); setUsdzUrl('');
setUsdzFile(null);
if (glbInputRef.current) glbInputRef.current.value = '';
if (usdzInputRef.current) usdzInputRef.current.value = '';
fetchAssets(); fetchAssets();
} catch (err: any) { } catch (err: any) {
@ -114,14 +83,13 @@ export default function Dashboard() {
</div> </div>
<div className='rounded-2xl bg-white p-6 shadow-sm'> <div className='rounded-2xl bg-white p-6 shadow-sm'>
<h2 className='mb-5 text-lg font-semibold text-gray-800'> <h2 className='mb-4 text-lg font-semibold text-gray-800'>
Create New Asset Create New Asset
</h2> </h2>
<div className='space-y-5 text-black'> <div className='grid gap-4 md:grid-cols-3 text-black'>
{/* Asset Name */} <div className="space-y-1">
<div className='space-y-1'> <label className="text-xs font-semibold text-gray-600 ml-1">Asset Name</label>
<label className='text-xs font-semibold text-gray-600 ml-1'>Asset Name</label>
<input <input
className='w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none' className='w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder='Asset name' placeholder='Asset name'
@ -129,48 +97,34 @@ export default function Dashboard() {
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
</div> </div>
<div className="space-y-1">
{/* GLB — Android / Web */} <label className="text-xs font-semibold text-gray-600 ml-1">GLB URL (Android/Web)</label>
<FileOrUrlField <input
label='GLB File (Android / Web)' type="url"
required className='w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
mode={glbMode} placeholder='https://example.com/model.glb'
onModeChange={setGlbMode} value={glbUrl}
accept='.glb' onChange={(e) => setGlbUrl(e.target.value)}
file={glbFile} />
onFileChange={setGlbFile} </div>
inputRef={glbInputRef} <div className="space-y-1">
url={glbUrl} <label className="text-xs font-semibold text-gray-600 ml-1">USDZ URL (iOS) - Optional</label>
onUrlChange={setGlbUrl} <input
urlPlaceholder='https://example.com/model.glb' type="url"
/> className='w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder='USDZ URL'
{/* USDZ — iOS */} value={usdzUrl}
<FileOrUrlField onChange={(e) => setUsdzUrl(e.target.value)}
label='USDZ File (iOS) — Optional' />
mode={usdzMode} </div>
onModeChange={setUsdzMode}
accept='.usdz,.reality'
file={usdzFile}
onFileChange={setUsdzFile}
inputRef={usdzInputRef}
url={usdzUrl}
onUrlChange={setUsdzUrl}
urlPlaceholder='https://example.com/model.usdz'
/>
</div> </div>
<button <button
onClick={createAsset} onClick={createAsset}
disabled={loading} disabled={loading}
className='mt-6 inline-flex items-center justify-center gap-2 rounded-xl bg-black px-6 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60' className='mt-6 inline-flex items-center justify-center rounded-xl bg-black px-6 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60'
> >
{loading ? ( {loading ? 'Creating…' : 'Create Asset'}
<>
<div className='w-3.5 h-3.5 rounded-full border-2 border-white/30 border-t-white animate-spin' />
Creating
</>
) : 'Create Asset'}
</button> </button>
</div> </div>
@ -222,114 +176,3 @@ export default function Dashboard() {
); );
} }
// ─── 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<HTMLInputElement | null>;
url: string;
onUrlChange: (u: string) => void;
urlPlaceholder: string;
}
function FileOrUrlField({
label, required, mode, onModeChange,
accept, file, onFileChange, inputRef,
url, onUrlChange, urlPlaceholder,
}: FileOrUrlFieldProps) {
return (
<div className='space-y-1.5'>
{/* Label + toggle */}
<div className='flex items-center justify-between ml-1'>
<label className='text-xs font-semibold text-gray-600'>
{label}{required && <span className='text-red-500 ml-0.5'>*</span>}
</label>
<div className='flex items-center gap-1 rounded-lg border border-gray-200 p-0.5 text-xs'>
<button
type='button'
onClick={() => onModeChange('file')}
className={`flex items-center gap-1 rounded-md px-2.5 py-1 transition-all font-medium ${
mode === 'file'
? 'bg-black text-white'
: 'text-gray-500 hover:text-gray-800'
}`}
>
<Upload size={11} />
Upload
</button>
<button
type='button'
onClick={() => onModeChange('url')}
className={`flex items-center gap-1 rounded-md px-2.5 py-1 transition-all font-medium ${
mode === 'url'
? 'bg-black text-white'
: 'text-gray-500 hover:text-gray-800'
}`}
>
<Link size={11} />
URL
</button>
</div>
</div>
{mode === 'file' ? (
<div
onClick={() => 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'
}`}
>
<input
ref={inputRef}
type='file'
accept={accept}
className='hidden'
onChange={(e) => onFileChange(e.target.files?.[0] ?? null)}
/>
{file ? (
<>
<div className='flex flex-1 items-center gap-2 min-w-0'>
<div className='w-7 h-7 rounded bg-black flex items-center justify-center shrink-0'>
<Upload size={13} className='text-white' />
</div>
<span className='text-sm text-gray-800 font-medium truncate'>{file.name}</span>
<span className='text-xs text-gray-400 shrink-0'>
{(file.size / 1024 / 1024).toFixed(1)} MB
</span>
</div>
<button
type='button'
onClick={(e) => {
e.stopPropagation();
onFileChange(null);
if (inputRef.current) inputRef.current.value = '';
}}
className='shrink-0 text-gray-400 hover:text-gray-700'
>
<X size={15} />
</button>
</>
) : (
<div className='flex flex-1 items-center gap-3 text-gray-400'>
<Upload size={16} />
<span className='text-sm'>Click to choose a file <span className='text-gray-300'>({accept})</span></span>
</div>
)}
</div>
) : (
<input
type='url'
className='w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder={urlPlaceholder}
value={url}
onChange={(e) => onUrlChange(e.target.value)}
/>
)}
</div>
);
}