From be1ae20d9089e81cb84a8294be76fe680a24b60f Mon Sep 17 00:00:00 2001 From: Hyteq Date: Sun, 20 Jul 2025 11:00:22 +0300 Subject: format --- apps/web/src/app/(auth)/login/page.tsx | 248 +++--- apps/web/src/app/(auth)/signup/page.tsx | 282 +++---- apps/web/src/app/api/auth/[...all]/route.ts | 8 +- apps/web/src/app/api/health/route.ts | 2 +- apps/web/src/app/api/waitlist/route.ts | 188 +++-- apps/web/src/app/api/waitlist/token/route.ts | 87 +- apps/web/src/app/contributors/page.tsx | 554 ++++++------- apps/web/src/app/editor/[project_id]/page.tsx | 252 +++--- apps/web/src/app/globals.css | 158 ++-- apps/web/src/app/layout.tsx | 68 +- apps/web/src/app/metadata.ts | 158 ++-- apps/web/src/app/page.tsx | 14 +- apps/web/src/app/privacy/page.tsx | 506 ++++++------ apps/web/src/app/projects/page.tsx | 1086 ++++++++++++------------- apps/web/src/app/roadmap/page.tsx | 494 +++++------ apps/web/src/app/terms/page.tsx | 634 +++++++-------- apps/web/src/app/why-not-capcut/page.tsx | 382 ++++----- 17 files changed, 2579 insertions(+), 2542 deletions(-) (limited to 'apps/web/src/app') diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index f28cd191..170be83f 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -3,11 +3,11 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { memo, Suspense } from "react"; import { Input } from "@/components/ui/input"; @@ -20,127 +20,127 @@ import { GoogleIcon } from "@/components/icons"; import { useLogin } from "@/hooks/auth/useLogin"; const LoginPage = () => { - const router = useRouter(); - const { - email, - setEmail, - password, - setPassword, - error, - isAnyLoading, - isEmailLoading, - isGoogleLoading, - handleLogin, - handleGoogleLogin, - } = useLogin(); + const router = useRouter(); + const { + email, + setEmail, + password, + setPassword, + error, + isAnyLoading, + isEmailLoading, + isGoogleLoading, + handleLogin, + handleGoogleLogin, + } = useLogin(); - return ( -
- - - - Welcome back - - Sign in to your account to continue - - - - - -
- } - > -
- {error && ( - - Error - {error} - - )} + return ( +
+ + + + Welcome back + + Sign in to your account to continue + + + + + +
+ } + > +
+ {error && ( + + Error + {error} + + )} - -
-
- -
-
- - Or continue with - -
-
-
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
-
- Don't have an account?{" "} - - Sign up - -
- - - -
- ); + +
+
+ +
+
+ + Or continue with + +
+
+
+
+ + setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> +
+
+ + setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> +
+ +
+ +
+ Don't have an account?{" "} + + Sign up + +
+ + + + + ); }; export default memo(LoginPage); diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx index c3a0f899..a3cd1faf 100644 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ b/apps/web/src/app/(auth)/signup/page.tsx @@ -3,11 +3,11 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { memo, Suspense } from "react"; import { Input } from "@/components/ui/input"; @@ -20,143 +20,143 @@ import { GoogleIcon } from "@/components/icons"; import { useSignUp } from "@/hooks/auth/useSignUp"; const SignUpPage = () => { - const router = useRouter(); - const { - name, - setName, - email, - setEmail, - password, - setPassword, - error, - isAnyLoading, - isEmailLoading, - isGoogleLoading, - handleSignUp, - handleGoogleSignUp, - } = useSignUp(); + const router = useRouter(); + const { + name, + setName, + email, + setEmail, + password, + setPassword, + error, + isAnyLoading, + isEmailLoading, + isGoogleLoading, + handleSignUp, + handleGoogleSignUp, + } = useSignUp(); - return ( -
- - - - - Create your account - - - Get started with your free account today - - - - - -
- } - > -
- {error && ( - - Error - {error} - - )} - -
-
- -
-
- - Or continue with - -
-
-
-
- - setName(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
-
- Already have an account?{" "} - - Sign in - -
- - - - - ); + return ( +
+ + + + + Create your account + + + Get started with your free account today + + + + + +
+ } + > +
+ {error && ( + + Error + {error} + + )} + +
+
+ +
+
+ + Or continue with + +
+
+
+
+ + setName(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> +
+
+ + setEmail(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> +
+
+ + setPassword(e.target.value)} + disabled={isAnyLoading} + className="h-11" + /> +
+ +
+
+
+ Already have an account?{" "} + + Sign in + +
+ + + + + ); }; export default memo(SignUpPage); diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts index 55510dcb..5deef204 100644 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@opencut/auth"; -import { toNextJsHandler } from "better-auth/next-js"; - -export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file +import { auth } from "@opencut/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts index ecb9ce04..f8c951fe 100644 --- a/apps/web/src/app/api/health/route.ts +++ b/apps/web/src/app/api/health/route.ts @@ -1,5 +1,5 @@ import { NextRequest } from "next/server"; export async function GET(request: NextRequest) { - return new Response("OK", { status: 200 }); + return new Response("OK", { status: 200 }); } diff --git a/apps/web/src/app/api/waitlist/route.ts b/apps/web/src/app/api/waitlist/route.ts index e5c9fd94..98232e49 100644 --- a/apps/web/src/app/api/waitlist/route.ts +++ b/apps/web/src/app/api/waitlist/route.ts @@ -1,83 +1,105 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db, eq, waitlist } from "@opencut/db"; -import { checkBotId } from "botid/server"; -import { nanoid } from "nanoid"; -import { waitlistRateLimit } from "@/lib/rate-limit"; -import { z } from "zod"; -import { env } from "@/env"; -import { cookies } from "next/headers"; -import crypto from "crypto"; - -const waitlistSchema = z.object({ - email: z.string().email("Invalid email format").min(1, "Email is required"), -}); - -const CSRF_TOKEN_NAME = "waitlist-csrf"; -const TOKEN_EXPIRY = 60 * 60 * 1000; - -async function validateCSRFToken(request: NextRequest): Promise { - const clientToken = request.headers.get("x-csrf-token"); - if (!clientToken) return false; - - const cookieStore = await cookies(); - const cookieValue = cookieStore.get(CSRF_TOKEN_NAME)?.value; - if (!cookieValue) return false; - - const [token, timestamp, signature] = cookieValue.split(":"); - if (!token || !timestamp || !signature) return false; - - if (clientToken !== token) return false; - - const now = Date.now(); - const tokenTime = parseInt(timestamp); - if (now - tokenTime > TOKEN_EXPIRY) return false; - - const expectedSignature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex"); - - return signature === expectedSignature; -} - -export async function POST(request: NextRequest) { - const verification = await checkBotId(); - - if (verification.isBot) { - return NextResponse.json({ error: "Access denied" }, { status: 403 }); - } - - const identifier = request.headers.get("x-forwarded-for") ?? "127.0.0.1"; - const { success } = await waitlistRateLimit.limit(identifier); - - if (!success) { - return NextResponse.json({ error: "Too many requests. Please try again later." }, { status: 429 }); - } - const isValidToken = await validateCSRFToken(request); - if (!isValidToken) { - return NextResponse.json({ error: "Invalid security token" }, { status: 403 }); - } - - try { - const body = await request.json(); - const { email } = waitlistSchema.parse(body); - - const existingEmail = await db.select().from(waitlist).where(eq(waitlist.email, email.toLowerCase())).limit(1); - - if (existingEmail.length > 0) { - return NextResponse.json({ error: "Email already registered" }, { status: 409 }); - } - - await db.insert(waitlist).values({ - id: nanoid(), - email: email.toLowerCase(), - }); - - return NextResponse.json({ message: "Successfully joined waitlist!" }, { status: 201 }); - } catch (error) { - if (error instanceof z.ZodError) { - const firstError = error.errors[0]; - return NextResponse.json({ error: firstError.message }, { status: 400 }); - } - - console.error("Waitlist signup error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} +import { NextRequest, NextResponse } from "next/server"; +import { db, eq, waitlist } from "@opencut/db"; +import { checkBotId } from "botid/server"; +import { nanoid } from "nanoid"; +import { waitlistRateLimit } from "@/lib/rate-limit"; +import { z } from "zod"; +import { env } from "@/env"; +import { cookies } from "next/headers"; +import crypto from "crypto"; + +const waitlistSchema = z.object({ + email: z.string().email("Invalid email format").min(1, "Email is required"), +}); + +const CSRF_TOKEN_NAME = "waitlist-csrf"; +const TOKEN_EXPIRY = 60 * 60 * 1000; + +async function validateCSRFToken(request: NextRequest): Promise { + const clientToken = request.headers.get("x-csrf-token"); + if (!clientToken) return false; + + const cookieStore = await cookies(); + const cookieValue = cookieStore.get(CSRF_TOKEN_NAME)?.value; + if (!cookieValue) return false; + + const [token, timestamp, signature] = cookieValue.split(":"); + if (!token || !timestamp || !signature) return false; + + if (clientToken !== token) return false; + + const now = Date.now(); + const tokenTime = parseInt(timestamp); + if (now - tokenTime > TOKEN_EXPIRY) return false; + + const expectedSignature = crypto + .createHmac("sha256", env.BETTER_AUTH_SECRET) + .update(`${token}:${timestamp}`) + .digest("hex"); + + return signature === expectedSignature; +} + +export async function POST(request: NextRequest) { + const verification = await checkBotId(); + + if (verification.isBot) { + return NextResponse.json({ error: "Access denied" }, { status: 403 }); + } + + const identifier = request.headers.get("x-forwarded-for") ?? "127.0.0.1"; + const { success } = await waitlistRateLimit.limit(identifier); + + if (!success) { + return NextResponse.json( + { error: "Too many requests. Please try again later." }, + { status: 429 }, + ); + } + const isValidToken = await validateCSRFToken(request); + if (!isValidToken) { + return NextResponse.json( + { error: "Invalid security token" }, + { status: 403 }, + ); + } + + try { + const body = await request.json(); + const { email } = waitlistSchema.parse(body); + + const existingEmail = await db + .select() + .from(waitlist) + .where(eq(waitlist.email, email.toLowerCase())) + .limit(1); + + if (existingEmail.length > 0) { + return NextResponse.json( + { error: "Email already registered" }, + { status: 409 }, + ); + } + + await db.insert(waitlist).values({ + id: nanoid(), + email: email.toLowerCase(), + }); + + return NextResponse.json( + { message: "Successfully joined waitlist!" }, + { status: 201 }, + ); + } catch (error) { + if (error instanceof z.ZodError) { + const firstError = error.errors[0]; + return NextResponse.json({ error: firstError.message }, { status: 400 }); + } + + console.error("Waitlist signup error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/waitlist/token/route.ts b/apps/web/src/app/api/waitlist/token/route.ts index 4c7695fa..6f5ee18f 100644 --- a/apps/web/src/app/api/waitlist/token/route.ts +++ b/apps/web/src/app/api/waitlist/token/route.ts @@ -5,42 +5,57 @@ import { env } from "@/env"; const CSRF_TOKEN_NAME = "waitlist-csrf"; const TOKEN_EXPIRY = 60 * 60 * 1000; -const allowedHosts = env.NODE_ENV === "development" ? ["localhost:3000", "127.0.0.1:3000"] : ["opencut.app", "www.opencut.app"]; +const allowedHosts = + env.NODE_ENV === "development" + ? ["localhost:3000", "127.0.0.1:3000"] + : ["opencut.app", "www.opencut.app"]; export async function GET(request: NextRequest) { - const referer = request.headers.get("referer"); - const host = request.headers.get("host"); - - if (referer) { - const refererUrl = new URL(referer); - - if (!allowedHosts.some((allowed) => refererUrl.host === allowed || refererUrl.host.endsWith(allowed))) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - } else if (host) { - if (!allowedHosts.some((allowed) => host === allowed || host.endsWith(allowed))) { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - } else { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }); - } - - if (!env.BETTER_AUTH_SECRET) { - throw new Error("BETTER_AUTH_SECRET must be configured"); - } - - const token = crypto.randomBytes(32).toString("hex"); - const timestamp = Date.now(); - const signature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex"); - - const cookieStore = await cookies(); - cookieStore.set(CSRF_TOKEN_NAME, `${token}:${timestamp}:${signature}`, { - httpOnly: true, - secure: env.NODE_ENV === "production", - sameSite: "strict", - maxAge: TOKEN_EXPIRY / 1000, - path: "/", - }); - - return NextResponse.json({ token }); + const referer = request.headers.get("referer"); + const host = request.headers.get("host"); + + if (referer) { + const refererUrl = new URL(referer); + + if ( + !allowedHosts.some( + (allowed) => + refererUrl.host === allowed || refererUrl.host.endsWith(allowed), + ) + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + } else if (host) { + if ( + !allowedHosts.some( + (allowed) => host === allowed || host.endsWith(allowed), + ) + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + } else { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + if (!env.BETTER_AUTH_SECRET) { + throw new Error("BETTER_AUTH_SECRET must be configured"); + } + + const token = crypto.randomBytes(32).toString("hex"); + const timestamp = Date.now(); + const signature = crypto + .createHmac("sha256", env.BETTER_AUTH_SECRET) + .update(`${token}:${timestamp}`) + .digest("hex"); + + const cookieStore = await cookies(); + cookieStore.set(CSRF_TOKEN_NAME, `${token}:${timestamp}:${signature}`, { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "strict", + maxAge: TOKEN_EXPIRY / 1000, + path: "/", + }); + + return NextResponse.json({ token }); } diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx index 5613f1f9..2c602e25 100644 --- a/apps/web/src/app/contributors/page.tsx +++ b/apps/web/src/app/contributors/page.tsx @@ -1,277 +1,277 @@ -import { Metadata } from "next"; -import { Header } from "@/components/header"; -import { Card, CardContent } from "@/components/ui/card"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { ExternalLink } from "lucide-react"; -import Link from "next/link"; -import { GithubIcon } from "@/components/icons"; -import { Badge } from "@/components/ui/badge"; - -export const metadata: Metadata = { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - openGraph: { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - type: "website", - }, -}; - -interface Contributor { - id: number; - login: string; - avatar_url: string; - html_url: string; - contributions: number; - type: string; -} - -async function getContributors(): Promise { - try { - const response = await fetch( - "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", - { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCut-Web-App", - }, - next: { revalidate: 600 }, // 10 minutes - } - ); - - if (!response.ok) { - console.error("Failed to fetch contributors"); - return []; - } - - const contributors = (await response.json()) as Contributor[]; - - const filteredContributors = contributors.filter( - (contributor: Contributor) => contributor.type === "User" - ); - - return filteredContributors; - } catch (error) { - console.error("Error fetching contributors:", error); - return []; - } -} - -export default async function ContributorsPage() { - const contributors = await getContributors(); - const topContributors = contributors.slice(0, 2); - const otherContributors = contributors.slice(2); - - return ( -
-
- -
-
-
-
-
- -
-
-
- - - - Open Source - - -

- Contributors -

-

- Meet the amazing developers who are building the future of video - editing -

- -
-
-
- {contributors.length} - contributors -
-
-
- - {contributors.reduce((sum, c) => sum + c.contributions, 0)} - - contributions -
-
-
- - {topContributors.length > 0 && ( -
-
-

- Top Contributors -

-

- Leading the way in contributions -

-
- -
- {topContributors.map((contributor, index) => ( - -
-
- - -
- - - - {contributor.login.charAt(0).toUpperCase()} - - -
-

- {contributor.login} -

-
- - {contributor.contributions} - - contributions -
-
-
-
- - ))} -
-
- )} - - {otherContributors.length > 0 && ( -
-
-

- All Contributors -

-

- Everyone who makes OpenCut better -

-
- -
- {otherContributors.map((contributor, index) => ( - -
- - - - {contributor.login.charAt(0).toUpperCase()} - - -

- {contributor.login} -

-

- {contributor.contributions} -

-
- - ))} -
-
- )} - - {contributors.length === 0 && ( -
-
- -
-

- No contributors found -

-

- Unable to load contributors at the moment. Check back later or - view on GitHub. -

- - - -
- )} - -
-
-

Join the community

-

- OpenCut is built by developers like you. Every contribution, - no matter how small, helps make video editing more accessible - for everyone. -

- -
- - - - - - -
-
-
-
-
-
-
- ); -} +import { Metadata } from "next"; +import { Header } from "@/components/header"; +import { Card, CardContent } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { ExternalLink } from "lucide-react"; +import Link from "next/link"; +import { GithubIcon } from "@/components/icons"; +import { Badge } from "@/components/ui/badge"; + +export const metadata: Metadata = { + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + openGraph: { + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + type: "website", + }, +}; + +interface Contributor { + id: number; + login: string; + avatar_url: string; + html_url: string; + contributions: number; + type: string; +} + +async function getContributors(): Promise { + try { + const response = await fetch( + "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCut-Web-App", + }, + next: { revalidate: 600 }, // 10 minutes + }, + ); + + if (!response.ok) { + console.error("Failed to fetch contributors"); + return []; + } + + const contributors = (await response.json()) as Contributor[]; + + const filteredContributors = contributors.filter( + (contributor: Contributor) => contributor.type === "User", + ); + + return filteredContributors; + } catch (error) { + console.error("Error fetching contributors:", error); + return []; + } +} + +export default async function ContributorsPage() { + const contributors = await getContributors(); + const topContributors = contributors.slice(0, 2); + const otherContributors = contributors.slice(2); + + return ( +
+
+ +
+
+
+
+
+ +
+
+
+ + + + Open Source + + +

+ Contributors +

+

+ Meet the amazing developers who are building the future of video + editing +

+ +
+
+
+ {contributors.length} + contributors +
+
+
+ + {contributors.reduce((sum, c) => sum + c.contributions, 0)} + + contributions +
+
+
+ + {topContributors.length > 0 && ( +
+
+

+ Top Contributors +

+

+ Leading the way in contributions +

+
+ +
+ {topContributors.map((contributor, index) => ( + +
+
+ + +
+ + + + {contributor.login.charAt(0).toUpperCase()} + + +
+

+ {contributor.login} +

+
+ + {contributor.contributions} + + contributions +
+
+
+
+ + ))} +
+
+ )} + + {otherContributors.length > 0 && ( +
+
+

+ All Contributors +

+

+ Everyone who makes OpenCut better +

+
+ +
+ {otherContributors.map((contributor, index) => ( + +
+ + + + {contributor.login.charAt(0).toUpperCase()} + + +

+ {contributor.login} +

+

+ {contributor.contributions} +

+
+ + ))} +
+
+ )} + + {contributors.length === 0 && ( +
+
+ +
+

+ No contributors found +

+

+ Unable to load contributors at the moment. Check back later or + view on GitHub. +

+ + + +
+ )} + +
+
+

Join the community

+

+ OpenCut is built by developers like you. Every contribution, + no matter how small, helps make video editing more accessible + for everyone. +

+ +
+ + + + + + +
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index aa5b7315..6c7186bc 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -3,9 +3,9 @@ import { useEffect, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, } from "../../../components/ui/resizable"; import { MediaPanel } from "../../../components/editor/media-panel"; import { PropertiesPanel } from "../../../components/editor/properties-panel"; @@ -19,127 +19,127 @@ import { usePlaybackControls } from "@/hooks/use-playback-controls"; import { Onboarding } from "@/components/onboarding"; export default function Editor() { - const { - toolsPanel, - previewPanel, - mainContent, - timeline, - setToolsPanel, - setPreviewPanel, - setMainContent, - setTimeline, - propertiesPanel, - setPropertiesPanel, - } = usePanelStore(); - - const { activeProject, loadProject, createNewProject } = useProjectStore(); - const params = useParams(); - const router = useRouter(); - const projectId = params.project_id as string; - const handledProjectIds = useRef>(new Set()); - const [isOnboardingOpen, setIsOnboardingOpen] = useState(true); - - usePlaybackControls(); - - useEffect(() => { - const initProject = async () => { - if (!projectId) return; - - if (activeProject?.id === projectId) { - return; - } - - if (handledProjectIds.current.has(projectId)) { - return; - } - - try { - await loadProject(projectId); - } catch (error) { - handledProjectIds.current.add(projectId); - - const newProjectId = await createNewProject("Untitled Project"); - router.replace(`/editor/${newProjectId}`); - return; - } - }; - - initProject(); - }, [projectId, activeProject?.id, loadProject, createNewProject, router]); - - return ( - -
- -
- - - {/* Main content area */} - - {/* Tools Panel */} - - - - - - - {/* Preview Area */} - - - - - - - - - - - - - - - {/* Timeline */} - - - - -
-
-
- ); + const { + toolsPanel, + previewPanel, + mainContent, + timeline, + setToolsPanel, + setPreviewPanel, + setMainContent, + setTimeline, + propertiesPanel, + setPropertiesPanel, + } = usePanelStore(); + + const { activeProject, loadProject, createNewProject } = useProjectStore(); + const params = useParams(); + const router = useRouter(); + const projectId = params.project_id as string; + const handledProjectIds = useRef>(new Set()); + const [isOnboardingOpen, setIsOnboardingOpen] = useState(true); + + usePlaybackControls(); + + useEffect(() => { + const initProject = async () => { + if (!projectId) return; + + if (activeProject?.id === projectId) { + return; + } + + if (handledProjectIds.current.has(projectId)) { + return; + } + + try { + await loadProject(projectId); + } catch (error) { + handledProjectIds.current.add(projectId); + + const newProjectId = await createNewProject("Untitled Project"); + router.replace(`/editor/${newProjectId}`); + return; + } + }; + + initProject(); + }, [projectId, activeProject?.id, loadProject, createNewProject, router]); + + return ( + +
+ +
+ + + {/* Main content area */} + + {/* Tools Panel */} + + + + + + + {/* Preview Area */} + + + + + + + + + + + + + + + {/* Timeline */} + + + + +
+
+
+ ); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 2f7158cc..e7c575a1 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -3,86 +3,86 @@ @tailwind utilities; @layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 1rem; - --sidebar-background: 0 0% 100%; - --sidebar-foreground: 0 0% 3.9%; - --sidebar-primary: 0 0% 9%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 0 0% 96.1%; - --sidebar-accent-foreground: 0 0% 9%; - --sidebar-border: 0 0% 89.8%; - --sidebar-ring: 0 0% 3.9%; - } - .dark { - --background: 0 0% 4%; - --foreground: 0 0% 89%; - --card: 0 0% 14.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 14.9%; - --popover-foreground: 0 0% 98%; - --primary: 180 95% 40%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 100% 60%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 17%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 0 0% 3.9%; - --sidebar-foreground: 0 0% 98%; - --sidebar-primary: 0 0% 98%; - --sidebar-primary-foreground: 0 0% 9%; - --sidebar-accent: 0 0% 14.9%; - --sidebar-accent-foreground: 0 0% 98%; - --sidebar-border: 0 0% 14.9%; - --sidebar-ring: 0 0% 83.1%; - --panel-background: 0 0% 11%; - --panel-accent: 0 0% 15%; - } + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 1rem; + --sidebar-background: 0 0% 100%; + --sidebar-foreground: 0 0% 3.9%; + --sidebar-primary: 0 0% 9%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 0 0% 96.1%; + --sidebar-accent-foreground: 0 0% 9%; + --sidebar-border: 0 0% 89.8%; + --sidebar-ring: 0 0% 3.9%; + } + .dark { + --background: 0 0% 4%; + --foreground: 0 0% 89%; + --card: 0 0% 14.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 14.9%; + --popover-foreground: 0 0% 98%; + --primary: 180 95% 40%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 100% 60%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 17%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 0 0% 3.9%; + --sidebar-foreground: 0 0% 98%; + --sidebar-primary: 0 0% 98%; + --sidebar-primary-foreground: 0 0% 9%; + --sidebar-accent: 0 0% 14.9%; + --sidebar-accent-foreground: 0 0% 98%; + --sidebar-border: 0 0% 14.9%; + --sidebar-ring: 0 0% 83.1%; + --panel-background: 0 0% 11%; + --panel-accent: 0 0% 15%; + } } @layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - /* Prevent back/forward swipe */ - overscroll-behavior-x: contain; - } + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + /* Prevent back/forward swipe */ + overscroll-behavior-x: contain; + } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 5c1d7520..41c84304 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -13,43 +13,43 @@ import { env } from "@/env"; export const metadata = baseMetaData; const protectedRoutes = [ - { - path: "/api/waitlist", - method: "POST", - }, + { + path: "/api/waitlist", + method: "POST", + }, ]; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - - - - {children} - - -