From ab122584321d5620480884793c41bb650fe1d73e Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 20 Jul 2025 21:02:06 +0200 Subject: [PATCH 1/6] feat: remove protection from middleware --- apps/web/src/middleware.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index b571061c..e2562ef1 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,36 +1,12 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -import { env } from "./env"; -import { getSessionCookie } from "better-auth/cookies"; export async function middleware(request: NextRequest) { - const protectedPaths = ["/editor", "/projects"]; - const sessionCookie = getSessionCookie(request); - - const canAccessProtectedPaths = (request: NextRequest) => { - if (env.NODE_ENV === "development") { - return true; - } - - if (sessionCookie) { - return true; - } - return false; - }; - // Handle fuckcapcut.com domain redirect if (request.headers.get("host") === "fuckcapcut.com") { return NextResponse.redirect("https://opencut.app/why-not-capcut", 301); } - const path = request.nextUrl.pathname; - - if (protectedPaths.includes(path) && !canAccessProtectedPaths(request)) { - const homeUrl = new URL("/", request.url); - homeUrl.searchParams.set("redirect", request.url); - return NextResponse.redirect(homeUrl); - } - return NextResponse.next(); } From cac4fcaab80b13edef49320940eed8b770c8c2a9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 20 Jul 2025 21:38:41 +0200 Subject: [PATCH 2/6] styling: dialog responsiveness --- apps/web/src/components/ui/dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index ebfd8907..e8decbd7 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< { @@ -55,7 +55,7 @@ const DialogContent = React.forwardRef<
{children}
- + Close From c7c394b2ef1f99581233e32b6434ccaeecdcce55 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 20 Jul 2025 21:42:02 +0200 Subject: [PATCH 3/6] feat: onboarding --- apps/web/src/app/editor/[project_id]/page.tsx | 4 +- apps/web/src/components/onboarding.tsx | 132 +++++++++++++++--- 2 files changed, 114 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index aa5b7315..26fdcf8a 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useParams, useRouter } from "next/navigation"; import { ResizablePanelGroup, @@ -37,7 +37,6 @@ export default function Editor() { const router = useRouter(); const projectId = params.project_id as string; const handledProjectIds = useRef>(new Set()); - const [isOnboardingOpen, setIsOnboardingOpen] = useState(true); usePlaybackControls(); @@ -139,6 +138,7 @@ export default function Editor() { + ); diff --git a/apps/web/src/components/onboarding.tsx b/apps/web/src/components/onboarding.tsx index a2943677..80b5f99c 100644 --- a/apps/web/src/components/onboarding.tsx +++ b/apps/web/src/components/onboarding.tsx @@ -1,29 +1,121 @@ "use client"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; +import { Dialog, DialogContent } from "./ui/dialog"; +import { Button } from "./ui/button"; +import { ArrowRightIcon } from "lucide-react"; +import { useState, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; -interface OnboardingProps { - isOpen: boolean; - onClose: () => void; -} +export function Onboarding() { + const [step, setStep] = useState(0); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const hasSeenOnboarding = localStorage.getItem("hasSeenOnboarding"); + if (!hasSeenOnboarding) { + setIsOpen(true); + } + }, []); + + const handleNext = () => { + setStep(step + 1); + }; + + const handleClose = () => { + setIsOpen(false); + localStorage.setItem("hasSeenOnboarding", "true"); + }; + + const renderStepContent = () => { + switch (step) { + case 0: + return ( +
+
+ + <Description description="You're among the first to try OpenCut - the fully open source CapCut alternative." /> + </div> + <NextButton onClick={handleNext}>Next</NextButton> + </div> + ); + case 1: + return ( + <div className="space-y-5"> + <div className="space-y-3"> + <Title title="⚠️ This is a super early beta!" /> + <Description description="OpenCut started just one month ago. There's still a ton of things to do to make this editor amazing." /> + <Description description="If you're curious, check out our roadmap [here](https://opencut.app/roadmap)" /> + </div> + <NextButton onClick={handleNext}>Next</NextButton> + </div> + ); + case 2: + return ( + <div className="space-y-5"> + <div className="space-y-3"> + <Title title="🦋 Have fun testing!" /> + <Description description="Join our [Discord](https://discord.gg/zmR9N35cjK), chat with cool people and share feedback to help make OpenCut the best editor ever." /> + </div> + <NextButton onClick={handleClose}>Finish</NextButton> + </div> + ); + default: + return null; + } + }; -export function Onboarding({ isOpen, onClose }: OnboardingProps) { return ( - <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent> - <DialogHeader> - <DialogTitle>Welcome to Clipture</DialogTitle> - <DialogDescription> - Let's get you started with the basics. - </DialogDescription> - </DialogHeader> + <Dialog open={isOpen} onOpenChange={handleClose}> + <DialogContent className="sm:max-w-[425px] !outline-none"> + {renderStepContent()} </DialogContent> </Dialog> ); } + +function Title({ title }: { title: string }) { + return <h2 className="text-lg md:text-xl font-bold">{title}</h2>; +} + +function Subtitle({ subtitle }: { subtitle: string }) { + return <h3 className="text-lg font-medium">{subtitle}</h3>; +} + +function Description({ description }: { description: string }) { + return ( + <div className="text-muted-foreground"> + <ReactMarkdown + components={{ + p: ({ children }) => <p className="mb-0">{children}</p>, + a: ({ href, children }) => ( + <a + href={href} + target="_blank" + rel="noopener noreferrer" + className="text-foreground hover:text-foreground/80 underline" + > + {children} + </a> + ), + }} + > + {description} + </ReactMarkdown> + </div> + ); +} + +function NextButton({ + children, + onClick, +}: { + children: React.ReactNode; + onClick: () => void; +}) { + return ( + <Button onClick={onClick} variant="default" className="w-full"> + {children} + <ArrowRightIcon className="w-4 h-4" /> + </Button> + ); +} From efabec8bcb4f9025712475ad42d22bf9ed3209e6 Mon Sep 17 00:00:00 2001 From: Maze Winther <mazewinther@gmail.com> Date: Sun, 20 Jul 2025 21:44:20 +0200 Subject: [PATCH 4/6] feat: make button on header consistent for beta --- apps/web/src/components/header.tsx | 40 +++++------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index bfe314d6..720abd3d 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -4,28 +4,9 @@ import Link from "next/link"; import { Button } from "./ui/button"; import { ArrowRight } from "lucide-react"; import { HeaderBase } from "./header-base"; -import { useSession } from "@opencut/auth/client"; -import { getStars } from "@/lib/fetch-github-stars"; -import { useEffect, useState } from "react"; import Image from "next/image"; export function Header() { - const { data: session } = useSession(); - const [star, setStar] = useState<string>(""); - - useEffect(() => { - const fetchStars = async () => { - try { - const data = await getStars(); - setStar(data); - } catch (err) { - console.error("Failed to fetch GitHub stars", err); - } - }; - - fetchStars(); - }, []); - const leftContent = ( <Link href="/" className="flex items-center gap-3"> <Image src="/logo.svg" alt="OpenCut Logo" width={32} height={32} /> @@ -40,21 +21,12 @@ export function Header() { Contributors </Button> </Link> - {process.env.NODE_ENV === "development" ? ( - <Link href="/projects"> - <Button size="sm" className="text-sm ml-4"> - Projects - <ArrowRight className="h-4 w-4" /> - </Button> - </Link> - ) : ( - <Link href="https://github.com/OpenCut-app/OpenCut" target="_blank"> - <Button size="sm" className="text-sm ml-4"> - GitHub {star}+ - <ArrowRight className="h-4 w-4" /> - </Button> - </Link> - )} + <Link href="/projects"> + <Button size="sm" className="text-sm ml-4"> + Projects + <ArrowRight className="h-4 w-4" /> + </Button> + </Link> </nav> ); From 07e068b658be8e5f650f56bd90bbc90588f487f5 Mon Sep 17 00:00:00 2001 From: Maze Winther <mazewinther@gmail.com> Date: Sun, 20 Jul 2025 21:46:50 +0200 Subject: [PATCH 5/6] feat: get hero ready for beta --- apps/web/src/components/landing/hero.tsx | 47 +++++------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index d5fe38d1..32415d5d 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -121,7 +121,7 @@ export function Hero() { transition={{ delay: 0.6, duration: 0.8 }} className="mb-8 flex justify-center" > - <SponsorButton + <SponsorButton href="https://vercel.com/?utm_source=opencut" logo={VercelIcon} companyName="Vercel" @@ -148,48 +148,21 @@ export function Hero() { </motion.p> <motion.div - className="mt-12 flex gap-8 justify-center" + className="mt-8 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" + <Button + type="submit" + size="lg" + className="px-6 h-11 text-base bg-foreground" + disabled={isSubmitting || !csrfToken} > - <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> + Try early beta + <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" /> + </Button> </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> ); From 4896dce2686efa293cb4c8330113c2a18da20965 Mon Sep 17 00:00:00 2001 From: Maze Winther <mazewinther@gmail.com> Date: Sun, 20 Jul 2025 21:48:47 +0200 Subject: [PATCH 6/6] hero: cleanup and fix button to redirect --- apps/web/src/components/landing/hero.tsx | 110 +++-------------------- 1 file changed, 11 insertions(+), 99 deletions(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 32415d5d..b905bbde 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -2,104 +2,15 @@ import { motion } from "motion/react"; import { Button } from "../ui/button"; -import { Input } from "../ui/input"; import { SponsorButton } from "../ui/sponsor-button"; import { VercelIcon } from "../icons"; import { ArrowRight } from "lucide-react"; -import { useState, useEffect } from "react"; -import { toast } from "sonner"; import Image from "next/image"; import { Handlebars } from "./handlebars"; +import Link from "next/link"; 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(); - - 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; - } - - 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() }), - }); - - 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(""); - - 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 @@ -153,15 +64,16 @@ export function Hero() { animate={{ opacity: 1 }} transition={{ delay: 0.6, duration: 0.8 }} > - <Button - type="submit" - size="lg" - className="px-6 h-11 text-base bg-foreground" - disabled={isSubmitting || !csrfToken} - > - Try early beta - <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" /> - </Button> + <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>