Compare commits
No commits in common. "temp-placeholder" and "main" have entirely different histories.
temp-place
...
main
@ -1,159 +0,0 @@
|
|||||||
---
|
|
||||||
globs: src/components/**/*.tsx
|
|
||||||
---
|
|
||||||
|
|
||||||
# Component Patterns
|
|
||||||
|
|
||||||
## Component Structure
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
- Place reusable components in `src/components/`
|
|
||||||
- Use PascalCase for component files: `ComponentName.tsx`
|
|
||||||
- Group related components in subdirectories when needed
|
|
||||||
|
|
||||||
### Component Template
|
|
||||||
```typescript
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
className?: string
|
|
||||||
children?: React.ReactNode
|
|
||||||
// Other props
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ComponentName({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComponentProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn("base-styles", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## shadcn/ui Integration
|
|
||||||
|
|
||||||
### Using shadcn/ui Components
|
|
||||||
```typescript
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
|
|
||||||
export function ProjectCard({ project }: { project: Project }) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-black border-gray-800">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-white">{project.title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Badge variant="outline">{project.category}</Badge>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extending shadcn/ui Components
|
|
||||||
```typescript
|
|
||||||
// Create wrapper components for common patterns
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface PrimaryButtonProps extends React.ComponentProps<typeof Button> {
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PrimaryButton({
|
|
||||||
loading,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: PrimaryButtonProps) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn("bg-primary hover:bg-primary/90", className)}
|
|
||||||
disabled={loading}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{loading ? "Loading..." : children}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Animation Patterns
|
|
||||||
|
|
||||||
### Framer Motion Usage
|
|
||||||
```typescript
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
|
|
||||||
export function AnimatedCard({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="card-styles"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Composition
|
|
||||||
|
|
||||||
### Compound Components
|
|
||||||
```typescript
|
|
||||||
// Parent component
|
|
||||||
export function Accordion({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="accordion-container">{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Child components
|
|
||||||
export function AccordionItem({ children }: { children: React.ReactNode }) {
|
|
||||||
return <div className="accordion-item">{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AccordionTrigger({ children }: { children: React.ReactNode }) {
|
|
||||||
return <button className="accordion-trigger">{children}</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
<Accordion>
|
|
||||||
<AccordionItem>
|
|
||||||
<AccordionTrigger>Title</AccordionTrigger>
|
|
||||||
<AccordionContent>Content</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Props and TypeScript
|
|
||||||
|
|
||||||
### Interface Patterns
|
|
||||||
```typescript
|
|
||||||
// Use descriptive interface names
|
|
||||||
interface ProjectCardProps {
|
|
||||||
project: Project
|
|
||||||
variant?: 'default' | 'featured'
|
|
||||||
showDescription?: boolean
|
|
||||||
onSelect?: (project: Project) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use React.ComponentProps for extending HTML elements
|
|
||||||
interface CustomButtonProps extends React.ComponentProps<'button'> {
|
|
||||||
variant?: 'primary' | 'secondary'
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling Guidelines
|
|
||||||
|
|
||||||
1. **Tailwind First**: Use utility classes before custom CSS
|
|
||||||
2. **Conditional Classes**: Use `cn()` utility for conditional styling
|
|
||||||
3. **Responsive Design**: Mobile-first approach with responsive utilities
|
|
||||||
4. **Dark Theme**: Ensure all components work in dark mode
|
|
||||||
5. **Accessibility**: Include proper ARIA labels and semantic HTML
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
globs: src/data/**/*.ts,src/data/**/*.json
|
|
||||||
---
|
|
||||||
|
|
||||||
# Data and Content Management
|
|
||||||
|
|
||||||
## Data Structure
|
|
||||||
|
|
||||||
Non-secret content belongs in `src/data/` as TypeScript modules or JSON files. Keep data presentation-agnostic.
|
|
||||||
|
|
||||||
## Current Data Files
|
|
||||||
|
|
||||||
- [src/data/projects.ts](mdc:src/data/projects.ts) - Project portfolio data
|
|
||||||
- [src/data/services.ts](mdc:src/data/services.ts) - Service offerings data
|
|
||||||
|
|
||||||
## Data Patterns
|
|
||||||
|
|
||||||
### TypeScript Data Modules
|
|
||||||
```typescript
|
|
||||||
// src/data/projects.ts
|
|
||||||
export interface Project {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
category: string
|
|
||||||
images: string[]
|
|
||||||
videoUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const projects: Project[] = [
|
|
||||||
{
|
|
||||||
id: 'project-1',
|
|
||||||
title: 'Project Title',
|
|
||||||
description: 'Project description',
|
|
||||||
category: 'commercial',
|
|
||||||
images: ['/image1.jpg', '/image2.jpg']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Data Files
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"id": "vfx",
|
|
||||||
"name": "Visual Effects",
|
|
||||||
"description": "High-end VFX services"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Usage Rules
|
|
||||||
|
|
||||||
1. **Server Components**: Prefer server components for data fetching
|
|
||||||
2. **File Imports**: Use direct imports instead of client-side fetching for static data
|
|
||||||
3. **Type Safety**: Define TypeScript interfaces for all data structures
|
|
||||||
4. **Separation**: Keep data separate from presentation logic
|
|
||||||
|
|
||||||
## Content Guidelines
|
|
||||||
|
|
||||||
- Use descriptive, SEO-friendly content
|
|
||||||
- Include proper alt text for images
|
|
||||||
- Maintain consistent naming conventions
|
|
||||||
- Keep content up-to-date and accurate
|
|
||||||
|
|
||||||
## Data Fetching Patterns
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Server component with direct import
|
|
||||||
import { projects } from '@/data/projects'
|
|
||||||
|
|
||||||
export default function PortfolioPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{projects.map(project => (
|
|
||||||
<ProjectCard key={project.id} project={project} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ Avoid: Client-side fetching of static data
|
|
||||||
'use client'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export function PortfolioPage() {
|
|
||||||
const [projects, setProjects] = useState([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/projects').then(res => res.json())
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Deployment and Build Process
|
|
||||||
|
|
||||||
## Cloudflare Workers with OpenNext
|
|
||||||
|
|
||||||
This project deploys to Cloudflare Workers using OpenNext for Next.js compatibility.
|
|
||||||
|
|
||||||
### Build Process
|
|
||||||
|
|
||||||
1. **Quality Gates** (run before build):
|
|
||||||
```bash
|
|
||||||
npm run type-check # TypeScript validation
|
|
||||||
npm run lint # ESLint validation
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Production Build**:
|
|
||||||
```bash
|
|
||||||
npm run build # Next.js build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **OpenNext Build**:
|
|
||||||
```bash
|
|
||||||
npx open-next@latest build # Generate Cloudflare-compatible build
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Deploy**:
|
|
||||||
```bash
|
|
||||||
npx wrangler deploy .open-next/worker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
|
|
||||||
- [wrangler.toml](mdc:wrangler.toml) - Cloudflare Workers configuration
|
|
||||||
- [open-next.config.ts](mdc:open-next.config.ts) - OpenNext build configuration
|
|
||||||
- [next.config.ts](mdc:next.config.ts) - Next.js configuration
|
|
||||||
|
|
||||||
### Required wrangler.toml Settings
|
|
||||||
|
|
||||||
```toml
|
|
||||||
name = "site-worker"
|
|
||||||
main = ".open-next/worker/index.mjs"
|
|
||||||
compatibility_date = "2024-09-23"
|
|
||||||
compatibility_flags = ["nodejs_compat"]
|
|
||||||
assets = { directory = ".open-next/assets" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Create `.env.sample` and keep it synchronized. Typical keys:
|
|
||||||
```
|
|
||||||
NEXT_PUBLIC_SITE_URL=
|
|
||||||
RESEND_API_KEY=
|
|
||||||
CF_PAGES_URL=
|
|
||||||
```
|
|
||||||
|
|
||||||
**Security**: Never commit real secrets. Use `.env` locally and environment variables in production.
|
|
||||||
|
|
||||||
## Build Configuration
|
|
||||||
|
|
||||||
- ESLint and TypeScript errors are ignored during build for deployment speed
|
|
||||||
- CI still gates on `lint` and `type-check` before merge
|
|
||||||
- Always fix errors instead of bypassing checks
|
|
||||||
|
|
||||||
## Deployment Checklist
|
|
||||||
|
|
||||||
- [ ] Run `npm run type-check` and `npm run lint`
|
|
||||||
- [ ] Ensure `assets.directory` matches OpenNext output
|
|
||||||
- [ ] Keep compatibility date at or after 2024-09-23
|
|
||||||
- [ ] Test build locally with `npm run build`
|
|
||||||
- [ ] Verify OpenNext build artifacts in `.open-next/`
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Development Workflow
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
|
|
||||||
- **Default Branch**: `main` is protected
|
|
||||||
- **Workflow**: feature branches → PR → required checks → squash merge
|
|
||||||
- **Commit Format**: Conventional Commits
|
|
||||||
- `feat: add contact form schema`
|
|
||||||
- `fix: correct Image remote pattern`
|
|
||||||
- `chore: bump dependencies`
|
|
||||||
|
|
||||||
## Required Checks
|
|
||||||
|
|
||||||
Before any merge:
|
|
||||||
- `npm run lint` - ESLint validation
|
|
||||||
- `npm run type-check` - TypeScript validation
|
|
||||||
- `npm run build` - Production build (optional locally, required in CI)
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup
|
|
||||||
npm ci # Install dependencies
|
|
||||||
|
|
||||||
# Development
|
|
||||||
npm run dev # Dev server with Turbopack
|
|
||||||
npm run type-check # TypeScript validation
|
|
||||||
npm run lint # ESLint validation
|
|
||||||
|
|
||||||
# Build & Deploy
|
|
||||||
npm run build # Production build
|
|
||||||
npm run start # Preview production build
|
|
||||||
npx open-next@latest build # OpenNext build
|
|
||||||
npx wrangler deploy .open-next/worker # Deploy to Cloudflare
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
### TypeScript
|
|
||||||
- Use strict mode (enabled in [tsconfig.json](mdc:tsconfig.json))
|
|
||||||
- Prefer type inference over explicit types
|
|
||||||
- Use absolute imports with `@` alias
|
|
||||||
|
|
||||||
### ESLint
|
|
||||||
- Follow Next.js ESLint config
|
|
||||||
- Fix all linting errors before committing
|
|
||||||
- Don't bypass checks in production builds
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
- **Unit Tests**: Place close to sources, name with `.test.ts` or `.test.tsx`
|
|
||||||
- **E2E Tests**: Optional Playwright setup
|
|
||||||
- **Manual Testing**: Test all user flows before deployment
|
|
||||||
|
|
||||||
## Pull Request Guidelines
|
|
||||||
|
|
||||||
- Keep PRs small and reviewable
|
|
||||||
- Include screenshots for UI changes
|
|
||||||
- Update [AGENTS.md](mdc:AGENTS.md) if conventions change
|
|
||||||
- Justify new dependencies in PR description
|
|
||||||
- Never commit secrets or sensitive data
|
|
||||||
|
|
||||||
## Common Pitfalls to Avoid
|
|
||||||
|
|
||||||
1. Adding remote image domains without updating [next.config.ts](mdc:next.config.ts)
|
|
||||||
2. Introducing client components unnecessarily
|
|
||||||
3. Duplicating navigation in nested layouts
|
|
||||||
4. Bypassing Tailwind utilities for custom CSS
|
|
||||||
5. Forgetting to update middleware whitelist for new static assets
|
|
||||||
6. Committing secrets instead of using environment variables
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
---
|
|
||||||
globs: **/*form*.tsx,**/*Form*.tsx
|
|
||||||
---
|
|
||||||
|
|
||||||
# Forms and Validation
|
|
||||||
|
|
||||||
## Form Library Stack
|
|
||||||
|
|
||||||
- **Forms**: react-hook-form for form state management
|
|
||||||
- **Validation**: Zod schemas for type-safe validation
|
|
||||||
- **Resolvers**: @hookform/resolvers for integration
|
|
||||||
|
|
||||||
## Form Structure
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
// Define schema
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(1, 'Name is required'),
|
|
||||||
email: z.string().email('Invalid email address'),
|
|
||||||
message: z.string().min(10, 'Message must be at least 10 characters')
|
|
||||||
})
|
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>
|
|
||||||
|
|
||||||
export function ContactForm() {
|
|
||||||
const form = useForm<FormData>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
message: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = (data: FormData) => {
|
|
||||||
// Handle form submission
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
{/* Form fields with error handling */}
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling Rules
|
|
||||||
|
|
||||||
1. **Field-level errors**: Show validation errors under each field
|
|
||||||
2. **Generic submit error**: Display general submission errors
|
|
||||||
3. **Never swallow errors**: Always surface validation and submission errors
|
|
||||||
4. **Loading states**: Show loading indicators during submission
|
|
||||||
|
|
||||||
## Form Components
|
|
||||||
|
|
||||||
Use shadcn/ui form components from [src/components/ui/form.tsx](mdc:src/components/ui/form.tsx):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="your@email.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation Patterns
|
|
||||||
|
|
||||||
- Use Zod for all form validation
|
|
||||||
- Provide clear, user-friendly error messages
|
|
||||||
- Validate on both client and server side
|
|
||||||
- Handle async validation (e.g., email uniqueness)
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
---
|
|
||||||
globs: **/*.tsx,**/*.ts
|
|
||||||
---
|
|
||||||
|
|
||||||
# Images and Assets
|
|
||||||
|
|
||||||
## Image Handling
|
|
||||||
|
|
||||||
### Next.js Image Component
|
|
||||||
Always use Next.js Image component for remote images:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import Image from 'next/image'
|
|
||||||
|
|
||||||
<Image
|
|
||||||
src="https://images.unsplash.com/photo-123"
|
|
||||||
alt="Descriptive alt text"
|
|
||||||
width={800}
|
|
||||||
height={600}
|
|
||||||
className="rounded-lg"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remote Image Domains
|
|
||||||
Current allowed domains in [next.config.ts](mdc:next.config.ts):
|
|
||||||
- `images.unsplash.com`
|
|
||||||
|
|
||||||
**When adding new external domains:**
|
|
||||||
1. Add to `remotePatterns` in [next.config.ts](mdc:next.config.ts)
|
|
||||||
2. Document the change in [AGENTS.md](mdc:AGENTS.md)
|
|
||||||
|
|
||||||
## Static Assets
|
|
||||||
|
|
||||||
### Public Directory
|
|
||||||
- Keep `public/` for truly static assets only
|
|
||||||
- Current assets: favicon files, images (OLIVER.jpeg, etc.), GIFs
|
|
||||||
|
|
||||||
### Middleware Whitelist
|
|
||||||
**CRITICAL**: When adding new static assets to `public/`, update the middleware allowlist in [src/middleware.ts](mdc:src/middleware.ts) line 8:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Add new asset paths here
|
|
||||||
if (pathname === '/' ||
|
|
||||||
pathname.startsWith('/_next') ||
|
|
||||||
pathname.startsWith('/favicon.') ||
|
|
||||||
pathname === '/OLIVER.jpeg' ||
|
|
||||||
pathname === '/new-asset.jpg' || // Add new assets here
|
|
||||||
pathname === '/reel.mp4') {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common symptom**: If assets return "text/html" Content-Type error, the path isn't whitelisted.
|
|
||||||
|
|
||||||
## Asset Optimization
|
|
||||||
|
|
||||||
- Use appropriate image formats (WebP when possible)
|
|
||||||
- Provide proper alt text for accessibility
|
|
||||||
- Use responsive images with `sizes` prop
|
|
||||||
- Optimize file sizes for web delivery
|
|
||||||
|
|
||||||
## Video Files
|
|
||||||
|
|
||||||
Custom headers are configured in [next.config.ts](mdc:next.config.ts) for `.mp4` files:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
source: "/:path*.mp4",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "Content-Type",
|
|
||||||
value: "video/mp4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Biohazard VFX Website - Project Structure
|
|
||||||
|
|
||||||
This is a Next.js 15.5.4 VFX studio website built with React 19, TypeScript, Tailwind CSS 4, and shadcn/ui components.
|
|
||||||
|
|
||||||
## Core Architecture
|
|
||||||
|
|
||||||
- **Framework**: Next.js 15.5.4 with App Router
|
|
||||||
- **Styling**: Tailwind CSS 4 + shadcn/ui components
|
|
||||||
- **Animation**: Framer Motion for subtle transitions
|
|
||||||
- **Forms**: react-hook-form + Zod validation
|
|
||||||
- **Deployment**: Cloudflare Workers via OpenNext
|
|
||||||
- **Package Manager**: npm
|
|
||||||
|
|
||||||
## Project Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├─ app/ # App Router pages and layouts
|
|
||||||
│ ├─ (marketing)/ # Route groups
|
|
||||||
│ ├─ api/ # Route handlers
|
|
||||||
│ └─ layout.tsx # Root layout with global providers
|
|
||||||
├─ components/ # Reusable UI components
|
|
||||||
│ └─ ui/ # shadcn/ui primitives
|
|
||||||
├─ data/ # JSON/TS data objects
|
|
||||||
├─ lib/ # Utilities, hooks, server actions
|
|
||||||
├─ styles/ # globals.css, Tailwind utilities
|
|
||||||
└─ types/ # Shared TypeScript types
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
- [AGENTS.md](mdc:AGENTS.md) - Single source of truth for development guidelines
|
|
||||||
- [package.json](mdc:package.json) - Dependencies and scripts
|
|
||||||
- [next.config.ts](mdc:next.config.ts) - Next.js configuration
|
|
||||||
- [tsconfig.json](mdc:tsconfig.json) - TypeScript configuration with @ alias
|
|
||||||
- [src/middleware.ts](mdc:src/middleware.ts) - Route protection and redirects
|
|
||||||
- [src/app/layout.tsx](mdc:src/app/layout.tsx) - Root layout with fonts and metadata
|
|
||||||
|
|
||||||
## Import Aliases
|
|
||||||
|
|
||||||
Always use absolute imports with `@` mapped to `src/`:
|
|
||||||
```typescript
|
|
||||||
import { Component } from '@/components/Component'
|
|
||||||
import { data } from '@/data/projects'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm ci # Install dependencies
|
|
||||||
npm run dev # Development server with Turbopack
|
|
||||||
npm run type-check # TypeScript validation
|
|
||||||
npm run lint # ESLint validation
|
|
||||||
npm run build # Production build
|
|
||||||
npx open-next@latest build # OpenNext build for Cloudflare
|
|
||||||
```
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
---
|
|
||||||
globs: src/app/**/*.tsx
|
|
||||||
---
|
|
||||||
|
|
||||||
# Routing and Layout Rules
|
|
||||||
|
|
||||||
## App Router Structure
|
|
||||||
|
|
||||||
- **Pages**: Live in `src/app/` directory
|
|
||||||
- **Server Components**: Default to server components, promote to client only when needed
|
|
||||||
- **Layout Hierarchy**: Root layout owns global providers, navigation, and footer
|
|
||||||
- **Route Groups**: Use `(marketing)` for grouped routes without affecting URL structure
|
|
||||||
|
|
||||||
## Layout Rules
|
|
||||||
|
|
||||||
### Root Layout ([src/app/layout.tsx](mdc:src/app/layout.tsx))
|
|
||||||
- Owns global providers and theme class
|
|
||||||
- Contains `<Navigation />` and `<Footer />` components
|
|
||||||
- Sets up font variables and metadata
|
|
||||||
- **DO NOT** duplicate these in child layouts
|
|
||||||
|
|
||||||
### Page Layouts
|
|
||||||
- Keep server components as default
|
|
||||||
- Add `"use client"` only when necessary for interactivity
|
|
||||||
- Define unique metadata for each route
|
|
||||||
|
|
||||||
## Metadata Requirements
|
|
||||||
|
|
||||||
Every page must have:
|
|
||||||
```typescript
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Unique Page Title",
|
|
||||||
description: "Unique description for SEO",
|
|
||||||
// Include Open Graph and Twitter cards
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Route Protection
|
|
||||||
|
|
||||||
The [src/middleware.ts](mdc:src/middleware.ts) currently redirects all routes to `/` except:
|
|
||||||
- Home page (`/`)
|
|
||||||
- Next.js internal routes (`/_next/*`)
|
|
||||||
- Favicon files
|
|
||||||
- Specific static assets (OLIVER.jpeg, OLIVER_depth.jpeg, etc.)
|
|
||||||
|
|
||||||
## Static Assets
|
|
||||||
|
|
||||||
When adding new files to `public/`, update the middleware allowlist in [src/middleware.ts](mdc:src/middleware.ts) line 8 to prevent 307 redirects.
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Page component
|
|
||||||
export default function PageName() {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto px-4">
|
|
||||||
{/* Page content */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Page Title",
|
|
||||||
description: "Page description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
globs: src/app/**/page.tsx,src/app/**/layout.tsx
|
|
||||||
---
|
|
||||||
|
|
||||||
# SEO and Metadata
|
|
||||||
|
|
||||||
## Metadata API Requirements
|
|
||||||
|
|
||||||
Every page must define unique metadata using the Next.js Metadata API:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { Metadata } from "next"
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Unique Page Title | Biohazard VFX",
|
|
||||||
description: "Unique, descriptive page description for SEO",
|
|
||||||
metadataBase: new URL("https://biohazardvfx.com"),
|
|
||||||
openGraph: {
|
|
||||||
title: "Page Title",
|
|
||||||
description: "Page description",
|
|
||||||
type: "website",
|
|
||||||
locale: "en_US",
|
|
||||||
siteName: "Biohazard VFX",
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: "Page Title",
|
|
||||||
description: "Page description",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root Layout Metadata
|
|
||||||
|
|
||||||
The root layout in [src/app/layout.tsx](mdc:src/app/layout.tsx) includes:
|
|
||||||
- Global site metadata
|
|
||||||
- Open Graph configuration
|
|
||||||
- Twitter card setup
|
|
||||||
- JSON-LD structured data
|
|
||||||
- Canonical URLs
|
|
||||||
|
|
||||||
## SEO Best Practices
|
|
||||||
|
|
||||||
1. **Unique Titles**: Each page must have a unique, descriptive title
|
|
||||||
2. **Descriptions**: Write compelling meta descriptions (150-160 characters)
|
|
||||||
3. **Structured Data**: Use JSON-LD for rich snippets
|
|
||||||
4. **Canonical URLs**: Set canonical URLs to prevent duplicate content
|
|
||||||
5. **Open Graph**: Include OG tags for social media sharing
|
|
||||||
6. **Twitter Cards**: Configure Twitter card metadata
|
|
||||||
|
|
||||||
## Structured Data Example
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const jsonLd = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "Organization",
|
|
||||||
name: "Biohazard VFX",
|
|
||||||
description: "Visual effects studio",
|
|
||||||
url: "https://biohazardvfx.com",
|
|
||||||
logo: "https://biohazardvfx.com/logo.png",
|
|
||||||
sameAs: ["https://instagram.com/biohazardvfx"],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Page-Specific Metadata
|
|
||||||
|
|
||||||
- **Home**: Focus on main services and value proposition
|
|
||||||
- **Portfolio**: Highlight featured projects and capabilities
|
|
||||||
- **Services**: Target specific service keywords
|
|
||||||
- **About**: Include company information and team details
|
|
||||||
- **Contact**: Include location and contact information
|
|
||||||
|
|
||||||
## Image SEO
|
|
||||||
|
|
||||||
- Use descriptive alt text for all images
|
|
||||||
- Optimize image file names
|
|
||||||
- Include image dimensions
|
|
||||||
- Use appropriate image formats (WebP when possible)
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
globs: *.tsx,*.ts,*.css
|
|
||||||
---
|
|
||||||
|
|
||||||
# UI System Guidelines
|
|
||||||
|
|
||||||
## Theme & Design System
|
|
||||||
|
|
||||||
- **Default Theme**: Dark mode only - do not introduce light-first designs
|
|
||||||
- **Typography**: Use CSS variables for fonts (Geist, Geist Mono, Bebas Neue, Orbitron, etc.)
|
|
||||||
- **Components**: Use shadcn/ui primitives from [src/components/ui/](mdc:src/components/ui/)
|
|
||||||
- **Spacing**: Follow Tailwind 4 defaults, prefer utility classes over custom CSS
|
|
||||||
- **Animation**: Keep Framer Motion subtle and meaningful only
|
|
||||||
|
|
||||||
## Component Structure
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Use shadcn/ui primitives as base
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card } from '@/components/ui/card'
|
|
||||||
|
|
||||||
// Extend with local wrappers when needed
|
|
||||||
export function CustomComponent() {
|
|
||||||
return (
|
|
||||||
<Card className="bg-black border-gray-800">
|
|
||||||
<Button variant="outline" className="text-white">
|
|
||||||
Action
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling Rules
|
|
||||||
|
|
||||||
1. **Dark Theme**: All components must work in dark mode
|
|
||||||
2. **Tailwind First**: Use utility classes before custom CSS
|
|
||||||
3. **Component Variants**: Use class-variance-authority for component variants
|
|
||||||
4. **Responsive**: Mobile-first responsive design
|
|
||||||
5. **Accessibility**: Include proper ARIA labels and semantic HTML
|
|
||||||
|
|
||||||
## Font Usage
|
|
||||||
|
|
||||||
Available font variables from [src/app/layout.tsx](mdc:src/app/layout.tsx):
|
|
||||||
- `--font-geist-sans` (default)
|
|
||||||
- `--font-geist-mono`
|
|
||||||
- `--font-bebas`
|
|
||||||
- `--font-orbitron`
|
|
||||||
- `--font-inter`
|
|
||||||
- `--font-jetbrains-mono`
|
|
||||||
- `--font-space-mono`
|
|
||||||
- `--font-rajdhani`
|
|
||||||
- `--font-exo-2`
|
|
||||||
|
|
||||||
## Animation Guidelines
|
|
||||||
|
|
||||||
- Use Framer Motion sparingly for meaningful transitions
|
|
||||||
- Prefer CSS transitions for simple hover effects
|
|
||||||
- Keep animations under 300ms for UI feedback
|
|
||||||
- Respect `prefers-reduced-motion` for accessibility
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
---
|
|
||||||
description: VFX studio specific patterns and requirements
|
|
||||||
---
|
|
||||||
|
|
||||||
# VFX Studio Specific Guidelines
|
|
||||||
|
|
||||||
## Media Handling
|
|
||||||
|
|
||||||
### Video Components
|
|
||||||
- Use [src/components/VideoPlayer.tsx](mdc:src/components/VideoPlayer.tsx) for video playback
|
|
||||||
- Use [src/components/ReelPlayer.tsx](mdc:src/components/ReelPlayer.tsx) for demo reels
|
|
||||||
- Support multiple video formats (MP4, WebM)
|
|
||||||
- Include proper video metadata and thumbnails
|
|
||||||
|
|
||||||
### Image Components
|
|
||||||
- Use [src/components/DepthMap.tsx](mdc:src/components/DepthMap.tsx) for depth map visualizations
|
|
||||||
- Implement lazy loading for portfolio images
|
|
||||||
- Use Next.js Image optimization for all media
|
|
||||||
|
|
||||||
## Portfolio Patterns
|
|
||||||
|
|
||||||
### Project Showcase
|
|
||||||
```typescript
|
|
||||||
// Use ProjectCard component for consistent project display
|
|
||||||
import { ProjectCard } from '@/components/ProjectCard'
|
|
||||||
import { ProjectShowcase } from '@/components/ProjectShowcase'
|
|
||||||
|
|
||||||
// Project data structure from src/data/projects.ts
|
|
||||||
interface Project {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
category: 'commercial' | 'music-video' | 'film' | 'animation'
|
|
||||||
client?: string
|
|
||||||
year: number
|
|
||||||
images: string[]
|
|
||||||
videoUrl?: string
|
|
||||||
tags: string[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service Categories
|
|
||||||
- Visual Effects (VFX)
|
|
||||||
- Motion Graphics
|
|
||||||
- 3D Animation
|
|
||||||
- Compositing
|
|
||||||
- Color Grading
|
|
||||||
- Post-Production
|
|
||||||
|
|
||||||
## Client Work Patterns
|
|
||||||
|
|
||||||
### Client Logo Grid
|
|
||||||
- Use [src/components/ClientLogoGrid.tsx](mdc:src/components/ClientLogoGrid.tsx)
|
|
||||||
- Display client logos with proper attribution
|
|
||||||
- Ensure logos are high-quality and properly sized
|
|
||||||
|
|
||||||
### Project Filtering
|
|
||||||
- Implement category-based filtering
|
|
||||||
- Support tag-based search
|
|
||||||
- Include year-based sorting
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Media Optimization
|
|
||||||
- Compress images and videos for web delivery
|
|
||||||
- Use appropriate formats (WebP for images, MP4 for videos)
|
|
||||||
- Implement progressive loading for large media files
|
|
||||||
- Use CDN for media delivery
|
|
||||||
|
|
||||||
### Loading States
|
|
||||||
- Show skeleton loaders for media content
|
|
||||||
- Implement progressive image loading
|
|
||||||
- Use intersection observer for lazy loading
|
|
||||||
|
|
||||||
## VFX-Specific UI Elements
|
|
||||||
|
|
||||||
### Before/After Comparisons
|
|
||||||
- Implement split-screen comparisons
|
|
||||||
- Use slider controls for reveal effects
|
|
||||||
- Include toggle for before/after views
|
|
||||||
|
|
||||||
### Process Showcases
|
|
||||||
- Show breakdowns of VFX work
|
|
||||||
- Include wireframe and final render comparisons
|
|
||||||
- Display technical specifications
|
|
||||||
|
|
||||||
## Content Guidelines
|
|
||||||
|
|
||||||
### Project Descriptions
|
|
||||||
- Include technical details (software used, techniques)
|
|
||||||
- Mention client and project scope
|
|
||||||
- Highlight challenges and solutions
|
|
||||||
- Use industry-standard terminology
|
|
||||||
|
|
||||||
### Service Descriptions
|
|
||||||
- Be specific about capabilities
|
|
||||||
- Include typical project timelines
|
|
||||||
- Mention software and hardware capabilities
|
|
||||||
- Provide clear pricing structure (if applicable)
|
|
||||||
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 943 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 80 KiB |
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
|
||||||
.specstory/**
|
|
||||||
6
.gitignore
vendored
@ -47,9 +47,3 @@ wrangler.toml.backup
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# No longer save SpecStory auto-save files to the repo
|
|
||||||
.specstory/**
|
|
||||||
|
|
||||||
# No longer save SpecStory AI rules backups to the repo
|
|
||||||
.specstory/ai_rules_backups/**
|
|
||||||
|
|||||||
4
.specstory/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
# SpecStory project identity file
|
|
||||||
/.project.json
|
|
||||||
# SpecStory explanation file
|
|
||||||
/.what-is-this.md
|
|
||||||
211
CLAUDE.md
@ -1,211 +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. 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`.
|
|
||||||
|
|
||||||
41
README.md
@ -128,46 +128,19 @@ The site includes:
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Cloudflare Workers (Current)
|
### Vercel (Recommended)
|
||||||
|
|
||||||
This project is deployed to Cloudflare Workers using OpenNext:
|
1. Push your code to GitHub
|
||||||
|
2. Import the project to Vercel
|
||||||
|
3. Deploy automatically
|
||||||
|
|
||||||
**Prerequisites:**
|
### Other Platforms
|
||||||
- Cloudflare account with Workers enabled
|
|
||||||
- Domain configured in Cloudflare (if using custom domain)
|
|
||||||
|
|
||||||
**Build & Deploy:**
|
Build the project and deploy the `.next` directory:
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build for Cloudflare Workers
|
|
||||||
npx opennextjs-cloudflare build
|
|
||||||
|
|
||||||
# Deploy to Cloudflare
|
|
||||||
npx wrangler deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration Files:**
|
|
||||||
- `wrangler.toml` - Cloudflare Workers configuration
|
|
||||||
- `open-next.config.ts` - OpenNext adapter settings
|
|
||||||
- `next.config.ts` - Next.js configuration
|
|
||||||
|
|
||||||
**Live URLs:**
|
|
||||||
- Production: https://biohazardvfx.com
|
|
||||||
- Worker: https://biohazard-vfx-website.nicholaivogelfilms.workers.dev
|
|
||||||
|
|
||||||
**Important Notes:**
|
|
||||||
- Linting and TypeScript errors are ignored during build (can be re-enabled in `next.config.ts`)
|
|
||||||
- Compatible with Next.js 15.5.4
|
|
||||||
- Uses `nodejs_compat` compatibility flag
|
|
||||||
- Requires compatibility date `2024-09-23` or later
|
|
||||||
|
|
||||||
### Alternative: Vercel
|
|
||||||
|
|
||||||
For Vercel deployment, revert `next.config.ts` to remove Cloudflare-specific settings:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
# Then deploy via Vercel dashboard or CLI
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
272
design.json
@ -1,272 +0,0 @@
|
|||||||
{
|
|
||||||
"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,12 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Image optimization
|
output: "standalone",
|
||||||
|
// OpenNext configuration for Cloudflare deployment
|
||||||
|
experimental: {
|
||||||
|
// Enable any experimental features if needed
|
||||||
|
},
|
||||||
|
// Image optimization for Cloudflare
|
||||||
images: {
|
images: {
|
||||||
unoptimized: false,
|
unoptimized: false,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
@ -13,27 +18,6 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Ignore lint and TypeScript errors during build for deployment
|
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: true,
|
|
||||||
},
|
|
||||||
// Custom headers for video files
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/:path*.mp4",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "Content-Type",
|
|
||||||
value: "video/mp4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@ -1,24 +1,7 @@
|
|||||||
const config = {
|
const config = {
|
||||||
default: {
|
default: {
|
||||||
override: {
|
override: {
|
||||||
wrapper: "cloudflare-node",
|
wrapper: "cloudflare",
|
||||||
converter: "edge",
|
|
||||||
proxyExternalRequest: "fetch",
|
|
||||||
incrementalCache: "dummy",
|
|
||||||
tagCache: "dummy",
|
|
||||||
queue: "dummy",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
edgeExternals: ["node:crypto"],
|
|
||||||
middleware: {
|
|
||||||
external: true,
|
|
||||||
override: {
|
|
||||||
wrapper: "cloudflare-edge",
|
|
||||||
converter: "edge",
|
|
||||||
proxyExternalRequest: "fetch",
|
|
||||||
incrementalCache: "dummy",
|
|
||||||
tagCache: "dummy",
|
|
||||||
queue: "dummy",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
6210
package-lock.json
generated
@ -12,10 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@opennextjs/cloudflare": "^1.10.1",
|
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
@ -24,7 +21,6 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.23.24",
|
|
||||||
"lucide-react": "^0.545.0",
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"open-next": "^3.1.3",
|
"open-next": "^3.1.3",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@ -1,84 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 999.95 999.44">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #2b3232;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #3e4545;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path class="cls-2" d="M134.97,857.96l14.71,9.12c110.92,64.72,252.65,17.33,302.61-99.95,24.78-58.16,21.96-125.65-7.26-181.6l13.22-6.95c19.87,23.56,55.39,27.49,79.33,7.43,1.55-1.3,6.85-7.43,8.07-7.54l12.73,7.35c-11.88,23.76-20.32,49.61-23.04,76.21-15.99,156.6,137.55,276.06,285.61,220.88,17.42-6.49,33.5-15.68,49.03-25.72-1.47,1.94-3.62,3.86-5.5,5.53-79.85,71.07-197.4,90.69-297.15,52.8-23.39-8.88-45.1-21.17-65.86-34.98-39.19,27.07-84.5,45.55-132.02,51.59-79.21,10.07-173.92-15.36-232.24-71.16-.44-.42-2.78-2.5-2.24-3.02Z"/>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M389.34,83.79c6.08-2.22,12.15-4.63,18.31-6.53l-.75,1.51c-.53.21-1.04.32-1.51.5-1.44.56-4.99,1.45-5.02,3.01-.14.06-.36-.06-.5,0-1.27.5-4.23,1.05-4.01,2.51-.15.05-.35-.05-.5,0-.98.34-2.32.12-2.01,1.51-.15.05-.35-.05-.5,0-1,.32-2.31.14-2.01,1.51-.15.05-.36-.06-.5,0-1.6.65-4.81,1.87-5.02,3.51-.15.07-.46-.08-.5,0h-1s0,1,0,1c-.16.06-.46-.07-.5,0h-1s0,1,0,1c-.17.04-.36-.05-.5,0-1.24.41-2.94,1-3.01,2.51-.12.08-.43-.08-.5,0-1-.04-1.54.48-1.51,1.51-.11.08-.42-.08-.5,0-1-.04-1.54.49-1.51,1.51-.12.07-.46-.08-.5,0h-1s0,1,0,1c-.28.13-.87.36-1,.5-1,0-1.55.5-1.51,1.51-.15.05-.36-.06-.5,0-1.09.43-1.92.4-2.01,2.01-.33.17-.67.33-1,.5-.94.49-1.99.33-2.01,2.01-.33.17-.68.32-1,.5-1.08.62-2.41.69-2.51,2.51-.69.48-1.32,1.02-2.01,1.51-1.25.87-3.31,1.48-3.51,3.51-3.05,2.54-6.52,5.97-9.03,9.03-1.97.11-2.28,1.96-3.01,3.01-.48.69-1.03,1.31-1.51,2.01-1.79.12-1.9,1.37-2.51,2.51-.18.33-.32.68-.5,1-1.01-.05-1.5.5-1.51,1.51-.2.22-.29.73-.5,1-1.67.02-1.52,1.06-2.01,2.01-.17.33-.33.67-.5,1-1.02-.03-1.55.5-1.51,1.51-.08.08.08.39,0,.5-1.02-.03-1.55.5-1.51,1.51-.08.08.08.39,0,.5-1.02-.03-1.55.5-1.51,1.51-.08.08.08.39,0,.5-1.02-.03-1.55.51-1.51,1.51-.08.07.06.34,0,.5h-1s0,1,0,1c-.07.04.06.35,0,.5h-1s0,1,0,1c-.08.05.07.36,0,.5-1.88.54-3.5,3.77-4.01,5.52-.04.15.04.34,0,.5h-1s0,1,0,1c-.08.04.06.35,0,.5-1.37-.31-1.18,1.01-1.51,2.01-.05.15.05.35,0,.5-1.37-.31-1.18,1.01-1.51,2.01-.05.15.05.35,0,.5-1.38-.31-1.17,1.02-1.51,2.01-.05.15.05.36,0,.5-1.21-.27-1.04.62-1.41,1.32-13.06,24.31-22.69,50.73-25.18,78.45-.93-.35-.54-2.94-.49-3.76.3-4.81,1.34-10.58,2.16-15.4,9.37-55.34,41.69-105.86,87.64-137.38.99,1.04,1.31.15,2.02-.23,2.76-1.49,4.38-1.82,7.01-2.78Z"/>
|
|
||||||
<path class="cls-2" d="M493.2,487.18c-.03,1.7.41,6.9-.49,7.79-1.01.99-7.89,2.19-10.04,3.01-18.93,7.2-33.2,24.45-36.64,44.39,0-.04-.48.04-.49-.74-.38-26.63,21.56-50.83,47.65-54.45Z"/>
|
|
||||||
<path class="cls-2" d="M557.42,542.87c-3-20.25-17.88-37.92-37-45.04-2.08-.77-8.58-1.82-9.44-2.6-1.25-1.12-.69-6.1-.72-8.06,26.42,3.79,47.99,28.05,47.65,54.95-.01.78-.48.7-.49.74Z"/>
|
|
||||||
<path class="cls-1" d="M389.34,83.79c-.3-1.33.69-.95,1.46-1.27,9.09-3.77,18.32-7.37,27.88-9.77l.25,1c-1.72.62-3.34,1.52-5,2.28-1.97.91-4.82,1.84-7.03,2.74l.75-1.51c-6.16,1.9-12.23,4.31-18.31,6.53ZM409.41,76.77h-1v.49h1v-.49Z"/>
|
|
||||||
<path class="cls-1" d="M327.13,148.51c-.81,1.64-2.18,4.92-4.01,5.52.51-1.75,2.13-4.97,4.01-5.52Z"/>
|
|
||||||
<path class="cls-1" d="M390.35,87.8c-.21,1.69-3.43,2.81-5.02,3.51.21-1.64,3.41-2.86,5.02-3.51Z"/>
|
|
||||||
<path class="cls-1" d="M405.4,79.27c-.04,1.6-3.58,2.41-5.02,3.01.03-1.56,3.58-2.45,5.02-3.01Z"/>
|
|
||||||
<path class="cls-1" d="M359.74,110.88c-.07,2.05-2.53,2.69-3.51,3.51.2-2.04,2.27-2.64,3.51-3.51Z"/>
|
|
||||||
<path class="cls-1" d="M347.2,123.43c-.67.82-1.04,3.01-3.01,3.01.73-1.05,1.04-2.9,3.01-3.01Z"/>
|
|
||||||
<path class="cls-1" d="M399.88,82.28c.22,1.42-2.75,2.05-4.01,2.51-.22-1.46,2.75-2.01,4.01-2.51Z"/>
|
|
||||||
<path class="cls-1" d="M381.82,93.32c.04,1.76-2.08,1.89-3.01,2.51.07-1.51,1.77-2.1,3.01-2.51Z"/>
|
|
||||||
<path class="cls-1" d="M342.68,128.44c-.61.9-.65,2.48-2.51,2.51.61-1.14.72-2.39,2.51-2.51Z"/>
|
|
||||||
<path class="cls-1" d="M364.26,106.87c-.02,1.87-1.63,1.9-2.51,2.51.1-1.82,1.43-1.89,2.51-2.51Z"/>
|
|
||||||
<path class="cls-1" d="M337.66,134.46c-.43.55-.27,2.13-2.01,2.01.49-.94.33-1.98,2.01-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M367.27,104.36c-.02,1.68-1.07,1.52-2.01,2.01.02-1.67,1.07-1.52,2.01-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M370.28,101.85c-.02,1.67-1.07,1.52-2.01,2.01.08-1.6.92-1.58,2.01-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M447.54,567.46c1.11-.5,1.47,1.58,1.51,2.51l-.93-.06c-.15-.82-.44-1.63-.58-2.45Z"/>
|
|
||||||
<path class="cls-1" d="M395.36,84.79c.31,1.37-1.01,1.18-2.01,1.51-.31-1.38,1.02-1.17,2.01-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M392.85,86.3c.31,1.39-1.02,1.17-2.01,1.51-.31-1.37,1.01-1.18,2.01-1.51Z"/>
|
|
||||||
<path class="cls-2" d="M290.5,247.35c0,.5,0,1,0,1.51-.2-1.14-.93-2.49-.5-4.01h.49c.01.83,0,1.67,0,2.51Z"/>
|
|
||||||
<path class="cls-1" d="M322.11,156.04c-.39.92-.06,2.33-1.51,2.01.32-1,.14-2.31,1.51-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M320.6,158.55c-.32,1-.14,2.31-1.51,2.01.32-1,.14-2.31,1.51-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M319.1,161.05c-.32,1-.14,2.31-1.51,2.01.33-.99.13-2.32,1.51-2.01Z"/>
|
|
||||||
<path class="cls-1" d="M330.64,143.49c-1,1.32-.82.85-1.51,1.51-.05-1,.49-1.54,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M378.3,95.83c-.79.9-.19.52-1.51,1.51-.03-1.02.5-1.55,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M376.3,97.34c-.58.62.22.44-1.51,1.51-.03-1.02.5-1.55,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M372.28,100.35c-.42.46.52.79-1.51,1.51-.05-1.01.5-1.5,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M332.14,141.49c-1,1.32-.78.82-1.51,1.51-.04-1,.49-1.54,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M333.65,139.48c-1,1.33-.78.82-1.51,1.51-.04-1,.49-1.54,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M335.15,137.47c-1.01,1.89-.96,1-1.51,1.51-.04-1,.49-1.54,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M339.67,131.95c-1.02,1.87-1.14,1.12-1.51,1.51,0-1,.5-1.55,1.51-1.51Z"/>
|
|
||||||
<path class="cls-1" d="M666.3,382.82c1.09,1.72-.23.69-.5,1l.5-1Z"/>
|
|
||||||
<path class="cls-1" d="M328.13,147.01c-.1.25.12.88,0,1s-.81-.11-1,0v-1s1,0,1,0Z"/>
|
|
||||||
<path class="cls-1" d="M374.29,98.84c-.09.16.17.74-.05.96-.21.21-.78-.03-.95.05v-1s1,0,1,0Z"/>
|
|
||||||
<path class="cls-1" d="M329.13,145.5c-.09.23.08.69,0,1h-1s0-1,0-1h1Z"/>
|
|
||||||
<path class="cls-1" d="M384.83,91.31c-.11.19.13.88,0,1s-.75-.1-1,0v-1s1,0,1,0Z"/>
|
|
||||||
<path class="cls-1" d="M383.32,92.32c-.5.84.76.59-1,1v-1s1,0,1,0Z"/>
|
|
||||||
<path class="cls-1" d="M323.11,154.53c-.38,1.77-.19.52-1,1v-1s1,0,1,0Z"/>
|
|
||||||
<path class="cls-1" d="M209.22,372.78c-.35-.17-.59-.25-.52-.72.49-4.19-.39-8.54-.51-12.56-1.66-55.99,11.35-111.12,39.26-159.47,37.23-64.51,98.71-113.5,170.23-134.82.78.46-1.05,1.06-1.32,1.19-6.94,3.35-13.62,6.42-20.34,10.27-5.44,3.12-10.56,6.58-15.72,10.12-45.96,31.52-78.28,82.04-87.64,137.38-.82,4.82-1.86,10.58-2.16,15.4-.05.82-.43,3.41.49,3.76,2.49-27.72,12.13-54.14,25.18-78.45.38-.7.2-1.59,1.41-1.32-2.33,6.37-6.78,13.23-9.62,19.73-8.63,19.73-14.19,40.86-16.48,62.31-.1.95.61,2.03-.99,1.75,0-.83,0-1.67,0-2.51h-.49c-.43,1.52.3,2.87.5,4.01.56,3.19.03,3.52-.02,6.25-.37,19.38.79,36.11,5.28,54.97,21.72,91.22,103.42,158.77,197.44,162.05-.07,5.01.08,10.05,0,15.05-26.09,3.62-48.03,27.81-47.65,54.45.01.78.49.7.49.74.16,5.94.18,9.3,1.65,15.16.61,2.44,3.36,6.16-.14,6.92-.03-.05-.74.13-1.01-.34-13.17-23.13-22.69-40.8-40.88-60.87-47.78-52.74-112.12-75.94-180.85-88.57l-9.59-17c-1.52-8.1-2.69-16.63-4.97-24.55-.19-.65-.78-.2-1.02-.32Z"/>
|
|
||||||
<rect class="cls-2" x="408.41" y="76.77" width="1" height=".49"/>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M621.14,86.3c.48,0,1.44-.6,1.5-.51l.06,1.42c.62.19,1.3.06,1.9.37,2.36,1.21,7.97,5.88,10.32,7.76,41.67,33.28,73.36,89.08,77.52,142.73.09,1.22.37,3.29-.49,4.26-7.32-63.66-46.72-122.59-101.19-155.44-7.85-4.74-16.18-8.53-24.24-12.89.32-.6,1.48-.14,2.03-.02,7.36,1.61,18.26,6.22,25.41,9.21,2.16.9,4.8,3.06,7.18,3.1Z"/>
|
|
||||||
<path class="cls-2" d="M794.24,325.12c-2.05-.82-.58-2.66-.5-3.51,1.37,1.15.51,2.75.5,3.51Z"/>
|
|
||||||
<path class="cls-2" d="M794.74,333.65c-.16-.11-.91.12-1,0-.58-.78.48-3.22.5-3.52.85.97.46,2.34.5,3.51Z"/>
|
|
||||||
<path class="cls-2" d="M793.74,319.1c-1.72-1.13-.63-2.11-.5-3.01,1.16,1,.55,2.16.5,3.01Z"/>
|
|
||||||
<path class="cls-2" d="M793.23,314.58c-1.29-.95-.53-1.85-.5-2.51,1.62,1.02.58,1.83.5,2.51Z"/>
|
|
||||||
<path class="cls-2" d="M792.23,304.05h-1c.04-.8-.32-2.1.5-2.51.08.64.42,1.32.5,2.51Z"/>
|
|
||||||
<path class="cls-2" d="M792.73,310.07c-1.48-.74-.49-1.85-.5-2.01,1.47.83.52,1.59.5,2.01Z"/>
|
|
||||||
<path class="cls-2" d="M792.23,306.05c0-.19,0-.92,0-1l.34.4-.34.6Z"/>
|
|
||||||
<path class="cls-1" d="M791.73,301.54c-.82.41-.46,1.71-.5,2.51h1c.02.33-.01.67,0,1,0,.08,0,.81,0,1,0,.66-.05,1.35,0,2.01.01.16-.98,1.26.5,2.01-.03.67.03,1.34,0,2.01s-.79,1.56.5,2.51c-.06.49.07,1.02,0,1.51-.12.9-1.22,1.89.5,3.01-.05.83.07,1.68,0,2.51s-1.55,2.69.5,3.51c-.02,1.66.1,3.37,0,5.02-.02.29-1.08,2.73-.5,3.52.09.12.84-.12,1,0,.5.36.44.98.5,1.51-1.52-.28-.97.77-1,1.73-.38,11.42.47,23.77-1.05,35.09-1.09,8.1-4.78,16.16-5.09,24.51l-9.64,17.96c-10.77,2.85-21.91,4.72-32.79,7.34-86.07,20.73-148.55,61.58-188.82,141.84-.23.46-.89.74-.92.81l-2.03-1.3c2.92-6.78,3.31-13.06,3.54-20.28,0-.04.48.04.49-.74.35-26.9-21.23-51.16-47.65-54.95-.07-5.01.06-10.04,0-15.05,65.56-2.22,127.35-36.49,164.68-89.95,27.95-40.03,42.65-90.84,37.02-139.84.86-.97.59-3.04.49-4.26-4.16-53.66-35.85-109.46-77.52-142.73-2.35-1.87-7.96-6.55-10.32-7.76-.6-.31-1.28-.18-1.9-.37l-.06-1.42c-.06-.09-1.02.51-1.5.51.1-.8-1.1-1.75-1.64-2.15-9.72-7.27-22.19-12.65-32.98-18.17,4.28.36,8.84,2.2,12.93,3.63,98.18,34.35,179.46,127.3,192.28,231.93Z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M795.24,335.15c.94,7.86-.2,18.35-.5,26.35-.16,4.4-.15,8.82-.42,13.21,51.24,23.12,95.35,61.75,125.36,109.2,36.8,58.18,52.36,128.59,43.07,197.12-1.25,9.2-3.35,18.28-4.95,27.41-.88,0-.55-2.2-.52-2.78,3.22-49.21-4.73-91.57-30.88-133.65-78.44-126.22-260.37-133.85-349.62-15.05-3.16,4.2-6.55,8.8-8.85,13.48-1.06.75-6.38-4.04-8.19-3.53l-2.9,5.49-2.92-1.23c1.15-1.45.99-3.29,1.49-4.73h1s-.5-1-.5-1c.1-.26-.1-.78,0-1,.03-.08.69-.35.92-.81,40.27-80.26,102.75-121.11,188.82-141.84,10.89-2.62,22.03-4.49,32.79-7.34l9.64-17.96c.32-8.34,4-16.41,5.09-24.51,1.53-11.32.67-23.67,1.05-35.09.03-.96-.51-2.01,1-1.73Z"/>
|
|
||||||
<path class="cls-2" d="M209.22,372.78c.24.11.83-.33,1.02.32,2.29,7.92,3.45,16.45,4.97,24.55l9.59,17c68.74,12.64,133.07,35.83,180.85,88.57,18.18,20.07,27.71,37.74,40.88,60.87.27.47.99.3,1.01.34.2.36-.12,2.26,0,3.01.13.82.42,1.63.58,2.45l.93.06c.03.65.07,1.29-.47,1.79-2.57,1.87-4.14-4.72-4.85-4.84-1.94-.07-6.29,4.25-7.74,3.32-54.91-87.22-167.44-122.14-262.74-81.14-83.19,35.79-131.75,117.26-128.13,207.57.12,2.98.87,6.5,1.04,9.5.03.55.32,1.9-.52,1.77-20.68-89.33,1.99-184.46,60.21-254.61,27.52-33.16,59.01-56.2,96.98-76.11,1.95-1.02,6.42-1.67,6.36-4.43Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="cls-2" d="M338.59,499.8c-.55,6.45-2.46,12.84-3.36,19.31-11.58,83.63,40.08,162.87,121,185.41l.78.72c-1.7,22.22-8.24,44.39-17.82,64.45-77.77-20.51-140.92-83.05-163.49-160.09-11.94-40.75-12.74-84.85-2.01-125.94l.54-.43c22.18,2.16,44,7.62,64.35,16.58Z"/>
|
|
||||||
<path class="cls-2" d="M729.99,483.69c1.35,7.32,3.25,14.58,4.39,21.95,17.58,112.76-48.14,223.68-156.13,260.01-2.13.72-13.13,4.52-14.32,3.78-.83-.52-6.75-15.87-7.53-18.08-5.14-14.63-8.96-30.63-9.96-46.1l.78-.72c42.31-11.65,78.88-40.56,100.59-78.53,21.56-37.7,27.58-83.63,16.48-125.74,20.78-8.92,42.99-15.19,65.69-16.56Z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="cls-1" d="M339.17,380.31c-.06-.05.03-.94-.29-1.44-.77-1.22-2.66-3.07-1.99-4.56,11.6-10.97,23.98-21.19,37.44-29.83,84.29-54.11,193.48-48.98,272.34,12.55,6.97,5.44,13.34,11.59,20.03,17.35.69,1.19-1.56,4.49-2.42,5.43-8.06,8.87-15.94,18.72-24.82,27.11-2.32,2.19-17.99,15.82-19.67,15.97-4.28-3.5-8.17-7.47-12.47-10.96-58.46-47.44-141.99-49.75-203.3-6.14-7.37,5.24-13.9,11.37-20.87,17.1-10.92-8.13-21.53-17.46-30.71-27.52-2.52-2.77-10.22-13.71-11.95-14.93-.45-.32-1.26-.06-1.34-.13Z"/>
|
|
||||||
<path class="cls-2" d="M664.29,379.81c.14,1.34,1.32,1.93,2.01,3.01l-.5,1c-.75.87-2.55,4.75-3.81,6.47-11.49,15.66-26.46,29.79-42.18,41.12-4.28-3.5-8.18-7.45-12.47-10.96-43.55-35.55-102.76-46.6-156.54-29.62-25.28,7.98-48.29,22.29-67.29,40.61-11.4-8.15-22.11-17.67-31.55-28.05-2.42-2.66-15.66-18.38-15.34-20.78l2.55-2.31c.08.07.89-.19,1.34.13,1.72,1.22,9.42,12.16,11.95,14.93,9.18,10.06,19.78,19.39,30.71,27.52,6.96-5.73,13.5-11.86,20.87-17.1,61.31-43.6,144.84-41.3,203.3,6.14,4.3,3.49,8.18,7.46,12.47,10.96,1.67-.15,17.35-13.78,19.67-15.97,8.88-8.39,16.76-18.23,24.82-27.11Z"/>
|
|
||||||
</g>
|
|
||||||
<path class="cls-1" d="M555.92,565.45l.5,1h-1c.19-.55.45-.86.5-1Z"/>
|
|
||||||
<polygon class="cls-2" points="132.96 857.45 132.96 856.96 133.7 856.96 134.45 857.71 132.96 857.45"/>
|
|
||||||
<path class="cls-2" d="M871.26,855.96c.5.33-.23.66-.51.98l-.24-.24.75-.75Z"/>
|
|
||||||
<path class="cls-2" d="M585.77,73.25c.33.17.33.33,0,.5v-.5Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
1
public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 46 KiB |
1
public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@ -1,95 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const resolvedParams = await params;
|
|
||||||
const path = resolvedParams.path.join('/');
|
|
||||||
|
|
||||||
// @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) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Media bucket not configured' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the object from R2
|
|
||||||
const object = await MEDIA.get(path);
|
|
||||||
|
|
||||||
if (!object) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `File not found: ${path}` },
|
|
||||||
{ status: 404 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get range header for video streaming support
|
|
||||||
const range = request.headers.get('range');
|
|
||||||
|
|
||||||
// Get the full object body to handle range requests properly
|
|
||||||
const body = await object.body.arrayBuffer();
|
|
||||||
const totalLength = body.byteLength;
|
|
||||||
|
|
||||||
let start = 0;
|
|
||||||
let end = totalLength - 1;
|
|
||||||
let status = 200;
|
|
||||||
|
|
||||||
if (range) {
|
|
||||||
// Extract start and end positions from range header
|
|
||||||
const match = range.match(/bytes=(\d+)-(\d+)?/);
|
|
||||||
if (match) {
|
|
||||||
start = parseInt(match[1], 10);
|
|
||||||
end = match[2] ? parseInt(match[2], 10) : end;
|
|
||||||
|
|
||||||
// Ensure end is within the bounds
|
|
||||||
if (end >= totalLength) {
|
|
||||||
end = totalLength - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = 206; // Partial content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the length for the response
|
|
||||||
const contentLength = end - start + 1;
|
|
||||||
const slicedBody = body.slice(start, end + 1);
|
|
||||||
|
|
||||||
// Set headers for the response
|
|
||||||
const headers = new Headers();
|
|
||||||
object.writeHttpMetadata(headers);
|
|
||||||
headers.set('etag', object.httpEtag);
|
|
||||||
headers.set('cache-control', 'public, max-age=31536000, immutable');
|
|
||||||
|
|
||||||
// Add CORS headers to allow video requests from the same origin
|
|
||||||
headers.set('access-control-allow-origin', '*');
|
|
||||||
headers.set('access-control-allow-headers', 'range, content-type, accept');
|
|
||||||
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS');
|
|
||||||
headers.set('access-control-expose-headers', 'content-range, accept-ranges, content-length, content-encoding');
|
|
||||||
|
|
||||||
// Add range response headers if needed
|
|
||||||
if (range) {
|
|
||||||
headers.set('content-range', `bytes ${start}-${end}/${totalLength}`);
|
|
||||||
headers.set('accept-ranges', 'bytes');
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.set('content-length', contentLength.toString());
|
|
||||||
headers.set('content-type', path.endsWith('.mp4') ? 'video/mp4' : object.httpMetadata?.contentType || 'application/octet-stream');
|
|
||||||
|
|
||||||
return new NextResponse(slicedBody, {
|
|
||||||
status,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error serving media:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to serve media file' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +1,53 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 0%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 3.9%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
--popover: 0 0% 3.9%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--primary: 188 100% 50%;
|
--primary: 0 0% 9%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 0 0% 96.1%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 9%;
|
||||||
--muted: 0 0% 14.9%;
|
--muted: 0 0% 96.1%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 0 0% 45.1%;
|
||||||
--accent: 188 100% 50%;
|
--accent: 0 0% 96.1%;
|
||||||
--accent-foreground: 0 0% 9%;
|
--accent-foreground: 0 0% 9%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 0 0% 89.8%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 0 0% 89.8%;
|
||||||
--ring: 188 100% 50%;
|
--ring: 0 0% 3.9%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 12 76% 61%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 173 58% 39%;
|
||||||
--chart-3: 30 80% 55%;
|
--chart-3: 197 37% 24%;
|
||||||
--chart-4: 280 65% 60%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 340 75% 55%;
|
--chart-5: 27 87% 67%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 0%;
|
--background: 0 0% 3.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 0 0% 3.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 3.9%;
|
--popover: 0 0% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 188 100% 50%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 0 0% 14.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 0 0% 14.9%;
|
--muted: 0 0% 14.9%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 188 100% 50%;
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 9%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 0 0% 14.9%;
|
||||||
--ring: 188 100% 50%;
|
--ring: 0 0% 83.1%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
--chart-3: 30 80% 55%;
|
--chart-3: 30 80% 55%;
|
||||||
@ -58,13 +58,6 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
--font-brand: var(--font-bebas);
|
|
||||||
--font-orbitron: var(--font-orbitron);
|
|
||||||
--font-inter: var(--font-inter);
|
|
||||||
--font-jetbrains-mono: var(--font-jetbrains-mono);
|
|
||||||
--font-space-mono: var(--font-space-mono);
|
|
||||||
--font-rajdhani: var(--font-rajdhani);
|
|
||||||
--font-exo-2: var(--font-exo-2);
|
|
||||||
--color-background: hsl(var(--background));
|
--color-background: hsl(var(--background));
|
||||||
--color-foreground: hsl(var(--foreground));
|
--color-foreground: hsl(var(--foreground));
|
||||||
--color-border: hsl(var(--border));
|
--color-border: hsl(var(--border));
|
||||||
@ -73,125 +66,5 @@
|
|||||||
body {
|
body {
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
font-family: var(--font-jetbrains-mono), Arial, Helvetica, sans-serif;
|
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||||
}
|
|
||||||
|
|
||||||
.font-brand {
|
|
||||||
font-family: var(--font-brand), var(--font-sans), Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-orbitron {
|
|
||||||
font-family: var(--font-orbitron), var(--font-sans), Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-inter {
|
|
||||||
font-family: var(--font-inter), Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-terminal {
|
|
||||||
font-family: var(--font-jetbrains-mono), monospace;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-space-mono {
|
|
||||||
font-family: var(--font-space-mono), monospace;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-rajdhani {
|
|
||||||
font-family: var(--font-rajdhani), sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-exo-2 {
|
|
||||||
font-family: var(--font-exo-2), sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dot effect for black text - simplified approach */
|
|
||||||
.text-dots {
|
|
||||||
position: relative;
|
|
||||||
color: #000000;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle, #000000 1px, transparent 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
background-position: 0 0;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom dotted border with solid corner dots */
|
|
||||||
.dotted-border-corners {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dotted-border-corners::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border: 2px dotted #f5f5f5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dotted-border-corners::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
left: -3px;
|
|
||||||
right: -3px;
|
|
||||||
bottom: -3px;
|
|
||||||
background:
|
|
||||||
/* Corner dots */
|
|
||||||
radial-gradient(circle, #f5f5f5 2px, transparent 2px) 0 0 / 6px 6px,
|
|
||||||
radial-gradient(circle, #f5f5f5 2px, transparent 2px) 100% 0 / 6px 6px,
|
|
||||||
radial-gradient(circle, #f5f5f5 2px, transparent 2px) 0 100% / 6px 6px,
|
|
||||||
radial-gradient(circle, #f5f5f5 2px, transparent 2px) 100% 100% / 6px 6px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar for all elements */
|
|
||||||
* {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome, Safari, Opera */
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
html::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome, Safari, Opera */
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome, Safari, Opera */
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono, Bebas_Neue, Orbitron, Inter, JetBrains_Mono, Space_Mono, Space_Grotesk, Rajdhani, Exo_2 } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Navigation } from "@/components/Navigation";
|
import { Navigation } from "@/components/Navigation";
|
||||||
import { Footer } from "@/components/Footer";
|
import { Footer } from "@/components/Footer";
|
||||||
@ -18,91 +18,24 @@ const geistMono = Geist_Mono({
|
|||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bebasNeue = Bebas_Neue({
|
|
||||||
variable: "--font-bebas",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: "400",
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const orbitron = Orbitron({
|
|
||||||
variable: "--font-orbitron",
|
|
||||||
subsets: ["latin"],
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const inter = Inter({
|
|
||||||
variable: "--font-inter",
|
|
||||||
subsets: ["latin"],
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
|
||||||
variable: "--font-jetbrains-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const spaceMono = Space_Mono({
|
|
||||||
variable: "--font-space-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["400", "700"],
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rajdhani = Rajdhani({
|
|
||||||
variable: "--font-rajdhani",
|
|
||||||
subsets: ["latin"],
|
|
||||||
weight: ["300", "400", "500", "600", "700"],
|
|
||||||
display: "swap",
|
|
||||||
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"],
|
|
||||||
weight: ["400", "700", "800", "900"],
|
|
||||||
display: "swap",
|
|
||||||
preload: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Biohazard VFX - Visual Effects Studio",
|
title: "Biohazard VFX - Professional Visual Effects Studio",
|
||||||
description: "Creating stunning visual effects for commercials, music videos, and digital media. Expert VFX, motion graphics, compositing, and 3D animation services.",
|
description: "Creating stunning visual effects for film, television, and digital media. Expert VFX, motion graphics, compositing, and 3D animation services.",
|
||||||
metadataBase: new URL("https://biohazardvfx.com"),
|
metadataBase: new URL("https://biohazardvfx.com"),
|
||||||
icons: {
|
|
||||||
icon: [
|
|
||||||
{ url: '/favicon.ico', sizes: 'any' },
|
|
||||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: "/",
|
canonical: "/",
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Biohazard VFX - Visual Effects Studio",
|
title: "Biohazard VFX - Professional Visual Effects Studio",
|
||||||
description: "Creating stunning visual effects for commercials, music videos, and digital media.",
|
description: "Creating stunning visual effects for film, television, and digital media.",
|
||||||
type: "website",
|
type: "website",
|
||||||
locale: "en_US",
|
locale: "en_US",
|
||||||
siteName: "Biohazard VFX",
|
siteName: "Biohazard VFX",
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "Biohazard VFX - Visual Effects Studio",
|
title: "Biohazard VFX - Professional Visual Effects Studio",
|
||||||
description: "Creating stunning visual effects for commercials, music videos, and digital media.",
|
description: "Creating stunning visual effects for film, television, and digital media.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,14 +48,14 @@ export default function RootLayout({
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
name: "Biohazard VFX",
|
name: "Biohazard VFX",
|
||||||
description: "Visual effects studio specializing in commercials, music videos, and digital media",
|
description: "Professional visual effects studio specializing in film, television, and digital media",
|
||||||
url: "https://biohazardvfx.com",
|
url: "https://biohazardvfx.com",
|
||||||
logo: "https://biohazardvfx.com/logo.png",
|
logo: "https://biohazardvfx.com/logo.png",
|
||||||
sameAs: [],
|
sameAs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
@ -132,11 +65,13 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
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`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<Navigation />
|
||||||
<main className="min-h-screen">
|
<main className="min-h-screen">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
129
src/app/page.tsx
@ -1,18 +1,131 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { Hero } from "@/components/Hero";
|
import { Hero } from "@/components/Hero";
|
||||||
import { ClientLogoGrid } from "@/components/ClientLogoGrid";
|
import { VideoPlayer } from "@/components/VideoPlayer";
|
||||||
import { ProjectShowcase } from "@/components/ProjectShowcase";
|
import { ProjectCard } from "@/components/ProjectCard";
|
||||||
import { MissionSection } from "@/components/MissionSection";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ContactSection } from "@/components/ContactSection";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { BrandingSection } from "@/components/BrandingSection";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { projects } from "@/data/projects";
|
import { projects, studioReel } from "@/data/projects";
|
||||||
import { TempPlaceholder } from "@/components/Temp-Placeholder";
|
import { Sparkles, Zap, Award } from "lucide-react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const featuredProjects = projects.filter((p) => p.featured);
|
const featuredProjects = projects.filter((p) => p.featured);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TempPlaceholder />
|
<Hero
|
||||||
|
title="Crafting Extraordinary Visual Experiences"
|
||||||
|
subtitle="Award-winning visual effects studio delivering stunning VFX, motion graphics, and 3D animation for film, television, and digital media."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Studio Reel Section */}
|
||||||
|
<section className="container py-16">
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
|
||||||
|
Our Studio Reel
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-muted-foreground">
|
||||||
|
A showcase of our best work from the past year
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<VideoPlayer
|
||||||
|
videoUrl={studioReel.videoUrl}
|
||||||
|
thumbnailUrl={studioReel.thumbnailUrl}
|
||||||
|
title={studioReel.title}
|
||||||
|
className="aspect-video w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator className="container" />
|
||||||
|
|
||||||
|
{/* Featured Projects Section */}
|
||||||
|
<section className="container py-16">
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
|
||||||
|
Featured Projects
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-muted-foreground">
|
||||||
|
A selection of our recent work across film, commercial, and digital media
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{featuredProjects.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link href="/portfolio">View Full Portfolio</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator className="container" />
|
||||||
|
|
||||||
|
{/* Capabilities Section */}
|
||||||
|
<section className="container py-16">
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
|
||||||
|
Our Capabilities
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-muted-foreground">
|
||||||
|
Comprehensive visual effects services backed by years of industry experience
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Sparkles className="h-8 w-8 text-primary" />
|
||||||
|
<CardTitle>Industry-Leading Technology</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Utilizing the latest tools and techniques to deliver photorealistic VFX that seamlessly integrate with your footage.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Zap className="h-8 w-8 text-primary" />
|
||||||
|
<CardTitle>Fast Turnaround</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Efficient pipeline and experienced team ensure your project stays on schedule without compromising quality.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Award className="h-8 w-8 text-primary" />
|
||||||
|
<CardTitle>Award-Winning Team</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Our artists have contributed to numerous award-winning productions across film and television.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="container py-16">
|
||||||
|
<Card className="border-2">
|
||||||
|
<CardContent className="flex flex-col items-center gap-6 p-12 text-center">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl">
|
||||||
|
Ready to Bring Your Vision to Life?
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-2xl text-muted-foreground">
|
||||||
|
Let's discuss your project and how we can help you create something extraordinary.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link href="/contact">Start a Project</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<Link href="/services">Explore Services</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
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 />;
|
|
||||||
}
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
export function BrandingSection() {
|
|
||||||
return (
|
|
||||||
<section className="py-24 bg-black">
|
|
||||||
<div className="container">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="font-brand text-6xl sm:text-7xl md:text-8xl lg:text-9xl text-white leading-none">
|
|
||||||
BIOHAZARD.
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
export function ClientLogoGrid() {
|
|
||||||
const clientLogos = [
|
|
||||||
"Vercel", "NEXT", "scale", "APOLLO", "Calcom", "Linear",
|
|
||||||
"knock", "FLOX", "trunk", "Replay", "Graphite", "spiral",
|
|
||||||
"haastes", "CURSOR", "KREA", "Harvey", "ElevenLabs", "Black Forest Labs",
|
|
||||||
"Superplastic", "Triplicate", "SOLANA", "Basement", "MY BEAST", "EDGELORD",
|
|
||||||
"VIRTUAL REALITY", "VIVID", "SHADCN", "KARMA", "G"
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="py-8 bg-black">
|
|
||||||
<div className="mx-auto max-w-[980px] px-4">
|
|
||||||
<p className="mb-6 text-[10px] uppercase tracking-[0.2em] text-gray-400/80 text-center">
|
|
||||||
Trusted by basement.
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-x-10 gap-y-5">
|
|
||||||
{clientLogos.map((logo, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="w-full text-left text-slate-300/70 text-[11px] sm:text-xs md:text-sm hover:text-slate-200 transition-colors border border-gray-600/30 p-3 rounded-sm"
|
|
||||||
>
|
|
||||||
{logo}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
export function ContactSection() {
|
|
||||||
return (
|
|
||||||
<section className="py-10 bg-black">
|
|
||||||
<div className="mx-auto max-w-5xl px-4">
|
|
||||||
{/* Hatched bar */}
|
|
||||||
<div className="h-8 w-full bg-[repeating-linear-gradient(45deg,rgba(255,255,255,0.08)_0,rgba(255,255,255,0.08)_8px,transparent_8px,transparent_16px)] mb-6" />
|
|
||||||
<div className="text-left space-y-3">
|
|
||||||
<div className="text-xs text-gray-400">contact</div>
|
|
||||||
<h2 className="text-white text-2xl font-bold">Let's make an impact together.</h2>
|
|
||||||
<a
|
|
||||||
href="mailto:hello@basement.studio"
|
|
||||||
className="text-white text-lg underline hover:text-gray-300"
|
|
||||||
>
|
|
||||||
hello@basement.studio
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
|
||||||
|
|
||||||
interface CursorDotBackgroundProps {
|
|
||||||
dotSize?: number;
|
|
||||||
dotSpacing?: number;
|
|
||||||
fadeDistance?: number;
|
|
||||||
opacity?: number;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CursorDotBackground({
|
|
||||||
dotSize = 1,
|
|
||||||
dotSpacing = 20,
|
|
||||||
fadeDistance = 100,
|
|
||||||
opacity = 0.3,
|
|
||||||
className = "",
|
|
||||||
}: CursorDotBackgroundProps) {
|
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
setMousePosition({
|
|
||||||
x: e.clientX - rect.left,
|
|
||||||
y: e.clientY - rect.top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseEnter = () => setIsHovering(true);
|
|
||||||
const handleMouseLeave = () => setIsHovering(false);
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (container) {
|
|
||||||
container.addEventListener("mousemove", handleMouseMove);
|
|
||||||
container.addEventListener("mouseenter", handleMouseEnter);
|
|
||||||
container.addEventListener("mouseleave", handleMouseLeave);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (container) {
|
|
||||||
container.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
container.removeEventListener("mouseenter", handleMouseEnter);
|
|
||||||
container.removeEventListener("mouseleave", handleMouseLeave);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Generate dots based on container size
|
|
||||||
const generateDots = () => {
|
|
||||||
if (!containerRef.current) return [];
|
|
||||||
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
const width = rect.width;
|
|
||||||
const height = rect.height;
|
|
||||||
|
|
||||||
const dots = [];
|
|
||||||
const cols = Math.ceil(width / dotSpacing);
|
|
||||||
const rows = Math.ceil(height / dotSpacing);
|
|
||||||
|
|
||||||
for (let row = 0; row < rows; row++) {
|
|
||||||
for (let col = 0; col < cols; col++) {
|
|
||||||
const x = col * dotSpacing;
|
|
||||||
const y = row * dotSpacing;
|
|
||||||
|
|
||||||
// Calculate distance from mouse position
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(x - mousePosition.x, 2) + Math.pow(y - mousePosition.y, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate opacity based on distance and hover state
|
|
||||||
let dotOpacity = 0;
|
|
||||||
if (isHovering && distance <= fadeDistance) {
|
|
||||||
dotOpacity = opacity * (1 - distance / fadeDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dotOpacity > 0) {
|
|
||||||
dots.push({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
opacity: dotOpacity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dots;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
|
||||||
style={{
|
|
||||||
backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px)`,
|
|
||||||
backgroundSize: `${dotSpacing}px ${dotSpacing}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{generateDots().map((dot, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="absolute rounded-full bg-white transition-opacity duration-150 ease-out"
|
|
||||||
style={{
|
|
||||||
left: dot.x,
|
|
||||||
top: dot.y,
|
|
||||||
width: dotSize,
|
|
||||||
height: dotSize,
|
|
||||||
opacity: dot.opacity,
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
interface DepthMapProps {
|
|
||||||
originalImg: string;
|
|
||||||
depthImg: string;
|
|
||||||
verticalThreshold?: number;
|
|
||||||
horizontalThreshold?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DepthMap({
|
|
||||||
originalImg,
|
|
||||||
depthImg,
|
|
||||||
verticalThreshold = 15,
|
|
||||||
horizontalThreshold = 15,
|
|
||||||
}: DepthMapProps) {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const [mousePos, setMousePos] = useState({ x: 0.5, y: 0.5 });
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
const originalImgRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
const depthImgRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Load images
|
|
||||||
const original = new Image();
|
|
||||||
const depth = new Image();
|
|
||||||
|
|
||||||
original.crossOrigin = "anonymous";
|
|
||||||
depth.crossOrigin = "anonymous";
|
|
||||||
|
|
||||||
let loadedCount = 0;
|
|
||||||
const onLoad = () => {
|
|
||||||
loadedCount++;
|
|
||||||
if (loadedCount === 2) {
|
|
||||||
originalImgRef.current = original;
|
|
||||||
depthImgRef.current = depth;
|
|
||||||
setLoaded(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
original.onload = onLoad;
|
|
||||||
depth.onload = onLoad;
|
|
||||||
original.src = originalImg;
|
|
||||||
depth.src = depthImg;
|
|
||||||
}, [originalImg, depthImg]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loaded || !canvasRef.current) return;
|
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx || !originalImgRef.current || !depthImgRef.current) return;
|
|
||||||
|
|
||||||
const original = originalImgRef.current;
|
|
||||||
const depth = depthImgRef.current;
|
|
||||||
|
|
||||||
// Set canvas size to match image
|
|
||||||
canvas.width = original.width;
|
|
||||||
canvas.height = original.height;
|
|
||||||
|
|
||||||
// Calculate displacement based on mouse position
|
|
||||||
const offsetX = (mousePos.x - 0.5) * horizontalThreshold;
|
|
||||||
const offsetY = (mousePos.y - 0.5) * verticalThreshold;
|
|
||||||
|
|
||||||
// Clear canvas
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
// Draw the original image
|
|
||||||
ctx.drawImage(original, 0, 0);
|
|
||||||
|
|
||||||
// Get image data for manipulation
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
const pixels = imageData.data;
|
|
||||||
|
|
||||||
// Draw depth map to get depth values
|
|
||||||
ctx.drawImage(depth, 0, 0);
|
|
||||||
const depthData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
const depthPixels = depthData.data;
|
|
||||||
|
|
||||||
// Create displaced image
|
|
||||||
const displaced = ctx.createImageData(canvas.width, canvas.height);
|
|
||||||
|
|
||||||
for (let y = 0; y < canvas.height; y++) {
|
|
||||||
for (let x = 0; x < canvas.width; x++) {
|
|
||||||
const idx = (y * canvas.width + x) * 4;
|
|
||||||
|
|
||||||
// Get depth value (using red channel) - inverted so darker = more movement
|
|
||||||
const depthValue = 1 - (depthPixels[idx] / 255);
|
|
||||||
|
|
||||||
// Calculate displacement
|
|
||||||
const displaceX = Math.round(offsetX * depthValue);
|
|
||||||
const displaceY = Math.round(offsetY * depthValue);
|
|
||||||
|
|
||||||
// Source pixel position
|
|
||||||
const srcX = Math.max(0, Math.min(canvas.width - 1, x - displaceX));
|
|
||||||
const srcY = Math.max(0, Math.min(canvas.height - 1, y - displaceY));
|
|
||||||
const srcIdx = (srcY * canvas.width + srcX) * 4;
|
|
||||||
|
|
||||||
// Copy pixel
|
|
||||||
displaced.data[idx] = pixels[srcIdx];
|
|
||||||
displaced.data[idx + 1] = pixels[srcIdx + 1];
|
|
||||||
displaced.data[idx + 2] = pixels[srcIdx + 2];
|
|
||||||
displaced.data[idx + 3] = pixels[srcIdx + 3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(displaced, 0, 0);
|
|
||||||
}, [loaded, mousePos, horizontalThreshold, verticalThreshold]);
|
|
||||||
|
|
||||||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const x = (e.clientX - rect.left) / rect.width;
|
|
||||||
const y = (e.clientY - rect.top) / rect.height;
|
|
||||||
|
|
||||||
setMousePos({ x, y });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setMousePos({ x: 0.5, y: 0.5 });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
style={{ display: loaded ? 'block' : 'none' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -4,31 +4,81 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
export function Footer() {
|
export function Footer() {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const footerLinks = [
|
|
||||||
{ href: "/", label: "Home" },
|
|
||||||
{ href: "/services", label: "Services" },
|
|
||||||
{ href: "/portfolio", label: "Showcase" },
|
|
||||||
{ href: "/about", label: "People" },
|
|
||||||
{ href: "#", label: "Blog" },
|
|
||||||
{ href: "#", label: "Lab" },
|
|
||||||
{ href: "/contact", label: "Contact Us" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-gray-800 bg-black">
|
<footer className="border-t bg-background">
|
||||||
<div className="mx-auto max-w-5xl px-4 py-12">
|
<div className="container py-12">
|
||||||
<div className="grid grid-cols-1 gap-2 text-center">
|
<div className="grid grid-cols-1 gap-8 md:grid-cols-4">
|
||||||
{footerLinks.map((link) => (
|
<div className="space-y-4">
|
||||||
<Link
|
<h3 className="text-lg font-semibold">Biohazard VFX</h3>
|
||||||
key={link.href}
|
<p className="text-sm text-muted-foreground">
|
||||||
href={link.href}
|
Creating stunning visual effects for film, television, and digital media.
|
||||||
className="text-xs text-gray-400 hover:text-white transition-colors"
|
</p>
|
||||||
>
|
</div>
|
||||||
{link.label}
|
|
||||||
</Link>
|
<div className="space-y-4">
|
||||||
))}
|
<h4 className="text-sm font-semibold">Navigation</h4>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li>
|
||||||
|
<Link href="/" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/about" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/services" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
Services
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/portfolio" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
Portfolio
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-semibold">Services</h4>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="text-muted-foreground">Visual Effects</li>
|
||||||
|
<li className="text-muted-foreground">Motion Graphics</li>
|
||||||
|
<li className="text-muted-foreground">Compositing</li>
|
||||||
|
<li className="text-muted-foreground">3D Animation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-semibold">Contact</h4>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li>
|
||||||
|
<Link href="/contact" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
Get in Touch
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground">info@biohazardvfx.com</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-8" />
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
© {currentYear} Biohazard VFX. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 text-sm text-muted-foreground">
|
||||||
|
<Link href="#" className="hover:text-foreground transition-colors">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-foreground transition-colors">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 text-center text-[10px] text-gray-500">© {currentYear} Basement</p>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,39 +1,40 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface HeroProps {
|
interface HeroProps {
|
||||||
videoSrc?: string;
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ctaText?: string;
|
||||||
|
ctaLink?: string;
|
||||||
|
secondaryCtaText?: string;
|
||||||
|
secondaryCtaLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Hero({ videoSrc = "/hero-video.mp4" }: HeroProps) {
|
export function Hero({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
ctaText = "View Portfolio",
|
||||||
|
ctaLink = "/portfolio",
|
||||||
|
secondaryCtaText = "Get in Touch",
|
||||||
|
secondaryCtaLink = "/contact",
|
||||||
|
}: HeroProps) {
|
||||||
return (
|
return (
|
||||||
<section className="relative h-[85vh] w-full overflow-hidden">
|
<section className="container flex flex-col items-center justify-center space-y-8 py-24 text-center md:py-32">
|
||||||
{/* Video Background */}
|
<div className="space-y-4 max-w-4xl">
|
||||||
<video
|
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl">
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
{title}
|
||||||
autoPlay
|
</h1>
|
||||||
muted
|
<p className="mx-auto max-w-2xl text-lg text-muted-foreground sm:text-xl">
|
||||||
loop
|
{subtitle}
|
||||||
playsInline
|
</p>
|
||||||
>
|
</div>
|
||||||
<source src={videoSrc} type="video/mp4" />
|
<div className="flex flex-col gap-4 sm:flex-row">
|
||||||
Your browser does not support the video tag.
|
<Button asChild size="lg">
|
||||||
</video>
|
<Link href={ctaLink}>{ctaText}</Link>
|
||||||
|
</Button>
|
||||||
{/* Gradient + dark overlay to match mockup */}
|
<Button asChild size="lg" variant="outline">
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/40 to-transparent" />
|
<Link href={secondaryCtaLink}>{secondaryCtaText}</Link>
|
||||||
|
</Button>
|
||||||
{/* Content bottom-aligned */}
|
|
||||||
<div className="relative z-10 flex h-full items-end">
|
|
||||||
<div className="container pb-10 text-white">
|
|
||||||
<div className="max-w-5xl space-y-3">
|
|
||||||
<h1 className="text-[28px] sm:text-[36px] md:text-[44px] lg:text-[48px] leading-tight font-bold tracking-tight">
|
|
||||||
A digital studio & branding powerhouse
|
|
||||||
<br />
|
|
||||||
making cool shit that performs.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-xl text-[10px] sm:text-xs text-gray-300">
|
|
||||||
We partner with the world's most ambitious brands to create powerful brands to unlock their full potential. We go beyond design to create compelling, strategic brand image.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
|
|
||||||
interface HorizontalAccordionProps {
|
|
||||||
trigger: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HorizontalAccordion({
|
|
||||||
trigger,
|
|
||||||
children,
|
|
||||||
className = ""
|
|
||||||
}: HorizontalAccordionProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-col gap-4 ${className}`}>
|
|
||||||
{/* Trigger Button */}
|
|
||||||
<motion.button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors whitespace-nowrap text-lg font-medium"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
{trigger}
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
rotate: isOpen ? 90 : 0,
|
|
||||||
color: isOpen ? '#ff4d00' : '#d1d5db'
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</motion.div>
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
{/* Animated Content */}
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{
|
|
||||||
height: 0,
|
|
||||||
opacity: 0,
|
|
||||||
y: -10
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
height: "auto",
|
|
||||||
opacity: 1,
|
|
||||||
y: 0
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
height: 0,
|
|
||||||
opacity: 0,
|
|
||||||
y: -10
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 0.4,
|
|
||||||
ease: [0.4, 0, 0.2, 1],
|
|
||||||
opacity: { duration: 0.3 }
|
|
||||||
}}
|
|
||||||
className="overflow-hidden w-full"
|
|
||||||
style={{ willChange: "height, opacity, transform" }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ x: -20, opacity: 0 }}
|
|
||||||
animate={{ x: 0, opacity: 1 }}
|
|
||||||
exit={{ x: -20, opacity: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.3,
|
|
||||||
delay: 0.15,
|
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
|
||||||
className="w-full max-w-2xl"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export function InstagramFeed() {
|
|
||||||
useEffect(() => {
|
|
||||||
// Load LightWidget script for Instagram feed
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://cdn.lightwidget.com/widgets/lightwidget.js';
|
|
||||||
script.async = true;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Cleanup script on unmount
|
|
||||||
if (document.body.contains(script)) {
|
|
||||||
document.body.removeChild(script);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-8">
|
|
||||||
<p className="mb-4 text-base sm:text-lg">
|
|
||||||
<strong>Latest from our studio:</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Instagram Feed Grid - Posts Only */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<iframe
|
|
||||||
src="https://cdn.lightwidget.com/widgets/dfd875efe9b05e47b5ff190cc0a71990.html"
|
|
||||||
scrolling="no"
|
|
||||||
className="lightwidget-widget"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '200px',
|
|
||||||
border: 'none',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
export function MissionSection() {
|
|
||||||
return (
|
|
||||||
<section className="py-24 bg-black">
|
|
||||||
<div className="container">
|
|
||||||
<div className="max-w-4xl mx-auto text-center mb-16">
|
|
||||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-white leading-tight">
|
|
||||||
We're here to create the extraordinary. No shortcuts, just bold, precision-engineered work that elevates the game & leaves a mark.
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-bold text-white">Website & Features</h3>
|
|
||||||
<p className="text-gray-300 leading-relaxed">
|
|
||||||
From sleek landing pages to complex web applications, we create responsive, user-friendly websites that look great and function flawlessly on any device.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-bold text-white">Visual Branding</h3>
|
|
||||||
<p className="text-gray-300 leading-relaxed">
|
|
||||||
From logo identities to comprehensive brand guidelines, we build brands that tell a story, resonate with audiences, and stand out in the market.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-bold text-white">UI/UX Design</h3>
|
|
||||||
<p className="text-gray-300 leading-relaxed">
|
|
||||||
We craft intuitive and engaging user interfaces that prioritize usability and accessibility, ensuring a seamless and enjoyable experience for every user.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xl font-bold text-white">Marketing & Growth</h3>
|
|
||||||
<p className="text-gray-300 leading-relaxed">
|
|
||||||
From social media campaigns to SEO optimization, we develop strategies that drive traffic, generate leads, and boost conversions for your business.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -22,10 +22,10 @@ export function Navigation() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 w-full border-b border-gray-800 bg-black/95 backdrop-blur supports-[backdrop-filter]:bg-black/60">
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="container flex h-16 items-center justify-between">
|
<div className="container flex h-16 items-center justify-between">
|
||||||
<Link href="/" className="flex items-center space-x-2">
|
<Link href="/" className="flex items-center space-x-2">
|
||||||
<span className="text-2xl font-bold text-white">basement.</span>
|
<span className="text-2xl font-bold">Biohazard VFX</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
@ -33,7 +33,7 @@ export function Navigation() {
|
|||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<NavigationMenuItem key={item.href}>
|
<NavigationMenuItem key={item.href}>
|
||||||
<NavigationMenuLink asChild active={pathname === item.href}>
|
<NavigationMenuLink asChild active={pathname === item.href}>
|
||||||
<Link href={item.href} className={`${navigationMenuTriggerStyle()} text-white hover:text-gray-300`}>
|
<Link href={item.href} className={navigationMenuTriggerStyle()}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
'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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { Project } from "@/data/projects";
|
|
||||||
|
|
||||||
interface ProjectShowcaseProps {
|
|
||||||
projects: Project[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProjectShowcase({ projects }: ProjectShowcaseProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-16 bg-black">
|
|
||||||
<div className="mx-auto max-w-5xl px-4">
|
|
||||||
<h2 className="text-white text-xl font-bold mb-8">Featured Projects</h2>
|
|
||||||
|
|
||||||
<div className="space-y-16">
|
|
||||||
{projects.map((project) => (
|
|
||||||
<div key={project.id} className="grid grid-cols-12 gap-6 items-start">
|
|
||||||
{/* Image left */}
|
|
||||||
<div className="col-span-12 md:col-span-8">
|
|
||||||
<div className="relative overflow-hidden">
|
|
||||||
<img
|
|
||||||
src={project.thumbnailUrl}
|
|
||||||
alt={project.title}
|
|
||||||
className="w-full h-auto object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Right-side label and description */}
|
|
||||||
<div className="col-span-12 md:col-span-4 text-gray-300 text-xs space-y-2">
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-white">{project.title}</div>
|
|
||||||
</div>
|
|
||||||
<p className="leading-relaxed">
|
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { motion, useScroll, useSpring } from "framer-motion";
|
|
||||||
|
|
||||||
export function ScrollProgressBar() {
|
|
||||||
const { scrollYProgress } = useScroll();
|
|
||||||
const scaleY = useSpring(scrollYProgress, {
|
|
||||||
stiffness: 100,
|
|
||||||
damping: 30,
|
|
||||||
restDelta: 0.001
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="fixed right-0 top-0 bottom-0 w-[3px] origin-top z-50 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
scaleY,
|
|
||||||
backgroundColor: '#ff4d00',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
interface SectionDividerProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SectionDivider({ className = "" }: SectionDividerProps) {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className={`h-[1px] bg-gray-800 my-8 md:my-12 ${className}`}
|
|
||||||
initial={{ scaleX: 0 }}
|
|
||||||
whileInView={{ scaleX: 1 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
|
||||||
style={{ transformOrigin: 'left' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,7 +9,7 @@ interface ServiceCardProps {
|
|||||||
|
|
||||||
export function ServiceCard({ service }: ServiceCardProps) {
|
export function ServiceCard({ service }: ServiceCardProps) {
|
||||||
// Dynamically get the icon component
|
// Dynamically get the icon component
|
||||||
const IconComponent = (LucideIcons as Record<string, unknown>)[service.icon] || LucideIcons.Box;
|
const IconComponent = (LucideIcons as any)[service.icon] || LucideIcons.Box;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full transition-shadow hover:shadow-lg">
|
<Card className="h-full transition-shadow hover:shadow-lg">
|
||||||
|
|||||||
@ -1,703 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
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";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
|
||||||
|
|
||||||
// Animation variants for page load
|
|
||||||
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 TempPlaceholder() {
|
|
||||||
const titleRef = useRef<HTMLHeadingElement | null>(null);
|
|
||||||
const titleInnerRef = useRef<HTMLSpanElement | null>(null);
|
|
||||||
const bioTextRef = useRef<HTMLSpanElement | null>(null);
|
|
||||||
const [titleWidth, setTitleWidth] = useState<number | null>(null);
|
|
||||||
const [bioFontSizePx, setBioFontSizePx] = useState<number | null>(null);
|
|
||||||
const baseBioFontSizeRef = useRef<number | null>(null);
|
|
||||||
const [isEasterEggOpen, setIsEasterEggOpen] = useState(false);
|
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
|
||||||
const [isPigeonEggOpen, setIsPigeonEggOpen] = useState(false);
|
|
||||||
const [pigeonMousePosition, setPigeonMousePosition] = useState({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const measure = () => {
|
|
||||||
const measuredTitleWidth = titleInnerRef.current?.offsetWidth ?? null;
|
|
||||||
setTitleWidth(measuredTitleWidth);
|
|
||||||
|
|
||||||
if (measuredTitleWidth && bioTextRef.current) {
|
|
||||||
const element = bioTextRef.current;
|
|
||||||
if (baseBioFontSizeRef.current === null) {
|
|
||||||
const initialFontSize = parseFloat(getComputedStyle(element).fontSize);
|
|
||||||
baseBioFontSizeRef.current = isNaN(initialFontSize) ? 16 : initialFontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily ensure we measure at base font size
|
|
||||||
const baseFontSize = baseBioFontSizeRef.current ?? 16;
|
|
||||||
const previousInlineFontSize = element.style.fontSize;
|
|
||||||
element.style.fontSize = `${baseFontSize}px`;
|
|
||||||
const bioNaturalWidth = element.offsetWidth;
|
|
||||||
// Restore previous inline style before we set state (will update after render)
|
|
||||||
element.style.fontSize = previousInlineFontSize;
|
|
||||||
|
|
||||||
if (bioNaturalWidth > 0) {
|
|
||||||
// On mobile, use a more conservative scaling to prevent cramped text
|
|
||||||
const isMobile = window.innerWidth < 640; // sm breakpoint
|
|
||||||
const isTablet = window.innerWidth < 1024; // lg breakpoint
|
|
||||||
|
|
||||||
let maxScale;
|
|
||||||
if (isMobile) {
|
|
||||||
maxScale = 0.8; // Limit scaling on mobile
|
|
||||||
} else if (isTablet) {
|
|
||||||
maxScale = 1.2; // Allow more scaling on tablet
|
|
||||||
} else {
|
|
||||||
maxScale = 1.8; // Allow much more scaling on desktop
|
|
||||||
}
|
|
||||||
|
|
||||||
const scale = Math.min(measuredTitleWidth / bioNaturalWidth, maxScale);
|
|
||||||
setBioFontSizePx(baseFontSize * scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
measure();
|
|
||||||
window.addEventListener("resize", measure);
|
|
||||||
return () => window.removeEventListener("resize", measure);
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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.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>
|
|
||||||
</p>
|
|
||||||
<ol className="list-decimal list-inside space-y-3 text-gray-300 leading-relaxed break-words">
|
|
||||||
<li>We needed a website (circa January 2023)</li>
|
|
||||||
<li>We tried to build one on squarespace (that shit sucks)</li>
|
|
||||||
<li>
|
|
||||||
Nicholai figured "I know some html and javascript, why not just{" "}
|
|
||||||
<em>make</em> one."
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
But of course, <strong>the html site sucked</strong> and was
|
|
||||||
difficult to host.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
And naturally, the website for some reason <em>needed</em> to look
|
|
||||||
good.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
So then began a longwinded journey of Nicholai learning <em>react</em>
|
|
||||||
</li>
|
|
||||||
<li>Nicholai should've stuck to python.</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</HorizontalAccordion>
|
|
||||||
</motion.div>
|
|
||||||
<HoverCard openDelay={300}>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.h1
|
|
||||||
onClick={(e) => {
|
|
||||||
setMousePosition({ x: e.clientX, y: e.clientY });
|
|
||||||
setIsEasterEggOpen(true);
|
|
||||||
}}
|
|
||||||
className="text-4xl sm:text-5xl md:text-7xl lg:text-8xl xl:text-9xl font-black mb-4 md:mb-6 font-exo-2 text-center mx-auto leading-none cursor-pointer transition-opacity hover:opacity-80"
|
|
||||||
style={{
|
|
||||||
color: '#000000',
|
|
||||||
textShadow: '2px 2px 0px #ff4d00, 4px 4px 0px #ff4d00',
|
|
||||||
width: titleWidth ? `${titleWidth}px` : undefined
|
|
||||||
}}
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span ref={bioTextRef} className="inline-block" style={{ fontSize: bioFontSizePx ? `${bioFontSizePx}px` : undefined }}>
|
|
||||||
BIOHAZARD
|
|
||||||
</span>
|
|
||||||
</motion.h1>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-auto px-3 py-2 bg-black border-gray-800 text-gray-300 text-sm">
|
|
||||||
Click to reveal
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isEasterEggOpen && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="fixed inset-0 z-50 bg-black/80"
|
|
||||||
onClick={() => setIsEasterEggOpen(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.7, x: '-50%', y: '-50%' }}
|
|
||||||
animate={{ opacity: 1, scale: 1, x: '-50%', y: '-50%' }}
|
|
||||||
exit={{ opacity: 0, scale: 0.7, x: '-50%', y: '-50%' }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.4,
|
|
||||||
ease: [0.16, 1, 0.3, 1]
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
left: mousePosition.x,
|
|
||||||
top: mousePosition.y,
|
|
||||||
}}
|
|
||||||
className="z-50 w-[90vw] max-w-[350px]"
|
|
||||||
onMouseLeave={() => setIsEasterEggOpen(false)}
|
|
||||||
>
|
|
||||||
<div className="relative bg-black overflow-hidden shadow-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.3,
|
|
||||||
delay: 0.1,
|
|
||||||
ease: [0.16, 1, 0.3, 1]
|
|
||||||
}}
|
|
||||||
className="relative w-full aspect-square"
|
|
||||||
>
|
|
||||||
<DepthMap
|
|
||||||
originalImg="/OLIVER.jpeg"
|
|
||||||
depthImg="/OLIVER_depth.jpeg"
|
|
||||||
verticalThreshold={40}
|
|
||||||
horizontalThreshold={70}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{isPigeonEggOpen && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="fixed inset-0 z-50 bg-black/80"
|
|
||||||
onClick={() => setIsPigeonEggOpen(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.7, x: '-50%', y: '-50%' }}
|
|
||||||
animate={{ opacity: 1, scale: 1, x: '-50%', y: '-50%' }}
|
|
||||||
exit={{ opacity: 0, scale: 0.7, x: '-50%', y: '-50%' }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.4,
|
|
||||||
ease: [0.16, 1, 0.3, 1]
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
left: pigeonMousePosition.x,
|
|
||||||
top: pigeonMousePosition.y,
|
|
||||||
}}
|
|
||||||
className="z-50 w-[90vw] max-w-[400px]"
|
|
||||||
onMouseLeave={() => setIsPigeonEggOpen(false)}
|
|
||||||
>
|
|
||||||
<div className="relative bg-black overflow-hidden shadow-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.3,
|
|
||||||
delay: 0.1,
|
|
||||||
ease: [0.16, 1, 0.3, 1]
|
|
||||||
}}
|
|
||||||
className="relative w-full"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/no_pigeons_zone.gif"
|
|
||||||
alt="No pigeons zone"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
className="w-full h-auto"
|
|
||||||
unoptimized
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<motion.p
|
|
||||||
className="mb-6 md:mb-8 text-base sm:text-lg text-gray-300"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<strong>> Here's our reel:</strong>
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<ReelPlayer src="/reel.mp4" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<SectionDivider />
|
|
||||||
|
|
||||||
<motion.p
|
|
||||||
className="mb-4 md:mb-6 text-base sm:text-lg"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<strong>> Some projects we've worked on:</strong>
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.ul
|
|
||||||
className="mb-6 md:mb-8 space-y-3 md:space-y-4 text-sm sm:text-base"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.instagram.com/biohazardvfx/"
|
|
||||||
className="block leading-relaxed relative"
|
|
||||||
style={{ color: '#ff4d00' }}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
whileHover={{ opacity: 0.8 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<span className="relative inline-block">
|
|
||||||
Gstar Raw - Pommelhorse
|
|
||||||
<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.li>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=4QIZE708gJ4"
|
|
||||||
className="block leading-relaxed 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">
|
|
||||||
Post Malone - I Had Some Help<br className="sm:hidden" />
|
|
||||||
<span className="sm:hidden">(feat. Morgan Wallen)</span>
|
|
||||||
<span className="hidden sm:inline"> (feat. Morgan Wallen)</span>
|
|
||||||
<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>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-80 p-0 bg-black border-gray-800 z-50">
|
|
||||||
<VideoPreview
|
|
||||||
videoId="4QIZE708gJ4"
|
|
||||||
title="Post Malone - I Had Some Help (feat. Morgan Wallen)"
|
|
||||||
/>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</motion.li>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=z2tUpLHdd4M"
|
|
||||||
className="block leading-relaxed 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">
|
|
||||||
The Wait Is Over | OFFICIAL TRAILER
|
|
||||||
<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>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-80 p-0 bg-black border-gray-800 z-50">
|
|
||||||
<VideoPreview
|
|
||||||
videoId="z2tUpLHdd4M"
|
|
||||||
title="The Wait Is Over | OFFICIAL TRAILER"
|
|
||||||
/>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</motion.li>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=RCZ9wl1Up40"
|
|
||||||
className="block leading-relaxed 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">
|
|
||||||
2hollis Star Album Trailer
|
|
||||||
<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>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-80 p-0 bg-black border-gray-800 z-50">
|
|
||||||
<VideoPreview
|
|
||||||
videoId="RCZ9wl1Up40"
|
|
||||||
title="2hollis Star Album Trailer"
|
|
||||||
/>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</motion.li>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=yLxoVrFpLmQ"
|
|
||||||
className="block leading-relaxed 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">
|
|
||||||
Thanksgiving With Kai, Kevin & Druski
|
|
||||||
<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>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-80 p-0 bg-black border-gray-800 z-50">
|
|
||||||
<VideoPreview
|
|
||||||
videoId="yLxoVrFpLmQ"
|
|
||||||
title="Thanksgiving With Kai, Kevin & Druski"
|
|
||||||
/>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</motion.li>
|
|
||||||
<motion.li
|
|
||||||
className="flex items-start"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 mr-3 mt-1">•</span>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=a2Zqdo9RbNs"
|
|
||||||
className="block leading-relaxed 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">
|
|
||||||
ENHYPEN (엔하이픈) Bad Desire<br className="sm:hidden" />
|
|
||||||
<span className="sm:hidden">(With or Without You) Official MV</span>
|
|
||||||
<span className="hidden sm:inline"> (With or Without You) Official MV</span>
|
|
||||||
<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>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent className="w-80 p-0 bg-black border-gray-800 z-50">
|
|
||||||
<VideoPreview
|
|
||||||
videoId="a2Zqdo9RbNs"
|
|
||||||
title="ENHYPEN (엔하이픈) Bad Desire (With or Without You) Official MV"
|
|
||||||
/>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</motion.li>
|
|
||||||
</motion.ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<SectionDivider />
|
|
||||||
|
|
||||||
{/* Studio Section */}
|
|
||||||
<section id="studio" className="mb-16 md:mb-20">
|
|
||||||
<motion.div
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<InstagramFeed />
|
|
||||||
</motion.div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<SectionDivider />
|
|
||||||
|
|
||||||
{/* Contact Section */}
|
|
||||||
<section id="contact">
|
|
||||||
<motion.div
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<motion.p
|
|
||||||
className="mb-4 text-sm sm:text-base text-gray-300"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<strong>> Contact us:</strong>{" "}
|
|
||||||
<motion.a
|
|
||||||
href="mailto:contact@biohazardvfx.com"
|
|
||||||
className="break-words inline-block 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">
|
|
||||||
contact@biohazardvfx.com
|
|
||||||
<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.p
|
|
||||||
className="text-sm sm:text-base text-gray-300 pb-6 border-b border-white/10"
|
|
||||||
variants={itemVariants}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<strong>> File a complaint:</strong>{" "}
|
|
||||||
<motion.a
|
|
||||||
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
className="break-words inline-block 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">
|
|
||||||
help@biohazardvfx.com
|
|
||||||
<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.p
|
|
||||||
onClick={(e) => {
|
|
||||||
setPigeonMousePosition({ x: e.clientX, y: e.clientY });
|
|
||||||
setIsPigeonEggOpen(true);
|
|
||||||
}}
|
|
||||||
className="text-xs text-gray-600 mt-4 cursor-pointer hover:text-gray-500 transition-colors"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
|
|
||||||
>
|
|
||||||
No pigeons allowed in this zone
|
|
||||||
</motion.p>
|
|
||||||
</motion.div>
|
|
||||||
</section>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
|
|
||||||
interface VideoPreviewProps {
|
|
||||||
videoId: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoPreview({ videoId, title }: VideoPreviewProps) {
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full" style={{ width: 320, height: 180 }}>
|
|
||||||
<AnimatePresence>
|
|
||||||
{isLoading && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="absolute inset-0 bg-black rounded-md flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
className="w-16 h-16 border-2 border-gray-800 rounded-md"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<iframe
|
|
||||||
width="320"
|
|
||||||
height="180"
|
|
||||||
src={`https://www.youtube.com/embed/${videoId}`}
|
|
||||||
title={title}
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
className="rounded-md border-0"
|
|
||||||
onLoad={() => setIsLoading(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AccordionItem.displayName = "AccordionItem"
|
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
))
|
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
))
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const alertVariants = cva(
|
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDescription.displayName = "AlertDescription"
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<HoverCardPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
||||||
@ -13,47 +13,47 @@ export interface Project {
|
|||||||
export const projects: Project[] = [
|
export const projects: Project[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
title: "Wound Shop",
|
title: "Cinematic VFX Breakdown",
|
||||||
description: "Partnering with Wound Shop means building the fastest, most performant websites. We craft beautiful, responsive designs that make for an enjoyable user experience.",
|
description: "High-end visual effects for feature film production",
|
||||||
category: "Web Development",
|
category: "Film",
|
||||||
thumbnailUrl: "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80",
|
thumbnailUrl: "https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80",
|
||||||
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
featured: true,
|
featured: true,
|
||||||
tags: ["Web", "Development", "Design"],
|
tags: ["VFX", "Film", "CGI"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
title: "Daylight",
|
title: "Commercial Product Spot",
|
||||||
description: "A bold vision needs a strong launch. We crafted a dynamic brand identity for Daylight, ensuring it captured their innovative spirit and stood out in a crowded market, establishing a fresh, modern, and trustworthy presence.",
|
description: "Stunning product visualization and motion graphics",
|
||||||
category: "Branding",
|
category: "Commercial",
|
||||||
thumbnailUrl: "https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?w=800&q=80",
|
thumbnailUrl: "https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?w=800&q=80",
|
||||||
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
featured: true,
|
featured: true,
|
||||||
tags: ["Branding", "Identity", "Launch"],
|
tags: ["Commercial", "Motion Graphics"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
title: "KidSuper",
|
title: "Music Video Effects",
|
||||||
description: "Working with KidSuper, we developed a vibrant and playful brand identity that reflects their unique approach to fashion. Our designs brought their creative vision to life, ensuring a cohesive and memorable brand experience.",
|
description: "Creative visual effects for music production",
|
||||||
category: "Fashion",
|
category: "Music Video",
|
||||||
thumbnailUrl: "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=800&q=80",
|
thumbnailUrl: "https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=800&q=80",
|
||||||
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||||
aspectRatio: 9 / 16,
|
aspectRatio: 9 / 16,
|
||||||
featured: true,
|
featured: true,
|
||||||
tags: ["Fashion", "Creative", "Brand"],
|
tags: ["Music Video", "Creative"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "4",
|
id: "4",
|
||||||
title: "Shop MrBeast",
|
title: "Sci-Fi Environment Design",
|
||||||
description: "The world's biggest YouTuber needed a storefront that could keep up. We built a robust e-commerce platform for MrBeast, handling massive traffic and ensuring a seamless shopping experience for his millions of fans.",
|
description: "Futuristic world building and environment creation",
|
||||||
category: "E-commerce",
|
category: "Film",
|
||||||
thumbnailUrl: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80",
|
thumbnailUrl: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80",
|
||||||
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||||
aspectRatio: 21 / 9,
|
aspectRatio: 21 / 9,
|
||||||
featured: true,
|
featured: false,
|
||||||
tags: ["E-commerce", "Platform", "Scale"],
|
tags: ["VFX", "Environment", "3D"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "5",
|
id: "5",
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
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"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
// Instagram API utilities for fetching posts and media
|
|
||||||
// Based on Instagram Graph API documentation from Context7
|
|
||||||
|
|
||||||
export interface InstagramPost {
|
|
||||||
id: string;
|
|
||||||
caption: string;
|
|
||||||
media_url: string;
|
|
||||||
permalink: string;
|
|
||||||
timestamp: string;
|
|
||||||
media_type: string;
|
|
||||||
thumbnail_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instagram Graph API oEmbed endpoint for getting embeddable HTML
|
|
||||||
export async function getInstagramEmbedHtml(postUrl: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://graph.facebook.com/v23.0/instagram_oembed?url=${encodeURIComponent(postUrl)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch Instagram embed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data.html || null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching Instagram embed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch Instagram page media using Graph API (requires access token)
|
|
||||||
export async function getInstagramPageMedia(
|
|
||||||
pageId: string,
|
|
||||||
accessToken: string,
|
|
||||||
limit: number = 6
|
|
||||||
): Promise<InstagramPost[]> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://graph.facebook.com/v23.0/${pageId}/media?fields=id,caption,media_url,permalink,timestamp,media_type,thumbnail_url&limit=${limit}&access_token=${accessToken}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch Instagram media');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching Instagram media:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alternative: Use a third-party service like EmbedSocial or SnapWidget
|
|
||||||
export function getEmbedSocialWidget(username: string): string {
|
|
||||||
// This would return the embed code for services like EmbedSocial
|
|
||||||
return `
|
|
||||||
<div class="embedsocial-hashtag" data-ref="instagram" data-hashtag="${username}">
|
|
||||||
<a class="feed-powered-by-es" href="https://embedsocial.com/instagram/" target="_blank" title="Widget by EmbedSocial">Widget by EmbedSocial</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample data for development/demo purposes
|
|
||||||
export const sampleInstagramPosts: InstagramPost[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
caption: "Working on some sick VFX for an upcoming project... 🔥",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=1",
|
|
||||||
permalink: "https://www.instagram.com/p/sample1/",
|
|
||||||
timestamp: "2025-01-10T10:00:00Z",
|
|
||||||
media_type: "IMAGE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
caption: "Behind the scenes of our latest work 🎬",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=2",
|
|
||||||
permalink: "https://www.instagram.com/p/sample2/",
|
|
||||||
timestamp: "2025-01-08T15:30:00Z",
|
|
||||||
media_type: "VIDEO",
|
|
||||||
thumbnail_url: "https://picsum.photos/400/400?random=2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
caption: "Studio life - coffee, code, and VFX magic ✨",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=3",
|
|
||||||
permalink: "https://www.instagram.com/p/sample3/",
|
|
||||||
timestamp: "2025-01-05T12:00:00Z",
|
|
||||||
media_type: "IMAGE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
caption: "Client project reveal - can't wait to share this one! 🚀",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=4",
|
|
||||||
permalink: "https://www.instagram.com/p/sample4/",
|
|
||||||
timestamp: "2025-01-03T14:20:00Z",
|
|
||||||
media_type: "IMAGE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
caption: "Late night render session - the magic happens when everyone else is sleeping 🌙",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=5",
|
|
||||||
permalink: "https://www.instagram.com/p/sample5/",
|
|
||||||
timestamp: "2025-01-01T22:30:00Z",
|
|
||||||
media_type: "VIDEO",
|
|
||||||
thumbnail_url: "https://picsum.photos/400/400?random=5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "6",
|
|
||||||
caption: "New year, new projects, same dedication to quality VFX 💪",
|
|
||||||
media_url: "https://picsum.photos/400/400?random=6",
|
|
||||||
permalink: "https://www.instagram.com/p/sample6/",
|
|
||||||
timestamp: "2024-12-30T09:00:00Z",
|
|
||||||
media_type: "IMAGE"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
const { pathname } = request.nextUrl;
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect all other routes to home
|
|
||||||
return NextResponse.redirect(new URL('/', request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
/*
|
|
||||||
* Match all request paths except for the ones starting with:
|
|
||||||
* - api (API routes)
|
|
||||||
* - _next/static (static files)
|
|
||||||
* - _next/image (image optimization files)
|
|
||||||
* - favicon.ico (favicon file)
|
|
||||||
*/
|
|
||||||
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,18 +1,27 @@
|
|||||||
# Cloudflare Workers configuration for Next.js
|
# Cloudflare Workers configuration
|
||||||
|
# Update this file with your Cloudflare account details and deployment settings
|
||||||
|
|
||||||
name = "biohazard-vfx-website"
|
name = "biohazard-vfx-website"
|
||||||
account_id = "a19f770b9be1b20e78b8d25bdcfd3bbd"
|
compatibility_date = "2024-01-01"
|
||||||
compatibility_date = "2024-09-23"
|
main = ".open-next/worker.mjs"
|
||||||
compatibility_flags = ["nodejs_compat"]
|
|
||||||
main = ".open-next/worker.js"
|
|
||||||
|
|
||||||
# Custom domains
|
# Account ID and other deployment details should be configured through environment variables
|
||||||
routes = [
|
# or added here after initial setup
|
||||||
{ pattern = "biohazardvfx.com/*", zone_name = "biohazardvfx.com" },
|
|
||||||
{ pattern = "www.biohazardvfx.com/*", zone_name = "biohazardvfx.com" }
|
|
||||||
]
|
|
||||||
|
|
||||||
# Assets binding for OpenNext
|
[site]
|
||||||
[assets]
|
bucket = ".open-next/assets"
|
||||||
directory = ".open-next/assets"
|
|
||||||
binding = "ASSETS"
|
# Environment variables
|
||||||
|
[vars]
|
||||||
|
# Add your environment variables here
|
||||||
|
# EXAMPLE_VAR = "value"
|
||||||
|
|
||||||
|
# Uncomment and configure for production
|
||||||
|
# [env.production]
|
||||||
|
# name = "biohazard-vfx-website-production"
|
||||||
|
# route = "yourdomain.com/*"
|
||||||
|
|
||||||
|
# Uncomment and configure for preview/staging
|
||||||
|
# [env.preview]
|
||||||
|
# name = "biohazard-vfx-website-preview"
|
||||||
|
|
||||||
|
|||||||