- Changed wrapper settings in open-next.config.ts to use "cloudflare-node" and "cloudflare-edge". - Updated main entry point in wrangler.toml to point to ".open-next/worker.js". - Modified middleware to allow access to the speakers project path. - Added Space Grotesk font to layout.tsx for enhanced typography. - Improved media route handling by resolving parameters correctly in route.ts. - Adjusted ServiceCard component to use a more specific type for icon handling.
173 lines
5.6 KiB
TypeScript
173 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
interface PolycamEmbedProps {
|
|
captureId: string;
|
|
title: string;
|
|
index?: number;
|
|
}
|
|
|
|
export function PolycamEmbed({ captureId, title, index = 0 }: PolycamEmbedProps) {
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [hasError, setHasError] = useState(false);
|
|
const [isDesktop, setIsDesktop] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const checkDesktop = () => {
|
|
setIsDesktop(window.innerWidth >= 1024);
|
|
};
|
|
|
|
checkDesktop();
|
|
window.addEventListener('resize', checkDesktop);
|
|
return () => window.removeEventListener('resize', checkDesktop);
|
|
}, []);
|
|
|
|
const itemVariants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
y: 20,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
},
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
variants={itemVariants}
|
|
transition={{ duration: 0.4, ease: 'easeOut', delay: index * 0.1 }}
|
|
whileHover={{
|
|
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.6)',
|
|
transition: { duration: 0.2, ease: 'easeOut' },
|
|
}}
|
|
className="relative bg-[#1a1a1a] rounded-lg overflow-hidden p-4"
|
|
>
|
|
{/* Regular Embed */}
|
|
<div
|
|
className="relative overflow-hidden rounded-lg bg-black/40"
|
|
style={{ aspectRatio: isDesktop ? '16 / 10' : '5 / 4' }}
|
|
>
|
|
{/* Loading Skeleton */}
|
|
{isLoading && !hasError && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent animate-pulse" />
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{hasError && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-center">
|
|
<div>
|
|
<p className="text-gray-400 text-sm mb-2">Failed to load 3D scan</p>
|
|
<p className="text-gray-600 text-xs">Please try again later</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<iframe
|
|
src={`https://poly.cam/capture/${captureId}/embed`}
|
|
title={title}
|
|
frameBorder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
className="w-full h-full"
|
|
onLoad={() => setIsLoading(false)}
|
|
onError={() => {
|
|
setIsLoading(false);
|
|
setHasError(true);
|
|
}}
|
|
aria-label={`3D scan viewer: ${title}`}
|
|
/>
|
|
|
|
{/* Fullscreen Button */}
|
|
<motion.button
|
|
onClick={() => setIsFullscreen(true)}
|
|
className="absolute top-4 left-4 p-2 rounded-lg bg-black/70 hover:bg-black/85 transition-colors z-10"
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
aria-label="Open fullscreen view"
|
|
>
|
|
<svg
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="#ff4d00"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
|
</svg>
|
|
</motion.button>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
{title && (
|
|
<div className="mt-4">
|
|
<h2 className="text-lg md:text-xl font-semibold text-white">{title}</h2>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
|
|
{/* Fullscreen Modal */}
|
|
<AnimatePresence>
|
|
{isFullscreen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 sm:p-8 md:p-16 lg:p-20"
|
|
onClick={() => setIsFullscreen(false)}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Fullscreen 3D scan viewer"
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="relative w-full h-full"
|
|
style={{ aspectRatio: isDesktop ? '16 / 10' : '5 / 4' }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<iframe
|
|
src={`https://poly.cam/capture/${captureId}/embed`}
|
|
title={title}
|
|
frameBorder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
className="w-full h-full rounded-lg"
|
|
/>
|
|
|
|
{/* Close Button */}
|
|
<motion.button
|
|
onClick={() => setIsFullscreen(false)}
|
|
className="absolute top-4 left-4 p-2 rounded-lg bg-black/70 hover:bg-black/85 transition-colors"
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
aria-label="Close fullscreen view"
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="#ff4d00"
|
|
strokeWidth="2"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</motion.button>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|