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.
This commit is contained in:
parent
a76e20e91f
commit
b1feda521c
0
.claude/design-system-architect.md
Normal file
0
.claude/design-system-architect.md
Normal file
3
.cursorindexingignore
Normal file
3
.cursorindexingignore
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
4
.specstory/.gitignore
vendored
Normal file
4
.specstory/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# SpecStory project identity file
|
||||
/.project.json
|
||||
# SpecStory explanation file
|
||||
/.what-is-this.md
|
||||
1526
.specstory/history/2025-10-23_11-06Z-generate-cursor-rules.md
Normal file
1526
.specstory/history/2025-10-23_11-06Z-generate-cursor-rules.md
Normal file
File diff suppressed because it is too large
Load Diff
272
design.json
Normal file
272
design.json
Normal file
@ -0,0 +1,272 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"name": "BIOHAZARD VFX Website Design System",
|
||||
"description": "Design system for the BIOHAZARD VFX website based on Temp-Placeholder component",
|
||||
"colorPalette": {
|
||||
"background": {
|
||||
"primary": "#000000",
|
||||
"secondary": "#0a0a0a",
|
||||
"description": "Primary black backgrounds with very dark secondary"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#ffffff",
|
||||
"secondary": "#e5e5e5",
|
||||
"muted": "#a3a3a3",
|
||||
"subtle": "#808080",
|
||||
"verySubtle": "#606060",
|
||||
"description": "White primary text with decreasing opacity gray variants"
|
||||
},
|
||||
"accent": {
|
||||
"primary": "#ff4d00",
|
||||
"description": "Orange accent color used for interactive elements, links, and highlights"
|
||||
},
|
||||
"borders": {
|
||||
"subtle": "rgba(255, 255, 255, 0.05)",
|
||||
"muted": "rgba(255, 255, 255, 0.1)",
|
||||
"description": "Subtle white borders with low opacity for divisions"
|
||||
},
|
||||
"overlay": {
|
||||
"dark": "rgba(0, 0, 0, 0.8)",
|
||||
"description": "Dark overlay for modals and overlays"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
"fontFamilies": {
|
||||
"exo2": "font-exo-2",
|
||||
"geist": "Geist, sans-serif",
|
||||
"geistMono": "Geist Mono, monospace"
|
||||
},
|
||||
"scales": {
|
||||
"xl": {
|
||||
"sizes": ["9xl", "8xl", "7xl", "6xl", "5xl"],
|
||||
"description": "Extra large heading sizes for hero/main title (BIOHAZARD)"
|
||||
},
|
||||
"lg": {
|
||||
"sizes": ["4xl", "3xl"],
|
||||
"description": "Large heading sizes for section titles"
|
||||
},
|
||||
"base": {
|
||||
"sizes": ["lg", "base", "sm"],
|
||||
"description": "Base text sizes for body content"
|
||||
},
|
||||
"xs": {
|
||||
"sizes": ["xs"],
|
||||
"description": "Extra small text for meta information"
|
||||
}
|
||||
},
|
||||
"weights": {
|
||||
"normal": 400,
|
||||
"bold": 700,
|
||||
"black": 900,
|
||||
"description": "Font weights used throughout the design"
|
||||
},
|
||||
"lineHeight": {
|
||||
"tight": "1.2",
|
||||
"relaxed": "1.6",
|
||||
"description": "Line heights for text readability"
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"container": {
|
||||
"maxWidth": "900px",
|
||||
"padding": {
|
||||
"mobile": "px-4",
|
||||
"sm": "sm:px-6",
|
||||
"lg": "lg:px-8"
|
||||
},
|
||||
"description": "Main container width and responsive padding"
|
||||
},
|
||||
"sections": {
|
||||
"vertical": {
|
||||
"small": "mb-8",
|
||||
"medium": "md:mb-16",
|
||||
"large": "md:mb-20",
|
||||
"description": "Vertical spacing between major sections"
|
||||
},
|
||||
"horizontal": {
|
||||
"gap": "gap-6",
|
||||
"description": "Horizontal gaps between elements"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"padding": {
|
||||
"base": "p-6",
|
||||
"sm": "sm:p-8",
|
||||
"md": "md:p-12"
|
||||
},
|
||||
"description": "Card container padding (main content area)"
|
||||
},
|
||||
"elements": {
|
||||
"small": "mb-4",
|
||||
"medium": "mb-6",
|
||||
"large": "mb-8",
|
||||
"description": "Element spacing within sections"
|
||||
}
|
||||
},
|
||||
"breakpoints": {
|
||||
"mobile": "< 640px",
|
||||
"sm": "640px",
|
||||
"md": "768px",
|
||||
"lg": "1024px",
|
||||
"xl": "1280px",
|
||||
"description": "Tailwind CSS responsive breakpoints used"
|
||||
},
|
||||
"components": {
|
||||
"navigation": {
|
||||
"description": "Top navigation bar",
|
||||
"layout": "flex justify-between items-center",
|
||||
"padding": "py-6",
|
||||
"border": "border-b border-white/10",
|
||||
"typography": "text-lg font-mono tracking-tight",
|
||||
"interactive": {
|
||||
"links": "hover:text-[#ff4d00] transition-colors",
|
||||
"gap": "gap-6",
|
||||
"fontSize": "text-sm"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"description": "Main content card container",
|
||||
"background": "#0a0a0a",
|
||||
"border": "border border-white/5",
|
||||
"layout": "relative bg-[#0a0a0a] border border-white/5"
|
||||
},
|
||||
"heading": {
|
||||
"main": {
|
||||
"description": "Large BIOHAZARD heading with text shadow effect",
|
||||
"fontSize": ["text-3xl", "sm:text-4xl", "md:text-5xl"],
|
||||
"fontFamily": "font-exo-2",
|
||||
"fontWeight": "font-black",
|
||||
"color": "#000000",
|
||||
"textShadow": "2px 2px 0px #ff4d00, 4px 4px 0px #ff4d00",
|
||||
"interactive": "hover:opacity-80 cursor-pointer transition-opacity"
|
||||
},
|
||||
"heroTitle": {
|
||||
"description": "Hero section title",
|
||||
"fontSize": ["text-4xl", "sm:text-5xl", "md:text-7xl", "lg:text-8xl", "xl:text-9xl"],
|
||||
"fontWeight": "font-black",
|
||||
"fontFamily": "font-exo-2"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"description": "Interactive link styling",
|
||||
"color": "#ff4d00",
|
||||
"hover": "hover:opacity-80",
|
||||
"underline": {
|
||||
"description": "Animated underline on hover",
|
||||
"height": "h-[1px]",
|
||||
"animation": "scaleX animation on hover"
|
||||
}
|
||||
},
|
||||
"divider": {
|
||||
"description": "Section divider component",
|
||||
"type": "SectionDivider"
|
||||
},
|
||||
"accordion": {
|
||||
"description": "Horizontal expandable accordion",
|
||||
"type": "HorizontalAccordion"
|
||||
},
|
||||
"videoPlayer": {
|
||||
"description": "Reel video player component",
|
||||
"type": "ReelPlayer"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"sections": [
|
||||
{
|
||||
"name": "Navigation",
|
||||
"id": "nav",
|
||||
"content": "Brand name and navigation links"
|
||||
},
|
||||
{
|
||||
"name": "About",
|
||||
"id": "about",
|
||||
"content": "Hero message with accordion and main title"
|
||||
},
|
||||
{
|
||||
"name": "Work",
|
||||
"id": "work",
|
||||
"content": "Reel player and project list with video previews"
|
||||
},
|
||||
{
|
||||
"name": "Studio",
|
||||
"id": "studio",
|
||||
"content": "Instagram feed component"
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"id": "contact",
|
||||
"content": "Contact email and footer information"
|
||||
}
|
||||
]
|
||||
},
|
||||
"animations": {
|
||||
"containerVariants": {
|
||||
"hidden": "opacity: 0",
|
||||
"visible": {
|
||||
"opacity": 1,
|
||||
"staggerChildren": 0.1,
|
||||
"delayChildren": 0.1
|
||||
},
|
||||
"description": "Page load animation with stagger effect"
|
||||
},
|
||||
"itemVariants": {
|
||||
"hidden": "opacity: 0, y: 20",
|
||||
"visible": "opacity: 1, y: 0",
|
||||
"transition": "duration: 0.4, ease: easeOut",
|
||||
"description": "Individual item fade-in and slide-up animation"
|
||||
},
|
||||
"underlineAnimation": {
|
||||
"initial": "scaleX: 0",
|
||||
"hover": "scaleX: 1",
|
||||
"transition": "duration: 0.3, ease: easeOut",
|
||||
"description": "Animated underline on links"
|
||||
},
|
||||
"easterEgg": {
|
||||
"initial": "opacity: 0, scale: 0.7",
|
||||
"animate": "opacity: 1, scale: 1",
|
||||
"transition": "duration: 0.4, ease: [0.16, 1, 0.3, 1]",
|
||||
"description": "Modal popup animation for easter eggs"
|
||||
}
|
||||
},
|
||||
"interactions": {
|
||||
"links": {
|
||||
"color": "#ff4d00",
|
||||
"hoverEffect": "opacity 0.8, text-shadow glow",
|
||||
"tapEffect": "scale 0.98",
|
||||
"underlineAnimation": true,
|
||||
"description": "Standard link interaction pattern"
|
||||
},
|
||||
"easterEgg": {
|
||||
"trigger": "Click on main BIOHAZARD heading or footer text",
|
||||
"action": "Display modal with depth map or easter egg image",
|
||||
"closeAction": "Click outside or mouse leave",
|
||||
"description": "Hidden interactive elements"
|
||||
},
|
||||
"hover": {
|
||||
"cards": "opacity reduction on hover",
|
||||
"text": "color change to accent color or text-shadow glow"
|
||||
}
|
||||
},
|
||||
"responsiveness": {
|
||||
"strategy": "Mobile-first with progressive enhancement",
|
||||
"mobileOptimizations": {
|
||||
"fontSize": "Capped scaling to prevent cramped text",
|
||||
"maxScale": 0.8,
|
||||
"description": "Mobile (< 640px) uses conservative font scaling"
|
||||
},
|
||||
"tabletOptimizations": {
|
||||
"maxScale": 1.2,
|
||||
"description": "Tablet (640-1024px) allows moderate scaling"
|
||||
},
|
||||
"desktopOptimizations": {
|
||||
"maxScale": 1.8,
|
||||
"description": "Desktop (> 1024px) allows expansive scaling"
|
||||
}
|
||||
},
|
||||
"accessibility": {
|
||||
"colorContrast": "High contrast white text on black backgrounds",
|
||||
"interactiveElements": "Clear hover states and cursor pointers",
|
||||
"semanticHTML": "Proper heading hierarchy and section landmarks",
|
||||
"focus": "Default browser focus states on interactive elements"
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
const config = {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: "cloudflare",
|
||||
wrapper: "cloudflare-node",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
@ -13,7 +13,7 @@ const config = {
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: "cloudflare",
|
||||
wrapper: "cloudflare-edge",
|
||||
converter: "edge",
|
||||
proxyExternalRequest: "fetch",
|
||||
incrementalCache: "dummy",
|
||||
|
||||
@ -2,13 +2,14 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
try {
|
||||
const path = params.path.join('/');
|
||||
const resolvedParams = await params;
|
||||
const path = resolvedParams.path.join('/');
|
||||
|
||||
// @ts-ignore - MEDIA is bound via wrangler.toml and available in the Cloudflare context
|
||||
const cloudflareContext = (globalThis as any)[Symbol.for('__cloudflare-context__')];
|
||||
// @ts-expect-error - MEDIA is bound via wrangler.toml and available in the Cloudflare context
|
||||
const cloudflareContext = (globalThis as Record<string, unknown>)[Symbol.for('__cloudflare-context__')];
|
||||
const MEDIA = cloudflareContext?.env?.MEDIA;
|
||||
|
||||
if (!MEDIA) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Bebas_Neue, Orbitron, Inter, JetBrains_Mono, Space_Mono, Rajdhani, Exo_2 } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Bebas_Neue, Orbitron, Inter, JetBrains_Mono, Space_Mono, Space_Grotesk, Rajdhani, Exo_2 } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Navigation } from "@/components/Navigation";
|
||||
import { Footer } from "@/components/Footer";
|
||||
@ -63,6 +63,14 @@ const rajdhani = Rajdhani({
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
variable: "--font-space-grotesk",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const exo2 = Exo_2({
|
||||
variable: "--font-exo-2",
|
||||
subsets: ["latin"],
|
||||
@ -124,7 +132,7 @@ export default function RootLayout({
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${bebasNeue.variable} ${orbitron.variable} ${inter.variable} ${jetbrainsMono.variable} ${spaceMono.variable} ${rajdhani.variable} ${exo2.variable} antialiased bg-black text-white`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${bebasNeue.variable} ${orbitron.variable} ${inter.variable} ${jetbrainsMono.variable} ${spaceMono.variable} ${rajdhani.variable} ${spaceGrotesk.variable} ${exo2.variable} antialiased bg-black text-white`}
|
||||
>
|
||||
<main className="min-h-screen">
|
||||
{children}
|
||||
|
||||
19
src/app/projects/speakers/page.tsx
Normal file
19
src/app/projects/speakers/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Metadata } from 'next';
|
||||
import { SpeakersPageClient } from './speakers-client';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'SPEAKERS | Biohazard VFX',
|
||||
description: '3D visualization gallery',
|
||||
alternates: {
|
||||
canonical: '/projects/speakers',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'SPEAKERS | Biohazard VFX',
|
||||
description: '3D visualization gallery.',
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
|
||||
export default function SpeakersPage() {
|
||||
return <SpeakersPageClient />;
|
||||
}
|
||||
164
src/app/projects/speakers/speakers-client.tsx
Normal file
164
src/app/projects/speakers/speakers-client.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { PolycamEmbed } from '@/components/PolycamEmbed';
|
||||
import { speakers } from '@/data/speakers';
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export function SpeakersPageClient() {
|
||||
return (
|
||||
<section className="bg-[#0f0f0f] text-white min-h-screen flex flex-col" style={{ fontFamily: 'var(--font-space-grotesk)' }}>
|
||||
{/* Header Card */}
|
||||
<header className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<div className="container mx-auto max-w-[1200px]">
|
||||
<motion.div
|
||||
className="relative bg-[#1a1a1a] border border-white/10 rounded-xl px-6 py-4 md:px-8 md:py-5 shadow-2xl"
|
||||
style={{
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
variants={itemVariants}
|
||||
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
role="banner"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-start gap-6 lg:gap-8">
|
||||
{/* Title */}
|
||||
<motion.h1
|
||||
className="text-4xl md:text-5xl font-black font-exo-2 leading-none"
|
||||
style={{
|
||||
color: '#ff4d00',
|
||||
}}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut', delay: 0.1 }}
|
||||
>
|
||||
SPEAKERS
|
||||
</motion.h1>
|
||||
|
||||
{/* Info Grid - Left Aligned */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-left flex-1">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut', delay: 0.15 }}
|
||||
>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Code</p>
|
||||
<p className="text-xs md:text-sm font-mono text-white">SPKR</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut', delay: 0.2 }}
|
||||
className="hidden sm:block"
|
||||
>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Client</p>
|
||||
<p className="text-xs md:text-sm text-white whitespace-nowrap">Carly Gibert</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut', delay: 0.25 }}
|
||||
className="hidden sm:block"
|
||||
>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Studio</p>
|
||||
<p className="text-xs md:text-sm text-white whitespace-nowrap">Biohazard VFX</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="container mx-auto max-w-[1200px]">
|
||||
<div className="border-t border-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Embeds Section */}
|
||||
<main className="flex-1 px-4 sm:px-6 lg:px-8 py-12 md:py-16">
|
||||
<div className="container mx-auto max-w-[1200px]">
|
||||
{/* Visually hidden heading for screen readers */}
|
||||
<h2 className="sr-only">3D Scan Gallery</h2>
|
||||
|
||||
<motion.div
|
||||
className="relative space-y-8"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.1,
|
||||
}}
|
||||
role="region"
|
||||
aria-label="3D scan gallery"
|
||||
>
|
||||
{speakers.map((scan, index) => (
|
||||
<PolycamEmbed
|
||||
key={scan.id}
|
||||
captureId={scan.captureId}
|
||||
title={scan.title}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="container mx-auto max-w-[1200px]">
|
||||
<div className="border-t border-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="px-4 sm:px-6 lg:px-8 py-12 md:py-16 border-t border-white/10" role="contentinfo">
|
||||
<div className="container mx-auto max-w-[1200px]">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400 mb-4 font-semibold">Biohazard VFX</p>
|
||||
<p className="text-sm text-gray-300 max-w-sm">
|
||||
Artists and technical people specializing in VFX and 3D visualization.
|
||||
</p>
|
||||
</div>
|
||||
<motion.a
|
||||
href="mailto:contact@biohazardvfx.com"
|
||||
className="text-sm font-mono"
|
||||
style={{ color: '#ff4d00' }}
|
||||
whileHover={{ opacity: 0.8 }}
|
||||
aria-label="Email contact"
|
||||
>
|
||||
contact@biohazardvfx.com
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
172
src/components/PolycamEmbed.tsx
Normal file
172
src/components/PolycamEmbed.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,7 @@ interface ServiceCardProps {
|
||||
|
||||
export function ServiceCard({ service }: ServiceCardProps) {
|
||||
// Dynamically get the icon component
|
||||
const IconComponent = (LucideIcons as any)[service.icon] || LucideIcons.Box;
|
||||
const IconComponent = (LucideIcons as Record<string, unknown>)[service.icon] || LucideIcons.Box;
|
||||
|
||||
return (
|
||||
<Card className="h-full transition-shadow hover:shadow-lg">
|
||||
|
||||
15
src/data/speakers.ts
Normal file
15
src/data/speakers.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface PolycamScan {
|
||||
id: string;
|
||||
title: string;
|
||||
captureId: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export const speakers: PolycamScan[] = [
|
||||
{
|
||||
id: "scan-001",
|
||||
title: "Low quality 3d Gaussian Splatting test",
|
||||
captureId: "0306c02b-5cd2-4da2-92df-b8820eb9df67",
|
||||
tags: ["3d-scan", "polycam"],
|
||||
},
|
||||
];
|
||||
@ -4,8 +4,8 @@ import type { NextRequest } from 'next/server';
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Allow only the home page and Next.js internal routes
|
||||
if (pathname === '/' || pathname.startsWith('/_next') || pathname.startsWith('/favicon.') || pathname === '/OLIVER.jpeg' || pathname === '/OLIVER_depth.jpeg' || pathname === '/no_pigeons_zone.gif' || pathname === '/reel.mp4') {
|
||||
// Allow only the home page, speakers project, and Next.js internal routes
|
||||
if (pathname === '/' || pathname.startsWith('/projects/speakers') || pathname.startsWith('/_next') || pathname.startsWith('/favicon.') || pathname === '/OLIVER.jpeg' || pathname === '/OLIVER_depth.jpeg' || pathname === '/no_pigeons_zone.gif' || pathname === '/reel.mp4') {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ name = "biohazard-vfx-website"
|
||||
account_id = "a19f770b9be1b20e78b8d25bdcfd3bbd"
|
||||
compatibility_date = "2024-09-23"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
main = ".open-next/middleware/handler.mjs"
|
||||
main = ".open-next/worker.js"
|
||||
|
||||
# Custom domains
|
||||
routes = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user