diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/landing | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/landing')
| -rw-r--r-- | apps/web/src/components/landing/handlebars.tsx | 238 | ||||
| -rw-r--r-- | apps/web/src/components/landing/hero.tsx | 103 |
2 files changed, 187 insertions, 154 deletions
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index 758717e5..da0d895d 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -1,105 +1,173 @@ "use client"; -import { - motion, - useMotionTemplate, - useMotionValue, - useMotionValueEvent, - useTransform, -} from "motion/react"; import { type PropsWithChildren, useEffect, useRef, useState } from "react"; type HandlebarsProps = PropsWithChildren; export function Handlebars({ children }: HandlebarsProps) { - const containerRef = useRef<HTMLDivElement>(null); + const containerRef = useRef<HTMLDivElement>(null); + const leftHandleRef = useRef<HTMLDivElement>(null); + const rightHandleRef = useRef<HTMLDivElement>(null); - const [width, setWidth] = useState(0); + const [width, setWidth] = useState(0); + const [leftHandle, setLeftHandle] = useState(0); + const [rightHandle, setRightHandle] = useState(0); - const [leftHandle, setLeftHandle] = useState(0); - const [rightHandle, setRightHandle] = useState(width); + const widthRef = useRef(0); + const leftHandlePositionRef = useRef(0); + const rightHandlePositionRef = useRef(0); - const leftHandleX = useMotionValue(0); - const rightHandleX = useMotionValue(width); + const dragRef = useRef<{ + isDragging: boolean; + side: "left" | "right" | null; + pointerId: number | null; + startX: number; + initialPosition: number; + }>({ + isDragging: false, + side: null, + pointerId: null, + startX: 0, + initialPosition: 0, + }); - useEffect(() => { - const el = containerRef.current; - if (!el) return; + useEffect(() => { + const el = containerRef.current; + if (!el) return; - const observer = new ResizeObserver(() => { - setWidth(el.offsetWidth); - setRightHandle(el.offsetWidth); - rightHandleX.set(el.offsetWidth); - }); + const updateWidth = () => { + const newWidth = el.offsetWidth; + setWidth(newWidth); + setRightHandle(newWidth); + }; - observer.observe(el); - return () => observer.disconnect(); - }, [rightHandleX.set]); + const observer = new ResizeObserver(updateWidth); + observer.observe(el); + updateWidth(); - useMotionValueEvent(leftHandleX, "change", () => { - setLeftHandle(leftHandleX.get()); - }); - useMotionValueEvent(rightHandleX, "change", () => { - setRightHandle(rightHandleX.get()); - }); - const leftGradient = useTransform(leftHandleX, [0, width - 10], [0, 100]); - const rightGradient = useTransform(rightHandleX, [0, width + 10], [0, 100]); + return () => observer.disconnect(); + }, []); - return ( - <div className="flex justify-center gap-4 leading-16"> - <div ref={containerRef} className="relative -rotate-[2.76deg] mt-0.5"> - <div className="absolute inset-0 w-full h-full rounded-2xl border border-yellow-500 flex justify-between z-1"> - <motion.div - className="absolute z-10 left-0 h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none" - style={{ - x: leftHandleX, - }} - drag="x" - dragConstraints={{ left: 0, right: rightHandle - 60 }} - dragElastic={0} - dragMomentum={false} - whileHover={{ scale: 1.05, cursor: "grab" }} - whileDrag={{ scale: 1.1, cursor: "grabbing" }} - transition={{ type: "spring", stiffness: 400, damping: 30 }} - > - <div className="w-2 h-8 rounded-full bg-yellow-500" /> - </motion.div> + useEffect(() => { + widthRef.current = width; + leftHandlePositionRef.current = leftHandle; + rightHandlePositionRef.current = rightHandle; + }, [leftHandle, rightHandle, width]); - <motion.div - className="absolute z-10 -left-[30px] h-full border border-yellow-500 w-7 rounded-full bg-background flex items-center justify-center select-none" - style={{ - x: rightHandleX, - }} - drag="x" - dragConstraints={{ - left: leftHandle + 60, - right: width, - }} - dragElastic={0} - dragMomentum={false} - whileHover={{ scale: 1.05, cursor: "grab" }} - whileDrag={{ scale: 1.1, cursor: "grabbing" }} - transition={{ type: "spring", stiffness: 400, damping: 30 }} - > - <div className="w-2 h-8 rounded-full bg-yellow-500" /> - </motion.div> - </div> + useEffect(() => { + const handlePointerMove = (event: PointerEvent) => { + const { isDragging, side, pointerId, startX, initialPosition } = + dragRef.current; - <motion.span - className="inline-flex items-center justify-center w-full h-full px-9 relative rounded-2xl will-change-auto" - style={{ - mask: useMotionTemplate`linear-gradient(90deg, + if (!isDragging) return; + if (pointerId !== null && event.pointerId !== pointerId) return; + if (!side) return; + + const deltaX = event.clientX - startX; + + if (side === "left") { + const maxLeft = Math.max(0, rightHandlePositionRef.current - 60); + const nextLeftHandle = Math.max( + 0, + Math.min(maxLeft, initialPosition + deltaX), + ); + setLeftHandle(nextLeftHandle); + return; + } + + const minRight = Math.min( + widthRef.current, + leftHandlePositionRef.current + 60, + ); + const nextRightHandle = Math.max( + minRight, + Math.min(widthRef.current, initialPosition + deltaX), + ); + setRightHandle(nextRightHandle); + }; + + const handlePointerEnd = (event: PointerEvent) => { + const { pointerId } = dragRef.current; + if (pointerId !== null && event.pointerId !== pointerId) return; + + dragRef.current.isDragging = false; + dragRef.current.side = null; + dragRef.current.pointerId = null; + }; + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerEnd); + window.addEventListener("pointercancel", handlePointerEnd); + + return () => { + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerEnd); + window.removeEventListener("pointercancel", handlePointerEnd); + }; + }, []); + + const leftGradientPercent = width > 0 ? (leftHandle / (width - 10)) * 100 : 0; + const rightGradientPercent = + width > 0 ? (rightHandle / (width + 10)) * 100 : 0; + + return ( + <div className="flex justify-center gap-4 leading-16"> + <div ref={containerRef} className="relative mt-0.5 -rotate-[2.76deg]"> + <div className="absolute inset-0 z-10 flex size-full justify-between rounded-2xl border border-yellow-500"> + <div + ref={leftHandleRef} + className="bg-background absolute left-0 z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none" + style={{ + translate: `${leftHandle}px 0`, + }} + onPointerDown={(event) => { + event.preventDefault(); + leftHandleRef.current?.setPointerCapture(event.pointerId); + dragRef.current.isDragging = true; + dragRef.current.side = "left"; + dragRef.current.pointerId = event.pointerId; + dragRef.current.startX = event.clientX; + dragRef.current.initialPosition = leftHandlePositionRef.current; + }} + > + <div className="h-8 w-2 rounded-full bg-yellow-500" /> + </div> + + <div + ref={rightHandleRef} + className="bg-background absolute -left-[30px] z-20 flex h-full w-7 cursor-ew-resize touch-none items-center justify-center rounded-full border border-yellow-500 select-none" + style={{ + translate: `${rightHandle}px 0`, + }} + onPointerDown={(event) => { + event.preventDefault(); + rightHandleRef.current?.setPointerCapture(event.pointerId); + dragRef.current.isDragging = true; + dragRef.current.side = "right"; + dragRef.current.pointerId = event.pointerId; + dragRef.current.startX = event.clientX; + dragRef.current.initialPosition = rightHandlePositionRef.current; + }} + > + <div className="h-8 w-2 rounded-full bg-yellow-500" /> + </div> + </div> + + <span + className="relative z-0 inline-flex size-full items-center justify-center rounded-2xl px-9 will-change-auto" + style={{ + mask: `linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) ${leftGradient}%, - rgba(0, 0, 0) ${leftGradient}%, - rgba(0, 0, 0) ${rightGradient}%, - rgba(255, 255, 255, 0) ${rightGradient}%, + rgba(255, 255, 255, 0) ${leftGradientPercent}%, + rgba(0, 0, 0) ${leftGradientPercent}%, + rgba(0, 0, 0) ${rightGradientPercent}%, + rgba(255, 255, 255, 0) ${rightGradientPercent}%, rgba(255, 255, 255, 0) 100%)`, - }} - > - {children} - </motion.span> - </div> - </div> - ); + }} + > + {children} + </span> + </div> + </div> + ); } diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 1b557412..6f11ad7c 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -1,81 +1,46 @@ "use client"; -import { motion } from "motion/react"; import { Button } from "../ui/button"; -import { SponsorButton } from "../ui/sponsor-button"; -import { VercelIcon } from "../icons"; import { ArrowRight } from "lucide-react"; - import Image from "next/image"; import { Handlebars } from "./handlebars"; import Link from "next/link"; export function Hero() { - return ( - <div className="min-h-[calc(100svh-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 invert dark:invert-0 opacity-85" - src="/landing-page-dark.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-4 flex justify-center" - > - <SponsorButton - href="https://vercel.com/home?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="flex min-h-[calc(100svh-4.5rem)] flex-col items-center justify-between px-4 text-center"> + <Image + className="absolute top-0 left-0 -z-50 size-full object-cover opacity-85 invert dark:invert-0" + src="/landing-page-dark.png" + height={1903.5} + width={1269} + alt="landing-page.bg" + /> + <div className="mx-auto flex w-full max-w-3xl flex-1 flex-col justify-center"> + <div className="inline-block text-4xl font-bold tracking-tighter md:text-[4rem]"> + <h1>The open source</h1> + <Handlebars>Video editor</Handlebars> + </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> + <p className="text-muted-foreground mx-auto mt-10 max-w-xl text-base font-light tracking-wide sm:text-xl"> + A simple but powerful video editor that gets the job done. Works on + any platform. + </p> - <motion.div - className="mt-8 flex gap-8 justify-center" - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - transition={{ delay: 0.6, duration: 0.8 }} - > - <Link href="/projects"> - <Button - type="submit" - size="lg" - className="px-6 h-11 text-base bg-foreground" - > - Try early beta - <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" /> - </Button> - </Link> - </motion.div> - </motion.div> - </div> - ); + <div className="mt-8 flex justify-center gap-8"> + <Link href="/projects"> + <Button + variant="foreground" + type="submit" + size="lg" + className="h-11 text-base" + > + Try early beta + <ArrowRight className="ml-0.5" /> + </Button> + </Link> + </div> + </div> + </div> + ); } |
