commit 1f3af8f7f5bf0e3d4fb7e9fbcf7b468d6a3392ed Author: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Mon Oct 5 18:17:41 2026 +0200 Initial ttvfx.com site: Next.js static export Co-Authored-By: Claude Opus 5.5 diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..74eba1f --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ttvfx-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev", "--", "-p", "3210"], + "port": 3210 + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..a658db4 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,64 @@ +# Deploying ttvfx.com + +The site is a fully static export, so there is no Node process to run in production. + +## 1. Build + +```bash +npm ci +npm run build # outputs ./out +``` + +## 2. DNS + +`ttvfx.com` does not resolve yet. At the registrar or DNS host (Cloudflare, if +you want it alongside twotalesvfx.com), add: + +| Type | Name | Value | +|------|------|-------| +| A | `@` | `` | +| A | `www`| `` | + +## 3. Upload + +```bash +rsync -avz --delete out/ user@your-vps:/var/www/ttvfx.com/ +``` + +## 4. nginx + +`/etc/nginx/sites-available/ttvfx.com`: + +```nginx +server { + listen 80; + server_name ttvfx.com www.ttvfx.com; + root /var/www/ttvfx.com; + + # www -> apex + if ($host = www.ttvfx.com) { return 301 https://ttvfx.com$request_uri; } + + location / { + try_files $uri $uri/ $uri.html =404; + } + + # Hashed build assets can be cached forever + location /_next/static/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + error_page 404 /404.html; + + gzip on; + gzip_types text/css application/javascript image/svg+xml application/json; +} +``` + +```bash +sudo ln -s /etc/nginx/sites-available/ttvfx.com /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +sudo certbot --nginx -d ttvfx.com -d www.ttvfx.com # HTTPS via Let's Encrypt +``` + +(For Apache, set `DocumentRoot` to the same folder plus `ErrorDocument 404 /404.html`. +Because the build uses `trailingSlash`, every page is a folder with its own `index.html`.) diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfd7379 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# ttvfx.com + +The marketing site for **Two Tales VFX**: Next.js 16 (App Router), Tailwind CSS v4, exported as static HTML. + +```bash +npm install +npm run dev # http://localhost:3000 +npm run build # static site in ./out +``` + +## Editing content + +All copy (studio info, services, projects, contact email) lives in +[`content/site.ts`](content/site.ts). Items marked `TODO` are placeholders. + +- **Projects:** add an entry to `projects`. Put stills in `public/work/` and set + `image: "/work/name.jpg"` (2.39:1 looks best). `video` accepts an MP4 path + or a Vimeo/YouTube embed URL. Each project gets a page at `/work//`. +- **Showreel:** drop `public/reel.mp4` (muted, looping, ideally under 10 MB) and + `public/reel-poster.jpg`, then set `site.reel` and `site.reelPoster`. +- **Logo:** the fox mark is a vector in `components/Fox.tsx` / `public/fox.svg`, + traced from `G:\_ADMIN\TWOTALESLOGO.png`. Favicons are `app/icon.png` and `app/apple-icon.png`. + +## Structure + +``` +app/ + page.tsx home: hero, selected work, services, studio, contact + work/page.tsx project index + work/[slug]/ project detail pages (statically generated) + sitemap.ts, robots.ts +components/ Header, Footer, Hero, Frame, ProjectCard, Reveal, Timecode… +content/site.ts all site content +``` + +Deployment is covered in [DEPLOY.md](DEPLOY.md). diff --git a/app/apple-icon.png b/app/apple-icon.png new file mode 100644 index 0000000..bdfb2c0 Binary files /dev/null and b/app/apple-icon.png differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..a337210 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,87 @@ +@import "tailwindcss"; + +:root { + --ink: #0a0a0b; + --ink-2: #111113; + --line: rgb(255 255 255 / 0.1); + --fg: #ecebe8; + --muted: #8d8c88; + --accent: #e8572a; +} + +@theme inline { + --color-ink: var(--ink); + --color-ink-2: var(--ink-2); + --color-line: var(--line); + --color-fg: var(--fg); + --color-muted: var(--muted); + --color-accent: var(--accent); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --font-display: var(--font-josefin); +} + +html { + color-scheme: dark; + scroll-behavior: smooth; + scroll-padding-top: 5rem; +} + +body { + background: var(--ink); + color: var(--fg); +} + +::selection { + background: var(--accent); + color: var(--ink); +} + +/* Film grain overlay */ +.grain::after { + content: ""; + position: fixed; + inset: -50%; + pointer-events: none; + z-index: 60; + opacity: 0.07; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + animation: grain 0.8s steps(4) infinite; +} + +@keyframes grain { + 0% { transform: translate(0, 0); } + 25% { transform: translate(-3%, 2%); } + 50% { transform: translate(2%, -3%); } + 75% { transform: translate(-2%, -1%); } + 100% { transform: translate(1%, 3%); } +} + +/* Scroll reveal (toggled by ) */ +.reveal { + opacity: 0; + transform: translateY(24px); + transition: opacity 0.9s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.9s cubic-bezier(0.2, 0.7, 0.2, 1); +} +.reveal.is-visible { + opacity: 1; + transform: none; +} + +@keyframes drift { + 0%, 100% { transform: translate3d(-6%, -4%, 0) scale(1); } + 50% { transform: translate3d(6%, 4%, 0) scale(1.15); } +} +.animate-drift { animation: drift 18s ease-in-out infinite; } + +@keyframes scan { + from { transform: translateY(-10vh); } + to { transform: translateY(110vh); } +} +.animate-scan { animation: scan 7s linear infinite; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .grain::after, .animate-drift, .animate-scan { animation: none; } + .reveal { opacity: 1; transform: none; transition: none; } +} diff --git a/app/icon.png b/app/icon.png new file mode 100644 index 0000000..602a492 Binary files /dev/null and b/app/icon.png differ diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..d3f46c8 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,50 @@ +import type { Metadata, Viewport } from "next"; +import { Geist, Geist_Mono, Josefin_Sans } from "next/font/google"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import { site } from "@/content/site"; +import "./globals.css"; + +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); +const josefin = Josefin_Sans({ + variable: "--font-josefin", + subsets: ["latin"], + weight: ["100", "300", "400"], +}); + +export const metadata: Metadata = { + metadataBase: new URL(site.url), + title: { + default: `${site.name}: Visual Effects Studio`, + template: `%s | ${site.name}`, + }, + description: site.description, + openGraph: { + type: "website", + siteName: site.name, + title: `${site.name}: Visual Effects Studio`, + description: site.description, + url: site.url, + }, + twitter: { card: "summary_large_image" }, +}; + +export const viewport: Viewport = { + themeColor: "#0a0a0b", +}; + +export default function RootLayout({ children }: LayoutProps<"/">) { + return ( + + +
+
{children}
+