diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-16 13:38:26 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-16 13:38:26 +0200 |
| commit | 49d467cd83522983da0a59892838d65f605fa78f (patch) | |
| tree | e405fca919948bd18966fbee108c649ebdfcc40d /apps/web/src/components/landing/hero.tsx | |
| parent | 13db27c173a844239a3847369ee55216f7439c9e (diff) | |
| parent | 96f7516c94cc922519e5369ef132b62d9a52e85c (diff) | |
| download | opencut-49d467cd83522983da0a59892838d65f605fa78f.tar.gz | |
Merge branch 'staging' into pr/vishesh711/284
Diffstat (limited to 'apps/web/src/components/landing/hero.tsx')
| -rw-r--r-- | apps/web/src/components/landing/hero.tsx | 100 |
1 files changed, 50 insertions, 50 deletions
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 01ad7e7e..b1910dc5 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -4,19 +4,37 @@ import { motion } from "motion/react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { ArrowRight } from "lucide-react"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { toast } from "sonner"; import Image from "next/image"; import { Handlebars } from "./handlebars"; -interface HeroProps { - signupCount: number; -} - -export function Hero({ signupCount }: HeroProps) { +export function Hero() { const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [csrfToken, setCsrfToken] = useState<string | null>(null); + + useEffect(() => { + let isMounted = true; + fetch("/api/waitlist/token", { + credentials: "include", + }) + .then((res) => res.json()) + .then((data) => { + if (isMounted && data.token) { + setCsrfToken(data.token); + } + }) + .catch((err) => { + console.error("Failed to fetch CSRF token:", err); + if (isMounted) { + toast.error("Security initialization failed", { + description: "Please refresh the page to continue.", + }); + } + }); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -28,6 +46,13 @@ export function Hero({ signupCount }: HeroProps) { return; } + if (!csrfToken) { + toast.error("Security error", { + description: "Please refresh the page and try again.", + }); + return; + } + setIsSubmitting(true); try { @@ -35,7 +60,9 @@ export function Hero({ signupCount }: HeroProps) { method: "POST", headers: { "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, }, + credentials: "include", body: JSON.stringify({ email: email.trim() }), }); @@ -46,11 +73,18 @@ export function Hero({ signupCount }: HeroProps) { description: "You'll be notified when we launch.", }); setEmail(""); + + fetch("/api/waitlist/token", { credentials: "include" }) + .then((res) => res.json()) + .then((data) => { + if (data.token) setCsrfToken(data.token); + }) + .catch((err) => { + console.error("Failed to refresh CSRF token:", err); + }); } else { toast.error("Oops!", { - description: - (data as { error: string }).error || - "Something went wrong. Please try again.", + description: (data as { error: string }).error || "Something went wrong. Please try again.", }); } } catch (error) { @@ -64,13 +98,7 @@ export function Hero({ signupCount }: HeroProps) { return ( <div className="min-h-[calc(100vh-4.5rem)] supports-[height:100dvh]:min-h-[calc(100dvh-4.5rem)] flex flex-col justify-between items-center text-center px-4"> - <Image - className="absolute top-0 left-0 -z-50 size-full object-cover" - src="/landing-page-bg.png" - height={1903.5} - width={1269} - alt="landing-page.bg" - /> + <Image className="absolute top-0 left-0 -z-50 size-full object-cover" src="/landing-page-bg.png" height={1903.5} width={1269} alt="landing-page.bg" /> <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} @@ -93,20 +121,11 @@ export function Hero({ signupCount }: HeroProps) { animate={{ opacity: 1 }} transition={{ delay: 0.4, duration: 0.8 }} > - A simple but powerful video editor that gets the job done. Works on - any platform. + A simple but powerful video editor that gets the job done. Works on any platform. </motion.p> - <motion.div - className="mt-12 flex gap-8 justify-center" - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - transition={{ delay: 0.6, duration: 0.8 }} - > - <form - onSubmit={handleSubmit} - className="flex gap-3 w-full max-w-lg flex-col sm:flex-row" - > + <motion.div className="mt-12 flex gap-8 justify-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6, duration: 0.8 }}> + <form onSubmit={handleSubmit} className="flex gap-3 w-full max-w-lg flex-col sm:flex-row"> <div className="relative w-full"> <Input type="email" @@ -114,35 +133,16 @@ export function Hero({ signupCount }: HeroProps) { className="h-11 text-base flex-1" value={email} onChange={(e) => setEmail(e.target.value)} - disabled={isSubmitting} + disabled={isSubmitting || !csrfToken} required /> </div> - <Button - type="submit" - size="lg" - className="px-6 h-11 text-base !bg-foreground" - disabled={isSubmitting} - > - <span className="relative z-10"> - {isSubmitting ? "Joining..." : "Join waitlist"} - </span> + <Button type="submit" size="lg" className="px-6 h-11 text-base !bg-foreground" disabled={isSubmitting || !csrfToken}> + <span className="relative z-10">{isSubmitting ? "Joining..." : "Join waitlist"}</span> <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" /> </Button> </form> </motion.div> - - {signupCount > 0 && ( - <motion.div - initial={{ opacity: 0, y: 10 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.8, duration: 0.6 }} - className="mt-8 inline-flex items-center gap-2 text-sm text-muted-foreground justify-center" - > - <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" /> - <span>{signupCount.toLocaleString()} people already joined</span> - </motion.div> - )} </motion.div> </div> ); |
