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> + ); +}