commit ea71039071686ad908c29baa8602977b64477b08 Author: Mathew Date: Mon Jun 22 23:03:02 2026 +0200 (Feat) Init WebSite diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..7939355 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(npx -p typescript tsc --noEmit)", + "Bash(node_modules/.bin/tsc --noEmit)", + "PowerShell(Get-ChildItem public/video/ | Select-Object Name, @{N='SizeMB';E={[math]::Round\\($_.Length/1MB,2\\)}})", + "PowerShell($f = Get-Item \"public/Video/Header_Video.mp4\"; Write-Output \\(\"Taille: {0:N2} MB\" -f \\($f.Length/1MB\\)\\))", + "Read(//c/Users/mathe/Videos/**)", + "PowerShell(Get-ChildItem \"public/Video/\" | Select-Object Name, @{N='SizeMB';E={[math]::Round\\($_.Length/1MB,2\\)}}, LastWriteTime)", + "PowerShell($f = Get-ChildItem \"public\\\\Video\\\\\"; foreach \\($i in $f\\) { Write-Output \\(\"{0} - {1:N2} MB\" -f $i.Name, \\($i.Length/1MB\\)\\) })" + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..18db860 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "PowerShell(Move-Item public _backup_public)", + "PowerShell(npm install)", + "PowerShell(npm install framer-motion @next/mdx @mdx-js/loader @mdx-js/react @types/mdx gray-matter remark-gfm)", + "PowerShell(Move-Item ../_studio_logos_backup/Studio public/image -Force)", + "PowerShell(Move-Item ../_studio_logos_backup/image public/image -Force)", + "PowerShell(npm install next-mdx-remote)", + "PowerShell(npm run build)", + "PowerShell(npm run dev)", + "PowerShell(Copy-Item \"public/image/Studio/favicon.ico\" \"app/favicon.ico\" -Force)", + "Bash(npm run *)", + "Bash(npx tsc *)", + "Bash(echo \"exit=$?\")", + "Bash(npx next *)", + "Bash(npx eslint *)", + "Bash(identify public/image/Studio/favicon.png)", + "Bash(node -e \"console.log\\(require\\('child_process'\\).execSync\\('npm root -g'\\).toString\\(\\)\\)\")", + "Bash(magick --version)", + "Bash(convert --version)", + "Bash(node -e \"try { require\\('sharp'\\); console.log\\('sharp ok'\\); } catch\\(e\\) { console.log\\('no sharp'\\); }\")", + "Bash(node -e ' *)", + "Bash(node scripts/build-favicon.mjs)" + ] + } +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..04b1212 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,30 @@ +name: Deploy to VPS + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + port: ${{ secrets.SSH_PORT }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + set -e + cd /var/www/highland + git pull origin main + npm ci + npm run build + pm2 restart highland --update-env + pm2 save diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87a304c --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# 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 + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/actions/contact.ts b/app/actions/contact.ts new file mode 100644 index 0000000..0733870 --- /dev/null +++ b/app/actions/contact.ts @@ -0,0 +1,49 @@ +"use server"; + +export type ContactState = { + status: "idle" | "success" | "error"; + message: string; + fieldErrors?: Partial>; +}; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export async function sendContact( + _prev: ContactState, + formData: FormData, +): Promise { + const name = String(formData.get("name") ?? "").trim(); + const email = String(formData.get("email") ?? "").trim(); + const subject = String(formData.get("subject") ?? "").trim(); + const message = String(formData.get("message") ?? "").trim(); + const honeypot = String(formData.get("website") ?? ""); + + if (honeypot) { + return { status: "success", message: "Thanks for reaching out." }; + } + + const fieldErrors: ContactState["fieldErrors"] = {}; + if (!name) fieldErrors.name = "Required."; + if (!email || !EMAIL_RE.test(email)) fieldErrors.email = "Valid email required."; + if (!subject) fieldErrors.subject = "Required."; + if (!message || message.length < 10) { + fieldErrors.message = "Tell us a bit more (10+ chars)."; + } + + if (Object.keys(fieldErrors).length > 0) { + return { + status: "error", + message: "Please fix the highlighted fields.", + fieldErrors, + }; + } + + // TODO: wire to Resend / Postmark / SendGrid to actually deliver the email. + // For now we log so messages are visible in server logs. + console.log("[contact]", { name, email, subject, message }); + + return { + status: "success", + message: "Message sent. We'll get back to you soon.", + }; +} diff --git a/app/actions/subscribe.ts b/app/actions/subscribe.ts new file mode 100644 index 0000000..7282d35 --- /dev/null +++ b/app/actions/subscribe.ts @@ -0,0 +1,36 @@ +"use server"; + +export type SubscribeState = { + status: "idle" | "success" | "error"; + message: string; +}; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export async function subscribe( + _prev: SubscribeState, + formData: FormData, +): Promise { + const email = String(formData.get("email") ?? "").trim(); + const honeypot = String(formData.get("website") ?? ""); + + if (honeypot) { + return { status: "success", message: "Thanks — we'll keep you posted." }; + } + + if (!email || !EMAIL_RE.test(email)) { + return { + status: "error", + message: "Please enter a valid email address.", + }; + } + + // TODO: wire to a real provider (Buttondown, ConvertKit, Mailchimp, Resend Audiences). + // For now we just log the signup so you can collect addresses from server logs. + console.log(`[newsletter] new signup: ${email}`); + + return { + status: "success", + message: "Welcome to the climb. Check your inbox soon.", + }; +} diff --git a/app/components/ContactForm.tsx b/app/components/ContactForm.tsx new file mode 100644 index 0000000..1d0953d --- /dev/null +++ b/app/components/ContactForm.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useActionState } from "react"; +import { useFormStatus } from "react-dom"; +import { sendContact, type ContactState } from "@/app/actions/contact"; + +const initialState: ContactState = { status: "idle", message: "" }; + +function SubmitButton() { + const { pending } = useFormStatus(); + return ( + + ); +} + +const inputBase = + "w-full bg-surface border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none transition-colors"; + +export function ContactForm() { + const [state, formAction] = useActionState(sendContact, initialState); + + if (state.status === "success") { + return ( +
+

+ MESSAGE SENT +

+

{state.message}

+
+ ); + } + + const err = state.fieldErrors ?? {}; + + return ( +
+ + +
+
+ + + {err.name && ( +

{err.name}

+ )} +
+
+ + + {err.email && ( +

{err.email}

+ )} +
+
+ +
+ + + {err.subject && ( +

{err.subject}

+ )} +
+ +
+ +