forked from rajun/ar-test
192 lines
9.6 KiB
TypeScript
192 lines
9.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import { useLoaderData, Link, redirect, useNavigate } from 'react-router';
|
|
import type { Route } from './+types/asset-preview';
|
|
import { pb } from '~/lib/pocketbase';
|
|
import { Button } from '~/components/ui/button';
|
|
import { Badge } from '~/components/ui/badge';
|
|
import { Card } from '~/components/ui/card';
|
|
import { Download, ImageOff, FileCode, ArrowLeft, Box } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
|
if (!pb.authStore.isValid) {
|
|
return redirect('/login');
|
|
}
|
|
try {
|
|
const asset = await pb.collection('ar_assets').getOne(params.id);
|
|
return { asset, error: null };
|
|
} catch (err: any) {
|
|
console.error('Failed to load asset in clientLoader:', err);
|
|
return { asset: null, error: err?.message || 'Failed to fetch asset details' };
|
|
}
|
|
}
|
|
|
|
export default function Preview() {
|
|
const { asset, error } = useLoaderData<typeof clientLoader>();
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
if (error || !asset) {
|
|
toast.error(error || 'Asset not found');
|
|
navigate('/', { replace: true });
|
|
}
|
|
}, [error, asset, navigate]);
|
|
|
|
if (!asset) return null;
|
|
|
|
const glbUrl = asset.glb_file
|
|
? pb.files.getURL(asset, asset.glb_file)
|
|
: asset.glb_url;
|
|
const usdzUrl = asset.usdz_file
|
|
? pb.files.getURL(asset, asset.usdz_file)
|
|
: asset.usdz_url;
|
|
|
|
const downloadQR = async () => {
|
|
const qrUrl = asset.qr_image
|
|
? pb.files.getURL(asset, asset.qr_image)
|
|
: asset.qr_file
|
|
? pb.files.getURL(asset, asset.qr_file)
|
|
: null;
|
|
if (!qrUrl) {
|
|
toast.error('No QR code image found for this asset');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(qrUrl);
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${asset.name.replace(/\s+/g, '_')}_qr.png`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
toast.success('QR Code downloaded successfully');
|
|
} catch (err) {
|
|
console.error('Failed to download QR code:', err);
|
|
toast.error('Failed to download QR code');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0D0D0D] text-white px-4 py-8 md:p-10 font-sans selection:bg-orange-500/20">
|
|
<div className="mx-auto max-w-5xl space-y-6">
|
|
<div className="flex items-center justify-between pb-4 border-b border-neutral-900">
|
|
<Link
|
|
to="/"
|
|
className="inline-flex items-center gap-2 text-xs font-bold text-neutral-400 hover:text-orange-500 transition-colors uppercase tracking-wider cursor-pointer"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
<span>Back to Dashboard</span>
|
|
</Link>
|
|
<div className="w-8 h-8 bg-black border border-neutral-850 rounded-xl flex items-center justify-center shrink-0">
|
|
<span className="text-orange-500 font-extrabold text-lg pl-0.5 select-none">t.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="flex flex-col lg:flex-row gap-6 overflow-hidden shadow-2xl border-neutral-900">
|
|
<div className="flex-grow lg:flex-1 min-h-[350px] sm:min-h-[450px] lg:min-h-[600px] bg-black relative rounded-t-2xl lg:rounded-l-2xl lg:rounded-tr-none overflow-hidden">
|
|
{glbUrl ? (
|
|
<model-viewer
|
|
src={glbUrl}
|
|
ios-src={usdzUrl || undefined}
|
|
ar
|
|
ar-modes="webxr scene-viewer quick-look"
|
|
camera-controls
|
|
auto-rotate
|
|
shadow-intensity="1"
|
|
shadow-softness="0.8"
|
|
exposure="1.1"
|
|
style={{ width: '100%', height: '100%', display: 'block', position: 'absolute', top: 0, left: 0 }}
|
|
className="w-full h-full"
|
|
alt={asset.name}
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex flex-col items-center justify-center text-neutral-500 p-8">
|
|
<FileCode size={40} className="mb-2 text-neutral-600 animate-pulse" />
|
|
<span className="text-xs uppercase tracking-widest text-neutral-600">Initializing 3D viewport…</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full lg:w-[360px] bg-[#121212] p-6 sm:p-8 flex flex-col justify-between shrink-0 border-t lg:border-t-0 lg:border-l border-neutral-850">
|
|
<div className="space-y-6">
|
|
<div>
|
|
<Badge variant="orange" className="mb-2">Asset Details</Badge>
|
|
<h3 className="text-xl font-bold text-white leading-snug">{asset.name}</h3>
|
|
</div>
|
|
|
|
<div className="bg-white p-4 rounded-xl flex items-center justify-center max-w-[210px] mx-auto shadow-md border border-neutral-200">
|
|
{asset.qr_image ? (
|
|
<img
|
|
src={pb.files.getURL(asset, asset.qr_image)}
|
|
alt={asset.name}
|
|
className="w-full aspect-square rounded-lg object-contain"
|
|
/>
|
|
) : asset.qr_file ? (
|
|
<img
|
|
src={pb.files.getURL(asset, asset.qr_file)}
|
|
alt={asset.name}
|
|
className="w-full aspect-square rounded-lg object-contain"
|
|
/>
|
|
) : (
|
|
<div className="flex aspect-square w-full flex-col items-center justify-center text-neutral-400 bg-neutral-100 rounded-lg p-4">
|
|
<ImageOff size={32} />
|
|
<span className="text-[10px] mt-1 font-semibold uppercase">No QR code</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-xs text-neutral-400 text-center leading-relaxed px-2">
|
|
Scan this QR code with a smartphone camera to instantly project the 3D model in your space using AR.
|
|
</p>
|
|
|
|
<div className="border-t border-neutral-850 pt-5 space-y-3">
|
|
<div>
|
|
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-wider">GLB File Target</span>
|
|
<div className="text-xs font-mono text-neutral-300 truncate mt-1 bg-neutral-900 px-3 py-2 rounded-lg border border-neutral-850 select-all">
|
|
{glbUrl}
|
|
</div>
|
|
</div>
|
|
{usdzUrl && (
|
|
<div>
|
|
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-wider">USDZ File Target</span>
|
|
<div className="text-xs font-mono text-neutral-300 truncate mt-1 bg-neutral-900 px-3 py-2 rounded-lg border border-neutral-850 select-all">
|
|
{usdzUrl}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3 mt-8">
|
|
<Button
|
|
onClick={downloadQR}
|
|
variant="outline"
|
|
className="w-full py-6 text-xs uppercase tracking-wider"
|
|
>
|
|
<Download size={15} className="text-orange-500" />
|
|
<span>Download QR Code</span>
|
|
</Button>
|
|
<a
|
|
href={`${import.meta.env.VITE_FRONTEND_URL}/ar/${asset.id}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-orange-500 hover:bg-orange-400 text-black transition-all font-bold text-xs uppercase tracking-wider shadow-[0_0_15px_rgba(249,115,22,0.15)]"
|
|
>
|
|
<Box size={14} />
|
|
<span>Launch AR View</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|