Compare commits

..

No commits in common. "90e730c2fecc8f516897c225ff4e809f7775cf02" and "7b1acf5588e7706a8177cfc494736b47505fc51f" have entirely different histories.

9 changed files with 290 additions and 958 deletions

227
AGENTS.md
View File

@ -1,206 +1,55 @@
# Agents Guide (Single Source of Truth)
# AGENTS.md
This document is the canonical reference for humans and `Agents` working in this repository. It standardizes stack, structure, workflows, and guardrails so contributions stay consistent and safe.
This file provides guidance to Opencode when working with code in this repository.
## 1) Scope and goals
## Repository Snapshot
* Make onboarding fast with a single place to look
* Define conventions so changes are predictable
* Provide exact commands that `Agents` can run without guesswork
* Prevent accidental regressions in routing, theming, SEO, and deployment
TypeScript + React 19 (Next.js 15.5.4) site builder for Biohazard VFX studio. Single npm workspace. Deployed to Cloudflare Workers via OpenNext. ESLint + TypeScript strict mode enforced pre-commit. No automated tests; manual QA in Chromium/Safari only. (Sources: package.json, tsconfig.json, README.md)
## 2) Tech stack
## Commands
* **Framework**: Next.js 15.5.4, React 19, TypeScript
* **Styling**: Tailwind CSS 4, shadcn/ui
* **Animation**: Framer Motion
* **Forms**: react-hook-form + Zod
* **Platform**: Cloudflare Workers via OpenNext
* **Package manager**: npm
* **Node**: LTS 20 or 22
| Task | Command | Scope | Source |
|------|---------|-------|--------|
| Dev server | `npm run dev` | Root | package.json:6 |
| Build (Next.js) | `npm run build` | Root | package.json:7 |
| Build (Cloudflare Workers) | `npm run build:open-next` | Root | package.json:8 |
| Lint (ESLint) | `npm run lint` | Root | package.json:10 |
| Type check (TypeScript) | `npm run type-check` | Root | package.json:11 |
| Start production server | `npm run start` | Root | package.json:9 |
## 3) Project layout
**Run a single test:** Not applicable; no automated test runner configured.
```
root
├─ src/
│ ├─ app/ # App Router pages and layouts
│ │ ├─ (marketing)/ # Example route groups
│ │ ├─ api/ # Route handlers
│ │ └─ layout.tsx # Root layout, see rules below
│ ├─ components/ # Reusable UI
│ ├─ data/ # JSON or TS data objects consumed by pages
│ ├─ lib/ # Utilities, hooks, server actions
│ ├─ styles/ # globals.css, tailwind utilities if applicable
│ └─ types/ # Shared types
├─ public/ # Static assets
├─ next.config.ts
├─ tailwind.config.ts
├─ wrangler.toml # Cloudflare deploy config
└─ package.json
```
## Architecture Overview
### Import aliases
- **Entry point**: `src/app/layout.tsx` (root layout with Navigation & Footer); app routes in `src/app/{route}/page.tsx` (about, contact, portfolio, services). (Source: README.md:6169, src/app/layout.tsx)
- **Components**: Functional React + Radix UI via shadcn/ui. Primitives in `src/components/ui/`; feature components in `src/components/`. (Source: README.md:7079, package.json:1624)
- **Data & utils**: Project and service metadata in `src/data/projects.ts` and `src/data/services.ts`; shared utilities in `src/lib/utils.ts`. (Source: README.md:8084)
- **Styling**: Tailwind CSS 4 + CSS variables in `src/app/globals.css`; components use `clsx` + `tailwind-merge` for conditional styles. (Source: tsconfig.json, package.json:39)
- **Forms**: `react-hook-form` + Zod validation; e.g., MultiStepForm (contact page). (Source: package.json:1415, 33, 35)
- **External services**: Instagram Feed component integrates Instagram API via `src/lib/instagram-api.ts`. Remote images from unsplash.com. (Source: next.config.ts:713, src/lib/)
- **Deployment**: OpenNext adapter for Cloudflare Workers. Build output in `.open-next/` (worker.js + assets). Domain routes in wrangler.toml. (Source: wrangler.toml, open-next.config.ts, next.config.ts:1622)
* Prefer absolute imports using `@` mapped to `src` via `tsconfig.json` paths.
## Conventions and Rules
## 4) Authoritative UI system
From CONTRIBUTING.md:
* **Theme**: dark mode is the default. Do not introduce light-first designs without approval.
* **Typography**: default to Geist and Geist Mono via CSS variables. If adding a new font, add it as a variable and document it here before use.
* **Components**: use shadcn/ui primitives. Extend with local wrappers placed in `src/components/ui/`.
* **Spacing and rhythm**: follow Tailwind 4 defaults. Prefer utility classes over custom CSS unless componentized.
* **Animation**: keep motion subtle. Framer Motion only for meaningful transitions.
- **Commit format**: conventional commits (`feat(scope): subject`). Atomic commits. Examples: `feat(portfolio): add project filtering`, `fix(header): resolve mobile menu overflow`. (CONTRIBUTING.md:122147)
- **Branch strategy**: `main` (production), `develop` (integration), `feature/*`, `fix/*`, `hotfix/*`. Always create from `develop`. (CONTRIBUTING.md:4954, 5762)
- **React/Next.js**: Server components by default; client-only where needed. Single responsibility per component. Keep custom CSS minimal. (CONTRIBUTING.md:8092)
- **TypeScript**: Avoid `any`. Define interfaces for props and API responses. (CONTRIBUTING.md:7478)
## 5) Routing and layout rules
## CI Hooks That Matter Locally
* The **root layout** owns global providers, theme class, `<Nav />`, and `<Footer />`. Do not duplicate these in child layouts.
* Pages live in `src/app`. Keep server components as the default. Promote to client component only when needed.
* Metadata must be defined per route with the Next.js Metadata API.
## 6) SEO and metadata
* Use the Metadata API for title, description, Open Graph, and Twitter cards.
* Add structured data with JSON-LD in the root layout or specific routes when required.
* All pages must render a unique `title` and `description` suitable for indexing.
## 7) Forms and validation
* Use `react-hook-form` with Zod schemas.
* Surface field-level errors and a generic submit error. Never swallow validation errors.
## 8) Images and assets
* Use Next Image component for remote images.
* If a new external domain is introduced, add it to `next.config.ts` remote patterns and document it here.
* Keep `public/` for truly static assets only.
## 9) Environment variables
Provide a `.env.sample` and keep it in sync. Typical keys:
```
NEXT_PUBLIC_SITE_URL=
RESEND_API_KEY=
CF_PAGES_URL=
```
Do not commit real secrets. `Agents` must fail a task rather than hardcode a secret.
## 10) Local development
```
# install
npm ci
# run dev server
npm run dev
# type checks and linting
npm run typecheck
npm run lint
# build and preview
npm run build
npm run start
```
Notes
* The Next build may be configured to ignore ESLint and TS errors for production bundling speed. CI still gates on `lint` and `typecheck` before merge.
## 11) Deployment on Cloudflare Workers with OpenNext
### Required wrangler.toml settings
```
name = "site-worker"
main = ".open-next/worker/index.mjs"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
assets = { directory = ".open-next/assets" }
```
### Build and deploy
```
# produce OpenNext build artifacts
npx open-next@latest build
# deploy worker and assets
npx wrangler deploy .open-next/worker
```
Guidelines
* Always run `npm run typecheck` and `npm run lint` before build.
* Ensure `assets.directory` matches the OpenNext output.
* Keep the compatibility date at or after 2024-09-23.
## 12) Branching, commits, and CI
* **Default branch**: `main` is protected.
* **Workflow**: feature branches -> PR -> required checks -> squash merge.
* **Commit format**: Conventional Commits. Examples
* `feat: add contact form schema`
* `fix: correct Image remote pattern`
* `chore: bump dependencies`
* **Required checks**
* `npm run lint`
* `npm run typecheck`
* `npm run build` can be optional locally if CI runs it, but must succeed before deploy.
## 13) Testing
* **Unit**: place tests close to sources, name with `.test.ts` or `.test.tsx`.
* **E2E**: optional Playwright. If used, add a `playwright.config.ts` and a `npm run e2e` script.
## 14) Data and content
* Non-secret content belongs in `src/data` as TS modules or JSON. Keep it presentation-agnostic.
* Do not fetch static project data in client components. Prefer server components or file imports.
## 15) `Agents` operating rules
1. Read this guide before making changes.
2. Before adding a dependency, justify it in the PR description and update this document if it affects conventions.
3. When creating pages, set Metadata and verify unique title and description.
4. If a build ignores ESLint or TS errors, CI still blocks merges on `lint` and `typecheck`. Fix the errors instead of bypassing checks.
5. Never commit secrets. Use `.env` and keep `.env.sample` current.
6. If you change image domains or fonts, document the change here.
7. Prefer small, reviewable PRs. Include screenshots for UI changes.
## 16) Common pitfalls
* Adding a remote image domain but forgetting to allow it in `next.config.ts`.
* Introducing a client component unnecessarily and breaking streaming or SSR.
* Duplicating navigation inside nested layouts.
* Styling drift by bypassing Tailwind utilities and shadcn primitives.
## 17) Quick command reference
```
# install deps
npm ci
# develop
npm run dev
# quality gates
npm run lint
npm run typecheck
# build and preview
npm run build
npm run start
# open-next build and deploy
npx open-next@latest build
npx wrangler deploy .open-next/worker
```
From `.gitea/workflows/ci.yml`:
- **Lint & type-check** (lines 1435): `npm run lint` and `npm run type-check` run on every PR to main/develop. **Failures block merge.**
- **Build** (lines 3765): `npm run build` with `NODE_ENV=production` required before merge. Artifacts uploaded for 7 days.
Run both before pushing: `npm run lint && npm run type-check && npm run build`.
## Known Pitfalls
- **Build ignores errors**: next.config.ts (lines 1722) sets `eslint.ignoreDuringBuilds` and `typescript.ignoreBuildErrors` to `true`. Lint and typecheck **must pass locally** before deploy or CI will catch them. (Source: next.config.ts)
- **No test suite**: Manual QA only. Verify changes in Chromium and Safari before PR. (Source: README.md:55, AGENTS.md (this file, earlier))
- **Secrets in wrangler.toml**: Never commit credentials there. Use Gitea Secrets for env vars. (Source: CONTRIBUTING.md:153, AGENTS.md (this file, earlier))
- **Cloudflare compatibility**: Requires `nodejs_compat` flag and compatibility date ≥ 2024-09-23. (Source: wrangler.toml:4)

333
CLAUDE.md
View File

@ -1,211 +1,168 @@
# Agents Guide (Single Source of Truth)
# CLAUDE.md
This document is the canonical reference for humans and `Agents` working in this repository. It standardizes stack, structure, workflows, and guardrails so contributions stay consistent and safe.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 1) Scope and goals
## Project Overview
* Make onboarding fast with a single place to look
* Define conventions so changes are predictable
* Provide exact commands that `Agents` can run without guesswork
* Prevent accidental regressions in routing, theming, SEO, and deployment
Biohazard VFX is a modern Next.js 15 website for a visual effects studio, deployed to Cloudflare Workers using OpenNext. The site showcases portfolio work, services, and provides a multi-step contact form for client intake.
## 2) Tech stack
**Tech Stack:**
- Next.js 15.5.4 with App Router
- TypeScript (strict mode)
- React 19.1.0
- Tailwind CSS 4
- shadcn/ui (New York style)
- Framer Motion for animations
- Cloudflare Workers deployment via OpenNext
* **Framework**: Next.js 15.5.4, React 19, TypeScript
* **Styling**: Tailwind CSS 4, shadcn/ui
* **Animation**: Framer Motion
* **Forms**: react-hook-form + Zod
* **Platform**: Cloudflare Workers via OpenNext
* **Package manager**: npm
* **Node**: LTS 20 or 22
## Development Commands
## 3) Project layout
```bash
# Development
npm run dev # Start dev server with Turbopack
npm run type-check # Run TypeScript compiler (no emit)
npm run lint # Run ESLint
```
root
├─ src/
│ ├─ app/ # App Router pages and layouts
│ │ ├─ (marketing)/ # Example route groups
│ │ ├─ api/ # Route handlers
│ │ └─ layout.tsx # Root layout, see rules below
│ ├─ components/ # Reusable UI
│ ├─ data/ # JSON or TS data objects consumed by pages
│ ├─ lib/ # Utilities, hooks, server actions
│ ├─ styles/ # globals.css, tailwind utilities if applicable
│ └─ types/ # Shared types
├─ public/ # Static assets
├─ next.config.ts
├─ tailwind.config.ts
├─ wrangler.toml # Cloudflare deploy config
└─ package.json
# Building
npm run build # Standard Next.js build
npm run build:open-next # Build for Cloudflare Workers (runs next build + open-next build)
# Production
npm start # Start Next.js production server (not used for Cloudflare deployment)
```
### Import aliases
## Deployment (Cloudflare Workers)
* Prefer absolute imports using `@` mapped to `src` via `tsconfig.json` paths.
The site deploys to Cloudflare Workers, not Vercel. Critical deployment files:
## 4) Authoritative UI system
- **`wrangler.toml`**: Cloudflare Workers configuration with routes for biohazardvfx.com
- **`open-next.config.ts`**: OpenNext adapter configuration for Cloudflare
- **`next.config.ts`**: Ignores lint/TypeScript errors during build (required for deployment)
* **Theme**: dark mode is the default. Do not introduce light-first designs without approval.
* **Typography**: default to Geist and Geist Mono via CSS variables. If adding a new font, add it as a variable and document it here before use.
* **Components**: use shadcn/ui primitives. Extend with local wrappers placed in `src/components/ui/`.
* **Spacing and rhythm**: follow Tailwind 4 defaults. Prefer utility classes over custom CSS unless componentized.
* **Animation**: keep motion subtle. Framer Motion only for meaningful transitions.
## 5) Routing and layout rules
* The **root layout** owns global providers, theme class, `<Nav />`, and `<Footer />`. Do not duplicate these in child layouts.
* Pages live in `src/app`. Keep server components as the default. Promote to client component only when needed.
* Metadata must be defined per route with the Next.js Metadata API.
## 6) SEO and metadata
* Use the Metadata API for title, description, Open Graph, and Twitter cards.
* Add structured data with JSON-LD in the root layout or specific routes when required.
* All pages must render a unique `title` and `description` suitable for indexing.
## 7) Forms and validation
* Use `react-hook-form` with Zod schemas.
* Surface field-level errors and a generic submit error. Never swallow validation errors.
## 8) Images and assets
* Use Next Image component for remote images.
* If a new external domain is introduced, add it to `next.config.ts` remote patterns and document it here.
* Keep `public/` for truly static assets only.
## 9) Environment variables
Provide a `.env.sample` and keep it in sync. Typical keys:
```
NEXT_PUBLIC_SITE_URL=
RESEND_API_KEY=
CF_PAGES_URL=
**Deploy commands:**
```bash
npx opennextjs-cloudflare build # Build for Cloudflare
npx wrangler deploy # Deploy to Cloudflare Workers
```
Do not commit real secrets. `Agents` must fail a task rather than hardcode a secret.
**Live URLs:**
- Production: https://biohazardvfx.com
- Worker: https://biohazard-vfx-website.nicholaivogelfilms.workers.dev
## 10) Local development
**Cloudflare-specific requirements:**
- Requires `nodejs_compat` compatibility flag
- Compatibility date: `2024-09-23` or later
- Assets binding configured at `.open-next/assets`
```
# install
npm ci
## Architecture
# run dev server
npm run dev
### Path Aliases
- `@/*` maps to `./src/*` (configured in tsconfig.json)
- shadcn/ui aliases: `@/components`, `@/components/ui`, `@/lib/utils`
# type checks and linting
npm run typecheck
npm run lint
### App Structure (Next.js App Router)
# build and preview
npm run build
npm run start
Pages are in `src/app/`:
- `page.tsx` - Homepage with hero, featured projects, capabilities
- `about/page.tsx` - Studio origins, values, testimonials
- `services/page.tsx` - Service offerings with ServiceCard components
- `portfolio/page.tsx` - Masonry grid portfolio layout
- `contact/page.tsx` - Multi-step contact form
- `layout.tsx` - Root layout with Navigation and Footer (removed from individual pages)
**Important:** Navigation and Footer components are only in the root layout. Individual pages should NOT include them.
### Data Files
Content is separated from components in `src/data/`:
- `projects.ts` - Project data with Project interface (id, title, description, category, thumbnailUrl, videoUrl, aspectRatio, featured, tags)
- `services.ts` - Services data
When adding new projects or services, update these files rather than hardcoding data in components.
### Component Organization
Custom components in `src/components/`:
- **Layout:** Navigation, Footer
- **Page sections:** Hero, ContactSection, MissionSection, BrandingSection
- **Content display:** ProjectCard, ServiceCard, MasonryGrid, ProjectShowcase, VideoPlayer, VideoPreview
- **Interactive:** MultiStepForm (4-step client intake), HorizontalAccordion
- **Visual effects:** DepthMap, CursorDotBackground, ScrollProgressBar, SectionDivider
- **Third-party:** InstagramFeed, ClientLogoGrid
shadcn/ui components in `src/components/ui/`:
- Uses New York style variant
- Includes: accordion, button, card, dialog, hover-card, input, label, navigation-menu, progress, select, separator, textarea
### Styling System
**Fonts:** Nine Google Fonts preloaded in layout.tsx:
- Geist Sans (primary), Geist Mono
- Bebas Neue, Orbitron, Inter, JetBrains Mono, Space Mono, Rajdhani, Exo 2
- Available as CSS variables: `--font-geist-sans`, `--font-bebas`, etc.
**Theme:**
- Dark mode by default (`className="dark"` on html element)
- CSS variables in `src/app/globals.css`
- shadcn/ui config in `components.json` (New York style, neutral base color)
**Tailwind:**
- Tailwind CSS 4 with PostCSS
- Config: `tailwind.config.ts`
- Global styles: `src/app/globals.css`
### SEO & Metadata
All pages include Next.js Metadata API:
- Open Graph tags
- Twitter cards
- Canonical links
- JSON-LD structured data (Organization schema in root layout)
- metadataBase: `https://biohazardvfx.com`
### Forms & Validation
MultiStepForm component uses:
- react-hook-form for form state
- zod for validation (via @hookform/resolvers)
- 4 steps: Project Type → Budget/Timeline → Project Details → Contact Info
- Progress indicator using shadcn/ui Progress component
### Images
Next.js Image optimization configured in next.config.ts:
- Remote patterns allowed: `images.unsplash.com`
- `unoptimized: false` (optimization enabled)
## Key Constraints
1. **Cloudflare deployment:** Do not suggest Vercel-specific features that won't work with OpenNext
2. **Build config:** Lint and TypeScript errors are ignored during build (required for deployment). Do not remove these settings from next.config.ts
3. **Dark theme only:** Site uses dark mode by default. Do not create light mode variants unless explicitly requested
4. **Layout structure:** Navigation and Footer are in root layout only. Do not add them to individual pages
5. **Data separation:** Project and service data lives in `src/data/`. Keep content separate from components
## Common Tasks
**Add a new page:**
1. Create directory in `src/app/`
2. Add `page.tsx` with metadata export
3. Update Navigation component at `src/components/Navigation.tsx`
**Add a new shadcn/ui component:**
```bash
npx shadcn@latest add [component-name]
```
Notes
**Update project portfolio:**
1. Edit `src/data/projects.ts`
2. Add new Project object to projects array
3. Ensure aspectRatio is set correctly for masonry layout
* The Next build may be configured to ignore ESLint and TS errors for production bundling speed. CI still gates on `lint` and `typecheck` before merge.
## 11) Deployment on Cloudflare Workers with OpenNext
### Required wrangler.toml settings
```
name = "site-worker"
main = ".open-next/worker/index.mjs"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
assets = { directory = ".open-next/assets" }
```
### Build and deploy
```
# produce OpenNext build artifacts
npx open-next@latest build
# deploy worker and assets
npx wrangler deploy .open-next/worker
```
Guidelines
* Always run `npm run typecheck` and `npm run lint` before build.
* Ensure `assets.directory` matches the OpenNext output.
* Keep the compatibility date at or after 2024-09-23.
## 12) Branching, commits, and CI
* **Default branch**: `main` is protected.
* **Workflow**: feature branches -> PR -> required checks -> squash merge.
* **Commit format**: Conventional Commits. Examples
* `feat: add contact form schema`
* `fix: correct Image remote pattern`
* `chore: bump dependencies`
* **Required checks**
* `npm run lint`
* `npm run typecheck`
* `npm run build` can be optional locally if CI runs it, but must succeed before deploy.
## 13) Testing
* **Unit**: place tests close to sources, name with `.test.ts` or `.test.tsx`.
* **E2E**: optional Playwright. If used, add a `playwright.config.ts` and a `npm run e2e` script.
## 14) Data and content
* Non-secret content belongs in `src/data` as TS modules or JSON. Keep it presentation-agnostic.
* Do not fetch static project data in client components. Prefer server components or file imports.
## 15) `Agents` operating rules
1. Read this guide before making changes.
2. Do not alter the root layout structure for global nav or footer. Extend only via component props or slots.
3. Before adding a dependency, justify it in the PR description and update this document if it affects conventions.
4. When creating pages, set Metadata and verify unique title and description.
5. If a build ignores ESLint or TS errors, CI still blocks merges on `lint` and `typecheck`. Fix the errors instead of bypassing checks.
6. Never commit secrets. Use `.env` and keep `.env.sample` current.
7. If you change image domains or fonts, document the change here.
8. Prefer small, reviewable PRs. Include screenshots for UI changes.
9. **When adding files to `public/`**, always update the middleware whitelist in `src/middleware.ts` (line 8) to allow access to the new files.
## 16) Common pitfalls
* Adding a remote image domain but forgetting to allow it in `next.config.ts`.
* Introducing a client component unnecessarily and breaking streaming or SSR.
* Duplicating navigation inside nested layouts.
* Styling drift by bypassing Tailwind utilities and shadcn primitives.
* **⚠️ CRITICAL - Middleware Whitelist**: `src/middleware.ts` redirects ALL routes to `/` except explicitly whitelisted paths. When adding new static assets to `public/` (images, videos, PDFs, etc.), you MUST add the path to the middleware allowlist (line 8) or the file will return a 307 redirect to `/` instead of serving. Common symptom: video/image returns "text/html" Content-Type error.
## 17) Quick command reference
```
# install deps
npm ci
# develop
npm run dev
# quality gates
npm run lint
npm run typecheck
# build and preview
npm run build
npm run start
# open-next build and deploy
npx open-next@latest build
npx wrangler deploy .open-next/worker
```
## 18) Change management
* Any modification to guardrails in sections 4 to 12 requires a PR that updates this document.
* Keep this file the single place that defines expectations for humans and `Agents`.
**Update services:**
1. Edit `src/data/services.ts`
2. Add service object with required fields
**Modify theme colors:**
1. Edit CSS variables in `src/app/globals.css`
2. Theme uses neutral base color with CSS variables for theming

205
QWEN.md
View File

@ -1,205 +0,0 @@
# Agents Guide (Single Source of Truth)
This document is the canonical reference for humans and `Agents` working in this repository. It standardizes stack, structure, workflows, and guardrails so contributions stay consistent and safe.
## 1) Scope and goals
* Make onboarding fast with a single place to look
* Define conventions so changes are predictable
* Provide exact commands that `Agents` can run without guesswork
* Prevent accidental regressions in routing, theming, SEO, and deployment
## 2) Tech stack
* **Framework**: Next.js 15.5.4, React 19, TypeScript
* **Styling**: Tailwind CSS 4, shadcn/ui
* **Animation**: Framer Motion
* **Forms**: react-hook-form + Zod
* **Platform**: Cloudflare Workers via OpenNext
* **Package manager**: npm
* **Node**: LTS 20 or 22
## 3) Project layout
```
root
├─ src/
│ ├─ app/ # App Router pages and layouts
│ │ ├─ (marketing)/ # Example route groups
│ │ ├─ api/ # Route handlers
│ │ └─ layout.tsx # Root layout, see rules below
│ ├─ components/ # Reusable UI
│ ├─ data/ # JSON or TS data objects consumed by pages
│ ├─ lib/ # Utilities, hooks, server actions
│ ├─ styles/ # globals.css, tailwind utilities if applicable
│ └─ types/ # Shared types
├─ public/ # Static assets
├─ next.config.ts
├─ tailwind.config.ts
├─ wrangler.toml # Cloudflare deploy config
└─ package.json
```
### Import aliases
* Prefer absolute imports using `@` mapped to `src` via `tsconfig.json` paths.
## 4) Authoritative UI system
* **Theme**: dark mode is the default. Do not introduce light-first designs without approval.
* **Typography**: default to Geist and Geist Mono via CSS variables. If adding a new font, add it as a variable and document it here before use.
* **Components**: use shadcn/ui primitives. Extend with local wrappers placed in `src/components/ui/`.
* **Spacing and rhythm**: follow Tailwind 4 defaults. Prefer utility classes over custom CSS unless componentized.
* **Animation**: keep motion subtle. Framer Motion only for meaningful transitions.
## 5) Routing and layout rules
* The **root layout** owns global providers, theme class, `<Nav />`, and `<Footer />`. Do not duplicate these in child layouts.
* Pages live in `src/app`. Keep server components as the default. Promote to client component only when needed.
* Metadata must be defined per route with the Next.js Metadata API.
## 6) SEO and metadata
* Use the Metadata API for title, description, Open Graph, and Twitter cards.
* Add structured data with JSON-LD in the root layout or specific routes when required.
* All pages must render a unique `title` and `description` suitable for indexing.
## 7) Forms and validation
* Use `react-hook-form` with Zod schemas.
* Surface field-level errors and a generic submit error. Never swallow validation errors.
## 8) Images and assets
* Use Next Image component for remote images.
* If a new external domain is introduced, add it to `next.config.ts` remote patterns and document it here.
* Keep `public/` for truly static assets only.
## 9) Environment variables
Provide a `.env.sample` and keep it in sync. Typical keys:
```
NEXT_PUBLIC_SITE_URL=
RESEND_API_KEY=
CF_PAGES_URL=
```
Do not commit real secrets. `Agents` must fail a task rather than hardcode a secret.
## 10) Local development
```
# install
npm ci
# run dev server
npm run dev
# type checks and linting
npm run typecheck
npm run lint
# build and preview
npm run build
npm run start
```
Notes
* The Next build may be configured to ignore ESLint and TS errors for production bundling speed. CI still gates on `lint` and `typecheck` before merge.
## 11) Deployment on Cloudflare Workers with OpenNext
### Required wrangler.toml settings
```
name = "site-worker"
main = ".open-next/worker/index.mjs"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
assets = { directory = ".open-next/assets" }
```
### Build and deploy
```
# produce OpenNext build artifacts
npx open-next@latest build
# deploy worker and assets
npx wrangler deploy .open-next/worker
```
Guidelines
* Always run `npm run typecheck` and `npm run lint` before build.
* Ensure `assets.directory` matches the OpenNext output.
* Keep the compatibility date at or after 2024-09-23.
## 12) Branching, commits, and CI
* **Default branch**: `main` is protected.
* **Workflow**: feature branches -> PR -> required checks -> squash merge.
* **Commit format**: Conventional Commits. Examples
* `feat: add contact form schema`
* `fix: correct Image remote pattern`
* `chore: bump dependencies`
* **Required checks**
* `npm run lint`
* `npm run typecheck`
* `npm run build` can be optional locally if CI runs it, but must succeed before deploy.
## 13) Testing
* **Unit**: place tests close to sources, name with `.test.ts` or `.test.tsx`.
* **E2E**: optional Playwright. If used, add a `playwright.config.ts` and a `npm run e2e` script.
## 14) Data and content
* Non-secret content belongs in `src/data` as TS modules or JSON. Keep it presentation-agnostic.
* Do not fetch static project data in client components. Prefer server components or file imports.
## 15) `Agents` operating rules
1. Read this guide before making changes.
2. Before adding a dependency, justify it in the PR description and update this document if it affects conventions.
3. When creating pages, set Metadata and verify unique title and description.
4. If a build ignores ESLint or TS errors, CI still blocks merges on `lint` and `typecheck`. Fix the errors instead of bypassing checks.
5. Never commit secrets. Use `.env` and keep `.env.sample` current.
6. If you change image domains or fonts, document the change here.
7. Prefer small, reviewable PRs. Include screenshots for UI changes.
## 16) Common pitfalls
* Adding a remote image domain but forgetting to allow it in `next.config.ts`.
* Introducing a client component unnecessarily and breaking streaming or SSR.
* Duplicating navigation inside nested layouts.
* Styling drift by bypassing Tailwind utilities and shadcn primitives.
## 17) Quick command reference
```
# install deps
npm ci
# develop
npm run dev
# quality gates
npm run lint
npm run typecheck
# build and preview
npm run build
npm run start
# open-next build and deploy
npx open-next@latest build
npx wrangler deploy .open-next/worker
```

View File

@ -20,20 +20,6 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
// Custom headers for video files
async headers() {
return [
{
source: "/:path*.mp4",
headers: [
{
key: "Content-Type",
value: "video/mp4",
},
],
},
];
},
};
export default nextConfig;

Binary file not shown.

View File

@ -180,7 +180,6 @@ body {
html {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
scroll-behavior: smooth;
}
html::-webkit-scrollbar {

View File

@ -1,258 +0,0 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { Play, Pause, Volume2, VolumeX, Maximize, AlertCircle } from "lucide-react";
interface ReelPlayerProps {
src: string;
className?: string;
}
export function ReelPlayer({ src, className = "" }: ReelPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const progressBarRef = useRef<HTMLDivElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const handleLoadedMetadata = () => {
setDuration(video.duration);
};
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
};
const handleEnded = () => {
setIsPlaying(false);
};
const handleError = (e: Event) => {
setIsLoading(false);
const videoEl = e.target as HTMLVideoElement;
const errorCode = videoEl.error?.code;
const errorMessage = videoEl.error?.message;
let userMessage = "Failed to load video. ";
switch (errorCode) {
case 1:
userMessage += "Video loading was aborted.";
break;
case 2:
userMessage += "Network error occurred.";
break;
case 3:
userMessage += "Video format not supported by your browser.";
break;
case 4:
userMessage += "Video source not found.";
break;
default:
userMessage += errorMessage || "Unknown error.";
}
setError(userMessage);
console.error("Video error:", errorCode, errorMessage);
};
const handleCanPlay = () => {
console.log("Video canplay event fired");
setIsLoading(false);
setError(null);
};
const handleLoadedData = () => {
console.log("Video loadeddata event fired");
setIsLoading(false);
};
video.addEventListener("loadedmetadata", handleLoadedMetadata);
video.addEventListener("timeupdate", handleTimeUpdate);
video.addEventListener("ended", handleEnded);
video.addEventListener("error", handleError);
video.addEventListener("canplay", handleCanPlay);
video.addEventListener("loadeddata", handleLoadedData);
// Check if video is already loaded (in case events fired before listeners attached)
if (video.readyState >= 3) {
// HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA
console.log("Video already loaded, readyState:", video.readyState);
setIsLoading(false);
if (video.duration) {
setDuration(video.duration);
}
}
return () => {
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
video.removeEventListener("timeupdate", handleTimeUpdate);
video.removeEventListener("ended", handleEnded);
video.removeEventListener("error", handleError);
video.removeEventListener("canplay", handleCanPlay);
video.removeEventListener("loadeddata", handleLoadedData);
};
}, []);
const togglePlay = async () => {
const video = videoRef.current;
if (!video || error) return;
try {
if (isPlaying) {
video.pause();
setIsPlaying(false);
} else {
await video.play();
setIsPlaying(true);
}
} catch (err) {
console.error("Play error:", err);
setError("Unable to play video. " + (err as Error).message);
setIsPlaying(false);
}
};
const toggleMute = () => {
const video = videoRef.current;
if (!video) return;
video.muted = !isMuted;
setIsMuted(!isMuted);
};
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
const video = videoRef.current;
const progressBar = progressBarRef.current;
if (!video || !progressBar) return;
const rect = progressBar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percentage = clickX / rect.width;
video.currentTime = percentage * video.duration;
};
const toggleFullscreen = () => {
const video = videoRef.current;
if (!video) return;
if (!document.fullscreenElement) {
video.requestFullscreen();
} else {
document.exitFullscreen();
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
};
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div className={`relative bg-black border border-white/10 ${className}`}>
{/* Video Element */}
<video
ref={videoRef}
className="w-full aspect-video bg-black"
onClick={togglePlay}
preload="auto"
playsInline
>
<source src={src} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Loading State */}
{isLoading && !error && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
<div className="text-white text-sm">Loading video...</div>
</div>
)}
{/* Error State */}
{error && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 p-4">
<AlertCircle className="w-12 h-12 text-[#ff4d00] mb-3" />
<div className="text-white text-sm text-center max-w-md">
{error}
</div>
<div className="text-gray-400 text-xs mt-2">
Try refreshing the page or using a different browser.
</div>
</div>
)}
{/* Custom Controls */}
{!error && (
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/70 to-transparent p-4">
{/* Progress Bar */}
<div
ref={progressBarRef}
className="w-full h-1 bg-white/20 cursor-pointer mb-3 relative"
onClick={handleProgressClick}
>
<div
className="h-full bg-[#ff4d00] transition-all duration-100"
style={{ width: `${progress}%` }}
/>
</div>
{/* Controls Row */}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
{/* Play/Pause Button */}
<button
onClick={togglePlay}
className="text-white hover:text-[#ff4d00] transition-colors"
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? (
<Pause className="w-5 h-5" />
) : (
<Play className="w-5 h-5" />
)}
</button>
{/* Volume Button */}
<button
onClick={toggleMute}
className="text-white hover:text-[#ff4d00] transition-colors"
aria-label={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<VolumeX className="w-5 h-5" />
) : (
<Volume2 className="w-5 h-5" />
)}
</button>
{/* Time Display */}
<span className="text-white text-sm font-mono">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
{/* Fullscreen Button */}
<button
onClick={toggleFullscreen}
className="text-white hover:text-[#ff4d00] transition-colors"
aria-label="Fullscreen"
>
<Maximize className="w-5 h-5" />
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -1,11 +1,11 @@
"use client";
import { CursorDotBackground } from "./CursorDotBackground";
import { HorizontalAccordion } from "./HorizontalAccordion";
import { InstagramFeed } from "./InstagramFeed";
import { ScrollProgressBar } from "./ScrollProgressBar";
import { SectionDivider } from "./SectionDivider";
import { VideoPreview } from "./VideoPreview";
import { ReelPlayer } from "./ReelPlayer";
import { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { DepthMap } from "./DepthMap";
@ -95,73 +95,67 @@ export function TempPlaceholder() {
<>
<ScrollProgressBar />
<section className="py-8 md:py-16 bg-black text-white min-h-screen">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[900px]">
{/* Navigation */}
<nav className="mb-12 md:mb-16">
<div className="flex justify-between items-center py-6 border-b border-white/10">
<div className="text-lg font-mono tracking-tight">BIOHAZARD</div>
<div className="flex gap-6 text-sm">
<a href="#about" className="hover:text-[#ff4d00] transition-colors">About</a>
<a href="#work" className="hover:text-[#ff4d00] transition-colors">Work</a>
<a href="#studio" className="hover:text-[#ff4d00] transition-colors">Studio</a>
<a href="#contact" className="hover:text-[#ff4d00] transition-colors">Contact</a>
</div>
</div>
</nav>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-3xl">
<div className="relative">
<div className="relative rounded-[4px] bg-gradient-to-br from-[#1a0f24]/35 via-[#ff4d00]/30 to-[#1a0f24]/35 p-[1px]">
<div className="relative rounded-[3px] bg-black/92">
<div className="relative p-4 sm:p-6 md:p-8">
<CursorDotBackground
dotSize={2}
dotSpacing={20}
fadeDistance={80}
opacity={0.25}
className="rounded"
/>
<motion.div
className="relative z-10"
variants={containerVariants}
initial="hidden"
animate="visible"
transition={{
staggerChildren: 0.1,
delayChildren: 0.1,
}}
>
<motion.p
className="text-sm text-gray-500 mb-6"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
Last updated: 10-12-2025
</motion.p>
{/* Main Card Container */}
<div className="relative bg-[#0a0a0a] border border-white/5 p-6 sm:p-8 md:p-12">
<motion.div
className="relative"
variants={containerVariants}
initial="hidden"
animate="visible"
transition={{
staggerChildren: 0.1,
delayChildren: 0.1,
}}
>
{/* About Section */}
<section id="about" className="mb-16 md:mb-20">
<motion.p
className="text-sm text-gray-500 mb-6"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
Last updated: 10-12-2025
</motion.p>
<motion.h1
ref={titleRef}
className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 leading-tight"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<span ref={titleInnerRef} className="inline-block">
You've gotta be <em className="text-gray-400">shittin'</em> me.
</span>
</motion.h1>
<motion.p
className="text-base sm:text-lg mb-2 text-gray-300"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
This is the 20th time this has happened.
</motion.p>
<motion.p
className="text-base sm:text-lg mb-6 md:mb-8 text-gray-400"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<em>Nicholai broke the website, again.</em>
</motion.p>
<motion.h1
ref={titleRef}
className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 leading-tight"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<span ref={titleInnerRef} className="inline-block">
You've gotta be <em className="text-gray-400">shittin'</em> me.
</span>
</motion.h1>
<motion.p
className="text-base sm:text-lg mb-2 text-gray-300"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
This is the 20th time this has happened.
</motion.p>
<motion.p
className="text-base sm:text-lg mb-6 md:mb-8 text-gray-400"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<em>Nicholai broke the website, again.</em>
</motion.p>
<motion.div
className="mb-8"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<HorizontalAccordion trigger="How did we get here?">
<motion.div
className="mb-8"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<HorizontalAccordion trigger="How did we get here?">
<div className="w-full">
<p className="mb-4 text-gray-400 text-sm">
<em>(TLDR: perfectionism is the mind killer)</em>
@ -242,7 +236,7 @@ export function TempPlaceholder() {
className="z-50 w-[90vw] max-w-[350px]"
onMouseLeave={() => setIsEasterEggOpen(false)}
>
<div className="relative bg-black overflow-hidden shadow-2xl">
<div className="relative bg-black rounded-lg overflow-hidden shadow-2xl">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@ -293,7 +287,7 @@ export function TempPlaceholder() {
className="z-50 w-[90vw] max-w-[400px]"
onMouseLeave={() => setIsPigeonEggOpen(false)}
>
<div className="relative bg-black overflow-hidden shadow-2xl">
<div className="relative bg-black rounded-lg overflow-hidden shadow-2xl">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@ -318,7 +312,7 @@ export function TempPlaceholder() {
</>
)}
</AnimatePresence>
<motion.p
<motion.p
className="mb-6 md:mb-8 text-base sm:text-lg text-gray-300"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
@ -326,36 +320,48 @@ export function TempPlaceholder() {
<strong>Who we are:</strong> artists and technical people, we're
better at VFX than we are at web design, I promise.
</motion.p>
</section>
<SectionDivider />
{/* Work Section */}
<section id="work" className="mb-16 md:mb-20">
<motion.p
className="mb-6 text-base sm:text-lg"
<motion.p
className="mb-4 text-base sm:text-lg"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<strong>&gt; Here's our reel:</strong>
<strong>Here's our reel:</strong>{" "}
<motion.a
href="https://f.io/Wgx3EAHu"
className="inline-block break-words relative"
style={{ color: '#ff4d00' }}
target="_blank"
rel="noopener noreferrer"
whileHover={{
textShadow: '0 0 8px rgba(255, 77, 0, 0.6)',
}}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
<span className="relative inline-block">
Biohazard Reel Mar 2025 - Frame.io
<motion.span
className="absolute bottom-0 left-0 h-[1px] bg-current"
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
style={{ transformOrigin: 'left', width: '100%' }}
/>
</span>
</motion.a>
</motion.p>
<motion.div
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
className="mb-8"
>
<ReelPlayer src="/reel.mp4" />
</motion.div>
<SectionDivider />
<motion.p
<motion.p
className="mb-4 md:mb-6 text-base sm:text-lg"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<strong>&gt; Some projects we've worked on:</strong>
<strong>Some projects we've worked on:</strong>
</motion.p>
<motion.ul
@ -375,7 +381,9 @@ export function TempPlaceholder() {
style={{ color: '#ff4d00' }}
target="_blank"
rel="noopener noreferrer"
whileHover={{ opacity: 0.8 }}
whileHover={{
textShadow: '0 0 8px rgba(255, 77, 0, 0.6)',
}}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
@ -596,34 +604,28 @@ export function TempPlaceholder() {
</HoverCard>
</motion.li>
</motion.ul>
</section>
<SectionDivider />
{/* Studio Section */}
<section id="studio" className="mb-16 md:mb-20">
<motion.div
<motion.div
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<InstagramFeed />
</motion.div>
</section>
<SectionDivider />
{/* Contact Section */}
<section id="contact">
<motion.div
<motion.div
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<motion.p
<motion.p
className="mb-4 text-sm sm:text-base text-gray-300"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<strong>&gt; Contact us:</strong>{" "}
Contact us:{" "}
<motion.a
href="mailto:contact@biohazardvfx.com"
className="break-words inline-block relative"
@ -648,12 +650,12 @@ export function TempPlaceholder() {
</span>
</motion.a>
</motion.p>
<motion.p
className="text-sm sm:text-base text-gray-300 pb-6 border-b border-white/10"
<motion.p
className="text-sm sm:text-base text-gray-300 pb-6 border-b border-gray-800"
variants={itemVariants}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<strong>&gt; File a complaint:</strong>{" "}
File a complaint:{" "}
<motion.a
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
className="break-words inline-block relative"
@ -690,11 +692,13 @@ export function TempPlaceholder() {
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
>
No pigeons allowed in this zone
No pigeons allowed in this zone
</motion.p>
</motion.div>
</section>
</motion.div>
</motion.div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -5,7 +5,7 @@ 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') {
if (pathname === '/' || pathname.startsWith('/_next') || pathname.startsWith('/favicon.') || pathname === '/OLIVER.jpeg' || pathname === '/OLIVER_depth.jpeg' || pathname === '/no_pigeons_zone.gif') {
return NextResponse.next();
}