diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx new file mode 100644 index 00000000..42dd0f9a --- /dev/null +++ b/apps/web/src/app/privacy/page.tsx @@ -0,0 +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 +

+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx new file mode 100644 index 00000000..c56e8fc1 --- /dev/null +++ b/apps/web/src/app/terms/page.tsx @@ -0,0 +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 +

+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index c3a16b9a..033b79a6 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -33,6 +33,7 @@ export function TimelineTrackContent({ const { mediaItems } = useMediaStore(); const { tracks, + addTrack, moveElementToTrack, updateElementStartTime, addElementToTrack, @@ -495,6 +496,16 @@ export function TimelineTrackContent({ e.preventDefault(); e.stopPropagation(); + // Debug logging + console.log( + JSON.stringify({ + message: "Drop event started in timeline track", + dataTransferTypes: Array.from(e.dataTransfer.types), + trackId: track.id, + trackType: track.type, + }) + ); + // Reset all drag states dragCounterRef.current = 0; setIsDropping(false); @@ -727,68 +738,28 @@ export function TimelineTrackContent({ // Handle position-aware track creation for media elements if (!isCompatible || dropPosition !== "on") { - const needsNewTrack = !isCompatible || dropPosition !== "on"; + if (isVideoOrImage) { + // For video/image, check if we need a main track or additional media track + const mainTrack = getMainTrack(tracks); - if (needsNewTrack) { - if (isVideoOrImage) { - // For video/image, check if we need a main track or additional media track - const mainTrack = getMainTrack(tracks); - - if (!mainTrack) { - // No main track exists, create it - const updatedTracks = ensureMainTrack(tracks); - const newMainTrack = getMainTrack(updatedTracks); - if (newMainTrack && newMainTrack.elements.length === 0) { - targetTrackId = newMainTrack.id; - targetTrack = newMainTrack; - } else { - // Main track was created but somehow has elements, create new media track - const mainTrackIndex = updatedTracks.findIndex( - (t) => t.id === newMainTrack?.id - ); - targetTrackId = insertTrackAt("media", mainTrackIndex); - const updatedTracksAfterInsert = - useTimelineStore.getState().tracks; - const newTargetTrack = updatedTracksAfterInsert.find( - (t) => t.id === targetTrackId - ); - if (!newTargetTrack) return; - targetTrack = newTargetTrack; - } - } else if ( - mainTrack.elements.length === 0 && - dropPosition === "on" - ) { - // Main track exists and is empty, use it - targetTrackId = mainTrack.id; - targetTrack = mainTrack; - } else { - // Create new media track above main track - const mainTrackIndex = tracks.findIndex( - (t) => t.id === mainTrack.id - ); - let insertIndex: number; - - if (dropPosition === "above") { - insertIndex = currentTrackIndex; - } else if (dropPosition === "below") { - insertIndex = currentTrackIndex + 1; - } else { - // Insert above main track - insertIndex = mainTrackIndex; - } - - targetTrackId = insertTrackAt("media", insertIndex); - const updatedTracks = useTimelineStore.getState().tracks; - const newTargetTrack = updatedTracks.find( - (t) => t.id === targetTrackId - ); - if (!newTargetTrack) return; - targetTrack = newTargetTrack; - } - } else if (isAudio) { - // Audio tracks go at the bottom - const mainTrack = getMainTrack(tracks); + if (!mainTrack) { + // No main track exists, create it + targetTrackId = addTrack("media"); + const updatedTracks = useTimelineStore.getState().tracks; + const newTargetTrack = updatedTracks.find( + (t) => t.id === targetTrackId + ); + if (!newTargetTrack) return; + targetTrack = newTargetTrack; + } else if ( + mainTrack.elements.length === 0 && + dropPosition === "on" + ) { + // Main track exists and is empty, use it + targetTrackId = mainTrack.id; + targetTrack = mainTrack; + } else { + // Create new media track let insertIndex: number; if (dropPosition === "above") { @@ -796,18 +767,14 @@ export function TimelineTrackContent({ } else if (dropPosition === "below") { insertIndex = currentTrackIndex + 1; } else { - // Insert after main track (bottom area) - if (mainTrack) { - const mainTrackIndex = tracks.findIndex( - (t) => t.id === mainTrack.id - ); - insertIndex = mainTrackIndex + 1; - } else { - insertIndex = tracks.length; // Bottom of timeline - } + // Insert above main track + const mainTrackIndex = tracks.findIndex( + (t) => t.id === mainTrack.id + ); + insertIndex = mainTrackIndex; } - targetTrackId = insertTrackAt("audio", insertIndex); + targetTrackId = insertTrackAt("media", insertIndex); const updatedTracks = useTimelineStore.getState().tracks; const newTargetTrack = updatedTracks.find( (t) => t.id === targetTrackId @@ -815,6 +782,34 @@ export function TimelineTrackContent({ if (!newTargetTrack) return; targetTrack = newTargetTrack; } + } else if (isAudio) { + // Audio tracks go at the bottom + const mainTrack = getMainTrack(tracks); + let insertIndex: number; + + if (dropPosition === "above") { + insertIndex = currentTrackIndex; + } else if (dropPosition === "below") { + insertIndex = currentTrackIndex + 1; + } else { + // Insert after main track (bottom area) + if (mainTrack) { + const mainTrackIndex = tracks.findIndex( + (t) => t.id === mainTrack.id + ); + insertIndex = mainTrackIndex + 1; + } else { + insertIndex = tracks.length; // Bottom of timeline + } + } + + targetTrackId = insertTrackAt("audio", insertIndex); + const updatedTracks = useTimelineStore.getState().tracks; + const newTargetTrack = updatedTracks.find( + (t) => t.id === targetTrackId + ); + if (!newTargetTrack) return; + targetTrack = newTargetTrack; } } diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 776cc654..01ad7e7e 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -5,7 +5,7 @@ import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { ArrowRight } from "lucide-react"; import { useState } from "react"; -import { useToast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import Image from "next/image"; import { Handlebars } from "./handlebars"; @@ -17,16 +17,13 @@ interface HeroProps { export function Hero({ signupCount }: HeroProps) { const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - const { toast } = useToast(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!email.trim()) { - toast({ - title: "Email required", + toast.error("Email required", { description: "Please enter your email address.", - variant: "destructive", }); return; } @@ -45,25 +42,20 @@ export function Hero({ signupCount }: HeroProps) { const data = (await response.json()) as { error: string }; if (response.ok) { - toast({ - title: "Welcome to the waitlist! 🎉", + toast.success("Welcome to the waitlist! 🎉", { description: "You'll be notified when we launch.", }); setEmail(""); } else { - toast({ - title: "Oops!", + toast.error("Oops!", { description: (data as { error: string }).error || "Something went wrong. Please try again.", - variant: "destructive", }); } } catch (error) { - toast({ - title: "Network error", + toast.error("Network error", { description: "Please check your connection and try again.", - variant: "destructive", }); } finally { setIsSubmitting(false);