diff --git a/.env.example b/.env.example index 45123ec..9d87c1d 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,16 @@ NEXT_PUBLIC_POSTHOG_KEY= -NEXT_PUBLIC_POSTHOG_HOST= \ No newline at end of file +NEXT_PUBLIC_POSTHOG_HOST= + +# ------------------------------------------------------------------ +# Supabase (optional) +# These are OPTIONAL. When unset the app uses placeholder values and +# runs normally — waitlist submissions will simply not be persisted. +# +# For local development with a real database: +# 1. Install Docker Desktop and make sure it is running. +# 2. Run `npm run db:start` — it prints the credentials below. +# 3. Copy the printed values into a `.env.local` file. +# ------------------------------------------------------------------ +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 +NEXT_PUBLIC_SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 14adca4..ffdaa50 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -15,8 +15,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" cache-dependency-path: package-lock.json - name: Install dependencies @@ -35,8 +35,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" cache-dependency-path: package-lock.json - name: Install dependencies diff --git a/.gitignore b/.gitignore index e72b4d6..41daa59 100644 --- a/.gitignore +++ b/.gitignore @@ -32,10 +32,15 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env +.env.local +.env*.local # vercel .vercel +# supabase +supabase/.temp/ + # typescript *.tsbuildinfo next-env.d.ts diff --git a/README.md b/README.md index b854e7a..971831b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ ACTA Web provides a sophisticated frontend experience for managing verifiable cr - Node.js 18 or higher - npm or yarn package manager - Modern browser with WebAuthn support +- **Docker Desktop** (optional — required only for local Supabase database) +- **Supabase CLI** (optional — `npm i -g supabase` or use via `npx supabase`) + +> **Note:** Docker and Supabase CLI are only needed if you want a local database for waitlist persistence. Without them, the app runs normally using placeholder credentials — waitlist submissions will simply not be stored. ### Installation @@ -109,6 +113,65 @@ NEXT_PUBLIC_ENABLE_PASSKEY=true NEXT_PUBLIC_ENABLE_PARTICLES=true ``` +### Local Supabase (Docker) — Optional + +The project includes a full local Supabase setup for waitlist persistence. **This is entirely optional.** When Supabase environment variables are missing or contain placeholder values, the app starts normally and the waitlist form submits without errors (requests simply won't be persisted). + +#### Quick start + +1. **Install & start Docker Desktop** — make sure the Docker engine is running. +2. **Start Supabase locally:** + + ```bash + npm run db:start + ``` + + This pulls the Supabase Docker images (first run takes a few minutes) and prints the local credentials, including `API URL`, `anon key`, and `service_role key`. + +3. **Copy the printed credentials into `.env.local`:** + + ```env + NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 + NEXT_PUBLIC_SUPABASE_ANON_KEY= + SUPABASE_SERVICE_ROLE_KEY= + ``` + +4. **Run migrations and seed:** + + ```bash + npm run db:reset + ``` + + This applies all migrations in `supabase/migrations/` and runs `supabase/seed.sql`, which inserts 8 sample waitlist rows. + +5. **Start the dev server:** + + ```bash + npm run dev + ``` + +#### Available database scripts + +| Script | Command | Description | +| ----------------------------- | ------------------------ | ----------------------------------------- | +| `npm run db:start` | `supabase start` | Start local Supabase (Docker containers) | +| `npm run db:stop` | `supabase stop` | Stop local Supabase | +| `npm run db:reset` | `supabase db reset` | Drop & recreate DB, run migrations + seed | +| `npm run db:migration ` | `supabase migration new` | Create a new blank migration file | + +#### Supabase Studio + +When Supabase is running locally, you can access **Supabase Studio** at [http://127.0.0.1:54323](http://127.0.0.1:54323) to browse tables, run SQL, and inspect data. + +#### Credential fallbacks + +The Supabase client (`src/lib/supabase.ts`) is designed to be resilient: + +- If `NEXT_PUBLIC_SUPABASE_URL` is missing or contains `"your_supabase"` / `"placeholder"`, a safe placeholder URL is used. +- If `NEXT_PUBLIC_SUPABASE_ANON_KEY` is missing, a placeholder JWT is used. +- If `SUPABASE_SERVICE_ROLE_KEY` is missing, the server falls back to the anon client and logs a warning. +- **The app never throws at startup** regardless of whether Supabase env vars are set. + ### Development ```bash diff --git a/package-lock.json b/package-lock.json index a3626a4..cf6d329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.97.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -2472,6 +2473,86 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz", + "integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz", + "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz", + "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz", + "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz", + "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz", + "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.97.0", + "@supabase/functions-js": "2.97.0", + "@supabase/postgrest-js": "2.97.0", + "@supabase/realtime-js": "2.97.0", + "@supabase/storage-js": "2.97.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2856,12 +2937,17 @@ "version": "20.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", @@ -2890,6 +2976,15 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", @@ -5603,6 +5698,15 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8685,7 +8789,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -8972,6 +9075,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 274450a..9c9b8c0 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,15 @@ "dev": "next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "lint:check": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix --max-warnings 0", + "lint:check": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0", "format": "prettier --write .", "format:check": "prettier --check .", - "prepare": "husky" + "prepare": "husky", + "db:start": "npx supabase start", + "db:stop": "npx supabase stop", + "db:reset": "npx supabase db reset", + "db:migration": "npx supabase migration new" }, "dependencies": { "@radix-ui/react-accordion": "^1.2.12", @@ -40,6 +44,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.97.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/public/ACTA1.png b/public/ACTA1.png new file mode 100644 index 0000000..ce9024f Binary files /dev/null and b/public/ACTA1.png differ diff --git a/public/ACTA2.png b/public/ACTA2.png new file mode 100644 index 0000000..fd59072 Binary files /dev/null and b/public/ACTA2.png differ diff --git a/public/ACTA3.png b/public/ACTA3.png new file mode 100644 index 0000000..0ac2ec6 Binary files /dev/null and b/public/ACTA3.png differ diff --git a/public/credentials/1.avif b/public/credentials/1.avif new file mode 100644 index 0000000..1854b4c Binary files /dev/null and b/public/credentials/1.avif differ diff --git a/public/credentials/2.avif b/public/credentials/2.avif new file mode 100644 index 0000000..4bc47ff Binary files /dev/null and b/public/credentials/2.avif differ diff --git a/public/credentials/3.avif b/public/credentials/3.avif new file mode 100644 index 0000000..5c2ab36 Binary files /dev/null and b/public/credentials/3.avif differ diff --git a/public/credentials/4.avif b/public/credentials/4.avif new file mode 100644 index 0000000..4cbee2f Binary files /dev/null and b/public/credentials/4.avif differ diff --git a/public/credentials/5.avif b/public/credentials/5.avif new file mode 100644 index 0000000..501e463 Binary files /dev/null and b/public/credentials/5.avif differ diff --git a/public/startups/baf.png b/public/startups/baf.png new file mode 100644 index 0000000..31b5809 Binary files /dev/null and b/public/startups/baf.png differ diff --git a/public/startups/grantfox.png b/public/startups/grantfox.png new file mode 100644 index 0000000..33a9f03 Binary files /dev/null and b/public/startups/grantfox.png differ diff --git a/public/startups/interactuar.png b/public/startups/interactuar.png new file mode 100644 index 0000000..155bcfb Binary files /dev/null and b/public/startups/interactuar.png differ diff --git a/src/app/api/waitlist/route.ts b/src/app/api/waitlist/route.ts new file mode 100644 index 0000000..5bf57c5 --- /dev/null +++ b/src/app/api/waitlist/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import { getServiceSupabase } from "@/lib/supabase"; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +interface WaitlistPayload { + email?: string; + company_name?: string; + use_case?: string; +} + +export async function POST(request: Request) { + try { + const body = (await request.json()) as WaitlistPayload; + + const email = body.email?.trim().toLowerCase(); + if (!email || !EMAIL_REGEX.test(email)) { + return NextResponse.json( + { error: "A valid email address is required." }, + { status: 400 } + ); + } + + const company_name = body.company_name?.trim() || null; + const use_case = body.use_case?.trim() || null; + + const supabase = getServiceSupabase(); + + const { error } = await supabase + .from("waitlist") + .insert({ email, company_name, use_case }); + + if (error) { + if (error.code === "23505") { + return NextResponse.json( + { error: "This email is already on the waitlist." }, + { status: 409 } + ); + } + + console.error("[api/waitlist] Supabase error:", error.message); + return NextResponse.json( + { + error: "Waitlist is temporarily unavailable. Please try again later.", + }, + { status: 503 } + ); + } + + return NextResponse.json({ ok: true }, { status: 201 }); + } catch (err) { + console.error("[api/waitlist] Unexpected error:", err); + return NextResponse.json( + { error: "Something went wrong. Please try again later." }, + { status: 500 } + ); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f2e37c3..0352de2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -111,9 +111,13 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + // suppressHydrationWarning: extensions (e.g. Bitwarden) inject attrs on before hydrate return ( - - + +