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:
Nicholai 2025-11-18 13:37:20 -07:00
parent a76e20e91f
commit b1feda521c
15 changed files with 2196 additions and 12 deletions

View File

3
.cursorindexingignore Normal file
View 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
View File

@ -0,0 +1,4 @@
# SpecStory project identity file
/.project.json
# SpecStory explanation file
/.what-is-this.md

File diff suppressed because it is too large Load Diff

272
design.json Normal file
View 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"
}
}

View File

@ -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",

View File

@ -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) {

View File

@ -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}

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

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

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

View File

@ -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
View 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"],
},
];

View File

@ -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();
}

View File

@ -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 = [