Nicholai b1feda521c Update configuration and middleware for Cloudflare integration; add Space Grotesk font and improve media route handling
- 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.
2025-11-18 13:37:20 -07:00

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>
</>
);
}