diff options
| author | Hyteq <DerpsDer.es@gmail.com> | 2025-07-20 11:00:22 +0300 |
|---|---|---|
| committer | Hyteq <DerpsDer.es@gmail.com> | 2025-07-20 11:00:22 +0300 |
| commit | be1ae20d9089e81cb84a8294be76fe680a24b60f (patch) | |
| tree | 48472c1122d35f45f802bbd1c3cf2c15f653f9cf /apps/web/src/components/landing | |
| parent | ede08dac416b47eac2db151969c164bdf0f06002 (diff) | |
| download | opencut-be1ae20d9089e81cb84a8294be76fe680a24b60f.tar.gz | |
format
Diffstat (limited to 'apps/web/src/components/landing')
| -rw-r--r-- | apps/web/src/components/landing/handlebars.tsx | 274 | ||||
| -rw-r--r-- | apps/web/src/components/landing/hero.tsx | 336 |
2 files changed, 305 insertions, 305 deletions
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index 36705e90..d65fccff 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -4,161 +4,161 @@ import React, { useState, useRef, useEffect } from "react"; import { motion, useMotionValue, useTransform, PanInfo } from "motion/react"; interface HandlebarsProps { - children: React.ReactNode; - minWidth?: number; - maxWidth?: number; - onRangeChange?: (left: number, right: number) => void; + children: React.ReactNode; + minWidth?: number; + maxWidth?: number; + onRangeChange?: (left: number, right: number) => void; } export function Handlebars({ - children, - minWidth = 50, - maxWidth = 400, - onRangeChange, + children, + minWidth = 50, + maxWidth = 400, + onRangeChange, }: HandlebarsProps) { - const [leftHandle, setLeftHandle] = useState(0); - const [rightHandle, setRightHandle] = useState(maxWidth); - const [contentWidth, setContentWidth] = useState(maxWidth); + const [leftHandle, setLeftHandle] = useState(0); + const [rightHandle, setRightHandle] = useState(maxWidth); + const [contentWidth, setContentWidth] = useState(maxWidth); - const leftHandleX = useMotionValue(0); - const rightHandleX = useMotionValue(maxWidth); + const leftHandleX = useMotionValue(0); + const rightHandleX = useMotionValue(maxWidth); - const visibleWidth = useTransform( - [leftHandleX, rightHandleX], - (values: number[]) => values[1] - values[0] - ); + const visibleWidth = useTransform( + [leftHandleX, rightHandleX], + (values: number[]) => values[1] - values[0], + ); - const contentLeft = useTransform(leftHandleX, (left: number) => -left); + const contentLeft = useTransform(leftHandleX, (left: number) => -left); - const containerRef = useRef<HTMLDivElement>(null); - const measureRef = useRef<HTMLDivElement>(null); + const containerRef = useRef<HTMLDivElement>(null); + const measureRef = useRef<HTMLDivElement>(null); - useEffect(() => { - if (!measureRef.current) return; + useEffect(() => { + if (!measureRef.current) return; - const measureContent = () => { - if (measureRef.current) { - const width = measureRef.current.scrollWidth; - const paddedWidth = width + 32; - setContentWidth(paddedWidth); - setRightHandle(paddedWidth); - rightHandleX.set(paddedWidth); - } - }; + const measureContent = () => { + if (measureRef.current) { + const width = measureRef.current.scrollWidth; + const paddedWidth = width + 32; + setContentWidth(paddedWidth); + setRightHandle(paddedWidth); + rightHandleX.set(paddedWidth); + } + }; - measureContent(); - const timer = setTimeout(measureContent, 50); + measureContent(); + const timer = setTimeout(measureContent, 50); - return () => clearTimeout(timer); - }, [children, rightHandleX]); + return () => clearTimeout(timer); + }, [children, rightHandleX]); - useEffect(() => { - leftHandleX.set(leftHandle); - }, [leftHandle, leftHandleX]); + useEffect(() => { + leftHandleX.set(leftHandle); + }, [leftHandle, leftHandleX]); - useEffect(() => { - rightHandleX.set(rightHandle); - }, [rightHandle, rightHandleX]); + useEffect(() => { + rightHandleX.set(rightHandle); + }, [rightHandle, rightHandleX]); - useEffect(() => { - onRangeChange?.(leftHandle, rightHandle); - }, [leftHandle, rightHandle, onRangeChange]); + useEffect(() => { + onRangeChange?.(leftHandle, rightHandle); + }, [leftHandle, rightHandle, onRangeChange]); - const handleLeftDrag = (event: any, info: PanInfo) => { - const newLeft = Math.max( - 0, - Math.min(leftHandle + info.offset.x, rightHandle - minWidth) - ); - setLeftHandle(newLeft); - }; + const handleLeftDrag = (event: any, info: PanInfo) => { + const newLeft = Math.max( + 0, + Math.min(leftHandle + info.offset.x, rightHandle - minWidth), + ); + setLeftHandle(newLeft); + }; - const handleRightDrag = (event: any, info: PanInfo) => { - const newRight = Math.max( - leftHandle + minWidth, - Math.min(contentWidth, rightHandle + info.offset.x) - ); - setRightHandle(newRight); - }; + const handleRightDrag = (event: any, info: PanInfo) => { + const newRight = Math.max( + leftHandle + minWidth, + Math.min(contentWidth, rightHandle + info.offset.x), + ); + setRightHandle(newRight); + }; - return ( - <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2"> - <div - ref={measureRef} - className="absolute -left-[9999px] top-0 invisible px-4 whitespace-nowrap font-[inherit]" - > - {children} - </div> + return ( + <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2"> + <div + ref={measureRef} + className="absolute -left-[9999px] top-0 invisible px-4 whitespace-nowrap font-[inherit]" + > + {children} + </div> - <div - ref={containerRef} - className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2" - style={{ width: contentWidth }} - > - <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between"> - <motion.div - className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none" - style={{ - position: "absolute", - x: leftHandleX, - left: 0, - zIndex: 10, - }} - drag="x" - dragConstraints={{ left: 0, right: rightHandle - minWidth }} - dragElastic={0} - dragMomentum={false} - onDrag={handleLeftDrag} - whileHover={{ scale: 1.05 }} - whileDrag={{ scale: 1.1 }} - transition={{ type: "spring", stiffness: 400, damping: 30 }} - > - <div className="w-2 h-8 rounded-full bg-yellow-500"></div> - </motion.div> + <div + ref={containerRef} + className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2" + style={{ width: contentWidth }} + > + <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between"> + <motion.div + className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none" + style={{ + position: "absolute", + x: leftHandleX, + left: 0, + zIndex: 10, + }} + drag="x" + dragConstraints={{ left: 0, right: rightHandle - minWidth }} + dragElastic={0} + dragMomentum={false} + onDrag={handleLeftDrag} + whileHover={{ scale: 1.05 }} + whileDrag={{ scale: 1.1 }} + transition={{ type: "spring", stiffness: 400, damping: 30 }} + > + <div className="w-2 h-8 rounded-full bg-yellow-500"></div> + </motion.div> - <motion.div - className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none" - style={{ - position: "absolute", - x: rightHandleX, - left: -30, - zIndex: 10, - }} - drag="x" - dragConstraints={{ - left: leftHandle + minWidth, - right: contentWidth, - }} - dragElastic={0} - dragMomentum={false} - onDrag={handleRightDrag} - whileHover={{ scale: 1.05 }} - whileDrag={{ scale: 1.1 }} - transition={{ type: "spring", stiffness: 400, damping: 30 }} - > - <div className="w-2 h-8 rounded-full bg-yellow-500"></div> - </motion.div> - </div> + <motion.div + className="h-full border border-yellow-500 w-7 rounded-full bg-accent flex items-center justify-center cursor-ew-resize select-none" + style={{ + position: "absolute", + x: rightHandleX, + left: -30, + zIndex: 10, + }} + drag="x" + dragConstraints={{ + left: leftHandle + minWidth, + right: contentWidth, + }} + dragElastic={0} + dragMomentum={false} + onDrag={handleRightDrag} + whileHover={{ scale: 1.05 }} + whileDrag={{ scale: 1.1 }} + transition={{ type: "spring", stiffness: 400, damping: 30 }} + > + <div className="w-2 h-8 rounded-full bg-yellow-500"></div> + </motion.div> + </div> - <motion.div - className="relative overflow-hidden rounded-2xl" - style={{ - width: visibleWidth, - x: leftHandleX, - height: "100%", - }} - > - <motion.div - className="w-full h-full flex items-center justify-center px-4" - style={{ - x: contentLeft, - width: contentWidth, - whiteSpace: "nowrap", - }} - > - {children} - </motion.div> - </motion.div> - </div> - </div> - ); -}; + <motion.div + className="relative overflow-hidden rounded-2xl" + style={{ + width: visibleWidth, + x: leftHandleX, + height: "100%", + }} + > + <motion.div + className="w-full h-full flex items-center justify-center px-4" + style={{ + x: contentLeft, + width: contentWidth, + whiteSpace: "nowrap", + }} + > + {children} + </motion.div> + </motion.div> + </div> + </div> + ); +} diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index d5fe38d1..4030c438 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -13,184 +13,184 @@ import Image from "next/image"; import { Handlebars } from "./handlebars"; export function Hero() { - const [email, setEmail] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [csrfToken, setCsrfToken] = useState<string | null>(null); + 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.", - }); - } - }); - }, []); + 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(); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - if (!email.trim()) { - toast.error("Email required", { - description: "Please enter your email address.", - }); - return; - } + if (!email.trim()) { + toast.error("Email required", { + description: "Please enter your email address.", + }); + return; + } - if (!csrfToken) { - toast.error("Security error", { - description: "Please refresh the page and try again.", - }); - return; - } + if (!csrfToken) { + toast.error("Security error", { + description: "Please refresh the page and try again.", + }); + return; + } - setIsSubmitting(true); + setIsSubmitting(true); - try { - const response = await fetch("/api/waitlist", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": csrfToken, - }, - credentials: "include", - body: JSON.stringify({ email: email.trim() }), - }); + try { + const response = await fetch("/api/waitlist", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, + }, + credentials: "include", + body: JSON.stringify({ email: email.trim() }), + }); - const data = (await response.json()) as { error: string }; + const data = (await response.json()) as { error: string }; - if (response.ok) { - toast.success("Welcome to the waitlist! 🎉", { - description: "You'll be notified when we launch.", - }); - setEmail(""); + if (response.ok) { + toast.success("Welcome to the waitlist! 🎉", { + 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.", - }); - } - } catch (error) { - toast.error("Network error", { - description: "Please check your connection and try again.", - }); - } finally { - setIsSubmitting(false); - } - }; + 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.", + }); + } + } catch (error) { + toast.error("Network error", { + description: "Please check your connection and try again.", + }); + } finally { + setIsSubmitting(false); + } + }; - 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" - /> - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - transition={{ duration: 1 }} - className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center" - > - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.6, duration: 0.8 }} - className="mb-8 flex justify-center" - > - <SponsorButton - href="https://vercel.com/?utm_source=opencut" - logo={VercelIcon} - companyName="Vercel" - /> - </motion.div> - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.2, duration: 0.8 }} - className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]" - > - <h1>The Open Source</h1> - <Handlebars>Video Editor</Handlebars> - </motion.div> + 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" + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 1 }} + className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center" + > + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.6, duration: 0.8 }} + className="mb-8 flex justify-center" + > + <SponsorButton + href="https://vercel.com/?utm_source=opencut" + logo={VercelIcon} + companyName="Vercel" + /> + </motion.div> + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.2, duration: 0.8 }} + className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]" + > + <h1>The Open Source</h1> + <Handlebars>Video Editor</Handlebars> + </motion.div> - <motion.p - className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto" - initial={{ opacity: 0 }} - 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. - </motion.p> + <motion.p + className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto" + initial={{ opacity: 0 }} + 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. + </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" - > - <div className="relative w-full"> - <Input - type="email" - placeholder="Enter your email" - className="h-11 text-base flex-1" - value={email} - onChange={(e) => setEmail(e.target.value)} - disabled={isSubmitting || !csrfToken} - required - /> - </div> - <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> - <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>50k+ people already joined</span> - </motion.div> - </motion.div> - </div> - ); + <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" + placeholder="Enter your email" + className="h-11 text-base flex-1" + value={email} + onChange={(e) => setEmail(e.target.value)} + disabled={isSubmitting || !csrfToken} + required + /> + </div> + <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> + <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>50k+ people already joined</span> + </motion.div> + </motion.div> + </div> + ); } |
