refactor: improve the ar view page ui with button and loader and replace the use of supabase to pocketbase
This commit is contained in:
parent
e9b20504f4
commit
15a7cf3ac4
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { supabase } from '~/lib/supabase';
|
import { Box } from 'lucide-react';
|
||||||
|
import { pb } from '~/lib/pocketbase';
|
||||||
|
|
||||||
// --- A-FRAME/AR.JS CUSTOM ELEMENTS TYPING FIX ---
|
// --- A-FRAME/AR.JS CUSTOM ELEMENTS TYPING FIX ---
|
||||||
declare module 'react' {
|
declare module 'react' {
|
||||||
@ -47,30 +47,31 @@ declare module 'react' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AR_HINTS = [
|
||||||
|
'Arranging models to view…',
|
||||||
|
'Find a flat surface & step back…',
|
||||||
|
'Keep your camera steady…',
|
||||||
|
'Almost ready, clear some space…',
|
||||||
|
'Preparing your AR experience…',
|
||||||
|
];
|
||||||
|
|
||||||
export default function ARViewer() {
|
export default function ARViewer() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const modelViewerRef = useRef<any>(null);
|
const modelViewerRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
const [asset, setAsset] = useState<any>(null);
|
const [asset, setAsset] = useState<any>(null);
|
||||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||||
|
const [arLoading, setArLoading] = useState(false);
|
||||||
|
const [showHints, setShowHints] = useState(false);
|
||||||
|
const [hintIndex, setHintIndex] = useState(0);
|
||||||
|
const hintIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const hintDelayRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
const loadAsset = async () => {
|
const loadAsset = async () => {
|
||||||
const { data, error } = await supabase
|
const data = await pb.collection('ar_assets').getOne(id);
|
||||||
.from('ar_assets')
|
if (data) setAsset(data);
|
||||||
.select('*')
|
|
||||||
.eq('id', id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
console.log('Supabase response:', { data, error });
|
|
||||||
|
|
||||||
if (!error && data) {
|
|
||||||
setAsset(data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadAsset();
|
loadAsset();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
@ -79,55 +80,140 @@ export default function ARViewer() {
|
|||||||
setScriptLoaded(true);
|
setScriptLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.id = 'model-viewer-script';
|
script.id = 'model-viewer-script';
|
||||||
script.type = 'module';
|
script.type = 'module';
|
||||||
script.src =
|
script.src = 'https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js';
|
||||||
'https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js';
|
script.onload = () => setScriptLoaded(true);
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
console.log('model-viewer loaded');
|
|
||||||
setScriptLoaded(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!scriptLoaded) {
|
const handleARClick = () => {
|
||||||
|
const mv = modelViewerRef.current as any;
|
||||||
|
if (!mv?.activateAR || arLoading) return;
|
||||||
|
|
||||||
|
setArLoading(true);
|
||||||
|
setShowHints(false);
|
||||||
|
setHintIndex(0);
|
||||||
|
mv.activateAR();
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
setArLoading(false);
|
||||||
|
setShowHints(false);
|
||||||
|
if (hintIntervalRef.current) clearInterval(hintIntervalRef.current);
|
||||||
|
if (hintDelayRef.current) clearTimeout(hintDelayRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show hints if AR is still loading after 1.5s
|
||||||
|
hintDelayRef.current = setTimeout(() => {
|
||||||
|
setShowHints(true);
|
||||||
|
hintIntervalRef.current = setInterval(() => {
|
||||||
|
setHintIndex((prev) => (prev + 1) % AR_HINTS.length);
|
||||||
|
}, 2000);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
const onArStatus = (e: any) => {
|
||||||
|
const { status } = e.detail ?? {};
|
||||||
|
if (
|
||||||
|
status === 'session-started' ||
|
||||||
|
status === 'object-placed' ||
|
||||||
|
status === 'failed' ||
|
||||||
|
status === 'not-presenting'
|
||||||
|
) {
|
||||||
|
cleanup();
|
||||||
|
mv.removeEventListener('ar-status', onArStatus);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mv.addEventListener('ar-status', onArStatus);
|
||||||
|
|
||||||
|
// Fallback reset after 15s
|
||||||
|
setTimeout(cleanup, 15000);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!scriptLoaded || !asset) {
|
||||||
return (
|
return (
|
||||||
<pre style={{ padding: 20 }}>
|
<div className="fixed inset-0 bg-black flex flex-col items-center justify-center">
|
||||||
Loading AR Viewer…
|
<div className="w-10 h-10 rounded-full border-2 border-orange-500/20 border-t-orange-500 animate-spin mb-4" />
|
||||||
{'\n\n'}
|
<p className="text-white/40 text-xs tracking-widest uppercase">Loading AR Experience…</p>
|
||||||
asset: {JSON.stringify(asset, null, 2)}
|
</div>
|
||||||
{'\n'}
|
|
||||||
scriptLoaded: {String(scriptLoaded)}
|
|
||||||
{'\n'}
|
|
||||||
id: {String(id)}
|
|
||||||
</pre>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<model-viewer
|
<div className="fixed inset-0 bg-black flex flex-col overflow-hidden">
|
||||||
ref={modelViewerRef}
|
<style>{`
|
||||||
src={asset?.glb_url}
|
model-viewer::part(default-ar-button) { display: none !important; }
|
||||||
ios-src={asset?.usdz_url}
|
`}</style>
|
||||||
ar
|
|
||||||
ar-modes='webxr scene-viewer quick-look'
|
<model-viewer
|
||||||
camera-controls
|
ref={modelViewerRef}
|
||||||
auto-rotate
|
src={glbUrl}
|
||||||
style={{ width: '100vw', height: '80vh' }}
|
ios-src={usdzUrl}
|
||||||
onError={(e) => {
|
ar
|
||||||
console.error('Model viewer error:', e);
|
ar-modes="webxr scene-viewer quick-look"
|
||||||
console.error('Failed to load:', asset?.glb_url);
|
ar-scale="auto"
|
||||||
}}
|
ar-placement="floor"
|
||||||
onLoad={() => console.log('Model loaded successfully')}
|
camera-controls
|
||||||
/>
|
touch-action="none"
|
||||||
// <div className='bg-white text-2xl text-red-400'>
|
interaction-prompt="none"
|
||||||
// Id : {asset?.id}
|
shadow-intensity="1"
|
||||||
// AR Viewer for asset: {asset?.name}
|
shadow-softness="0.8"
|
||||||
// glb URL: {asset?.glb_url}
|
exposure="1"
|
||||||
// usdz URL: {asset?.usdz_url}
|
className="w-full flex-1 min-h-0 block"
|
||||||
// </div>
|
onError={(e: any) => {
|
||||||
|
console.error('Model viewer error:', e);
|
||||||
|
console.error('Failed to load:', glbUrl);
|
||||||
|
}}
|
||||||
|
onLoad={() => console.log('Model loaded successfully')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute top-0 left-0 right-0 flex items-start justify-between px-5 pt-5 pb-5 pointer-events-none">
|
||||||
|
{asset?.name && (
|
||||||
|
<span className="text-white text-sm font-bold tracking-widest uppercase">
|
||||||
|
{asset.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="pointer-events-auto w-9 h-9 bg-black border border-orange-500 rounded-lg flex items-center justify-center shrink-0 ml-3">
|
||||||
|
<span className="text-orange-500 font-black text-sm leading-none">t.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-end px-5 pt-5 pb-6">
|
||||||
|
<button
|
||||||
|
onClick={handleARClick}
|
||||||
|
disabled={arLoading}
|
||||||
|
className={`flex items-center gap-2 text-black font-bold text-xs tracking-widest uppercase px-5 py-3 rounded-lg transition-all duration-150 shadow-[0_0_24px_rgba(249,115,22,0.4)]
|
||||||
|
${arLoading
|
||||||
|
? 'bg-orange-500/80 cursor-not-allowed'
|
||||||
|
: 'bg-orange-500 hover:bg-orange-400 active:scale-95'
|
||||||
|
} ${showHints ? 'min-w-[220px] justify-center' : ''}`}
|
||||||
|
>
|
||||||
|
{arLoading && showHints ? (
|
||||||
|
<>
|
||||||
|
<div className="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin shrink-0" />
|
||||||
|
<span className="truncate">{AR_HINTS[hintIndex]}</span>
|
||||||
|
</>
|
||||||
|
) : arLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin shrink-0" />
|
||||||
|
View in AR
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box size={16} />
|
||||||
|
View in AR
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user