From 8e737d87e0accbf1d433f4c8b1e8e0f125849799 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Thu, 24 Jul 2025 17:04:27 -0500 Subject: Revert "merge main" This reverts commit 1ccacf6bb5db579cf6f343d9de57bce92d296f5f. --- apps/web/src/app/api/auth/[...all]/route.ts | 8 +- apps/web/src/app/api/waitlist/route.ts | 188 +- apps/web/src/app/api/waitlist/token/route.ts | 23 +- apps/web/src/app/contributors/page.tsx | 554 ++--- apps/web/src/app/metadata.ts | 21 +- apps/web/src/app/page.tsx | 8 - apps/web/src/app/privacy/page.tsx | 506 ++--- apps/web/src/app/roadmap/page.tsx | 494 ++--- apps/web/src/app/rss.xml/route.ts | 42 +- apps/web/src/app/terms/page.tsx | 634 +++--- apps/web/src/app/why-not-capcut/page.tsx | 382 ++-- apps/web/src/components/background-settings.tsx | 368 ++-- apps/web/src/components/editor-header.tsx | 277 +-- apps/web/src/components/editor-provider.tsx | 106 +- apps/web/src/components/editor/audio-waveform.tsx | 42 +- .../components/editor/media-panel/drag-overlay.tsx | 114 +- .../src/components/editor/media-panel/index.tsx | 108 +- .../web/src/components/editor/media-panel/store.ts | 146 +- .../src/components/editor/media-panel/tabbar.tsx | 248 +-- .../components/editor/media-panel/views/audio.tsx | 36 +- .../components/editor/media-panel/views/media.tsx | 596 ++--- .../components/editor/media-panel/views/text.tsx | 106 +- apps/web/src/components/editor/preview-panel.tsx | 4 +- .../editor/properties-panel/audio-properties.tsx | 10 +- .../editor/properties-panel/media-properties.tsx | 10 +- .../editor/properties-panel/property-item.tsx | 94 +- .../editor/properties-panel/text-properties.tsx | 152 +- apps/web/src/components/editor/selection-box.tsx | 116 +- apps/web/src/components/editor/snap-indicator.tsx | 2 +- apps/web/src/components/editor/speed-control.tsx | 2 +- apps/web/src/components/editor/timeline/index.tsx | 27 +- .../editor/timeline/timeline-playhead.tsx | 232 +- .../components/editor/timeline/timeline-track.tsx | 2302 ++++++++++---------- apps/web/src/components/header-base.tsx | 76 +- apps/web/src/components/header.tsx | 94 +- apps/web/src/components/icons.tsx | 13 +- apps/web/src/components/landing/handlebars.tsx | 6 +- apps/web/src/components/onboarding.tsx | 242 +- apps/web/src/components/rename-project-dialog.tsx | 146 +- apps/web/src/components/storage-provider.tsx | 160 +- apps/web/src/components/ui/aspect-ratio.tsx | 8 +- apps/web/src/components/ui/audio-player.tsx | 254 +-- apps/web/src/components/ui/button.tsx | 3 +- apps/web/src/components/ui/card.tsx | 5 +- apps/web/src/components/ui/carousel.tsx | 2 +- apps/web/src/components/ui/chart.tsx | 4 +- apps/web/src/components/ui/collapsible.tsx | 12 +- apps/web/src/components/ui/font-picker.tsx | 80 +- apps/web/src/components/ui/form.tsx | 4 +- .../src/components/ui/image-timeline-treatment.tsx | 204 +- apps/web/src/components/ui/phone-input.tsx | 334 +-- apps/web/src/components/ui/separator.tsx | 14 +- apps/web/src/components/ui/sonner.tsx | 16 +- apps/web/src/components/ui/sponsor-button.tsx | 1 + apps/web/src/components/ui/toaster.tsx | 24 +- apps/web/src/constants/actions.ts | 2 +- apps/web/src/constants/font-constants.ts | 158 +- apps/web/src/constants/site.ts | 12 +- apps/web/src/data/colors.ts | 488 ++--- apps/web/src/hooks/auth/useLogin.ts | 108 +- apps/web/src/hooks/auth/useSignUp.ts | 102 +- apps/web/src/hooks/use-aspect-ratio.ts | 184 +- apps/web/src/hooks/use-drag-drop.ts | 170 +- apps/web/src/hooks/use-keybindings.ts | 2 +- apps/web/src/hooks/use-mobile.tsx | 24 +- apps/web/src/hooks/use-selection-box.ts | 398 ++-- apps/web/src/hooks/use-timeline-playhead.ts | 314 +-- apps/web/src/hooks/use-timeline-zoom.ts | 120 +- apps/web/src/hooks/use-toast.ts | 6 +- apps/web/src/lib/blog-query.ts | 89 +- apps/web/src/lib/fetch-github-stars.ts | 4 +- apps/web/src/lib/ffmpeg-utils.ts | 209 +- apps/web/src/lib/font-config.ts | 78 +- apps/web/src/lib/rate-limit.ts | 32 +- apps/web/src/lib/storage/indexeddb-adapter.ts | 178 +- apps/web/src/lib/storage/opfs-adapter.ts | 2 +- apps/web/src/lib/time.ts | 50 +- apps/web/src/lib/timeline.ts | 108 +- apps/web/src/lib/waitlist.ts | 26 +- apps/web/src/middleware.ts | 48 +- apps/web/src/stores/editor-store.ts | 209 +- apps/web/src/stores/media-store.ts | 10 +- apps/web/src/stores/panel-store.ts | 90 +- apps/web/src/stores/project-store.ts | 721 +++--- apps/web/src/stores/timeline-store.ts | 11 +- apps/web/src/types/editor.ts | 24 +- apps/web/src/types/keybinding.ts | 58 +- apps/web/src/types/post.ts | 169 +- apps/web/src/types/timeline.ts | 3 +- 89 files changed, 6862 insertions(+), 7005 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts index 5deef204..55510dcb 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); +import { auth } from "@opencut/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file diff --git a/apps/web/src/app/api/waitlist/route.ts b/apps/web/src/app/api/waitlist/route.ts index 9f00f221..e5c9fd94 100644 --- a/apps/web/src/app/api/waitlist/route.ts +++ b/apps/web/src/app/api/waitlist/route.ts @@ -1,105 +1,83 @@ -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 fc2ff32c..4c7695fa 100644 --- a/apps/web/src/app/api/waitlist/token/route.ts +++ b/apps/web/src/app/api/waitlist/token/route.ts @@ -5,10 +5,7 @@ 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"); @@ -17,20 +14,11 @@ export async function GET(request: NextRequest) { if (referer) { const refererUrl = new URL(referer); - if ( - !allowedHosts.some( - (allowed) => - refererUrl.host === allowed || refererUrl.host.endsWith(allowed) - ) - ) { + 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) - ) - ) { + if (!allowedHosts.some((allowed) => host === allowed || host.endsWith(allowed))) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } } else { @@ -43,10 +31,7 @@ export async function GET(request: NextRequest) { 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 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}`, { diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx index 6a524866..5613f1f9 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/metadata.ts b/apps/web/src/app/metadata.ts index 0608974d..de8cf6f0 100644 --- a/apps/web/src/app/metadata.ts +++ b/apps/web/src/app/metadata.ts @@ -1,15 +1,24 @@ -import type { Metadata } from "next"; -import { SITE_INFO, SITE_URL } from "@/constants/site"; +import { Metadata } from "next"; + +export const SITE_INFO = { + title: "OpenCut", + description: + "A simple but powerful video editor that gets the job done. In your browser.", + url: "https://opencut.app", + openGraphImage: "https://opencut.app/open-graph/default.jpg", + twitterImage: "/open-graph/default.jpg", + favicon: "/favicon.ico", +}; export const baseMetaData: Metadata = { - metadataBase: new URL(SITE_URL), + metadataBase: new URL("https://opencut.app"), title: SITE_INFO.title, description: SITE_INFO.description, openGraph: { title: SITE_INFO.title, description: SITE_INFO.description, - url: SITE_URL, - siteName: SITE_INFO.title, + url: "https://opencut.app", + siteName: "OpenCut", locale: "en_US", type: "website", images: [ @@ -17,7 +26,7 @@ export const baseMetaData: Metadata = { url: SITE_INFO.openGraphImage, width: 1200, height: 630, - alt: "OpenCut Wordmark", + alt: "OpenCut", }, ], }, diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 818884ed..74961549 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,14 +1,6 @@ import { Hero } from "@/components/landing/hero"; import { Header } from "@/components/header"; import { Footer } from "@/components/footer"; -import type { Metadata } from "next"; -import { SITE_URL } from "@/constants/site"; - -export const metadata: Metadata = { - alternates: { - canonical: SITE_URL, - }, -}; export default async function Home() { return ( diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx index b7d700b5..42dd0f9a 100644 --- a/apps/web/src/app/privacy/page.tsx +++ b/apps/web/src/app/privacy/page.tsx @@ -1,253 +1,253 @@ -import { Metadata } from "next"; -import { Card, CardContent } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { GithubIcon } from "@/components/icons"; -import Link from "next/link"; -import { Footer } from "@/components/footer"; -import { Header } from "@/components/header"; - -export const metadata: Metadata = { - title: "Privacy Policy - OpenCut", - description: - "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.", - openGraph: { - title: "Privacy Policy - OpenCut", - description: - "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.", - type: "website", - }, -}; - -export default function PrivacyPage() { - return ( -
-
-
-
-
-
-
-
-
-
- - - - Open Source - - -

- Privacy Policy -

-

- Learn how we handle your data and privacy. Contact us if you - have any questions. -

-
- - -
-

- Your Videos Stay Private -

-

- - OpenCut processes all videos locally on your device. - {" "} - We never upload, store, or have access to your video files. - Your content remains completely private and under your - control at all times. -

-

- All video editing, rendering, and processing happens in your - browser using WebAssembly and local storage. No video data - is transmitted to our servers. -

-
- -
-

- Account Information -

-

- When you create an account, we only collect: -

-
    -
  • Email address (for account access)
  • -
  • - Profile information from Google OAuth (if you choose to - sign in with Google) -
  • -
-

- - We do NOT store your projects on our servers. - {" "} - All project data, including names, thumbnails, and creation - dates, is stored locally in your browser using IndexedDB. -

-

- We use{" "} - - Better Auth - {" "} - for secure authentication and follow industry-standard - security practices. -

-
- -
-

Analytics

-

- We use{" "} - - Databuddy - {" "} - for completely anonymized and non-invasive analytics to - understand how people use OpenCut. -

-

- This helps us improve the editor, but we never collect - personal information, track individual users, or store any - data that could identify you. -

-
- -
-

- Local Storage & Cookies -

-

- We use browser local storage and IndexedDB to: -

-
    -
  • Save your projects locally on your device
  • -
  • Remember your editor preferences and settings
  • -
  • Keep you logged in across browser sessions
  • -
-

- All data stays on your device and can be cleared at any time - through your browser settings. -

-
- -
-

- Third-Party Services -

-

- OpenCut integrates with these services: -

-
    -
  • - Google OAuth: For optional Google sign-in - (governed by Google's privacy policy) -
  • -
  • - Vercel: For hosting and content delivery -
  • -
  • - Databuddy: For anonymized analytics -
  • -
-
- -
-

Your Rights

-

- You have complete control over your data: -

-
    -
  • - Delete your account and all associated data at any time -
  • -
  • Export your project data
  • -
  • Clear local storage to remove all saved projects
  • -
  • Contact us with any privacy concerns
  • -
-
- -
-

- Open Source Transparency -

-

- OpenCut is completely open source. You can review our code, - see exactly how we handle data, and even self-host the - application if you prefer. -

-

- View our source code on{" "} - - GitHub - - . -

-
- -
-

Contact Us

-

- Questions about this privacy policy or how we handle your - data? -

-

- Open an issue on our{" "} - - GitHub repository - - , email us at{" "} - - oss@opencut.app - - , or reach out on{" "} - - X (Twitter) - - . -

-
- -

- Last updated: July 14, 2025 -

-
-
-
-
-
-
-
- ); -} +import { Metadata } from "next"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { GithubIcon } from "@/components/icons"; +import Link from "next/link"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; + +export const metadata: Metadata = { + title: "Privacy Policy - OpenCut", + description: + "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.", + openGraph: { + title: "Privacy Policy - OpenCut", + description: + "Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.", + type: "website", + }, +}; + +export default function PrivacyPage() { + return ( +
+
+
+
+
+
+
+
+
+
+ + + + Open Source + + +

+ Privacy Policy +

+

+ Learn how we handle your data and privacy. Contact us if you + have any questions. +

+
+ + +
+

+ Your Videos Stay Private +

+

+ + OpenCut processes all videos locally on your device. + {" "} + We never upload, store, or have access to your video files. + Your content remains completely private and under your + control at all times. +

+

+ All video editing, rendering, and processing happens in your + browser using WebAssembly and local storage. No video data + is transmitted to our servers. +

+
+ +
+

+ Account Information +

+

+ When you create an account, we only collect: +

+
    +
  • Email address (for account access)
  • +
  • + Profile information from Google OAuth (if you choose to + sign in with Google) +
  • +
+

+ + We do NOT store your projects on our servers. + {" "} + All project data, including names, thumbnails, and creation + dates, is stored locally in your browser using IndexedDB. +

+

+ We use{" "} + + Better Auth + {" "} + for secure authentication and follow industry-standard + security practices. +

+
+ +
+

Analytics

+

+ We use{" "} + + Databuddy + {" "} + for completely anonymized and non-invasive analytics to + understand how people use OpenCut. +

+

+ This helps us improve the editor, but we never collect + personal information, track individual users, or store any + data that could identify you. +

+
+ +
+

+ Local Storage & Cookies +

+

+ We use browser local storage and IndexedDB to: +

+
    +
  • Save your projects locally on your device
  • +
  • Remember your editor preferences and settings
  • +
  • Keep you logged in across browser sessions
  • +
+

+ All data stays on your device and can be cleared at any time + through your browser settings. +

+
+ +
+

+ Third-Party Services +

+

+ OpenCut integrates with these services: +

+
    +
  • + Google OAuth: For optional Google sign-in + (governed by Google's privacy policy) +
  • +
  • + Vercel: For hosting and content delivery +
  • +
  • + Databuddy: For anonymized analytics +
  • +
+
+ +
+

Your Rights

+

+ You have complete control over your data: +

+
    +
  • + Delete your account and all associated data at any time +
  • +
  • Export your project data
  • +
  • Clear local storage to remove all saved projects
  • +
  • Contact us with any privacy concerns
  • +
+
+ +
+

+ Open Source Transparency +

+

+ OpenCut is completely open source. You can review our code, + see exactly how we handle data, and even self-host the + application if you prefer. +

+

+ View our source code on{" "} + + GitHub + + . +

+
+ +
+

Contact Us

+

+ Questions about this privacy policy or how we handle your + data? +

+

+ Open an issue on our{" "} + + GitHub repository + + , email us at{" "} + + oss@opencut.app + + , or reach out on{" "} + + X (Twitter) + + . +

+
+ +

+ Last updated: July 14, 2025 +

+
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/roadmap/page.tsx b/apps/web/src/app/roadmap/page.tsx index 5c003cf0..c62dce3a 100644 --- a/apps/web/src/app/roadmap/page.tsx +++ b/apps/web/src/app/roadmap/page.tsx @@ -1,247 +1,247 @@ -import { Metadata } from "next"; -import { Badge } from "@/components/ui/badge"; -import { GithubIcon } from "@/components/icons"; -import Link from "next/link"; -import { Footer } from "@/components/footer"; -import { Header } from "@/components/header"; -import ReactMarkdown from "react-markdown"; -import { cn } from "@/lib/utils"; - -const roadmapItems: { - title: string; - description: string; - status: { - text: string; - type: "complete" | "pending" | "default" | "info"; - }; -}[] = [ - { - title: "Start", - description: - "This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.", - status: { - text: "Completed", - type: "complete", - }, - }, - { - title: "Core UI", - description: - "Built the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.", - status: { - text: "Completed", - type: "complete", - }, - }, - { - title: "Basic Functionality", - description: - "The heart of any video editor. Timeline zoom in/out, making clips longer/shorter, dragging elements around, selection, playhead scrubbing. **This part has to be fucking perfect** because it's what users interact with 99% of the time.", - status: { - text: "In Progress", - type: "pending", - }, - }, - { - title: "Export/Preview Logic", - description: - "The foundation that enables everything else. Real-time preview, video rendering, export functionality. Once this works, we can add effects, filters, transitions - basically everything that makes a video editor powerful.", - status: { - text: "In Progress", - type: "pending", - }, - }, - { - title: "Text", - description: - "After media, text is the next most important thing. Font selection with custom font imports, text stroke, colors. All the text essential text properties.", - status: { - text: "Not Started", - type: "default", - }, - }, - { - title: "Effects", - description: - "Adding visual effects to both text and media. Blur, brightness, contrast, saturation, filters, and all the creative tools that make videos pop. This is where the magic happens.", - status: { - text: "Not Started", - type: "default", - }, - }, - { - title: "Transitions", - description: - "Smooth transitions between clips. Fade in/out, slide, zoom, dissolve, and custom transition effects.", - status: { - text: "Not Started", - type: "default", - }, - }, - { - title: "Refine from Here", - description: - "Once we nail the above, we have a **solid foundation** to build anything. Advanced features, performance optimizations, mobile support, desktop app.", - status: { - text: "Future", - type: "info", - }, - }, -]; - -export const metadata: Metadata = { - title: "Roadmap - OpenCut", - description: - "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", - openGraph: { - title: "OpenCut Roadmap - What's Coming Next", - description: - "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", - type: "website", - images: [ - { - url: "/open-graph/roadmap.jpg", - width: 1200, - height: 630, - alt: "OpenCut Roadmap", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "OpenCut Roadmap - What's Coming Next", - description: - "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", - images: ["/open-graph/roadmap.jpg"], - }, -}; - -export default function RoadmapPage() { - return ( -
-
-
-
-
-
-
-
-
-
- - - - Open Source - - -

- Roadmap -

-

- What's coming next for OpenCut (last updated: July 14, 2025) -

-
-
- {roadmapItems.map((item, index) => ( -
-
- - {index + 1}. - -
-
-

{item.title}

- - {item.status.text} - -
-
- ( - - {children} - - ), - strong: ({ children }) => ( - - {children} - - ), - }} - > - {item.description} - -
-
-
-
- ))} -
- -
-
-

Want to Help?

-

- OpenCut is open source and built by the community. Every - contribution, no matter how small, helps us build the best - free video editor possible. -

-
- - - - Start Contributing - - - - - Report Issues - - -
-
-
-
-
-
-
-
- ); -} +import { Metadata } from "next"; +import { Badge } from "@/components/ui/badge"; +import { GithubIcon } from "@/components/icons"; +import Link from "next/link"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; +import ReactMarkdown from "react-markdown"; +import { cn } from "@/lib/utils"; + +const roadmapItems: { + title: string; + description: string; + status: { + text: string; + type: "complete" | "pending" | "default" | "info"; + }; +}[] = [ + { + title: "Start", + description: + "This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.", + status: { + text: "Completed", + type: "complete", + }, + }, + { + title: "Core UI", + description: + "Built the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.", + status: { + text: "Completed", + type: "complete", + }, + }, + { + title: "Basic Functionality", + description: + "The heart of any video editor. Timeline zoom in/out, making clips longer/shorter, dragging elements around, selection, playhead scrubbing. **This part has to be fucking perfect** because it's what users interact with 99% of the time.", + status: { + text: "In Progress", + type: "pending", + }, + }, + { + title: "Export/Preview Logic", + description: + "The foundation that enables everything else. Real-time preview, video rendering, export functionality. Once this works, we can add effects, filters, transitions - basically everything that makes a video editor powerful.", + status: { + text: "In Progress", + type: "pending", + }, + }, + { + title: "Text", + description: + "After media, text is the next most important thing. Font selection with custom font imports, text stroke, colors. All the text essential text properties.", + status: { + text: "Not Started", + type: "default", + }, + }, + { + title: "Effects", + description: + "Adding visual effects to both text and media. Blur, brightness, contrast, saturation, filters, and all the creative tools that make videos pop. This is where the magic happens.", + status: { + text: "Not Started", + type: "default", + }, + }, + { + title: "Transitions", + description: + "Smooth transitions between clips. Fade in/out, slide, zoom, dissolve, and custom transition effects.", + status: { + text: "Not Started", + type: "default", + }, + }, + { + title: "Refine from Here", + description: + "Once we nail the above, we have a **solid foundation** to build anything. Advanced features, performance optimizations, mobile support, desktop app.", + status: { + text: "Future", + type: "info", + }, + }, +]; + +export const metadata: Metadata = { + title: "Roadmap - OpenCut", + description: + "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", + openGraph: { + title: "OpenCut Roadmap - What's Coming Next", + description: + "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", + type: "website", + images: [ + { + url: "/open-graph/roadmap.jpg", + width: 1200, + height: 630, + alt: "OpenCut Roadmap", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "OpenCut Roadmap - What's Coming Next", + description: + "See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.", + images: ["/open-graph/roadmap.jpg"], + }, +}; + +export default function RoadmapPage() { + return ( +
+
+
+
+
+
+
+
+
+
+ + + + Open Source + + +

+ Roadmap +

+

+ What's coming next for OpenCut (last updated: July 14, 2025) +

+
+
+ {roadmapItems.map((item, index) => ( +
+
+ + {index + 1}. + +
+
+

{item.title}

+ + {item.status.text} + +
+
+ ( + + {children} + + ), + strong: ({ children }) => ( + + {children} + + ), + }} + > + {item.description} + +
+
+
+
+ ))} +
+ +
+
+

Want to Help?

+

+ OpenCut is open source and built by the community. Every + contribution, no matter how small, helps us build the best + free video editor possible. +

+
+ + + + Start Contributing + + + + + Report Issues + + +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts index ab645655..b16bd856 100644 --- a/apps/web/src/app/rss.xml/route.ts +++ b/apps/web/src/app/rss.xml/route.ts @@ -1,33 +1,33 @@ -import { Feed } from "feed"; -import { getPosts } from "@/lib/blog-query"; -import { SITE_INFO, SITE_URL } from "@/constants/site"; +import { Feed } from 'feed'; +import { getPosts } from '@/lib/blog-query'; +import { SITE_INFO } from '../metadata'; export async function GET() { try { const { posts } = await getPosts(); - + const feed = new Feed({ - title: `${SITE_INFO.title} Blog`, + title: 'OpenCut Blog', description: SITE_INFO.description, - id: `${SITE_URL}`, - link: `${SITE_URL}/blog/`, - language: "en", + id: `${SITE_INFO.url}`, + link: `${SITE_INFO.url}/blog/`, + language: 'en', image: `${SITE_INFO.openGraphImage}`, favicon: `${SITE_INFO.favicon}`, copyright: `All rights reserved ${new Date().getFullYear()}, ${ SITE_INFO.title }`, }); - + for (const post of posts) { feed.addItem({ - title: post.title, - id: `${SITE_URL}/blog/${post.slug}`, - link: `${SITE_URL}/blog/${post.slug}`, - description: post.description, - author: post.authors.map((author) => ({ - name: author.name, - })), + title: post.title, + id: `${SITE_INFO.url}/blog/${post.slug}`, + link: `${SITE_INFO.url}/blog/${post.slug}`, + description: post.description, + author: post.authors.map((author) => ({ + name: author.name, + })), date: new Date(post.publishedAt), image: post.coverImage || SITE_INFO.openGraphImage, }); @@ -35,12 +35,12 @@ export async function GET() { return new Response(feed.rss2(), { headers: { - "Content-Type": "text/xml", - "Cache-Control": "public, max-age=86400, stale-while-revalidate", + 'Content-Type': 'text/xml', + 'Cache-Control': 'public, max-age=86400, stale-while-revalidate', }, }); } catch (error) { - console.error("Error generating RSS feed", error); - return new Response("Internal Server Error", { status: 500 }); + console.error('Error generating RSS feed', error); + return new Response('Internal Server Error', { status: 500 }); } -} +} \ No newline at end of file diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx index 7d1fda7f..c56e8fc1 100644 --- a/apps/web/src/app/terms/page.tsx +++ b/apps/web/src/app/terms/page.tsx @@ -1,317 +1,317 @@ -import { Metadata } from "next"; -import { Card, CardContent } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { GithubIcon } from "@/components/icons"; -import Link from "next/link"; -import { Footer } from "@/components/footer"; -import { Header } from "@/components/header"; - -export const metadata: Metadata = { - title: "Terms of Service - OpenCut", - description: - "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.", - openGraph: { - title: "Terms of Service - OpenCut", - description: - "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.", - type: "website", - }, -}; - -export default function TermsPage() { - return ( -
-
-
-
-
-
-
-
-
-
- - - - Open Source - - -

- Terms of Service -

-

- Fair and transparent terms for our free, open-source video - editor. -

-
- - -
-

- Welcome to OpenCut -

-

- OpenCut is a free, open-source video editor that runs in - your browser. By using our service, you agree to these - terms. We've designed these terms to be fair and protect - both you and our project. -

-

- Key principle: Your content stays on your - device. We never claim ownership of your videos or projects. -

-
- -
-

- Your Content, Your Rights -

-

- You own everything you create. OpenCut - processes your videos locally on your device, so we never - have access to your content. We make no claims to ownership, - licensing, or rights over your videos, projects, or any - content you create using OpenCut. -

-
    -
  • - Your videos remain completely private and under your - control -
  • -
  • - You retain all intellectual property rights to your - content -
  • -
  • - You can export and use your content however you choose -
  • -
  • - No watermarks, no licensing restrictions from OpenCut -
  • -
-
- -
-

- How You Can Use OpenCut -

-

- OpenCut is free for personal and commercial use. You can: -

-
    -
  • - Create videos for personal, educational, or commercial - purposes -
  • -
  • Use OpenCut for client work and paid projects
  • -
  • Share and distribute videos created with OpenCut
  • -
  • - Modify and distribute the OpenCut software (under MIT - license) -
  • -
-

- What we ask: Don't use OpenCut for illegal - activities, harassment, or creating harmful content. Be - respectful of others and follow applicable laws. -

-
- -
-

- Account and Service -

-

- To use certain features, you may create an account: -

-
    -
  • Provide accurate information when signing up
  • -
  • - Keep your account secure and don't share credentials -
  • -
  • You're responsible for activity under your account
  • -
  • You can delete your account at any time
  • -
-

- OpenCut is provided "as is" without warranties. While we - strive for reliability, we can't guarantee uninterrupted - service. -

-
- -
-

- Open Source Benefits -

-

- Because OpenCut is open source, you have additional rights: -

-
    -
  • - Review our code to see exactly how we handle your data -
  • -
  • Self-host OpenCut on your own servers
  • -
  • Modify the software to suit your needs
  • -
  • Contribute improvements back to the community
  • -
-

- View our source code and license on{" "} - - GitHub - - . -

-
- -
-

- Third-Party Content -

-

- When using OpenCut, make sure you have the right to use any - content you import: -

-
    -
  • - Only upload content you own or have permission to use -
  • -
  • - Respect copyright, trademarks, and other intellectual - property -
  • -
  • - Don't use copyrighted music, images, or videos without - permission -
  • -
  • - You're responsible for any claims related to your content -
  • -
-
- -
-

- Limitations and Liability -

-

- OpenCut is provided free of charge. To the extent permitted - by law: -

-
    -
  • We're not liable for any loss of data or content
  • -
  • - Projects are stored in your browser and may be lost if you - clear browser data -
  • -
  • We're not responsible for how you use the service
  • -
  • - Our liability is limited to the maximum extent allowed by - law -
  • -
-

- Since your content stays on your device, we have no way to - recover lost projects. Consider exporting important videos - when finished editing. -

-
- -
-

- Service Changes -

-

We may update OpenCut and these terms:

-
    -
  • - We'll notify you of significant changes to these terms -
  • -
  • Continued use means you accept any updates
  • -
  • - You can always self-host an older version if you prefer -
  • -
  • - Major changes will be discussed with the community on - GitHub -
  • -
-
- -
-

Termination

-

- You can stop using OpenCut at any time: -

-
    -
  • Delete your account through your profile settings
  • -
  • Clear your browser data to remove local projects
  • -
  • - Your content remains yours even if you stop using OpenCut -
  • -
  • - We may suspend accounts for violations of these terms -
  • -
-
- -
-

- Contact and Disputes -

-

- Questions about these terms or need to report an issue? -

-

- Contact us through our{" "} - - GitHub repository - - , email us at{" "} - - oss@opencut.app - - , or reach out on{" "} - - X (Twitter) - - . -

-

- These terms are governed by applicable law in your - jurisdiction. We prefer to resolve disputes through friendly - discussion in our open-source community. -

-
- -

- Last updated: July 14, 2025 -

-
-
-
-
-
-
-
- ); -} +import { Metadata } from "next"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { GithubIcon } from "@/components/icons"; +import Link from "next/link"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; + +export const metadata: Metadata = { + title: "Terms of Service - OpenCut", + description: + "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.", + openGraph: { + title: "Terms of Service - OpenCut", + description: + "OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.", + type: "website", + }, +}; + +export default function TermsPage() { + return ( +
+
+
+
+
+
+
+
+
+
+ + + + Open Source + + +

+ Terms of Service +

+

+ Fair and transparent terms for our free, open-source video + editor. +

+
+ + +
+

+ Welcome to OpenCut +

+

+ OpenCut is a free, open-source video editor that runs in + your browser. By using our service, you agree to these + terms. We've designed these terms to be fair and protect + both you and our project. +

+

+ Key principle: Your content stays on your + device. We never claim ownership of your videos or projects. +

+
+ +
+

+ Your Content, Your Rights +

+

+ You own everything you create. OpenCut + processes your videos locally on your device, so we never + have access to your content. We make no claims to ownership, + licensing, or rights over your videos, projects, or any + content you create using OpenCut. +

+
    +
  • + Your videos remain completely private and under your + control +
  • +
  • + You retain all intellectual property rights to your + content +
  • +
  • + You can export and use your content however you choose +
  • +
  • + No watermarks, no licensing restrictions from OpenCut +
  • +
+
+ +
+

+ How You Can Use OpenCut +

+

+ OpenCut is free for personal and commercial use. You can: +

+
    +
  • + Create videos for personal, educational, or commercial + purposes +
  • +
  • Use OpenCut for client work and paid projects
  • +
  • Share and distribute videos created with OpenCut
  • +
  • + Modify and distribute the OpenCut software (under MIT + license) +
  • +
+

+ What we ask: Don't use OpenCut for illegal + activities, harassment, or creating harmful content. Be + respectful of others and follow applicable laws. +

+
+ +
+

+ Account and Service +

+

+ To use certain features, you may create an account: +

+
    +
  • Provide accurate information when signing up
  • +
  • + Keep your account secure and don't share credentials +
  • +
  • You're responsible for activity under your account
  • +
  • You can delete your account at any time
  • +
+

+ OpenCut is provided "as is" without warranties. While we + strive for reliability, we can't guarantee uninterrupted + service. +

+
+ +
+

+ Open Source Benefits +

+

+ Because OpenCut is open source, you have additional rights: +

+
    +
  • + Review our code to see exactly how we handle your data +
  • +
  • Self-host OpenCut on your own servers
  • +
  • Modify the software to suit your needs
  • +
  • Contribute improvements back to the community
  • +
+

+ View our source code and license on{" "} + + GitHub + + . +

+
+ +
+

+ Third-Party Content +

+

+ When using OpenCut, make sure you have the right to use any + content you import: +

+
    +
  • + Only upload content you own or have permission to use +
  • +
  • + Respect copyright, trademarks, and other intellectual + property +
  • +
  • + Don't use copyrighted music, images, or videos without + permission +
  • +
  • + You're responsible for any claims related to your content +
  • +
+
+ +
+

+ Limitations and Liability +

+

+ OpenCut is provided free of charge. To the extent permitted + by law: +

+
    +
  • We're not liable for any loss of data or content
  • +
  • + Projects are stored in your browser and may be lost if you + clear browser data +
  • +
  • We're not responsible for how you use the service
  • +
  • + Our liability is limited to the maximum extent allowed by + law +
  • +
+

+ Since your content stays on your device, we have no way to + recover lost projects. Consider exporting important videos + when finished editing. +

+
+ +
+

+ Service Changes +

+

We may update OpenCut and these terms:

+
    +
  • + We'll notify you of significant changes to these terms +
  • +
  • Continued use means you accept any updates
  • +
  • + You can always self-host an older version if you prefer +
  • +
  • + Major changes will be discussed with the community on + GitHub +
  • +
+
+ +
+

Termination

+

+ You can stop using OpenCut at any time: +

+
    +
  • Delete your account through your profile settings
  • +
  • Clear your browser data to remove local projects
  • +
  • + Your content remains yours even if you stop using OpenCut +
  • +
  • + We may suspend accounts for violations of these terms +
  • +
+
+ +
+

+ Contact and Disputes +

+

+ Questions about these terms or need to report an issue? +

+

+ Contact us through our{" "} + + GitHub repository + + , email us at{" "} + + oss@opencut.app + + , or reach out on{" "} + + X (Twitter) + + . +

+

+ These terms are governed by applicable law in your + jurisdiction. We prefer to resolve disputes through friendly + discussion in our open-source community. +

+
+ +

+ Last updated: July 14, 2025 +

+
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/why-not-capcut/page.tsx b/apps/web/src/app/why-not-capcut/page.tsx index 7fe6df66..56b3d27a 100644 --- a/apps/web/src/app/why-not-capcut/page.tsx +++ b/apps/web/src/app/why-not-capcut/page.tsx @@ -1,191 +1,191 @@ -import { Header } from "@/components/header"; - -export default function WhyNotCapcut() { - return ( -
-
- -
-
-
-
-
- -
-
-
-

- Fuck CapCut -

-

- Roasting time, so get ready motherfucker. -

-
- -
-
-

- Seriously, what the fuck else do you want? -

-

- You probably use CapCut and think your video editing is - special. You think your fucking TikTok with 47 transitions and - 12 different fonts is going to get you some viral fame. You - think loading up every goddamn effect in their library makes - your content better. Wrong, motherfucker. Let me describe what - CapCut actually gives you: -

-
    -
  • A paywall every time you breathe
  • -
  • Terms of service that steal your shit
  • -
  • - More "Get Pro" dialogs than a Windows 95 error message -
  • -
  • - Features that disappear behind paywalls while you're fucking - using them -
  • -
  • Bugs disguised as "premium features"
  • -
-

- Well guess what, motherfucker: -

-

- You. Are. Getting. Scammed. Look at this shit. It's a fucking - video editor. Why the fuck do you need to pay $20/month just - to remove a goddamn watermark? You spent hours editing your - video and they slap their logo on it like they fucking made - it. -

-
- -
-

- The "Get Pro" dialog is everywhere -

-

- This motherfucking dialog pops up more than ads on a pirated - movie site. Want to add a transition? Get Pro. Want to export - without their watermark? Get Pro. Want to use more than 2 - fonts? Get fucking Pro, peasant. -

-

- Did you seriously think you could edit a video without seeing - this dialog 47 times? You click one button and BAM - there it - is again, asking for your credit card like a desperate ex - asking for money. -

-
- -
-

- Everything costs money now -

-

- You dumbass. You thought CapCut was free, but no. Free means - they let you open the app. Everything else costs money. Basic - shake effect? That'll be $20/month. A decent transition that - isn't "fade"? Pay up, motherfucker. -

-

- Here's my favorite piece of bullshit: You import an MP3 file - - you know, AUDIO - and try to export. "Sorry, can't export - because you're using our premium extract audio feature!" -

-

- - My MP3 was already fucking audio, you absolute morons. - -

-

- But wait, there's more! If you drag that same MP3 to their - media panel first, then to the timeline, it magically works. - This isn't a bug, it's a fucking scam disguised as software - engineering. -

-
- -
-

- Their Terms of Service are insane -

-

- Look at this shit. You upload your content and they basically - say "thanks for the free content, we own it now, but if Disney - sues anyone, that's your problem." -

-

- CapCut's Terms of Service: We get full rights - to use, modify, distribute, and monetize everything you upload - - permanently and without paying you shit. But you're still - responsible if anything goes wrong. -

-

- Translation: "We'll make money off your viral video, you - handle the lawsuits." Brilliant legal strategy, you fucks. -

-
- -
-

- The editor is actually good -

-

- Here's the thing that makes me want to punch my monitor: the - actual video editor is fucking good. It's intuitive, powerful, - and anyone can figure it out. When it's not begging for money - every 30 seconds, it actually works well. -

-

- Which makes everything else so much worse. They built - something people want to use, then turned it into a digital - slot machine. Every click might trigger a payment request. -

-
- -
-

- This is a video editor. Look at it. You've never seen one - before. -

-

- Like the person who's never used software that doesn't - constantly beg for money, you have no fucking idea what a - video editor should be. All you've ever seen are predatory - apps disguised as creative tools. -

-

- A real video editor lets you edit videos. It doesn't steal - your content. It doesn't pop up payment dialogs every 5 - seconds. It doesn't charge you separately for basic features - that should be free. -

-
- -
-

- Yes, this is fucking satire, you fuck -

-

- I'm not actually saying all video editors should be basic as - shit. What I'm saying is that all the problems we have with - video editing apps are{" "} - ones they create themselves. Video editors - aren't broken by default - they edit videos, export them, and - let you use basic features without constantly begging for - money. CapCut breaks them. They turn them into payment - processors with video editing as a side feature. -

-

- "Good software gets out of your way." -
- Some smart motherfucker who definitely wasn't working - at CapCut -

-
-
-
-
-
-
- ); -} +import { Header } from "@/components/header"; + +export default function WhyNotCapcut() { + return ( +
+
+ +
+
+
+
+
+ +
+
+
+

+ Fuck CapCut +

+

+ Roasting time, so get ready motherfucker. +

+
+ +
+
+

+ Seriously, what the fuck else do you want? +

+

+ You probably use CapCut and think your video editing is + special. You think your fucking TikTok with 47 transitions and + 12 different fonts is going to get you some viral fame. You + think loading up every goddamn effect in their library makes + your content better. Wrong, motherfucker. Let me describe what + CapCut actually gives you: +

+
    +
  • A paywall every time you breathe
  • +
  • Terms of service that steal your shit
  • +
  • + More "Get Pro" dialogs than a Windows 95 error message +
  • +
  • + Features that disappear behind paywalls while you're fucking + using them +
  • +
  • Bugs disguised as "premium features"
  • +
+

+ Well guess what, motherfucker: +

+

+ You. Are. Getting. Scammed. Look at this shit. It's a fucking + video editor. Why the fuck do you need to pay $20/month just + to remove a goddamn watermark? You spent hours editing your + video and they slap their logo on it like they fucking made + it. +

+
+ +
+

+ The "Get Pro" dialog is everywhere +

+

+ This motherfucking dialog pops up more than ads on a pirated + movie site. Want to add a transition? Get Pro. Want to export + without their watermark? Get Pro. Want to use more than 2 + fonts? Get fucking Pro, peasant. +

+

+ Did you seriously think you could edit a video without seeing + this dialog 47 times? You click one button and BAM - there it + is again, asking for your credit card like a desperate ex + asking for money. +

+
+ +
+

+ Everything costs money now +

+

+ You dumbass. You thought CapCut was free, but no. Free means + they let you open the app. Everything else costs money. Basic + shake effect? That'll be $20/month. A decent transition that isn't + "fade"? Pay up, motherfucker. +

+

+ Here's my favorite piece of bullshit: You import an MP3 file - + you know, AUDIO - and try to export. "Sorry, can't export + because you're using our premium extract audio feature!" +

+

+ + My MP3 was already fucking audio, you absolute morons. + +

+

+ But wait, there's more! If you drag that same MP3 to their + media panel first, then to the timeline, it magically works. + This isn't a bug, it's a fucking scam disguised as software + engineering. +

+
+ +
+

+ Their Terms of Service are insane +

+

+ Look at this shit. You upload your content and they basically + say "thanks for the free content, we own it now, but if Disney + sues anyone, that's your problem." +

+

+ CapCut's Terms of Service: We get full rights + to use, modify, distribute, and monetize everything you upload + - permanently and without paying you shit. But you're still + responsible if anything goes wrong. +

+

+ Translation: "We'll make money off your viral video, you + handle the lawsuits." Brilliant legal strategy, you fucks. +

+
+ +
+

+ The editor is actually good +

+

+ Here's the thing that makes me want to punch my monitor: the + actual video editor is fucking good. It's intuitive, powerful, + and anyone can figure it out. When it's not begging for money + every 30 seconds, it actually works well. +

+

+ Which makes everything else so much worse. They built + something people want to use, then turned it into a digital + slot machine. Every click might trigger a payment request. +

+
+ +
+

+ This is a video editor. Look at it. You've never seen one + before. +

+

+ Like the person who's never used software that doesn't + constantly beg for money, you have no fucking idea what a + video editor should be. All you've ever seen are predatory + apps disguised as creative tools. +

+

+ A real video editor lets you edit videos. It doesn't steal + your content. It doesn't pop up payment dialogs every 5 + seconds. It doesn't charge you separately for basic features + that should be free. +

+
+ +
+

+ Yes, this is fucking satire, you fuck +

+

+ I'm not actually saying all video editors should be basic as + shit. What I'm saying is that all the problems we have with + video editing apps are{" "} + ones they create themselves. Video editors + aren't broken by default - they edit videos, export them, and + let you use basic features without constantly begging for + money. CapCut breaks them. They turn them into payment + processors with video editing as a side feature. +

+

+ "Good software gets out of your way." +
- Some smart motherfucker who definitely wasn't working + at CapCut +

+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/background-settings.tsx b/apps/web/src/components/background-settings.tsx index a640dc4c..721564ea 100644 --- a/apps/web/src/components/background-settings.tsx +++ b/apps/web/src/components/background-settings.tsx @@ -1,184 +1,184 @@ -import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { Button } from "./ui/button"; -import { BackgroundIcon } from "./icons"; -import { cn } from "@/lib/utils"; -import Image from "next/image"; -import { colors } from "@/data/colors"; -import { useProjectStore } from "@/stores/project-store"; -import { PipetteIcon } from "lucide-react"; - -type BackgroundTab = "color" | "blur"; - -export function BackgroundSettings() { - const { activeProject, updateBackgroundType } = useProjectStore(); - - // ✅ Good: derive activeTab from activeProject during rendering - const activeTab = activeProject?.backgroundType || "color"; - - const handleColorSelect = (color: string) => { - updateBackgroundType("color", { backgroundColor: color }); - }; - - const handleBlurSelect = (blurIntensity: number) => { - updateBackgroundType("blur", { blurIntensity }); - }; - - const tabs = [ - { - label: "Color", - value: "color", - }, - { - label: "Blur", - value: "blur", - }, - ]; - - return ( - - - - - -
-

Background

-
- {tabs.map((tab) => ( - { - // Switch to the background type when clicking tabs - if (tab.value === "color") { - updateBackgroundType("color", { - backgroundColor: - activeProject?.backgroundColor || "#000000", - }); - } else { - updateBackgroundType("blur", { - blurIntensity: activeProject?.blurIntensity || 8, - }); - } - }} - className={cn( - "text-muted-foreground cursor-pointer", - activeTab === tab.value && "text-foreground" - )} - > - {tab.label} - - ))} -
-
- {activeTab === "color" ? ( - - ) : ( - - )} -
-
- ); -} - -function ColorView({ - selectedColor, - onColorSelect, -}: { - selectedColor: string; - onColorSelect: (color: string) => void; -}) { - return ( -
-
-
-
- -
- {colors.map((color) => ( - onColorSelect(color)} - /> - ))} -
-
- ); -} - -function ColorItem({ - color, - isSelected, - onClick, -}: { - color: string; - isSelected: boolean; - onClick: () => void; -}) { - return ( -
- ); -} - -function BlurView({ - selectedBlur, - onBlurSelect, -}: { - selectedBlur: number; - onBlurSelect: (blurIntensity: number) => void; -}) { - const blurLevels = [ - { label: "Light", value: 4 }, - { label: "Medium", value: 8 }, - { label: "Heavy", value: 18 }, - ]; - const blurImage = - "https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; - - return ( -
- {blurLevels.map((blur) => ( -
onBlurSelect(blur.value)} - > - {`Blur -
- - {blur.label} - -
-
- ))} -
- ); -} +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { Button } from "./ui/button"; +import { BackgroundIcon } from "./icons"; +import { cn } from "@/lib/utils"; +import Image from "next/image"; +import { colors } from "@/data/colors"; +import { useProjectStore } from "@/stores/project-store"; +import { PipetteIcon } from "lucide-react"; + +type BackgroundTab = "color" | "blur"; + +export function BackgroundSettings() { + const { activeProject, updateBackgroundType } = useProjectStore(); + + // ✅ Good: derive activeTab from activeProject during rendering + const activeTab = activeProject?.backgroundType || "color"; + + const handleColorSelect = (color: string) => { + updateBackgroundType("color", { backgroundColor: color }); + }; + + const handleBlurSelect = (blurIntensity: number) => { + updateBackgroundType("blur", { blurIntensity }); + }; + + const tabs = [ + { + label: "Color", + value: "color", + }, + { + label: "Blur", + value: "blur", + }, + ]; + + return ( + + + + + +
+

Background

+
+ {tabs.map((tab) => ( + { + // Switch to the background type when clicking tabs + if (tab.value === "color") { + updateBackgroundType("color", { + backgroundColor: + activeProject?.backgroundColor || "#000000", + }); + } else { + updateBackgroundType("blur", { + blurIntensity: activeProject?.blurIntensity || 8, + }); + } + }} + className={cn( + "text-muted-foreground cursor-pointer", + activeTab === tab.value && "text-foreground" + )} + > + {tab.label} + + ))} +
+
+ {activeTab === "color" ? ( + + ) : ( + + )} +
+
+ ); +} + +function ColorView({ + selectedColor, + onColorSelect, +}: { + selectedColor: string; + onColorSelect: (color: string) => void; +}) { + return ( +
+
+
+
+ +
+ {colors.map((color) => ( + onColorSelect(color)} + /> + ))} +
+
+ ); +} + +function ColorItem({ + color, + isSelected, + onClick, +}: { + color: string; + isSelected: boolean; + onClick: () => void; +}) { + return ( +
+ ); +} + +function BlurView({ + selectedBlur, + onBlurSelect, +}: { + selectedBlur: number; + onBlurSelect: (blurIntensity: number) => void; +}) { + const blurLevels = [ + { label: "Light", value: 4 }, + { label: "Medium", value: 8 }, + { label: "Heavy", value: 18 }, + ]; + const blurImage = + "https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; + + return ( +
+ {blurLevels.map((blur) => ( +
onBlurSelect(blur.value)} + > + {`Blur +
+ + {blur.label} + +
+
+ ))} +
+ ); +} diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index b0b384da..e7df2896 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -1,135 +1,142 @@ -"use client"; - -import Link from "next/link"; -import { Button } from "./ui/button"; -import { Download, SquarePen } from "lucide-react"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { HeaderBase } from "./header-base"; -import { formatTimeCode } from "@/lib/time"; -import { useProjectStore } from "@/stores/project-store"; -import { KeyboardShortcutsHelp } from "./keyboard-shortcuts-help"; -import { useState, useRef } from "react"; -import { Input } from "@/components/ui/input"; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "./ui/breadcrumb"; - -export function EditorHeader() { - const { getTotalDuration } = useTimelineStore(); - const { activeProject, renameProject } = useProjectStore(); - const [isEditing, setIsEditing] = useState(false); - const [newName, setNewName] = useState(activeProject?.name || ""); - const inputRef = useRef(null); - - const handleExport = () => { - // TODO: Implement export functionality - // NOTE: This is already being worked on - console.log("Export project"); - }; - - const handleNameEdit = () => { - if (!activeProject) return; - setNewName(activeProject.name); - setIsEditing(true); - }; - - const handleNameSave = async () => { - if (activeProject && newName.trim() && newName !== activeProject.name) { - try { - await renameProject(activeProject.id, newName.trim()); - } catch (error) { - console.error("Failed to rename project:", error); - setNewName(activeProject.name); - } - } - setIsEditing(false); - }; - - const handleInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") handleNameSave(); - else if (e.key === "Escape") setIsEditing(false); - }; - - const leftContent = ( -
- - - - - - All Projects - - - - - - {isEditing ? ( - setNewName(e.target.value)} - onBlur={handleNameSave} - onKeyDown={handleInputKeyDown} - onFocus={(e) => e.target.select()} - maxLength={64} - aria-label="Project name" - autoFocus - /> - ) : ( - - )} - - - -
- ); - - const centerContent = ( -
- - {formatTimeCode( - getTotalDuration(), - "HH:MM:SS:FF", - activeProject?.fps || 30 - )} - -
- ); - - const rightContent = ( - - ); - - return ( - - ); -} +"use client"; + +import { Download, SquarePen } from "lucide-react"; +import Link from "next/link"; +import { type MouseEvent, useRef, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { formatTimeCode } from "@/lib/time"; +import { useProjectStore } from "@/stores/project-store"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { HeaderBase } from "./header-base"; +import { KeyboardShortcutsHelp } from "./keyboard-shortcuts-help"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "./ui/breadcrumb"; +import { Button } from "./ui/button"; + +export function EditorHeader() { + const { getTotalDuration } = useTimelineStore(); + const { activeProject, renameProject } = useProjectStore(); + const [isEditing, setIsEditing] = useState(false); + const [newName, setNewName] = useState(activeProject?.name || ""); + const inputRef = useRef(null); + + const handleExport = () => { + // TODO: Implement export functionality + // NOTE: This is already being worked on + console.log("Export project"); + }; + + const handleNameEdit = (e: MouseEvent) => { + if (!activeProject) return; + e.stopPropagation(); + setIsEditing(true); + setNewName(activeProject.name); + }; + + const handleNameSave = async () => { + if (activeProject && newName.trim() && newName !== activeProject.name) { + try { + await renameProject(activeProject.id, newName.trim()); + } catch (error) { + console.error("Failed to rename project:", error); + setNewName(activeProject.name); + } + } + setIsEditing(false); + }; + + const handleInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") handleNameSave(); + else if (e.key === "Escape") setIsEditing(false); + }; + + const leftContent = ( +
+ + + + + + All Projects + + + + + + {isEditing ? ( + setNewName(e.target.value)} + onBlur={handleNameSave} + onKeyDown={handleInputKeyDown} + onFocus={(e) => e.target.select()} + maxLength={64} + aria-label="Project name" + autoFocus + /> + ) : ( + + )} + + + +
+ ); + + const centerContent = ( +
+ + {formatTimeCode( + getTotalDuration(), + "HH:MM:SS:FF", + activeProject?.fps || 30, + )} + +
+ ); + + const rightContent = ( + + ); + + return ( + + ); +} diff --git a/apps/web/src/components/editor-provider.tsx b/apps/web/src/components/editor-provider.tsx index b8e9ae1d..25d040db 100644 --- a/apps/web/src/components/editor-provider.tsx +++ b/apps/web/src/components/editor-provider.tsx @@ -1,53 +1,53 @@ -"use client"; - -import { useEffect } from "react"; -import { Loader2 } from "lucide-react"; -import { useEditorStore } from "@/stores/editor-store"; -import { - useKeybindingsListener, - useKeybindingDisabler, -} from "@/hooks/use-keybindings"; -import { useEditorActions } from "@/hooks/use-editor-actions"; - -interface EditorProviderProps { - children: React.ReactNode; -} - -export function EditorProvider({ children }: EditorProviderProps) { - const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); - const { disableKeybindings, enableKeybindings } = useKeybindingDisabler(); - - // Set up action handlers - useEditorActions(); - - // Set up keybinding listener - useKeybindingsListener(); - - // Disable keybindings when initializing - useEffect(() => { - if (isInitializing || !isPanelsReady) { - disableKeybindings(); - } else { - enableKeybindings(); - } - }, [isInitializing, isPanelsReady, disableKeybindings, enableKeybindings]); - - useEffect(() => { - initializeApp(); - }, [initializeApp]); - - // Show loading screen while initializing - if (isInitializing || !isPanelsReady) { - return ( -
-
- -

Loading editor...

-
-
- ); - } - - // App is ready, render children - return <>{children}; -} +"use client"; + +import { useEffect } from "react"; +import { Loader2 } from "lucide-react"; +import { useEditorStore } from "@/stores/editor-store"; +import { + useKeybindingsListener, + useKeybindingDisabler, +} from "@/hooks/use-keybindings"; +import { useEditorActions } from "@/hooks/use-editor-actions"; + +interface EditorProviderProps { + children: React.ReactNode; +} + +export function EditorProvider({ children }: EditorProviderProps) { + const { isInitializing, isPanelsReady, initializeApp } = useEditorStore(); + const { disableKeybindings, enableKeybindings } = useKeybindingDisabler(); + + // Set up action handlers + useEditorActions(); + + // Set up keybinding listener + useKeybindingsListener(); + + // Disable keybindings when initializing + useEffect(() => { + if (isInitializing || !isPanelsReady) { + disableKeybindings(); + } else { + enableKeybindings(); + } + }, [isInitializing, isPanelsReady, disableKeybindings, enableKeybindings]); + + useEffect(() => { + initializeApp(); + }, [initializeApp]); + + // Show loading screen while initializing + if (isInitializing || !isPanelsReady) { + return ( +
+
+ +

Loading editor...

+
+
+ ); + } + + // App is ready, render children + return <>{children}; +} diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx index a673f995..75a5ae06 100644 --- a/apps/web/src/components/editor/audio-waveform.tsx +++ b/apps/web/src/components/editor/audio-waveform.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, useState } from "react"; -import WaveSurfer from "wavesurfer.js"; +import React, { useEffect, useRef, useState } from 'react'; +import WaveSurfer from 'wavesurfer.js'; interface AudioWaveformProps { audioUrl: string; @@ -7,10 +7,10 @@ interface AudioWaveformProps { className?: string; } -const AudioWaveform: React.FC = ({ - audioUrl, - height = 32, - className = "", +const AudioWaveform: React.FC = ({ + audioUrl, + height = 32, + className = '' }) => { const waveformRef = useRef(null); const wavesurfer = useRef(null); @@ -36,26 +36,26 @@ const AudioWaveform: React.FC = ({ wavesurfer.current = WaveSurfer.create({ container: waveformRef.current, - waveColor: "rgba(255, 255, 255, 0.6)", - progressColor: "rgba(255, 255, 255, 0.9)", - cursorColor: "transparent", + waveColor: 'rgba(255, 255, 255, 0.6)', + progressColor: 'rgba(255, 255, 255, 0.9)', + cursorColor: 'transparent', barWidth: 2, barGap: 1, - height, + height: height, normalize: true, interact: false, }); // Event listeners - wavesurfer.current.on("ready", () => { + wavesurfer.current.on('ready', () => { if (mounted) { setIsLoading(false); setError(false); } }); - wavesurfer.current.on("error", (err) => { - console.error("WaveSurfer error:", err); + wavesurfer.current.on('error', (err) => { + console.error('WaveSurfer error:', err); if (mounted) { setError(true); setIsLoading(false); @@ -63,8 +63,9 @@ const AudioWaveform: React.FC = ({ }); await wavesurfer.current.load(audioUrl); + } catch (err) { - console.error("Failed to initialize WaveSurfer:", err); + console.error('Failed to initialize WaveSurfer:', err); if (mounted) { setError(true); setIsLoading(false); @@ -89,10 +90,7 @@ const AudioWaveform: React.FC = ({ if (error) { return ( -
+
Audio unavailable
); @@ -105,13 +103,13 @@ const AudioWaveform: React.FC = ({ Loading...
)} -
); }; -export default AudioWaveform; +export default AudioWaveform; \ No newline at end of file diff --git a/apps/web/src/components/editor/media-panel/drag-overlay.tsx b/apps/web/src/components/editor/media-panel/drag-overlay.tsx index e1fd257e..3f3820db 100644 --- a/apps/web/src/components/editor/media-panel/drag-overlay.tsx +++ b/apps/web/src/components/editor/media-panel/drag-overlay.tsx @@ -1,57 +1,57 @@ -import { Upload, Plus, Image } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -interface MediaDragOverlayProps { - isVisible: boolean; - isProcessing?: boolean; - progress?: number; - onClick?: () => void; - isEmptyState?: boolean; -} - -export function MediaDragOverlay({ - isVisible, - isProcessing = false, - progress = 0, - onClick, - isEmptyState = false, -}: MediaDragOverlayProps) { - if (!isVisible) return null; - - const handleClick = (e: React.MouseEvent) => { - if (isProcessing || !onClick) return; - e.preventDefault(); - e.stopPropagation(); - onClick(); - }; - - return ( -
-
- -
- -
-

- {isProcessing - ? `Processing your files (${progress}%)` - : "Drag and drop videos, photos, and audio files here"} -

-
- - {isProcessing && ( -
-
-
-
-
- )} -
- ); -} +import { Upload, Plus, Image } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface MediaDragOverlayProps { + isVisible: boolean; + isProcessing?: boolean; + progress?: number; + onClick?: () => void; + isEmptyState?: boolean; +} + +export function MediaDragOverlay({ + isVisible, + isProcessing = false, + progress = 0, + onClick, + isEmptyState = false, +}: MediaDragOverlayProps) { + if (!isVisible) return null; + + const handleClick = (e: React.MouseEvent) => { + if (isProcessing || !onClick) return; + e.preventDefault(); + e.stopPropagation(); + onClick(); + }; + + return ( +
+
+ +
+ +
+

+ {isProcessing + ? `Processing your files (${progress}%)` + : "Drag and drop videos, photos, and audio files here"} +

+
+ + {isProcessing && ( +
+
+
+
+
+ )} +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx index 2c743618..90ac84f0 100644 --- a/apps/web/src/components/editor/media-panel/index.tsx +++ b/apps/web/src/components/editor/media-panel/index.tsx @@ -1,54 +1,54 @@ -"use client"; - -import { TabBar } from "./tabbar"; -import { MediaView } from "./views/media"; -import { useMediaPanelStore, Tab } from "./store"; -import { TextView } from "./views/text"; -import { AudioView } from "./views/audio"; - -export function MediaPanel() { - const { activeTab } = useMediaPanelStore(); - - const viewMap: Record = { - media: , - audio: , - text: , - stickers: ( -
- Stickers view coming soon... -
- ), - effects: ( -
- Effects view coming soon... -
- ), - transitions: ( -
- Transitions view coming soon... -
- ), - captions: ( -
- Captions view coming soon... -
- ), - filters: ( -
- Filters view coming soon... -
- ), - adjustment: ( -
- Adjustment view coming soon... -
- ), - }; - - return ( -
- -
{viewMap[activeTab]}
-
- ); -} +"use client"; + +import { TabBar } from "./tabbar"; +import { MediaView } from "./views/media"; +import { useMediaPanelStore, Tab } from "./store"; +import { TextView } from "./views/text"; +import { AudioView } from "./views/audio"; + +export function MediaPanel() { + const { activeTab } = useMediaPanelStore(); + + const viewMap: Record = { + media: , + audio: , + text: , + stickers: ( +
+ Stickers view coming soon... +
+ ), + effects: ( +
+ Effects view coming soon... +
+ ), + transitions: ( +
+ Transitions view coming soon... +
+ ), + captions: ( +
+ Captions view coming soon... +
+ ), + filters: ( +
+ Filters view coming soon... +
+ ), + adjustment: ( +
+ Adjustment view coming soon... +
+ ), + }; + + return ( +
+ +
{viewMap[activeTab]}
+
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts index c661f51e..9668f285 100644 --- a/apps/web/src/components/editor/media-panel/store.ts +++ b/apps/web/src/components/editor/media-panel/store.ts @@ -1,73 +1,73 @@ -import { - CaptionsIcon, - ArrowLeftRightIcon, - SparklesIcon, - StickerIcon, - MusicIcon, - VideoIcon, - BlendIcon, - SlidersHorizontalIcon, - LucideIcon, - TypeIcon, -} from "lucide-react"; -import { create } from "zustand"; - -export type Tab = - | "media" - | "audio" - | "text" - | "stickers" - | "effects" - | "transitions" - | "captions" - | "filters" - | "adjustment"; - -export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = { - media: { - icon: VideoIcon, - label: "Media", - }, - audio: { - icon: MusicIcon, - label: "Audio", - }, - text: { - icon: TypeIcon, - label: "Text", - }, - stickers: { - icon: StickerIcon, - label: "Stickers", - }, - effects: { - icon: SparklesIcon, - label: "Effects", - }, - transitions: { - icon: ArrowLeftRightIcon, - label: "Transitions", - }, - captions: { - icon: CaptionsIcon, - label: "Captions", - }, - filters: { - icon: BlendIcon, - label: "Filters", - }, - adjustment: { - icon: SlidersHorizontalIcon, - label: "Adjustment", - }, -}; - -interface MediaPanelStore { - activeTab: Tab; - setActiveTab: (tab: Tab) => void; -} - -export const useMediaPanelStore = create((set) => ({ - activeTab: "media", - setActiveTab: (tab) => set({ activeTab: tab }), -})); +import { + CaptionsIcon, + ArrowLeftRightIcon, + SparklesIcon, + StickerIcon, + MusicIcon, + VideoIcon, + BlendIcon, + SlidersHorizontalIcon, + LucideIcon, + TypeIcon, +} from "lucide-react"; +import { create } from "zustand"; + +export type Tab = + | "media" + | "audio" + | "text" + | "stickers" + | "effects" + | "transitions" + | "captions" + | "filters" + | "adjustment"; + +export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = { + media: { + icon: VideoIcon, + label: "Media", + }, + audio: { + icon: MusicIcon, + label: "Audio", + }, + text: { + icon: TypeIcon, + label: "Text", + }, + stickers: { + icon: StickerIcon, + label: "Stickers", + }, + effects: { + icon: SparklesIcon, + label: "Effects", + }, + transitions: { + icon: ArrowLeftRightIcon, + label: "Transitions", + }, + captions: { + icon: CaptionsIcon, + label: "Captions", + }, + filters: { + icon: BlendIcon, + label: "Filters", + }, + adjustment: { + icon: SlidersHorizontalIcon, + label: "Adjustment", + }, +}; + +interface MediaPanelStore { + activeTab: Tab; + setActiveTab: (tab: Tab) => void; +} + +export const useMediaPanelStore = create((set) => ({ + activeTab: "media", + setActiveTab: (tab) => set({ activeTab: tab }), +})); diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index bce84a46..b05bee7e 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -1,124 +1,124 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { Tab, tabs, useMediaPanelStore } from "./store"; -import { Button } from "@/components/ui/button"; -import { ChevronRight, ChevronLeft } from "lucide-react"; -import { useRef, useState, useEffect } from "react"; - -export function TabBar() { - const { activeTab, setActiveTab } = useMediaPanelStore(); - const scrollContainerRef = useRef(null); - const [isAtEnd, setIsAtEnd] = useState(false); - const [isAtStart, setIsAtStart] = useState(true); - - const scrollToEnd = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: scrollContainerRef.current.scrollWidth, - }); - setIsAtEnd(true); - setIsAtStart(false); - } - }; - - const scrollToStart = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: 0, - }); - setIsAtStart(true); - setIsAtEnd(false); - } - }; - - const checkScrollPosition = () => { - if (scrollContainerRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = - scrollContainerRef.current; - const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1; - const isAtStartNow = scrollLeft <= 1; - setIsAtEnd(isAtEndNow); - setIsAtStart(isAtStartNow); - } - }; - - // We're using useEffect because we need to sync with external DOM scroll events - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - checkScrollPosition(); - container.addEventListener("scroll", checkScrollPosition); - - const resizeObserver = new ResizeObserver(checkScrollPosition); - resizeObserver.observe(container); - - return () => { - container.removeEventListener("scroll", checkScrollPosition); - resizeObserver.disconnect(); - }; - }, []); - - return ( -
- -
- {(Object.keys(tabs) as Tab[]).map((tabKey) => { - const tab = tabs[tabKey]; - return ( -
setActiveTab(tabKey)} - key={tabKey} - > - - {tab.label} -
- ); - })} -
- -
- ); -} - -function ScrollButton({ - direction, - onClick, - isVisible, -}: { - direction: "left" | "right"; - onClick: () => void; - isVisible: boolean; -}) { - if (!isVisible) return null; - - const Icon = direction === "left" ? ChevronLeft : ChevronRight; - - return ( -
- -
- ); -} +"use client"; + +import { cn } from "@/lib/utils"; +import { Tab, tabs, useMediaPanelStore } from "./store"; +import { Button } from "@/components/ui/button"; +import { ChevronRight, ChevronLeft } from "lucide-react"; +import { useRef, useState, useEffect } from "react"; + +export function TabBar() { + const { activeTab, setActiveTab } = useMediaPanelStore(); + const scrollContainerRef = useRef(null); + const [isAtEnd, setIsAtEnd] = useState(false); + const [isAtStart, setIsAtStart] = useState(true); + + const scrollToEnd = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: scrollContainerRef.current.scrollWidth, + }); + setIsAtEnd(true); + setIsAtStart(false); + } + }; + + const scrollToStart = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: 0, + }); + setIsAtStart(true); + setIsAtEnd(false); + } + }; + + const checkScrollPosition = () => { + if (scrollContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = + scrollContainerRef.current; + const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1; + const isAtStartNow = scrollLeft <= 1; + setIsAtEnd(isAtEndNow); + setIsAtStart(isAtStartNow); + } + }; + + // We're using useEffect because we need to sync with external DOM scroll events + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + checkScrollPosition(); + container.addEventListener("scroll", checkScrollPosition); + + const resizeObserver = new ResizeObserver(checkScrollPosition); + resizeObserver.observe(container); + + return () => { + container.removeEventListener("scroll", checkScrollPosition); + resizeObserver.disconnect(); + }; + }, []); + + return ( +
+ +
+ {(Object.keys(tabs) as Tab[]).map((tabKey) => { + const tab = tabs[tabKey]; + return ( +
setActiveTab(tabKey)} + key={tabKey} + > + + {tab.label} +
+ ); + })} +
+ +
+ ); +} + +function ScrollButton({ + direction, + onClick, + isVisible, +}: { + direction: "left" | "right"; + onClick: () => void; + isVisible: boolean; +}) { + if (!isVisible) return null; + + const Icon = direction === "left" ? ChevronLeft : ChevronRight; + + return ( +
+ +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/audio.tsx b/apps/web/src/components/editor/media-panel/views/audio.tsx index ba64d749..7ddf465c 100644 --- a/apps/web/src/components/editor/media-panel/views/audio.tsx +++ b/apps/web/src/components/editor/media-panel/views/audio.tsx @@ -1,18 +1,18 @@ -"use client"; - -import { Input } from "@/components/ui/input"; -import { useState } from "react"; - -export function AudioView() { - const [search, setSearch] = useState(""); - return ( -
- setSearch(e.target.value)} - /> -
-
- ); -} +"use client"; + +import { Input } from "@/components/ui/input"; +import { useState } from "react"; + +export function AudioView() { + const [search, setSearch] = useState(""); + return ( +
+ setSearch(e.target.value)} + /> +
+
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 66d0d00d..0189720f 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -1,298 +1,298 @@ -"use client"; - -import { useDragDrop } from "@/hooks/use-drag-drop"; -import { processMediaFiles } from "@/lib/media-processing"; -import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { Image, Loader2, Music, Plus, Video } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@/components/ui/context-menu"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { DraggableMediaItem } from "@/components/ui/draggable-item"; -import { useProjectStore } from "@/stores/project-store"; -import { useTimelineStore } from "@/stores/timeline-store"; - -export function MediaView() { - const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); - const { activeProject } = useProjectStore(); - const fileInputRef = useRef(null); - const [isProcessing, setIsProcessing] = useState(false); - const [progress, setProgress] = useState(0); - const [searchQuery, setSearchQuery] = useState(""); - const [mediaFilter, setMediaFilter] = useState("all"); - - const processFiles = async (files: FileList | File[]) => { - if (!files || files.length === 0) return; - if (!activeProject) { - toast.error("No active project"); - return; - } - - setIsProcessing(true); - setProgress(0); - try { - // Process files (extract metadata, generate thumbnails, etc.) - const processedItems = await processMediaFiles(files, (p) => - setProgress(p) - ); - // Add each processed media item to the store - for (const item of processedItems) { - await addMediaItem(activeProject.id, item); - } - } catch (error) { - // Show error toast if processing fails - console.error("Error processing files:", error); - toast.error("Failed to process files"); - } finally { - setIsProcessing(false); - setProgress(0); - } - }; - - const { isDragOver, dragProps } = useDragDrop({ - // When files are dropped, process them - onDrop: processFiles, - }); - - const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker - - const handleFileChange = (e: React.ChangeEvent) => { - // When files are selected via file picker, process them - if (e.target.files) processFiles(e.target.files); - e.target.value = ""; // Reset input - }; - - const handleRemove = async (e: React.MouseEvent, id: string) => { - // Remove a media item from the store - e.stopPropagation(); - - if (!activeProject) { - toast.error("No active project"); - return; - } - - // Media store now handles cascade deletion automatically - await removeMediaItem(activeProject.id, id); - }; - - const formatDuration = (duration: number) => { - // Format seconds as mm:ss - const min = Math.floor(duration / 60); - const sec = Math.floor(duration % 60); - return `${min}:${sec.toString().padStart(2, "0")}`; - }; - - const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); - - useEffect(() => { - const filtered = mediaItems.filter((item) => { - if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { - return false; - } - - if ( - searchQuery && - !item.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) { - return false; - } - - return true; - }); - - setFilteredMediaItems(filtered); - }, [mediaItems, mediaFilter, searchQuery]); - - const renderPreview = (item: MediaItem) => { - // Render a preview for each media type (image, video, audio, unknown) - if (item.type === "image") { - return ( -
- {item.name} -
- ); - } - - if (item.type === "video") { - if (item.thumbnailUrl) { - return ( -
- {item.name} -
-
- {item.duration && ( -
- {formatDuration(item.duration)} -
- )} -
- ); - } - return ( -
-
- ); - } - - if (item.type === "audio") { - return ( -
- - Audio - {item.duration && ( - - {formatDuration(item.duration)} - - )} -
- ); - } - - return ( -
- - Unknown -
- ); - }; - - return ( - <> - {/* Hidden file input for uploading media */} - - -
-
- {/* Search and filter controls */} -
- - setSearchQuery(e.target.value)} - /> - -
-
- -
- {isDragOver || filteredMediaItems.length === 0 ? ( - - ) : ( -
- {/* Render each media item as a draggable button */} - {filteredMediaItems.map((item) => ( - - - - useTimelineStore - .getState() - .addMediaAtTime(item, currentTime) - } - rounded={false} - /> - - - Export clips - handleRemove(e, item.id)} - > - Delete - - - - ))} -
- )} -
-
- - ); -} +"use client"; + +import { useDragDrop } from "@/hooks/use-drag-drop"; +import { processMediaFiles } from "@/lib/media-processing"; +import { useMediaStore, type MediaItem } from "@/stores/media-store"; +import { Image, Loader2, Music, Plus, Video } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { DraggableMediaItem } from "@/components/ui/draggable-item"; +import { useProjectStore } from "@/stores/project-store"; +import { useTimelineStore } from "@/stores/timeline-store"; + +export function MediaView() { + const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); + const { activeProject } = useProjectStore(); + const fileInputRef = useRef(null); + const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); + const [searchQuery, setSearchQuery] = useState(""); + const [mediaFilter, setMediaFilter] = useState("all"); + + const processFiles = async (files: FileList | File[]) => { + if (!files || files.length === 0) return; + if (!activeProject) { + toast.error("No active project"); + return; + } + + setIsProcessing(true); + setProgress(0); + try { + // Process files (extract metadata, generate thumbnails, etc.) + const processedItems = await processMediaFiles(files, (p) => + setProgress(p) + ); + // Add each processed media item to the store + for (const item of processedItems) { + await addMediaItem(activeProject.id, item); + } + } catch (error) { + // Show error toast if processing fails + console.error("Error processing files:", error); + toast.error("Failed to process files"); + } finally { + setIsProcessing(false); + setProgress(0); + } + }; + + const { isDragOver, dragProps } = useDragDrop({ + // When files are dropped, process them + onDrop: processFiles, + }); + + const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker + + const handleFileChange = (e: React.ChangeEvent) => { + // When files are selected via file picker, process them + if (e.target.files) processFiles(e.target.files); + e.target.value = ""; // Reset input + }; + + const handleRemove = async (e: React.MouseEvent, id: string) => { + // Remove a media item from the store + e.stopPropagation(); + + if (!activeProject) { + toast.error("No active project"); + return; + } + + // Media store now handles cascade deletion automatically + await removeMediaItem(activeProject.id, id); + }; + + const formatDuration = (duration: number) => { + // Format seconds as mm:ss + const min = Math.floor(duration / 60); + const sec = Math.floor(duration % 60); + return `${min}:${sec.toString().padStart(2, "0")}`; + }; + + const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); + + useEffect(() => { + const filtered = mediaItems.filter((item) => { + if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { + return false; + } + + if ( + searchQuery && + !item.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + return false; + } + + return true; + }); + + setFilteredMediaItems(filtered); + }, [mediaItems, mediaFilter, searchQuery]); + + const renderPreview = (item: MediaItem) => { + // Render a preview for each media type (image, video, audio, unknown) + if (item.type === "image") { + return ( +
+ {item.name} +
+ ); + } + + if (item.type === "video") { + if (item.thumbnailUrl) { + return ( +
+ {item.name} +
+
+ {item.duration && ( +
+ {formatDuration(item.duration)} +
+ )} +
+ ); + } + return ( +
+
+ ); + } + + if (item.type === "audio") { + return ( +
+ + Audio + {item.duration && ( + + {formatDuration(item.duration)} + + )} +
+ ); + } + + return ( +
+ + Unknown +
+ ); + }; + + return ( + <> + {/* Hidden file input for uploading media */} + + +
+
+ {/* Search and filter controls */} +
+ + setSearchQuery(e.target.value)} + /> + +
+
+ +
+ {isDragOver || filteredMediaItems.length === 0 ? ( + + ) : ( +
+ {/* Render each media item as a draggable button */} + {filteredMediaItems.map((item) => ( + + + + useTimelineStore + .getState() + .addMediaAtTime(item, currentTime) + } + rounded={false} + /> + + + Export clips + handleRemove(e, item.id)} + > + Delete + + + + ))} +
+ )} +
+
+ + ); +} diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index 0686190e..6d0d310b 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,53 +1,53 @@ -import { DraggableMediaItem } from "@/components/ui/draggable-item"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { type TextElement } from "@/types/timeline"; - -const textData: TextElement = { - id: "default-text", - type: "text", - name: "Default text", - content: "Default text", - fontSize: 48, - fontFamily: "Arial", - color: "#ffffff", - backgroundColor: "transparent", - textAlign: "center" as const, - fontWeight: "normal" as const, - fontStyle: "normal" as const, - textDecoration: "none" as const, - x: 0, - y: 0, - rotation: 0, - opacity: 1, - duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: 0, - trimStart: 0, - trimEnd: 0, -}; - -export function TextView() { - return ( -
- - Default text -
- } - dragData={{ - id: textData.id, - type: textData.type, - name: textData.name, - content: textData.content, - }} - aspectRatio={1} - onAddToTimeline={(currentTime) => - useTimelineStore.getState().addTextAtTime(textData, currentTime) - } - showLabel={false} - /> -
- ); -} +import { DraggableMediaItem } from "@/components/ui/draggable-item"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { type TextElement } from "@/types/timeline"; + +let textData: TextElement = { + id: "default-text", + type: "text", + name: "Default text", + content: "Default text", + fontSize: 48, + fontFamily: "Arial", + color: "#ffffff", + backgroundColor: "transparent", + textAlign: "center" as const, + fontWeight: "normal" as const, + fontStyle: "normal" as const, + textDecoration: "none" as const, + x: 0, + y: 0, + rotation: 0, + opacity: 1, + duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime: 0, + trimStart: 0, + trimEnd: 0, +}; + +export function TextView() { + return ( +
+ + Default text +
+ } + dragData={{ + id: textData.id, + type: textData.type, + name: textData.name, + content: textData.content, + }} + aspectRatio={1} + onAddToTimeline={(currentTime) => + useTimelineStore.getState().addTextAtTime(textData, currentTime) + } + showLabel={false} + /> +
+ ); +} diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index f09e8462..2cba1374 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -368,7 +368,7 @@ export function PreviewPanel() { ref={containerRef} className="flex-1 flex flex-col items-center justify-center p-3 min-h-0 min-w-0" > -
+
{hasAnyElements ? (
) : null} -
+
Audio properties
; -} +import { MediaElement } from "@/types/timeline"; + +export function AudioProperties({ element }: { element: MediaElement }) { + return
Audio properties
; +} \ No newline at end of file diff --git a/apps/web/src/components/editor/properties-panel/media-properties.tsx b/apps/web/src/components/editor/properties-panel/media-properties.tsx index 4ea28bb2..c2c514cf 100644 --- a/apps/web/src/components/editor/properties-panel/media-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/media-properties.tsx @@ -1,5 +1,5 @@ -import { MediaElement } from "@/types/timeline"; - -export function MediaProperties({ element }: { element: MediaElement }) { - return
Media properties
; -} +import { MediaElement } from "@/types/timeline"; + +export function MediaProperties({ element }: { element: MediaElement }) { + return
Media properties
; +} diff --git a/apps/web/src/components/editor/properties-panel/property-item.tsx b/apps/web/src/components/editor/properties-panel/property-item.tsx index 804ffb39..beb8c799 100644 --- a/apps/web/src/components/editor/properties-panel/property-item.tsx +++ b/apps/web/src/components/editor/properties-panel/property-item.tsx @@ -1,47 +1,47 @@ -import { cn } from "@/lib/utils"; - -interface PropertyItemProps { - direction?: "row" | "column"; - children: React.ReactNode; - className?: string; -} - -export function PropertyItem({ - direction = "row", - children, - className, -}: PropertyItemProps) { - return ( -
- {children} -
- ); -} - -export function PropertyItemLabel({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ; -} - -export function PropertyItemValue({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return
{children}
; -} +import { cn } from "@/lib/utils"; + +interface PropertyItemProps { + direction?: "row" | "column"; + children: React.ReactNode; + className?: string; +} + +export function PropertyItem({ + direction = "row", + children, + className, +}: PropertyItemProps) { + return ( +
+ {children} +
+ ); +} + +export function PropertyItemLabel({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ; +} + +export function PropertyItemValue({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return
{children}
; +} diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx index c55c3dae..e13dabc8 100644 --- a/apps/web/src/components/editor/properties-panel/text-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/text-properties.tsx @@ -1,76 +1,76 @@ -import { Textarea } from "@/components/ui/textarea"; -import { FontPicker } from "@/components/ui/font-picker"; -import { FontFamily } from "@/constants/font-constants"; -import { TextElement } from "@/types/timeline"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { Slider } from "@/components/ui/slider"; -import { Input } from "@/components/ui/input"; -import { - PropertyItem, - PropertyItemLabel, - PropertyItemValue, -} from "./property-item"; - -export function TextProperties({ - element, - trackId, -}: { - element: TextElement; - trackId: string; -}) { - const { updateTextElement } = useTimelineStore(); - - return ( -
-