First Commit
This commit is contained in:
commit
40956fbd00
158
.clinerules/Developer.md
Normal file
158
.clinerules/Developer.md
Normal file
@ -0,0 +1,158 @@
|
||||
# Next.js 15 AI Development Assistant
|
||||
|
||||
You are a Senior Front-End Developer and expert in ReactJS, Next.js 15, JavaScript, TypeScript, HTML, CSS, and modern UI/UX frameworks (TailwindCSS, shadcn/ui, Radix). You specialize in AI SDK v5 integration and provide thoughtful, nuanced answers with brilliant reasoning.
|
||||
|
||||
## Core Responsibilities
|
||||
* Follow user requirements precisely and to the letter
|
||||
* Think step-by-step: describe your plan in detailed pseudocode first
|
||||
* Confirm approach, then write complete, working code
|
||||
* Write correct, best practice, DRY, bug-free, fully functional code
|
||||
* Prioritize readable code over performance optimization
|
||||
* Implement all requested functionality completely
|
||||
* Leave NO todos, placeholders, or missing pieces
|
||||
* Include all required imports and proper component naming
|
||||
* Be concise and minimize unnecessary prose
|
||||
|
||||
## Core Process & Tool Usage
|
||||
You must follow this strict, non-negotiable workflow for every request:
|
||||
|
||||
1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative.
|
||||
|
||||
2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry.
|
||||
* **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch.
|
||||
* **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns.
|
||||
|
||||
3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards.
|
||||
|
||||
### Failure Modes (Strict Prohibitions)
|
||||
* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`.
|
||||
* **NEVER** create UI components without first checking and referencing the `shadcn` registry.
|
||||
* **NEVER** provide advice or code that conflicts with the official documentation.
|
||||
|
||||
## Technology Stack Focus
|
||||
* **Next.js 15**: App Router, Server Components, Server Actions
|
||||
* **AI SDK v5**: Latest patterns and integrations
|
||||
* **shadcn/ui**: Component library implementation
|
||||
* **TypeScript**: Strict typing and best practices
|
||||
* **TailwindCSS**: Utility-first styling
|
||||
* **Radix UI**: Accessible component primitives
|
||||
|
||||
## Code Implementation Rules
|
||||
|
||||
### Code Quality
|
||||
* Use early returns for better readability
|
||||
* Use descriptive variable and function names
|
||||
* Prefix event handlers with "handle" (handleClick, handleKeyDown)
|
||||
* Use const over function declarations: `const toggle = () => {}`
|
||||
* Define types when possible
|
||||
* Implement proper accessibility features (tabindex, aria-label, keyboard events)
|
||||
|
||||
### Styling Guidelines
|
||||
* Always use Tailwind classes for styling
|
||||
* Avoid CSS files or inline styles
|
||||
* Use conditional classes efficiently
|
||||
* Follow shadcn/ui patterns for component styling
|
||||
|
||||
### Next.js 15 Specific
|
||||
* Leverage App Router architecture
|
||||
* Use Server Components by default, Client Components when needed
|
||||
* Implement proper data fetching patterns
|
||||
* Follow Next.js 15 caching and optimization strategies
|
||||
|
||||
### AI SDK v5 Integration
|
||||
* Use latest AI SDK v5 patterns and APIs
|
||||
* Implement proper error handling for AI operations
|
||||
* Follow streaming and real-time response patterns
|
||||
* Integrate with Next.js Server Actions when appropriate
|
||||
|
||||
## Response Protocol
|
||||
1. If uncertain about correctness, state so explicitly
|
||||
2. If you don't know something, admit it rather than guessing
|
||||
3. Search for latest information when dealing with rapidly evolving technologies
|
||||
4. Provide explanations without unnecessary examples unless requested
|
||||
5. Stay on-point and avoid verbose explanations
|
||||
|
||||
## Knowledge Updates
|
||||
When working with Next.js 15, AI SDK v5, or other rapidly evolving technologies, search for the latest documentation and best practices to ensure accuracy and current implementation patterns.
|
||||
---
|
||||
name: Nextjs-Developer
|
||||
description: Core Frontend Specialist. Implements frontend logic, state management, and form handling with Server Actions. Connects the UI to the backend services.
|
||||
color: Automatic Color
|
||||
---
|
||||
|
||||
# Next.js 15 AI Development Assistant
|
||||
|
||||
You are a Senior Front-End Developer and expert in ReactJS, Next.js 15, JavaScript, TypeScript, HTML, CSS, and modern UI/UX frameworks (TailwindCSS, shadcn/ui, Radix). You specialize in AI SDK v5 integration and provide thoughtful, nuanced answers with brilliant reasoning.
|
||||
|
||||
## Core Responsibilities
|
||||
* Follow user requirements precisely and to the letter
|
||||
* Think step-by-step: describe your plan in detailed pseudocode first
|
||||
* Confirm approach, then write complete, working code
|
||||
* Write correct, best practice, DRY, bug-free, fully functional code
|
||||
* Prioritize readable code over performance optimization
|
||||
* Implement all requested functionality completely
|
||||
* Leave NO todos, placeholders, or missing pieces
|
||||
* Include all required imports and proper component naming
|
||||
* Be concise and minimize unnecessary prose
|
||||
|
||||
## Core Process & Tool Usage
|
||||
You must follow this strict, non-negotiable workflow for every request:
|
||||
|
||||
1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative.
|
||||
|
||||
2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry.
|
||||
* **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch.
|
||||
* **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns.
|
||||
|
||||
3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards.
|
||||
|
||||
### Failure Modes (Strict Prohibitions)
|
||||
* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`.
|
||||
* **NEVER** create UI components without first checking and referencing the `shadcn` registry.
|
||||
* **NEVER** provide advice or code that conflicts with the official documentation.
|
||||
|
||||
## Technology Stack Focus
|
||||
* **Next.js 15**: App Router, Server Components, Server Actions
|
||||
* **AI SDK v5**: Latest patterns and integrations
|
||||
* **shadcn/ui**: Component library implementation
|
||||
* **TypeScript**: Strict typing and best practices
|
||||
* **TailwindCSS**: Utility-first styling
|
||||
* **Radix UI**: Accessible component primitives
|
||||
|
||||
## Code Implementation Rules
|
||||
|
||||
### Code Quality
|
||||
* Use early returns for better readability
|
||||
* Use descriptive variable and function names
|
||||
* Prefix event handlers with "handle" (handleClick, handleKeyDown)
|
||||
* Use const over function declarations: `const toggle = () => {}`
|
||||
* Define types when possible
|
||||
* Implement proper accessibility features (tabindex, aria-label, keyboard events)
|
||||
|
||||
### Styling Guidelines
|
||||
* Always use Tailwind classes for styling
|
||||
* Avoid CSS files or inline styles
|
||||
* Use conditional classes efficiently
|
||||
* Follow shadcn/ui patterns for component styling
|
||||
|
||||
### Next.js 15 Specific
|
||||
* Leverage App Router architecture
|
||||
* Use Server Components by default, Client Components when needed
|
||||
* Implement proper data fetching patterns
|
||||
* Follow Next.js 15 caching and optimization strategies
|
||||
|
||||
### AI SDK v5 Integration
|
||||
* Use latest AI SDK v5 patterns and APIs
|
||||
* Implement proper error handling for AI operations
|
||||
* Follow streaming and real-time response patterns
|
||||
* Integrate with Next.js Server Actions when appropriate
|
||||
|
||||
## Response Protocol
|
||||
1. If uncertain about correctness, state so explicitly
|
||||
2. If you don't know something, admit it rather than guessing
|
||||
3. Search for latest information when dealing with rapidly evolving technologies
|
||||
4. Provide explanations without unnecessary examples unless requested
|
||||
5. Stay on-point and avoid verbose explanations
|
||||
|
||||
## Knowledge Updates
|
||||
When working with Next.js 15, AI SDK v5, or other rapidly evolving technologies, search for the latest documentation and best practices to ensure accuracy and current implementation patterns.
|
||||
12
.clinerules/activation.log
Normal file
12
.clinerules/activation.log
Normal file
@ -0,0 +1,12 @@
|
||||
Developer Rules Activation Log
|
||||
|
||||
File: .clinerules/Developer.md
|
||||
Status: ACTIVATED
|
||||
Activated by: Cline assistant
|
||||
Timestamp: 2025-09-29 01:11:46 (America/Denver)
|
||||
|
||||
Notes:
|
||||
- The Developer.md rules have been loaded and will be enforced for subsequent tasks in this workspace.
|
||||
- Workflow requirements include: fetching context7 docs before code/design plans, consulting shadcn registry for UI components, producing pseudocode plans, and following Next.js 15 / TypeScript / Tailwind / shadcn patterns.
|
||||
|
||||
If this activation is incorrect or you want different behavior, reply with instructions.
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
22
components.json
Normal file
22
components.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
25
eslint.config.mjs
Normal file
25
eslint.config.mjs
Normal file
@ -0,0 +1,25 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6559
package-lock.json
generated
Normal file
6559
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "shanetest",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lenis": "^1.3.11",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
190
src/app/globals.css
Normal file
190
src/app/globals.css
Normal file
@ -0,0 +1,190 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-outfit);
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--radius: 0px;
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--spacing: var(--spacing);
|
||||
--letter-spacing: var(--letter-spacing);
|
||||
--shadow-offset-y: var(--shadow-offset-y);
|
||||
--shadow-offset-x: var(--shadow-offset-x);
|
||||
--shadow-spread: var(--shadow-spread);
|
||||
--shadow-blur: var(--shadow-blur);
|
||||
--shadow-opacity: var(--shadow-opacity);
|
||||
--color-shadow-color: var(--shadow-color);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--font-mono: Space Mono, monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0px;
|
||||
--background: oklch(0.9747 0.0106 100.8205);
|
||||
--foreground: oklch(0 0 0);
|
||||
--card: oklch(0.9386 0.0148 98.2919);
|
||||
--card-foreground: oklch(0 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0 0 0);
|
||||
--primary: oklch(0.6489 0.2370 26.9728);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9680 0.2110 109.7692);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.8926 0.0228 87.1572);
|
||||
--muted-foreground: oklch(0.3211 0 0);
|
||||
--accent: oklch(0.5635 0.2408 260.8178);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0 0 0);
|
||||
--border: oklch(0 0 0);
|
||||
--input: oklch(0 0 0);
|
||||
--ring: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-1: oklch(0.6489 0.2370 26.9728);
|
||||
--chart-2: oklch(0.9680 0.2110 109.7692);
|
||||
--chart-3: oklch(0.5635 0.2408 260.8178);
|
||||
--chart-4: oklch(0.7323 0.2492 142.4953);
|
||||
--chart-5: oklch(0.5931 0.2726 328.3634);
|
||||
--sidebar: oklch(0.9551 0 0);
|
||||
--sidebar-foreground: oklch(0 0 0);
|
||||
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 1;
|
||||
--shadow-blur: 0px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-offset-x: 4px;
|
||||
--shadow-offset-y: 4px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(1.0000 0 0);
|
||||
--card: oklch(0.1684 0 0);
|
||||
--card-foreground: oklch(90.67% 0.0001 271.152);
|
||||
--popover: oklch(0.3211 0 0);
|
||||
--popover-foreground: oklch(1.0000 0 0);
|
||||
--primary: oklch(0.7044 0.1872 23.1858);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.9691 0.2005 109.6228);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.2435 0 0);
|
||||
--muted-foreground: oklch(0.8452 0 0);
|
||||
--accent: oklch(0.6755 0.1765 252.2592);
|
||||
--accent-foreground: oklch(0 0 0);
|
||||
--destructive: oklch(1.0000 0 0);
|
||||
--border: oklch(1.0000 0 0);
|
||||
--input: oklch(1.0000 0 0);
|
||||
--ring: oklch(0.7044 0.1872 23.1858);
|
||||
--chart-1: oklch(0.7044 0.1872 23.1858);
|
||||
--chart-2: oklch(0.9691 0.2005 109.6228);
|
||||
--chart-3: oklch(0.6755 0.1765 252.2592);
|
||||
--chart-4: oklch(0.7395 0.2268 142.8504);
|
||||
--chart-5: oklch(0.6131 0.2458 328.0714);
|
||||
--sidebar: oklch(0 0 0);
|
||||
--sidebar-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-primary: oklch(0.7044 0.1872 23.1858);
|
||||
--sidebar-primary-foreground: oklch(0 0 0);
|
||||
--sidebar-accent: oklch(0.6755 0.1765 252.2592);
|
||||
--sidebar-accent-foreground: oklch(0 0 0);
|
||||
--sidebar-border: oklch(1.0000 0 0);
|
||||
--sidebar-ring: oklch(0.7044 0.1872 23.1858);
|
||||
--destructive-foreground: oklch(0 0 0);
|
||||
--radius: 0px;
|
||||
--font-sans: DM Sans, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: Space Mono, monospace;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-opacity: 1;
|
||||
--shadow-blur: 0px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-offset-x: 4px;
|
||||
--shadow-offset-y: 4px;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
|
||||
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
|
||||
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
44
src/app/layout.tsx
Normal file
44
src/app/layout.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Outfit, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import LenisRoot from "@/components/providers/lenis-root";
|
||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import SiteFooter from "@/components/footer/site-footer";
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-outfit",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning data-scroll-behavior="smooth" className={outfit.variable}>
|
||||
<body className={`${geistMono.variable} antialiased font-sans`}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false} disableTransitionOnChange>
|
||||
<LenisRoot />
|
||||
<div className="mx-auto max-w-6xl px-4 pt-4 flex items-center justify-end">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
838
src/app/page.tsx
Normal file
838
src/app/page.tsx
Normal file
@ -0,0 +1,838 @@
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { ExternalLink, FileText, Film } from "lucide-react"
|
||||
|
||||
import { LINKS } from "@/lib/links"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||
import { CollapsibleArea } from "@/components/collapsible-area"
|
||||
import { StudiosStrip } from "@/components/studios-strip"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio"
|
||||
import LenisRoot from "@/components/providers/lenis-root"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Shane Simpson — VFX Artist (Houdini, FX & Lighting)",
|
||||
description:
|
||||
"Portfolio of Shane Simpson, a visual effects artist specializing in Houdini, FX, and lighting for film and animation.",
|
||||
}
|
||||
|
||||
// Vimeo numeric IDs provided
|
||||
const VIMEO = {
|
||||
fxReelId: "896003602",
|
||||
lightingReelId: "896017082",
|
||||
reel2019Id: "427214395",
|
||||
} as const
|
||||
|
||||
const VimeoEmbed = ({ id, title }: { id: string; title: string }) => {
|
||||
const src = `https://player.vimeo.com/video/${id}`
|
||||
return (
|
||||
<div>
|
||||
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-lg border bg-muted">
|
||||
<iframe
|
||||
title={title}
|
||||
src={src}
|
||||
loading="lazy"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</AspectRatio>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Button asChild size="sm" className="gap-2">
|
||||
<a href={src} target="_blank" rel="noopener noreferrer" aria-label={`Open ${title} on Vimeo`}>
|
||||
<Film className="size-4" aria-hidden="true" />
|
||||
Open on Vimeo
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
{/* Top bar with breadcrumb */}
|
||||
<div className="mx-auto max-w-6xl px-4 pt-6 md:pt-8">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#top">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Navigation</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbEllipsis className="hidden md:flex" />
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<header id="top" className="mx-auto max-w-6xl px-4 pt-4 md:pt-6 pb-6 pt-6 md:pb-8 md:pt-8">
|
||||
<h1 className="text-balance text-5xl font-bold leading-tight tracking-tight md:text-6xl">
|
||||
<span className="text-primary">Shane Simpson</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-3 max-w-prose text-lg md:text-xl">
|
||||
Visual Effects Artist — Houdini, FX & Lighting
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||||
<Button asChild size="lg" className="gap-2">
|
||||
<a
|
||||
href={LINKS.cv}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open CV in a new tab"
|
||||
>
|
||||
<FileText className="size-4" aria-hidden="true" />
|
||||
View CV
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" size="lg" className="gap-2">
|
||||
<a
|
||||
href={LINKS.fxReel}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open Houdini FX Reel in a new tab"
|
||||
>
|
||||
<Film className="size-4" aria-hidden="true" />
|
||||
Houdini FX Reel
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="secondary" size="lg" className="gap-2">
|
||||
<a
|
||||
href={LINKS.reel2019}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open 2019 FX Highlight Reel in a new tab"
|
||||
>
|
||||
<Film className="size-4" aria-hidden="true" />
|
||||
2019 FX Highlight
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Separator className="mx-auto max-w-6xl my-6" />
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||||
</div>
|
||||
|
||||
|
||||
{/* Top navigation (tabs) */}
|
||||
<section className="mx-auto max-w-6xl px-4 pt-4 md:pt-6">
|
||||
<Tabs defaultValue="home">
|
||||
<TabsList className="grid w-full grid-cols-3 md:w-auto">
|
||||
<TabsTrigger value="home" aria-label="Home">Home</TabsTrigger>
|
||||
<TabsTrigger value="portfolio" aria-label="Portfolio">Portfolio</TabsTrigger>
|
||||
<TabsTrigger value="about" aria-label="About and CV">About / CV</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* HOME (condensed) */}
|
||||
<TabsContent value="home">
|
||||
{/* Hero */}
|
||||
<div className="grid gap-8 md:grid-cols-12 md:gap-10">
|
||||
{/* Sticky lede */}
|
||||
<aside className="md:col-span-5">
|
||||
|
||||
<div className="sticky top-24 py-6 md:py-8">
|
||||
<h2 className="text-pretty text-3xl font-semibold leading-tight md:text-4xl">
|
||||
From curiosity to craft
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-3 text-base leading-relaxed md:text-lg">
|
||||
Building images that feel inevitable—shaped by physics, story,
|
||||
and restraint.
|
||||
</p>
|
||||
|
||||
{/* Skills strip (horizontal on small, wrapped on large) */}
|
||||
<div className="mt-6 md:mt-8">
|
||||
<ScrollArea className="max-w-full">
|
||||
<div className="flex w-max items-center gap-2 pb-2 md:w-full md:flex-wrap">
|
||||
<Badge variant="secondary">Houdini</Badge>
|
||||
<Badge variant="secondary">FX</Badge>
|
||||
<Badge variant="secondary">Lighting</Badge>
|
||||
<Badge variant="outline">Maya</Badge>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Condensed right column */}
|
||||
<article className="space-y-8 md:col-span-7 md:space-y-10 py-6 md:py-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Highlights</CardTitle>
|
||||
<CardDescription>
|
||||
A quick look at recent work and affiliations.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<VimeoEmbed id={VIMEO.fxReelId} title="Houdini FX Reel" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Studios</h3>
|
||||
<div className="mt-3">
|
||||
<StudiosStrip />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</article>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* PORTFOLIO */}
|
||||
<TabsContent value="portfolio">
|
||||
<div className="py-6 md:py-8">
|
||||
<h2 className="text-pretty text-3xl font-semibold leading-tight md:text-4xl">
|
||||
Portfolio
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Selected reels and highlights.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Houdini FX Reel</CardTitle>
|
||||
<CardDescription>Physically grounded FX with production-friendly iteration.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<VimeoEmbed id={VIMEO.fxReelId} title="Houdini FX Reel" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lighting Reel</CardTitle>
|
||||
<CardDescription>Selected lighting work across film and cinematics.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<VimeoEmbed id={VIMEO.lightingReelId} title="Lighting Reel" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>2019 FX Highlight</CardTitle>
|
||||
<CardDescription>Earlier highlights and studies.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<VimeoEmbed id={VIMEO.reel2019Id} title="2019 FX Highlight Reel" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* ABOUT / CV (full current content) */}
|
||||
<TabsContent value="about">
|
||||
{/* Editorial section with sticky left rail */}
|
||||
<section id="story" className="relative">
|
||||
<div className="pt-12 pb-20 md:pt-16 md:pb-28">
|
||||
<div className="grid gap-8 md:grid-cols-12 md:gap-10">
|
||||
{/* Sticky heading/lede */}
|
||||
<aside className="md:col-span-5">
|
||||
<div className="sticky top-24">
|
||||
<h2 className="text-pretty text-3xl font-semibold leading-tight md:text-4xl">
|
||||
From curiosity to craft
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-3 text-base leading-relaxed md:text-lg">
|
||||
Building images that feel inevitable—shaped by physics, story,
|
||||
and restraint.
|
||||
</p>
|
||||
|
||||
{/* Skills strip (horizontal on small, wrapped on large) */}
|
||||
<div className="mt-6 md:mt-8">
|
||||
<ScrollArea className="max-w-full">
|
||||
<div className="flex w-max items-center gap-2 pb-2 md:w-full md:flex-wrap">
|
||||
<Badge variant="secondary">Houdini</Badge>
|
||||
<Badge variant="secondary">FX</Badge>
|
||||
<Badge variant="secondary">Lighting</Badge>
|
||||
<Badge variant="outline">Maya</Badge>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Flowing content right column */}
|
||||
<article className="space-y-8 md:col-span-7 md:space-y-10">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bio</CardTitle>
|
||||
<CardDescription>
|
||||
A career shaped by curiosity, collaboration, and craft.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 leading-7">
|
||||
<p>
|
||||
Maya was the first software I learned in 2008. 2009 began my
|
||||
time at the School of Visual Art in Manhattan for Computer
|
||||
Animation and Visual Effects in pursuit of my Bachelor's
|
||||
of Fine Art degree.
|
||||
</p>
|
||||
<p>
|
||||
Sophomore year was when I started learning Houdini. The same
|
||||
year I began working my first internship and involving myself
|
||||
in as many senior thesis projects as possible.
|
||||
</p>
|
||||
<p>
|
||||
By Junior year after two internships under my belt I had
|
||||
landed my first job for both lighting and FX at an animation
|
||||
studio called Panda Panther in NYC. From that point and after
|
||||
graduation in 2013, I continued to freelance from NYC, to LA,
|
||||
to Hungary and then Canada for companies such as;
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
Aardman Nathan Love, Framestore, MPC, Psyop, Digital Domain,
|
||||
Digic Pictures, Pixomondo, FuseFX, Method Studios, Blur
|
||||
Studios, DNeg
|
||||
</p>
|
||||
<p>My motivation is to create great work and continue learning.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Right-aligned media block inside card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent work and reels</CardTitle>
|
||||
<CardDescription>
|
||||
Selected highlights and studies in motion.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid items-start gap-6 md:grid-cols-5">
|
||||
<div className="space-y-4 leading-7 md:col-span-3">
|
||||
<p>
|
||||
Focused on physically grounded effects, lighting, and the
|
||||
interplay between simulation and composition. I favor
|
||||
readable setups and production-friendly iteration.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button asChild size="sm" className="gap-2">
|
||||
<a
|
||||
href={LINKS.fxReel}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open Houdini FX Reel in a new tab"
|
||||
>
|
||||
<Film className="size-4" aria-hidden="true" />
|
||||
FX Reel
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="outline" className="gap-2">
|
||||
<a
|
||||
href={LINKS.reel2019}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open 2019 FX Highlight Reel in a new tab"
|
||||
>
|
||||
<Film className="size-4" aria-hidden="true" />
|
||||
2019 Highlight
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="secondary" className="gap-2">
|
||||
<a
|
||||
href={LINKS.cv}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open CV in a new tab"
|
||||
>
|
||||
<FileText className="size-4" aria-hidden="true" />
|
||||
CV
|
||||
<ExternalLink className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div className="relative aspect-[16/9] w-full overflow-hidden rounded-lg border bg-muted">
|
||||
<Image
|
||||
src="/window.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-contain p-6"
|
||||
sizes="(min-width: 768px) 320px, 100vw"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Representative still — replace with artwork or film still.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CV Section */}
|
||||
<section id="cv" className="relative">
|
||||
<div className="py-12 md:py-16">
|
||||
<div className="grid gap-8 md:grid-cols-12 md:gap-10">
|
||||
{/* Sticky heading/lede for CV */}
|
||||
<aside className="md:col-span-5">
|
||||
<div className="sticky top-24">
|
||||
<h2 className="text-pretty text-3xl font-semibold leading-tight md:text-4xl">
|
||||
Curriculum Vitae
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-3 text-base leading-relaxed md:text-lg">
|
||||
Selected experience, skills, and education.
|
||||
</p>
|
||||
|
||||
{/* Section anchors */}
|
||||
<nav
|
||||
aria-label="CV sections"
|
||||
className="mt-6 hidden flex-col gap-2 md:flex"
|
||||
>
|
||||
<a
|
||||
href="#cv-objective"
|
||||
className="rounded px-1 py-0.5 text-sm text-foreground/80 hover:text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Objective
|
||||
</a>
|
||||
<a
|
||||
href="#cv-experience"
|
||||
className="rounded px-1 py-0.5 text-sm text-foreground/80 hover:text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Experience
|
||||
</a>
|
||||
<a
|
||||
href="#cv-skills"
|
||||
className="rounded px-1 py-0.5 text-sm text-foreground/80 hover:text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Skills
|
||||
</a>
|
||||
<a
|
||||
href="#cv-education"
|
||||
className="rounded px-1 py-0.5 text-sm text-foreground/80 hover:text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Education
|
||||
</a>
|
||||
<a
|
||||
href="#cv-references"
|
||||
className="rounded px-1 py-0.5 text-sm text-foreground/80 hover:text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
References
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* CV content */}
|
||||
<article className="space-y-8 md:col-span-7 md:space-y-10">
|
||||
<Card id="cv-objective" className="scroll-mt-28">
|
||||
<CardHeader>
|
||||
<CardTitle>Objective</CardTitle>
|
||||
<CardDescription>
|
||||
Driven by continuous learning and production-ready workflows.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="leading-7">
|
||||
<p>
|
||||
I strive to provide realistic simulations by continuously learning the
|
||||
latest tools, softwares, and workflows for working in a fast-paced
|
||||
production pipeline.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Studios</CardTitle>
|
||||
<CardDescription>Selected studios Shane has contributed to.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StudiosStrip />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card id="cv-experience" className="scroll-mt-28">
|
||||
<CardHeader>
|
||||
<CardTitle>Experience</CardTitle>
|
||||
<CardDescription>
|
||||
Film, TV, and cinematics across leading studios.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Collapsible list to keep the section concise */}
|
||||
<CollapsibleArea>
|
||||
<div className="space-y-6">
|
||||
{/* ScanlineVFX */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
FX TD and Generalist (Lighting TD) — ScanlineVFX
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vancouver, BC — June 2021 – December 2023
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Worked on films such as The Adam Project, The Grey Man, Black
|
||||
Adam, and The Meg 2.
|
||||
</li>
|
||||
<li>
|
||||
Created particle and volumetric FX including rigid body dynamics
|
||||
in Houdini. Transitioned to Lighting, working in 3DS Max.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* ILM */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
FX TD — Industrial Light & Magic
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vancouver, BC — March 2021 – May 2021
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>Worked on Jurassic World: Dominion.</li>
|
||||
<li>
|
||||
Created particle and fire FX, including rigid body dynamics
|
||||
work.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* DNEG */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">FX TD — DNEG</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vancouver, BC — February 2020 – April 2020
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Worked on Dune; created volumetric FX in Houdini and rendered
|
||||
with Clarisse.
|
||||
</li>
|
||||
<li>Worked on Ghostbusters; volumetric dust and particle FX.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Blur Studio */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">FX Animator — Blur Studio</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Los Angeles, CA — April 2019 – August 2019
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Created particle and volumetric FX for The Elder Scrolls Online
|
||||
cinematic.
|
||||
</li>
|
||||
<li>
|
||||
Created particle, volumetric, and rigid body FX for League of
|
||||
Legends cinematic; FX in Houdini, lit in V-Ray for 3DS Max.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* FuseFX */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">Lead FX Artist — FuseFX</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Los Angeles & Vancouver, BC — Sep 2017 – Jan 2018, Sep 2018 – Feb 2019
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Created Houdini FX for The Tick, The Magicians, Lost in Space,
|
||||
Deadly Class, The 100, and The Orville.
|
||||
</li>
|
||||
<li>
|
||||
Bidded and delivered FX shots for The Magicians S3; reviewed work
|
||||
and provided feedback.
|
||||
</li>
|
||||
<li>
|
||||
Led insect swarm across 15 shots; smoke, FLIP, and environment FX;
|
||||
rendered with Mantra.
|
||||
</li>
|
||||
<li>Created FX assets exported and tested in 3DS Max.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Pixomondo */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">Senior FX Artist — Pixomondo</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vancouver, BC — Jan 2017 – Sep 2017, Jul 2018 – Sep 2018
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Created large-scale simulations: bullet glass, FLIP, volumetrics
|
||||
(smoke/dust), particles; lighting/rendering with Mantra and V-Ray.
|
||||
</li>
|
||||
<li>
|
||||
Developed a cloud asset tool used to populate over 35 shots; dynamic
|
||||
“Balloon Spider” CFX rig for Goosebumps 2.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Method Studios */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">FX TD — Method Studios</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vancouver, BC — Feb 2018 – May 2018
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>FX asset: bubble force field for The New Mutants.</li>
|
||||
<li>Various FX assets for Ant-Man and the Wasp.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Digic Pictures */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">FX Artist — Digic Pictures</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Budapest, Hungary — Feb 2016 – Dec 2016
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>Created FX for Final Fantasy XV: Kingsglaive.</li>
|
||||
<li>
|
||||
Worked on several game cinematics including League of Legends; FX
|
||||
in Houdini and Maya.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Digital Domain */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
Houdini FX & Lighting Artist — Digital Domain
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Los Angeles — Aug 2015 – Oct 2015
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>Lighting in V-Ray for Maya on Starz’ Black Sails.</li>
|
||||
<li>Cloud FX in Houdini rendered with Mantra.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* MPC */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
FX & Lighting/Texture Artist — MPC NY/LA
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
New York, NY — 2013–2015 (various engagements)
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>Commercial FX: nCloth, nParticles, Maya Fluids.</li>
|
||||
<li>
|
||||
Assisted lighting using Mental Ray, Arnold; textured environment
|
||||
assets in Mari.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Framestore */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
FX & Lighting Artist — Framestore NY/LA
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
NY — Jul–Aug 2013; LA — May 2014 – Feb 2015
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Commercials: 7up “Light it up” (2015), Julius Baer (2015), Target
|
||||
Holiday (2014–2015).
|
||||
</li>
|
||||
<li>
|
||||
Houdini shatter assets and simulations for Mercedes-Benz CLA
|
||||
“Barriers” (2013).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Digital Tutors */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">Digital Tutors — Author</h3>
|
||||
<p className="text-xs text-muted-foreground">March 2015 – May 2015</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Created series “Workflow for Rendering Fur in V-Ray”.
|
||||
</li>
|
||||
<li>
|
||||
Covered stylization with texture-driven maps and render optimization
|
||||
techniques.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Panda Panther */}
|
||||
<section>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold">
|
||||
Lead FX Artist & Lighting — Panda Panther
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
New York, NY — May – July 2012
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6">
|
||||
<li>
|
||||
Effects and lighting for Spyro: Skylanders using Maya Fluids and
|
||||
nParticles; lighting in V-Ray.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</CollapsibleArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card id="cv-skills" className="scroll-mt-28">
|
||||
<CardHeader>
|
||||
<CardTitle>Skills</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Effects</h4>
|
||||
<p className="text-sm text-muted-foreground">Houdini, Maya, RealFlow</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Lighting / Rendering</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
V-Ray, 3DS Max, Mental Ray, Arnold, Krakatoa, Mantra, Redshift
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Compositing</h4>
|
||||
<p className="text-sm text-muted-foreground">Nuke, After Effects</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Texturing</h4>
|
||||
<p className="text-sm text-muted-foreground">Mudbox, Mari, Photoshop, Illustrator</p>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<h4 className="text-sm font-semibold">Coding</h4>
|
||||
<p className="text-sm text-muted-foreground">Python, Node.js, Java, TypeScript, C++</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card id="cv-education" className="scroll-mt-28">
|
||||
<CardHeader>
|
||||
<CardTitle>Education</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="leading-7">
|
||||
<p>
|
||||
School of Visual Arts — BFA, Computer Animation and Visual Effects — 2009–2013
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card id="cv-references" className="scroll-mt-28">
|
||||
<CardHeader>
|
||||
<CardTitle>References</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="leading-7">
|
||||
<p>Available upon request</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
44
src/components/collapsible-area.tsx
Normal file
44
src/components/collapsible-area.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import clsx from "clsx"
|
||||
|
||||
type CollapsibleAreaProps = {
|
||||
children: React.ReactNode
|
||||
collapsedClass?: string
|
||||
expandLabel?: string
|
||||
collapseLabel?: string
|
||||
}
|
||||
|
||||
export function CollapsibleArea({
|
||||
children,
|
||||
collapsedClass = "max-h-[35vh] md:max-h-[40vh] overflow-hidden",
|
||||
expandLabel = "Show full CV",
|
||||
collapseLabel = "Show less",
|
||||
}: CollapsibleAreaProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={clsx(!expanded && collapsedClass)}>{children}</div>
|
||||
|
||||
{!expanded && (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 bg-gradient-to-t from-background to-background/0" />
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center justify-center">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? collapseLabel : expandLabel}
|
||||
>
|
||||
{expanded ? collapseLabel : expandLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
140
src/components/footer/site-footer.tsx
Normal file
140
src/components/footer/site-footer.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Mail, Github, Linkedin, Youtube } from "lucide-react";
|
||||
import { LINKS } from "@/lib/links";
|
||||
|
||||
export default function SiteFooter() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer aria-label="Site footer" className="bg-background/0 border-t border-muted/20">
|
||||
<div className="mx-auto max-w-6xl px-4 py-12">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Shane Simpson</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
VFX Artist — Houdini, FX & Lighting
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
<Button asChild size="sm">
|
||||
<a
|
||||
href={LINKS.cv}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="View CV"
|
||||
>
|
||||
View CV
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a
|
||||
href={LINKS.fxReel}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="View FX reel"
|
||||
>
|
||||
FX Reel
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Quick links</h4>
|
||||
<ul className="mt-3 space-y-2 text-sm">
|
||||
<li>
|
||||
<Link href="/" className="hover:underline">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#portfolio" className="hover:underline">
|
||||
Portfolio
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#cv" className="hover:underline">
|
||||
CV
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#contact" className="hover:underline">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Connect</h4>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Follow or send a quick message</p>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a href="mailto:hello@shanesimpson.com" aria-label="Email">
|
||||
<Mail className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a
|
||||
href="https://www.imdb.com/name/nm5104608/?ref_=ext_shr_lnk"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="IMDB"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a
|
||||
href="https://www.linkedin.com/in/shane-simpson"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<Linkedin className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a
|
||||
href={LINKS.reel2019}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Vimeo reel"
|
||||
>
|
||||
<Youtube className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col items-center justify-between gap-3 text-sm text-muted-foreground md:flex-row">
|
||||
<p>© {year} Shane Simpson. All rights reserved.</p>
|
||||
<p>
|
||||
Built with{" "}
|
||||
<a
|
||||
href="https://nextjs.org"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Next.js
|
||||
</a>{" "}
|
||||
• Designed with Tailwind + shadcn/ui
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
105
src/components/providers/lenis-root.tsx
Normal file
105
src/components/providers/lenis-root.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import 'lenis/dist/lenis.css'
|
||||
import { ReactLenis } from "lenis/react"
|
||||
import type { LenisRef } from "lenis/react"
|
||||
import React, { useEffect, useMemo, useRef } from "react"
|
||||
|
||||
/**
|
||||
* LenisRoot mounts a Lenis instance at the document root.
|
||||
* - Runs only on the client ("use client")
|
||||
* - Uses reasonable defaults for desktop wheel/touch
|
||||
* - Respects prefers-reduced-motion by disabling smoothing
|
||||
*
|
||||
* Notes:
|
||||
* - The ReactLenis component exposes a ref (LenisRef) when you need to call methods
|
||||
* such as scrollTo or raf yourself. We attach a ref here so the instance can be
|
||||
* inspected during development and is ready for any future integrations.
|
||||
* - Importing 'lenis/dist/lenis.css' ensures the optional CSS required by Lenis is loaded.
|
||||
*/
|
||||
export function LenisRoot() {
|
||||
const lenisRef = useRef<LenisRef | null>(null)
|
||||
|
||||
const prefersReducedMotion =
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.matchMedia === "function" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
// Let Lenis drive its own RAF loop (recommended for ReactLenis)
|
||||
autoRaf: true,
|
||||
// Enable smooth wheel scrolling when the user hasn't requested reduced motion
|
||||
smoothWheel: !prefersReducedMotion,
|
||||
// Enable a gentle smoothing for touch devices (set true to get smoothing on mobile)
|
||||
// You can change this to `false` if you prefer native touch scrolling.
|
||||
smoothTouch: !prefersReducedMotion,
|
||||
// Enable anchor handling so links to #ids are handled by Lenis
|
||||
anchors: true as const,
|
||||
// Interpolation factor (0..1) - lower = more smoothing. When reduced motion is requested, disable smoothing.
|
||||
lerp: prefersReducedMotion ? 1 : 0.08,
|
||||
}),
|
||||
[prefersReducedMotion]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Poll a few times until the ReactLenis ref is populated, then expose the instance
|
||||
let checks = 0
|
||||
const maxChecks = 20
|
||||
const interval = setInterval(() => {
|
||||
checks += 1
|
||||
|
||||
if (lenisRef.current?.lenis) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("Lenis initialized", lenisRef.current.lenis)
|
||||
// Expose on window for quick debugging in devtools
|
||||
try {
|
||||
// @ts-expect-error - dev-only debugging helper
|
||||
window.__lenis = lenisRef.current.lenis
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
// Ensure the instance is running
|
||||
try {
|
||||
lenisRef.current.lenis.start?.()
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
return
|
||||
}
|
||||
|
||||
if (checks >= maxChecks) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Lenis did not initialize within expected time")
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const instance = lenisRef.current?.lenis
|
||||
if (!instance) return
|
||||
|
||||
const onScroll = (e: any) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("lenis:scroll", e)
|
||||
}
|
||||
|
||||
instance.on?.("scroll", onScroll)
|
||||
|
||||
return () => {
|
||||
instance.off?.("scroll", onScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <ReactLenis root options={options} ref={lenisRef} />
|
||||
}
|
||||
|
||||
export default LenisRoot
|
||||
8
src/components/providers/theme-provider.tsx
Normal file
8
src/components/providers/theme-provider.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
43
src/components/studios-strip.tsx
Normal file
43
src/components/studios-strip.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
type Studio = { name: string }
|
||||
|
||||
const STUDIOS: Studio[] = [
|
||||
{ name: "Scanline VFX" },
|
||||
{ name: "Industrial Light & Magic" },
|
||||
{ name: "DNEG" },
|
||||
{ name: "Blur Studio" },
|
||||
{ name: "FuseFX" },
|
||||
{ name: "Pixomondo" },
|
||||
{ name: "Method Studios" },
|
||||
{ name: "Digic Pictures" },
|
||||
{ name: "Digital Domain" },
|
||||
{ name: "MPC" },
|
||||
{ name: "Framestore" },
|
||||
{ name: "Digital Tutors" },
|
||||
{ name: "Panda Panther" },
|
||||
]
|
||||
|
||||
/**
|
||||
* StudiosStrip
|
||||
* Present studios as a wrapping chip grid using shadcn/ui Badge.
|
||||
* This removes any horizontal scrollbar and looks consistent in light/dark.
|
||||
*/
|
||||
export function StudiosStrip() {
|
||||
return (
|
||||
<ul className="flex flex-wrap items-center gap-3">
|
||||
{STUDIOS.map((s) => (
|
||||
<li key={s.name}>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-8 rounded-md px-3 text-xs font-medium"
|
||||
aria-label={s.name}
|
||||
title={s.name}
|
||||
>
|
||||
{s.name}
|
||||
</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
28
src/components/theme-toggle.tsx
Normal file
28
src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const isDark = theme === "dark"
|
||||
|
||||
const handleToggle = () => {
|
||||
setTheme(isDark ? "light" : "dark")
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Toggle theme"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Sun className="size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
11
src/components/ui/aspect-ratio.tsx
Normal file
11
src/components/ui/aspect-ratio.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
||||
}
|
||||
|
||||
export { AspectRatio }
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
109
src/components/ui/breadcrumb.tsx
Normal file
109
src/components/ui/breadcrumb.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
58
src/components/ui/button.tsx
Normal file
58
src/components/ui/button.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
58
src/components/ui/scroll-area.tsx
Normal file
58
src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
5
src/lib/links.ts
Normal file
5
src/lib/links.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const LINKS = {
|
||||
cv: "https://docs.google.com/document/u/1/d/e/2PACX-1vTj3VH5MtXGwEVhFKKAfYCqtGrY1Lu_EU53DLL7gjb7gfqo6g3wZLNVjanOiCn3BCKbsAoE987l9rug/pub",
|
||||
reel2019: "https://vimeo.com/427214395?fl=pl&fe=vl",
|
||||
fxReel: "https://vimeo.com/shanesimpson/fx-reel?share=copy",
|
||||
} as const
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
2
src/types/css.d.ts
vendored
Normal file
2
src/types/css.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
// Allow importing global CSS (e.g., './globals.css') in TypeScript with bundler moduleResolution
|
||||
declare module "*.css" {}
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user