From 7668145204ef80f968a93f3bde57890def9ae9db Mon Sep 17 00:00:00 2001 From: FabianSanchezD Date: Wed, 25 Feb 2026 11:06:21 -0600 Subject: [PATCH 01/25] refactor: update the How It Works component to new design --- src/app/page.tsx | 6 - src/components/HowItWorks.tsx | 440 ++++++++++++++++------------------ 2 files changed, 213 insertions(+), 233 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0482c85..c1a0f55 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -146,12 +146,6 @@ export default function ActaLanding() {
- - How It Works - diff --git a/src/components/HowItWorks.tsx b/src/components/HowItWorks.tsx index beab7f6..c62e08b 100644 --- a/src/components/HowItWorks.tsx +++ b/src/components/HowItWorks.tsx @@ -1,262 +1,248 @@ "use client"; -import { JSX, useState } from "react"; -import { motion } from "framer-motion"; -import { Globe, Link2, ShieldCheck, Copy } from "lucide-react"; -import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import { useState, useId } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Globe, Link2, ShieldCheck, ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; -const fadeInUp = { - initial: { opacity: 0, y: 18 }, - animate: { opacity: 1, y: 0 }, - transition: { duration: 0.45 }, -}; -const staggerChildren = { animate: { transition: { staggerChildren: 0.08 } } }; - -function Code({ children }: { children: React.ReactNode }) { - const [copied, setCopied] = useState(false); - const text = typeof children === "string" ? (children as string) : ""; - return ( -
- -
-        {children}
-      
-
- ); -} +const ease = [0.32, 0.72, 0, 1] as const; // cubic-bezier type Step = { - step: string; - icon: JSX.Element; + id: string; + number: string; + icon: React.ReactNode; title: string; description: string; bullets: string[]; - snippet?: string; }; -function StepCard({ data }: { data: Step }) { - const [flipped, setFlipped] = useState(false); +const stepsData: Step[] = [ + { + id: "01", + number: "01", + icon: , + title: "Issuer emits credential", + description: + "Issuer calls the API/SDK; ACTA encrypts the payload and anchors it on-chain.", + bullets: [ + "Canonical VC JSON.", + "Public on-chain anchor for verification (Active / Revoked / Expired)", + "We return verify link + QR code.", + ], + }, + { + id: "02", + number: "02", + icon: , + title: "Credential anchored on Stellar", + description: + "We publish the credential on Stellar Blockchain without sensitive PII.", + bullets: [ + "Selective disclosure", + "Minimal costs and latency", + "We make issuing credentials on the Stellar blockchain simple.", + ], + }, + { + id: "03", + number: "03", + icon: , + title: "Anyone verifies instantly", + description: + "Our dApp reader verifies the credential against the Stellar Blockchain.", + bullets: [ + "No account needed", + "The holder decides which PII to disclose.", + "We show √/Δ/X and metadata (Issuer, type, tx).", + ], + }, +]; +function StepRow({ + step, + isExpanded, + onToggle, + isLast, + triggerId, + contentId, +}: { + step: Step; + isExpanded: boolean; + onToggle: () => void; + isLast: boolean; + triggerId: string; + contentId: string; +}) { return ( - // explicit height so the grid doesn't collapse
- - {/* FRONT */} - + -
- )} - + {/* Chevron */} + + + + - {/* BACK */} -
); } export default function HowItWorks() { - const steps: Step[] = [ - { - step: "1", - icon: , - title: "Issuer emits credential", - description: - "Issuer calls the API/SDK; ACTA encrypts the payload and anchors it on-chain.", - bullets: [ - "Canonical VC JSON.", - "Public on-chain anchor for verification (Active / Revoked / Expired)", - "We return verify link + QR code.", - ], - snippet: `POST /v1/credentials -{ - "templateId": "tpl_escrow_completed_v1", - "data": { - "escrowId": "esc_7890", - "network": "stellar-mainnet", - "asset": "USDC", - "amount": "500.00", - "receiver": "GABC...RECEIVER", - "roles": { - "approver": "G...APP", - "serviceProvider": "G...PROV", - "releaseSigner": "G...SIG", - "disputeResolver": "G...RES", - "platform": "G...PLAT" - }, - "releasedAt": "2025-08-15T16:30:10Z" - } -}`, - }, - { - step: "2", - icon: , - title: "Credential anchored on Stellar", - description: - "We publish the credential on Stellar Blockchain without sensitive PII.", - bullets: [ - "Selective disclosure", - "Minimal costs and latency.", - "We make issuing credentials on the Stellar blockchain simple.", - ], - snippet: `On-chain record (conceptual) -{ - "id": "cred_3732", - "issuer": "did:pkh:stellar:testnet:GCPZYT...", - "subject": "did:example:ebfeb1f712e...", - "status": "Active", - "proof": "Ed25519Signature2020" -} -`, - }, - { - step: "3", - icon: , - title: "Anyone verifies instantly", - description: - "Our dApp reader verifies the credential against the Stellar Blockchain.", - bullets: [ - "No account needed", - "The holder desides wich PII to disclose.", - "We show ✔/⚠/✖ and metadata (issuer, type, tx).", - ], - snippet: `GET /v1/verify/cred_24f9 -{ - "verified": true, - "status": "Active", - "checks": { "hash_match": true, "onchain_status": "Active" }, - "summary": { "issuer": "Trustless Work", "type": "EscrowCompleted" }, - "proof": { "onchain": { "network": "stellar", "tx": "6b1d9a..." } } -}`, - }, - ]; + const [openStep, setOpenStep] = useState("01"); + const baseId = useId(); return ( -
-
- {/* gradient line (optional) */} - - - - - - - - - +
+ {/* Section label */} +

+ HOW IT WORKS +

- - {steps.map((s, i) => ( - - - - ))} - + {/* Headline */} +

+ Three steps. +
+ Zero complexity. +

+ + {/* Accordion steps */} +
+ {stepsData.map((step, index) => { + const triggerId = `${baseId}-trigger-${step.id}`; + const contentId = `${baseId}-content-${step.id}`; + const isExpanded = openStep === step.id; + return ( + setOpenStep(isExpanded ? "" : step.id)} + isLast={index === stepsData.length - 1} + triggerId={triggerId} + contentId={contentId} + /> + ); + })}
+ + {/* Footer */} +

+ Powered by Stellar +

); } From ac673de7e8970f6e32ccd0eff1591b81232bd267 Mon Sep 17 00:00:00 2001 From: Daniel Coto Jimenez Date: Wed, 25 Feb 2026 16:00:32 -0600 Subject: [PATCH 02/25] refactor: reorganize components and remove unused files - Removed the DappCredentialCard and FlipCredential components as they were no longer needed. - Refactored the page.tsx file to streamline imports and improve structure by organizing components into feature-based directories. - Added new components for layout and effects, including ScrollProgress, Footer, Aurora, and StarsBackground. - Introduced new FAQ and HowItWorks sections to enhance user experience and provide better information. - Updated the HeroSubtitle and AnimatedActa components for improved visual presentation. --- package-lock.json | 21 +- src/app/page.tsx | 47 +-- src/components/DappCredentialCard.tsx | 249 --------------- src/components/FlipCredential.tsx | 299 ------------------ src/components/{ => effects}/Aurora.tsx | 0 .../{ => effects}/StarsBackground.tsx | 7 +- src/components/{ => layout}/Footer.tsx | 0 .../{ => layout}/ScrollProgress.tsx | 0 src/components/ui/use-mobile.tsx | 21 -- src/components/ui/use-toast.ts | 191 ----------- src/{components => features/faq}/FAQ.tsx | 1 - .../hero}/AnimatedActa.tsx | 6 - .../hero}/HeroSubtitle.tsx | 0 .../how-it-works}/HowItWorks.tsx | 3 - .../use-cases}/UseCasesCarousel.tsx | 33 +- .../value-proposition}/ValueDetails.tsx | 0 .../ValueDetailsExtended.tsx | 4 +- .../value-proposition}/ValueProposition.tsx | 9 +- .../waitlist}/WaitlistForm.tsx | 14 +- .../theme-provider.tsx | 0 20 files changed, 30 insertions(+), 875 deletions(-) delete mode 100644 src/components/DappCredentialCard.tsx delete mode 100644 src/components/FlipCredential.tsx rename src/components/{ => effects}/Aurora.tsx (100%) rename src/components/{ => effects}/StarsBackground.tsx (86%) rename src/components/{ => layout}/Footer.tsx (100%) rename src/components/{ => layout}/ScrollProgress.tsx (100%) delete mode 100644 src/components/ui/use-mobile.tsx delete mode 100644 src/components/ui/use-toast.ts rename src/{components => features/faq}/FAQ.tsx (99%) rename src/{components => features/hero}/AnimatedActa.tsx (84%) rename src/{components => features/hero}/HeroSubtitle.tsx (100%) rename src/{components => features/how-it-works}/HowItWorks.tsx (98%) rename src/{components => features/use-cases}/UseCasesCarousel.tsx (80%) rename src/{components => features/value-proposition}/ValueDetails.tsx (100%) rename src/{components => features/value-proposition}/ValueDetailsExtended.tsx (96%) rename src/{components => features/value-proposition}/ValueProposition.tsx (90%) rename src/{components => features/waitlist}/WaitlistForm.tsx (95%) rename src/{components => providers}/theme-provider.tsx (100%) diff --git a/package-lock.json b/package-lock.json index a3626a4..540e223 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2868,7 +2868,6 @@ "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2879,7 +2878,6 @@ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2936,7 +2934,6 @@ "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/types": "8.40.0", @@ -3454,7 +3451,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4414,8 +4410,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4679,7 +4674,6 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4854,7 +4848,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6909,7 +6902,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -7449,7 +7441,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7480,7 +7471,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7508,15 +7498,13 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7645,8 +7633,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8493,7 +8480,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8653,7 +8639,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/page.tsx b/src/app/page.tsx index 0482c85..15d6b3a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,29 +1,24 @@ "use client"; import type React from "react"; +import Image from "next/image"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; -import Aurora from "@/components/Aurora"; -import UseCasesCarousel from "@/components/UseCasesCarousel"; - -import Image from "next/image"; -import ScrollProgress from "@/components/ScrollProgress"; -import HowItWorks from "@/components/HowItWorks"; - -import FAQ from "@/components/FAQ"; -import ValueProposition from "@/components/ValueProposition"; -import ValuePropositionDetails from "@/components/ValueDetails"; -import ValueDetailsExtended from "@/components/ValueDetailsExtended"; -import { TextAnimate } from "@/components/ui/text-animate"; -import Footer from "@/components/Footer"; import { Particles } from "@/components/ui/particles"; import { ShineBorder } from "@/components/ui/shine-border"; import { usePostHog } from "posthog-js/react"; -import AnimatedActa from "@/components/AnimatedActa"; -import WaitlistForm from "@/components/WaitlistForm"; -import FlipCredential from "@/components/FlipCredential"; -import HeroSubtitle from "@/components/HeroSubtitle"; +import ScrollProgress from "@/components/layout/ScrollProgress"; +import Footer from "@/components/layout/Footer"; +import AnimatedActa from "@/features/hero/AnimatedActa"; +import HeroSubtitle from "@/features/hero/HeroSubtitle"; +import ValueProposition from "@/features/value-proposition/ValueProposition"; +import ValuePropositionDetails from "@/features/value-proposition/ValueDetails"; +import ValueDetailsExtended from "@/features/value-proposition/ValueDetailsExtended"; +import HowItWorks from "@/features/how-it-works/HowItWorks"; +import UseCasesCarousel from "@/features/use-cases/UseCasesCarousel"; +import FAQ from "@/features/faq/FAQ"; +import WaitlistForm from "@/features/waitlist/WaitlistForm"; const fadeInUp = { initial: { opacity: 0, y: 20 }, @@ -59,9 +54,11 @@ export default function ActaLanding() {
{/* Logo difuminado */}
- ACTA Logo Background
@@ -158,20 +155,6 @@ export default function ActaLanding() {
- {/*
-
- - Credential Demo - - - - -
-
*/} -
diff --git a/src/components/DappCredentialCard.tsx b/src/components/DappCredentialCard.tsx deleted file mode 100644 index bcd84b3..0000000 --- a/src/components/DappCredentialCard.tsx +++ /dev/null @@ -1,249 +0,0 @@ -"use client"; - -import React, { useState, useMemo, useEffect } from "react"; -import { motion } from "framer-motion"; -import { QRCodeSVG } from "qrcode.react"; -import { ShineBorder } from "@/components/ui/shine-border"; - -type FrontFields = { - holder: string; - issuedBy: string; - issuedOn: string; - expiresOn: string; - category: string; -}; - -type BackField = { k: string; v: string }; - -type Props = { - title?: string; - front: FrontFields; - backFields: BackField[]; - qrFrontValue?: string; - qrBackValue?: string; -}; - -// Visual card inspired by the dApp screenshot, but ensuring -// all fields present in FlipCredential (front + back). -export default function DappCredentialCard({ - title = "Identity Credential", - front, - backFields, - qrFrontValue = "", - qrBackValue = "", -}: Props) { - const [flipped, setFlipped] = useState(false); - const toggle = () => setFlipped(f => !f); - - // Simple responsive scale for QR and fonts - const baseW = 920; - const scale = useMemo(() => 1, []); - // viewport-dependent font sizing (smaller on móvil) - const [vw, setVw] = useState(1024); - useEffect(() => { - const update = () => - setVw(typeof window !== "undefined" ? window.innerWidth : 1024); - update(); - window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); - }, []); - const isMobile = vw < 640; - const titleSize = isMobile ? `22px` : `clamp(16px, ${28 * scale}px, 34px)`; - const detailsSize = isMobile ? `15px` : `clamp(13px, ${18 * scale}px, 20px)`; - const disclaimerSize = isMobile - ? `12px` - : `clamp(10px, ${12 * scale}px, 14px)`; - - // Responsive QR size based on viewport - const qrFrontSize = vw < 640 ? 200 : 320; - const qrBackSize = vw < 640 ? 160 : 240; - const mobileHeight = 640; // altura suficiente para evitar corte en móviles - - return ( -
- - {/* Golden animated border that follows the flip */} - - {/* FRONT */} -
-
- {/* Subtle ACTA watermark on the left side */} -
- -
- -
- {/* Left info */} -
-
- ACTA -

- {title} -

-
- -
-

- Holder: {front.holder} -

-

- Issued by:{" "} - {front.issuedBy} -

-

- Issued on:{" "} - {front.issuedOn} -

-

- Expires on:{" "} - {front.expiresOn} -

-

- Category:{" "} - {front.category} -

-
- - {/* Verified visible en desktop; oculto en móvil */} -
-
- - ✓ - - Verified -
-
-
- - {/* Right QR */} -
- {qrFrontValue && ( - - )} -
- {/* Disclaimer global: esquina inferior derecha del card (solo desktop) */} -

- *Demo credential; no legal validity* -

-
-
- - {/* BACK */} -
-
- -
-
-
- {backFields.map(f => ( -
-
- {f.k} -
-
{f.v}
-
- ))} -
-
- -
- {qrBackValue && ( - - )} -
- -
-

Back • Acta

-
-
-
- -
- ); -} diff --git a/src/components/FlipCredential.tsx b/src/components/FlipCredential.tsx deleted file mode 100644 index 681fd28..0000000 --- a/src/components/FlipCredential.tsx +++ /dev/null @@ -1,299 +0,0 @@ -"use client"; - -import React, { - useEffect, - useMemo, - useRef, - useState, - KeyboardEvent, -} from "react"; -import { motion } from "framer-motion"; -import { QRCodeSVG } from "qrcode.react"; - -type Props = { - /** Star image path (PNG/SVG/WebP). */ - starUrl?: string; // default: /star.png - /** Star opacity (0–1). */ - starOpacity?: number; // default: 0.18 - /** Maximum card width on desktop; on mobile uses 92vw. */ - maxWidth?: number; // default: 980 -}; - -export default function FlipCredential({ - starUrl = "/ActaCard.png", - starOpacity = 0.18, - maxWidth = 980, -}: Props) { - const [flipped, setFlipped] = useState(false); - const toggle = () => { - setFlipped(f => { - const next = !f; - return next; - }); - }; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - toggle(); - } - }; - - // Measure real width to scale QR and typography. - const ref = useRef(null); - const [realW, setRealW] = useState(maxWidth); - useEffect(() => { - if (!ref.current) return; - const ro = new ResizeObserver(([entry]) => { - const w = Math.max(300, Math.round(entry.contentRect.width)); - setRealW(w); - }); - ro.observe(ref.current); - return () => ro.disconnect(); - }, []); - - // Flag: narrow screen? - const isNarrow = realW <= 640; - const isMobile = realW <= 480; - - // Improved fluid scaling - const base = 980; - const scale = useMemo( - () => Math.max(0.5, Math.min(1.35, realW / base)), - [realW] - ); - const titleSize = `clamp(16px, ${isMobile ? 20 : 28 * scale}px, 34px)`; - const bodySize = `clamp(12px, ${isMobile ? 14 : 16 * scale}px, 18px)`; - const detailsSize = `clamp(13px, ${isMobile ? 16 : 18 * scale}px, 20px)`; - const disclaimerSize = `clamp(10px, ${isMobile ? 11 : 12 * scale}px, 14px)`; - - // Improved QR sizes for mobile - const qrFront = Math.round( - Math.min(0.32 * realW, isMobile ? 200 : isNarrow ? 240 : 320) - ); - const qrBack = Math.max( - 100, - Math.round(isMobile ? 0.15 * realW : 0.18 * realW) - ); - - // Improved dynamic aspect ratio - const aspect = isMobile ? "4 / 5" : isNarrow ? "16 / 14" : "16 / 10"; - const minH = isMobile ? 500 : isNarrow ? 460 : 320; - - return ( -
- e.preventDefault()} - className="relative select-none rounded-2xl shadow-xl cursor-pointer" - style={{ - width: `min(${maxWidth}px, 92vw)`, - aspectRatio: aspect, // dynamic - minHeight: minH, // taller on mobile - transformStyle: "preserve-3d", - willChange: "transform", - }} - animate={{ rotateY: flipped ? 180 : 0 }} - transition={{ type: "spring", stiffness: 120, damping: 16, mass: 0.8 }} - whileTap={{ scale: 0.995 }} - > - {/* FRONT */} -
- {/* Solid black background */} -
- - {/* Star as overlay image */} -
-
-
- - {/* Content */} -
- {/* Left side */} -
-
- -

- Escrow Completed -

-
- -
-

- Holder: Daniel Coto -

-

- Issued by:{" "} - TrustlessWork -

-

- Issued on: August 26, - 2025 -

-

- Expires on: August 26, - 2026 -

-

- Category: Financial - Services -

-
-
- - {/* Right side: QR */} -
-
- -
-
- - {/* Disclaimer */} -
-

- *This credential is a demo issued by Acta. It has no legal - validity and does not represent an official verification* -

-
-
-
- - {/* BACK */} -
-
-
-
- {[ - { k: "Credential ID", v: "cred_9f2a-1234-abcd" }, - { k: "Standard", v: "W3C Verifiable Credential 2.0" }, - { k: "Signature", v: "Ed25519 (Stellar)" }, - { k: "Status", v: "Active" }, - { k: "On-chain hash", v: "0x8f7a…b21c" }, - ].map(f => ( -
-
- {f.k} -
-
{f.v}
-
- ))} -
-
- -
- -
- -
-

- Back • Acta -

-
-
-
- -
- ); -} - -/** ------ Simple hexagonal logo (placeholder) ------ */ -function HexLogo({ className = "" }: { className?: string }) { - return ( - - ); -} diff --git a/src/components/Aurora.tsx b/src/components/effects/Aurora.tsx similarity index 100% rename from src/components/Aurora.tsx rename to src/components/effects/Aurora.tsx diff --git a/src/components/StarsBackground.tsx b/src/components/effects/StarsBackground.tsx similarity index 86% rename from src/components/StarsBackground.tsx rename to src/components/effects/StarsBackground.tsx index 558d472..421114d 100644 --- a/src/components/StarsBackground.tsx +++ b/src/components/effects/StarsBackground.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useMemo, memo } from "react"; import { motion } from "framer-motion"; -const SLOWDOWN = 3; // 1 = same; >1 = slower; <1 = faster +const SLOWDOWN = 3; const StarsBackground = memo(() => { const [screenSize, setScreenSize] = useState({ width: 0, height: 0 }); @@ -24,9 +24,8 @@ const StarsBackground = memo(() => { size: Math.random() * 2.5 + 2, initialX: Math.random() * 120 - 10, initialY: Math.random() * 120 - 20, - // before: Math.random() * 12 + 8 (8s to 20s) - duration: (Math.random() * 12 + 8) * SLOWDOWN, // now slower - delay: Math.random() * 20, // you can increase it a bit if you want more "pauses" + duration: (Math.random() * 12 + 8) * SLOWDOWN, + delay: Math.random() * 20, opacity: Math.random() * 0.7 + 0.3, })); }, [isClient]); diff --git a/src/components/Footer.tsx b/src/components/layout/Footer.tsx similarity index 100% rename from src/components/Footer.tsx rename to src/components/layout/Footer.tsx diff --git a/src/components/ScrollProgress.tsx b/src/components/layout/ScrollProgress.tsx similarity index 100% rename from src/components/ScrollProgress.tsx rename to src/components/layout/ScrollProgress.tsx diff --git a/src/components/ui/use-mobile.tsx b/src/components/ui/use-mobile.tsx deleted file mode 100644 index 48fab93..0000000 --- a/src/components/ui/use-mobile.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -const MOBILE_BREAKPOINT = 768; - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState( - undefined - ); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener("change", onChange); - }, []); - - return !!isMobile; -} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts deleted file mode 100644 index 362b089..0000000 --- a/src/components/ui/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -"use client"; - -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -type ActionTypes = { - ADD_TOAST: "ADD_TOAST"; - UPDATE_TOAST: "UPDATE_TOAST"; - DISMISS_TOAST: "DISMISS_TOAST"; - REMOVE_TOAST: "REMOVE_TOAST"; -}; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = ActionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map(t => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach(toast => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map(t => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter(t => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach(listener => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: open => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/src/components/FAQ.tsx b/src/features/faq/FAQ.tsx similarity index 99% rename from src/components/FAQ.tsx rename to src/features/faq/FAQ.tsx index 1462c75..67aa837 100644 --- a/src/components/FAQ.tsx +++ b/src/features/faq/FAQ.tsx @@ -65,7 +65,6 @@ const faqData = [ }, ]; -// FAQPage Schema.org JSON-LD const faqPageSchema = { "@context": "https://schema.org", "@type": "FAQPage", diff --git a/src/components/AnimatedActa.tsx b/src/features/hero/AnimatedActa.tsx similarity index 84% rename from src/components/AnimatedActa.tsx rename to src/features/hero/AnimatedActa.tsx index 10d1106..ce8c21f 100644 --- a/src/components/AnimatedActa.tsx +++ b/src/features/hero/AnimatedActa.tsx @@ -24,23 +24,17 @@ export default function MeltingWord({ className={clsx( "relative inline-block font-bold tracking-tight", "text-4xl sm:text-6xl lg:text-8xl", - // main white text "text-white", - // subtle white glow via drop-shadow "drop-shadow-[0_0_14px_rgba(255,255,255,0.25)]", - // ---- glow layers (no CSS file needed) ---- - // layer 1 (wider, softer) "before:content-[attr(data-text)] before:absolute before:inset-0 before:-z-10", "before:text-white before:pointer-events-none", "before:blur-[16px] before:opacity-40", - // layer 2 (tighter, brighter) "after:content-[attr(data-text)] after:absolute after:inset-0 after:-z-10", "after:text-white after:pointer-events-none", "after:blur-[8px] after:opacity-50", className )} > - {/* Animated text character by character: maintains the same gradient on each span */}
Sample
- {/* Scrollable area for the code */}
{data.snippet ? ( {data.snippet} @@ -219,7 +217,6 @@ export default function HowItWorks() { return (
- {/* gradient line (optional) */} - {/* Row A (slower) */} - {/* Row B (reverse & even slower for calm feel) */} - {/* Optional hard fades if you prefer them over mask-image */}
diff --git a/src/components/ValueDetails.tsx b/src/features/value-proposition/ValueDetails.tsx similarity index 100% rename from src/components/ValueDetails.tsx rename to src/features/value-proposition/ValueDetails.tsx diff --git a/src/components/ValueDetailsExtended.tsx b/src/features/value-proposition/ValueDetailsExtended.tsx similarity index 96% rename from src/components/ValueDetailsExtended.tsx rename to src/features/value-proposition/ValueDetailsExtended.tsx index a8e41c6..dd39dcc 100644 --- a/src/components/ValueDetailsExtended.tsx +++ b/src/features/value-proposition/ValueDetailsExtended.tsx @@ -60,7 +60,7 @@ export default function ValueDetailsExtended() { value: "noanchor", icon: , title: "VCs without anchoring", - pain: "Pain: Rely entirely on the issuer’s backend; no public proof of authenticity or revocation.", + pain: "Pain: Rely entirely on the issuer's backend; no public proof of authenticity or revocation.", win: "ACTA: Anchored on Soroban public, tamper-proof verification that never depends on the issuer.", }, ].map(a => ( @@ -103,7 +103,7 @@ export default function ValueDetailsExtended() { “Zero databases architecture”

- You don’t need to store or manage credential data. ACTA keeps + You don't need to store or manage credential data. ACTA keeps the encrypted payload on-chain and manages the full lifecycle on Soroban.

diff --git a/src/components/ValueProposition.tsx b/src/features/value-proposition/ValueProposition.tsx similarity index 90% rename from src/components/ValueProposition.tsx rename to src/features/value-proposition/ValueProposition.tsx index 89c93d8..3668f50 100644 --- a/src/components/ValueProposition.tsx +++ b/src/features/value-proposition/ValueProposition.tsx @@ -21,7 +21,6 @@ const FEATURES: Feature[] = [ export default function ValueProposition() { return (
- {/* top badge stays */} - “Issue trust at the speed of light.” + “Issue trust at the speed of light.” @@ -56,21 +55,16 @@ export default function ValueProposition() { ); } -/** Card styled like the reference screenshot */ function FeatureCard({ title }: Feature) { return (
@@ -78,7 +72,6 @@ function FeatureCard({ title }: Feature) { className={[ "mb-3 font-extrabold uppercase tracking-[0.18em]", "text-transparent bg-clip-text", - // cream → mint gradient (como la imagen) "bg-[linear-gradient(180deg,#F0E7CC_0%,#E9F8D8_55%,#FFFFFF_100%)]", "drop-shadow-[0_0_10px_rgba(255,255,255,0.08)]", "text-lg sm:text-xl", diff --git a/src/components/WaitlistForm.tsx b/src/features/waitlist/WaitlistForm.tsx similarity index 95% rename from src/components/WaitlistForm.tsx rename to src/features/waitlist/WaitlistForm.tsx index 9a709db..9df9995 100644 --- a/src/components/WaitlistForm.tsx +++ b/src/features/waitlist/WaitlistForm.tsx @@ -24,7 +24,6 @@ export default function WaitlistForm() { const [isSubmitting, setIsSubmitting] = useState(false); const [status, setStatus] = useState("idle"); - // Honeypot for bots const [botField, setBotField] = useState(""); const validateEmail = (value: string) => @@ -34,7 +33,7 @@ export default function WaitlistForm() { e.preventDefault(); setStatus("idle"); - if (botField) return; // likely a bot + if (botField) return; if (!validateEmail(email)) { setStatus("error"); @@ -42,7 +41,6 @@ export default function WaitlistForm() { } setIsSubmitting(true); - // submit start try { const res = await fetch(FORMSPREE_ENDPOINT, { @@ -55,7 +53,7 @@ export default function WaitlistForm() { email, company, message, - _gotcha: botField, // honeypot + _gotcha: botField, _subject: "New waitlist signup · Acta", page: typeof window !== "undefined" ? window.location.href : "", }), @@ -63,16 +61,12 @@ export default function WaitlistForm() { if (!res.ok) throw new Error("Submission failed"); - // Reset form setEmail(""); setCompany(""); setMessage(""); setStatus("ok"); - - // submit ok - } catch (err) { + } catch { setStatus("error"); - // submit error } finally { setIsSubmitting(false); } @@ -97,7 +91,6 @@ export default function WaitlistForm() {
- {/* Honeypot field (hidden) */} - {/* soft inner vignette */}
diff --git a/src/components/theme-provider.tsx b/src/providers/theme-provider.tsx similarity index 100% rename from src/components/theme-provider.tsx rename to src/providers/theme-provider.tsx From 546e6f72f87c8c8c29b7319059d2c1fe80cefcf6 Mon Sep 17 00:00:00 2001 From: Daniel Coto Jimenez Date: Wed, 25 Feb 2026 16:04:33 -0600 Subject: [PATCH 03/25] chore: standardize quotes in workflow configuration and improve text formatting - Updated quotes in the GitHub Actions workflow for Node.js setup to use double quotes for consistency. - Reformatted text in ValueDetailsExtended component for better readability. --- .github/workflows/pr-checks.yml | 8 ++++---- src/features/value-proposition/ValueDetailsExtended.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) 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/src/features/value-proposition/ValueDetailsExtended.tsx b/src/features/value-proposition/ValueDetailsExtended.tsx index dd39dcc..b4cfeb8 100644 --- a/src/features/value-proposition/ValueDetailsExtended.tsx +++ b/src/features/value-proposition/ValueDetailsExtended.tsx @@ -103,9 +103,9 @@ export default function ValueDetailsExtended() { “Zero databases architecture”

- You don't need to store or manage credential data. ACTA keeps - the encrypted payload on-chain and manages the full lifecycle on - Soroban. + You don't need to store or manage credential data. ACTA + keeps the encrypted payload on-chain and manages the full + lifecycle on Soroban.

From 75059646802e7e81712de702fed724cf5509b9cc Mon Sep 17 00:00:00 2001 From: JuliobaCR Date: Wed, 25 Feb 2026 18:20:59 -0600 Subject: [PATCH 04/25] feat: implement local Supabase (Docker) integration with waitlist persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overview Add complete local Supabase setup using Docker and Supabase CLI, providing developers with a self-contained database environment for waitlist feature development. The implementation includes database schema, migrations, seeding, API integration, and comprehensive documentation. Core Implementation Database Layer - Initialize Supabase project with local Docker configuration (supabase/config.toml) - Create migration (20250225000000_create_waitlist_table.sql) with: * public.waitlist table: id (UUID PK), email (UNIQUE), company_name, use_case, created_at * Index on created_at DESC for efficient chronological queries * Row Level Security (RLS) policies enforcing anonymous inserts, service_role selects - Add idempotent seed.sql with 8 production-ready sample data rows - Use ON CONFLICT (email) DO NOTHING to ensure seed re-execution safety Backend Integration - Create Supabase client (src/lib/supabase.ts) with fallback resilience: * Placeholder credentials when environment variables missing or invalid * Service-only getServiceSupabase() function isolating service role key * Graceful console warnings when keys missing (non-blocking startup) - Implement POST /api/waitlist route handler with: * Email validation (RFC-compliant regex) * Honeypot anti-bot field handling * Duplicate email detection (PostgreSQL 23505 → HTTP 409 Conflict) * Proper status codes: 201 Created, 400 Bad Request, 409 Conflict, 500 Internal Error * Server-side payload sanitization and database insert Frontend Updates - Refactor WaitlistForm component (src/features/waitlist/WaitlistForm.tsx): * Replace Formspree endpoint with internal /api/waitlist * Map form fields to database schema: company → company_name, message → use_case * Implement 4-state UX: idle, ok (201), duplicate (409), error (400/500) * Preserve existing styling and user experience patterns * Maintain loading and success states with appropriate messaging Configuration & Scripts - Add 4 npm scripts for database lifecycle management: * npm run db:start → npx supabase start (Docker containers) * npm run db:stop → npx supabase stop (Graceful shutdown) * npm run db:reset → npx supabase db reset (Migrations + seed) * npm run db:migration → npx supabase migration new (New migration) - Install @supabase/supabase-js@^2.97.0 dependency - Update .env.example with optional Supabase environment variables and documentation - Extend .gitignore to exclude supabase/.temp/ (temporary files) and .env.local (local credentials) Documentation - Expand README.md with comprehensive "Local Supabase (Docker) — Optional" section: * Updated prerequisites (Docker Desktop, Supabase CLI marked as optional) * 5-step quick start guide for local development * Database scripts reference table * Supabase Studio access instructions * Detailed credential fallback behavior explanation * Testing instructions across different scenarios Architecture & Design Patterns Resilience & Graceful Degradation The implementation follows fail-safe principles: - App starts successfully with placeholder credentials (no environment variables required) - Waitlist submissions execute without errors in all scenarios - Real database persistence only when valid credentials provided - Non-blocking warnings logged to console when optional services unavailable Security Considerations - Service role key exclusively server-side (never in client bundle) - Row Level Security enforces anonymous insert-only, service-role full access - Email uniqueness enforced at both database and application layers - Input validation at multiple layers: client-side regex, server-side validation, database constraints - No hardcoded secrets (all credentials via environment variables) Code Quality - Full TypeScript type safety across all new code - Clear separation of concerns: client, API route, database client - Comprehensive code comments explaining fallback behavior - Follows Next.js and React best practices - Maintains consistency with existing project architecture and styling Acceptance Criteria Fulfillment ✓ README documents all prerequisites including optional Docker/Supabase CLI ✓ Documentation explains credentials are optional, app uses fallbacks ✓ Supabase client implements placeholder URLs and JWT tokens ✓ supabase/config.toml exists with default Supabase configuration ✓ All 4 npm scripts (db:start, db:stop, db:reset, db:migration) implemented ✓ Migration creates public.waitlist with required columns and indices ✓ Seed file contains exactly 8 rows with idempotent ON CONFLICT handling ✓ .env.example includes all 3 Supabase variables with documentation ✓ .gitignore properly excludes temporary and local credential files ✓ WaitlistForm successfully migrated from Formspree to Supabase backend ✓ API route validates all inputs, handles duplicates, manages database errors ✓ RLS policies enforce security while allowing anonymous signups Testing Verification - Syntax validation: All TypeScript files compile without errors - Import resolution: All path aliases (@/lib, @/components) resolve correctly - Dependency installation: @supabase/supabase-js available in node_modules - File structure: All required files exist in correct locations - Database schema: Migration includes table creation, indexing, RLS policies - API functionality: Route handler exports POST, validates inputs, handles errors - Component integration: WaitlistForm maps fields, handles all response states - Documentation: README complete, .env.example documented, gitignore updated Related Issue Closes #23 - Add local Supabase (Docker) and waitlist table with seed --- .env.example | 16 +- .gitignore | 5 + README.md | 63 +++ package-lock.json | 149 ++++++- package.json | 7 +- src/app/api/waitlist/route.ts | 74 ++++ src/features/waitlist/WaitlistForm.tsx | 22 +- src/lib/supabase.ts | 69 ++++ supabase/.gitignore | 8 + supabase/config.toml | 388 ++++++++++++++++++ .../20250225000000_create_waitlist_table.sql | 28 ++ supabase/seed.sql | 14 + 12 files changed, 828 insertions(+), 15 deletions(-) create mode 100644 src/app/api/waitlist/route.ts create mode 100644 src/lib/supabase.ts create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/migrations/20250225000000_create_waitlist_table.sql create mode 100644 supabase/seed.sql 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/.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..0c79591 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 540e223..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,18 +2937,24 @@ "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", "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2878,6 +2965,7 @@ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2888,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", @@ -2934,6 +3031,7 @@ "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/types": "8.40.0", @@ -3451,6 +3549,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4410,7 +4509,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4674,6 +4774,7 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4848,6 +4949,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5596,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", @@ -6902,6 +7013,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -7441,6 +7553,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7471,6 +7584,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7498,13 +7612,15 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7633,7 +7749,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8480,6 +8597,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8639,6 +8757,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8670,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": { @@ -8957,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..c9e78f7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "lint:check": "eslint . --ext .js,.jsx,.ts,.tsx", "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/src/app/api/waitlist/route.ts b/src/app/api/waitlist/route.ts new file mode 100644 index 0000000..be5f2ff --- /dev/null +++ b/src/app/api/waitlist/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { getServiceSupabase } from "@/lib/supabase"; + +// -------------------------------------------------------------------------- +// POST /api/waitlist – Insert a new waitlist signup into Supabase +// -------------------------------------------------------------------------- + +interface WaitlistPayload { + email: string; + company_name?: string; + use_case?: string; + _gotcha?: string; // honeypot field +} + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export async function POST(request: Request) { + try { + const body = (await request.json()) as WaitlistPayload; + + // Honeypot check – bots will fill this hidden field + if (body._gotcha) { + // Return 200 silently so bots think it succeeded + return NextResponse.json({ success: true }); + } + + // ---- Validation ---- + const email = body.email?.trim().toLowerCase(); + if (!email || !EMAIL_REGEX.test(email)) { + return NextResponse.json( + { success: false, 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; + + // ---- Insert into Supabase ---- + const supabase = getServiceSupabase(); + + const { error } = await supabase + .from("waitlist") + .insert({ email, company_name, use_case }); + + if (error) { + // Unique constraint violation → duplicate email + if (error.code === "23505") { + return NextResponse.json( + { + success: false, + error: "This email is already on the waitlist.", + code: "DUPLICATE_EMAIL", + }, + { status: 409 }, + ); + } + + console.error("[api/waitlist] Supabase insert error:", error); + return NextResponse.json( + { success: false, error: "Unable to join the waitlist right now." }, + { status: 500 }, + ); + } + + return NextResponse.json({ success: true }, { status: 201 }); + } catch (err) { + console.error("[api/waitlist] Unexpected error:", err); + return NextResponse.json( + { success: false, error: "Internal server error." }, + { status: 500 }, + ); + } +} diff --git a/src/features/waitlist/WaitlistForm.tsx b/src/features/waitlist/WaitlistForm.tsx index 9df9995..edfdf2c 100644 --- a/src/features/waitlist/WaitlistForm.tsx +++ b/src/features/waitlist/WaitlistForm.tsx @@ -13,9 +13,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { ShineBorder } from "@/components/ui/shine-border"; -const FORMSPREE_ENDPOINT = "https://formspree.io/f/xyzndpdo"; - -type Status = "idle" | "ok" | "error"; +type Status = "idle" | "ok" | "duplicate" | "error"; export default function WaitlistForm() { const [email, setEmail] = useState(""); @@ -43,7 +41,7 @@ export default function WaitlistForm() { setIsSubmitting(true); try { - const res = await fetch(FORMSPREE_ENDPOINT, { + const res = await fetch("/api/waitlist", { method: "POST", headers: { "Content-Type": "application/json", @@ -51,14 +49,17 @@ export default function WaitlistForm() { }, body: JSON.stringify({ email, - company, - message, + company_name: company, + use_case: message, _gotcha: botField, - _subject: "New waitlist signup · Acta", - page: typeof window !== "undefined" ? window.location.href : "", }), }); + if (res.status === 409) { + setStatus("duplicate"); + return; + } + if (!res.ok) throw new Error("Submission failed"); setEmail(""); @@ -141,6 +142,11 @@ export default function WaitlistForm() { Thank you! We will contact you soon.

)} + {status === "duplicate" && ( +

+ This email is already on the waitlist. We'll be in touch! +

+ )} {status === "error" && (

Something went wrong. Please check your email and try again. diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts new file mode 100644 index 0000000..9ed780a --- /dev/null +++ b/src/lib/supabase.ts @@ -0,0 +1,69 @@ +import { createClient, type SupabaseClient } from "@supabase/supabase-js"; + +// --------------------------------------------------------------------------- +// Placeholder values – used when env vars are missing or look like templates. +// The app will start without throwing; calls against the placeholder URL will +// simply fail (acceptable for dev without Docker / Supabase running). +// --------------------------------------------------------------------------- +const PLACEHOLDER_URL = "https://placeholder.supabase.co"; +const PLACEHOLDER_ANON_KEY = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +function isPlaceholder(value: string | undefined): boolean { + if (!value) return true; + const v = value.trim(); + if (v === "") return true; + if (v.includes("your_supabase")) return true; + if (v.includes("placeholder")) return true; + return false; +} + +// --------------------------------------------------------------------------- +// Resolve env vars with fallbacks +// --------------------------------------------------------------------------- +const rawUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const rawAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; +const rawServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +const supabaseUrl = isPlaceholder(rawUrl) ? PLACEHOLDER_URL : rawUrl!; +const supabaseAnonKey = isPlaceholder(rawAnonKey) + ? PLACEHOLDER_ANON_KEY + : rawAnonKey!; + +/** + * Public (anon) Supabase client – safe to use in both client and server code. + * When env vars are missing the client is created with placeholder values; + * requests will fail gracefully but the app will not crash on startup. + */ +export const supabase: SupabaseClient = createClient( + supabaseUrl, + supabaseAnonKey, +); + +/** + * Server-only Supabase client with the service-role key. + * Falls back to the anon client when SUPABASE_SERVICE_ROLE_KEY is not set. + * + * **Never import this in client components / bundles.** + */ +export function getServiceSupabase(): SupabaseClient { + if (!rawServiceRoleKey || isPlaceholder(rawServiceRoleKey)) { + if (typeof window === "undefined") { + console.warn( + "[supabase] SUPABASE_SERVICE_ROLE_KEY is missing – falling back to anon client. " + + "Waitlist inserts will use RLS policies. Set the key in .env.local for full access.", + ); + } + return supabase; + } + + return createClient(supabaseUrl, rawServiceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..f16ea73 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,388 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "websiteACTA" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +# This feature is only available on the hosted platform. +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/supabase/migrations/20250225000000_create_waitlist_table.sql b/supabase/migrations/20250225000000_create_waitlist_table.sql new file mode 100644 index 0000000..ba4f1c7 --- /dev/null +++ b/supabase/migrations/20250225000000_create_waitlist_table.sql @@ -0,0 +1,28 @@ +-- Create public.waitlist table for storing waitlist signups +create table public.waitlist ( + id uuid primary key default gen_random_uuid(), + + -- Form data + email text not null unique, + company_name text, + use_case text, + + -- Timestamps + created_at timestamptz not null default now() +); + +-- Index for listing by date +create index waitlist_created_at_idx on public.waitlist (created_at desc); + +-- Enable Row Level Security +alter table public.waitlist enable row level security; + +-- Allow anonymous inserts (for the waitlist form) +create policy "Allow anonymous inserts" on public.waitlist + for insert + with check (true); + +-- Restrict select to service role only (via supabase admin / service key) +create policy "Service role can read all" on public.waitlist + for select + using (auth.role() = 'service_role'); diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..4875cf0 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,14 @@ +-- Seed data for the waitlist table +-- Uses ON CONFLICT to make the seed idempotent (safe to re-run) + +insert into public.waitlist (email, company_name, use_case) +values + ('maria.gomez@example.com', 'TechFlow', 'We want to integrate Acta API to automate compliance onboarding.'), + ('juan.perez@startup.io', 'Startup.io', 'Looking to verify credentials of our freelancers through Acta.'), + ('karla.smith@finpay.com', 'FinPay', 'Using Acta to validate identity of our vendors and treasury operators.'), + ('andres.lopez@creatorshub.com', 'CreatorsHub', 'Need credential verification for creators and digital artists.'), + ('laura.ramirez@example.org', null, 'I am testing Acta API for a side project with academic credentials.'), + ('diego.martinez@securechain.dev', 'SecureChain', 'We want to build a trusted P2P verification flow.'), + ('sofia.hernandez@eduverse.com', 'EduVerse', 'Using Acta to verify student certificates and micro-credentials.'), + ('carlos.mena@testmail.dev', null, 'Exploring Acta for document verification in a personal project.') +on conflict (email) do nothing; From d653ef701238b66713239738e796ea2f5d5e81aa Mon Sep 17 00:00:00 2001 From: FabianSanchezD Date: Wed, 25 Feb 2026 22:45:38 -0600 Subject: [PATCH 05/25] fix: smoother transition for how it works component --- src/features/how-it-works/HowItWorks.tsx | 36 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/features/how-it-works/HowItWorks.tsx b/src/features/how-it-works/HowItWorks.tsx index c62e08b..f0f39d0 100644 --- a/src/features/how-it-works/HowItWorks.tsx +++ b/src/features/how-it-works/HowItWorks.tsx @@ -5,7 +5,9 @@ import { motion, AnimatePresence } from "framer-motion"; import { Globe, Link2, ShieldCheck, ChevronDown } from "lucide-react"; import { cn } from "@/lib/utils"; -const ease = [0.32, 0.72, 0, 1] as const; // cubic-bezier +const ease = [0.4, 0, 0.2, 1] as const; // smooth ease-out +const OPEN_DELAY = 0.10; // mini delay before opening (seconds) +const OPEN_DURATION = 0.4; // slightly longer for smoother feel type Step = { id: string; @@ -141,10 +143,12 @@ function StepRow({ {/* Expandable content: spans same columns as number, icon, then content in title column */}