summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorni-zav <106657905+Ni-zav@users.noreply.github.com>2025-07-14 23:43:34 +0700
committerGitHub <noreply@github.com>2025-07-14 23:43:34 +0700
commitffb254c339d362ea732ae447acd0710dbe635ab0 (patch)
treef52d871c00f4b974f3c0143e579d79bb19c3d527 /apps
parentacd42a29f6370417ef188d668c4567e9892a8ed1 (diff)
parent416511084a3bf26fbbab2fcb861ad916dbe851f6 (diff)
downloadopencut-ffb254c339d362ea732ae447acd0710dbe635ab0.tar.gz
Merge branch 'OpenCut-app:main' into refactor/move-add-button
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/app/privacy/page.tsx253
-rw-r--r--apps/web/src/app/terms/page.tsx317
-rw-r--r--apps/web/src/components/editor/timeline-track.tsx137
-rw-r--r--apps/web/src/components/landing/hero.tsx18
4 files changed, 641 insertions, 84 deletions
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 (
+ <div className="min-h-screen bg-background">
+ <Header />
+ <main className="relative">
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
+ <div className="absolute -top-40 -right-40 w-96 h-96 bg-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
+ <div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
+ </div>
+ <div className="relative container mx-auto px-4 py-16">
+ <div className="max-w-4xl mx-auto">
+ <div className="text-center mb-10">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut"
+ target="_blank"
+ >
+ <Badge variant="secondary" className="gap-2 mb-6">
+ <GithubIcon className="h-3 w-3" />
+ Open Source
+ </Badge>
+ </Link>
+ <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
+ Privacy Policy
+ </h1>
+ <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
+ Learn how we handle your data and privacy. Contact us if you
+ have any questions.
+ </p>
+ </div>
+ <Card className="bg-background/80 backdrop-blur-sm border-2 border-muted/30">
+ <CardContent className="p-8 text-base leading-relaxed space-y-8">
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Your Videos Stay Private
+ </h2>
+ <p className="mb-4">
+ <strong>
+ OpenCut processes all videos locally on your device.
+ </strong>{" "}
+ We never upload, store, or have access to your video files.
+ Your content remains completely private and under your
+ control at all times.
+ </p>
+ <p>
+ All video editing, rendering, and processing happens in your
+ browser using WebAssembly and local storage. No video data
+ is transmitted to our servers.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Account Information
+ </h2>
+ <p className="mb-4">
+ When you create an account, we only collect:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>Email address (for account access)</li>
+ <li>
+ Profile information from Google OAuth (if you choose to
+ sign in with Google)
+ </li>
+ </ul>
+ <p className="mb-4">
+ <strong>
+ We do NOT store your projects on our servers.
+ </strong>{" "}
+ All project data, including names, thumbnails, and creation
+ dates, is stored locally in your browser using IndexedDB.
+ </p>
+ <p>
+ We use{" "}
+ <a
+ href="https://www.better-auth.com"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ Better Auth
+ </a>{" "}
+ for secure authentication and follow industry-standard
+ security practices.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">Analytics</h2>
+ <p className="mb-4">
+ We use{" "}
+ <a
+ href="https://www.databuddy.cc"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ Databuddy
+ </a>{" "}
+ for completely anonymized and non-invasive analytics to
+ understand how people use OpenCut.
+ </p>
+ <p>
+ This helps us improve the editor, but we never collect
+ personal information, track individual users, or store any
+ data that could identify you.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Local Storage & Cookies
+ </h2>
+ <p className="mb-4">
+ We use browser local storage and IndexedDB to:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>Save your projects locally on your device</li>
+ <li>Remember your editor preferences and settings</li>
+ <li>Keep you logged in across browser sessions</li>
+ </ul>
+ <p>
+ All data stays on your device and can be cleared at any time
+ through your browser settings.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Third-Party Services
+ </h2>
+ <p className="mb-4">
+ OpenCut integrates with these services:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ <strong>Google OAuth:</strong> For optional Google sign-in
+ (governed by Google's privacy policy)
+ </li>
+ <li>
+ <strong>Vercel:</strong> For hosting and content delivery
+ </li>
+ <li>
+ <strong>Databuddy:</strong> For anonymized analytics
+ </li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">Your Rights</h2>
+ <p className="mb-4">
+ You have complete control over your data:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ Delete your account and all associated data at any time
+ </li>
+ <li>Export your project data</li>
+ <li>Clear local storage to remove all saved projects</li>
+ <li>Contact us with any privacy concerns</li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Open Source Transparency
+ </h2>
+ <p className="mb-4">
+ 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.
+ </p>
+ <p>
+ View our source code on{" "}
+ <a
+ href="https://github.com/OpenCut-app/OpenCut"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub
+ </a>
+ .
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">Contact Us</h2>
+ <p className="mb-4">
+ Questions about this privacy policy or how we handle your
+ data?
+ </p>
+ <p>
+ Open an issue on our{" "}
+ <a
+ href="https://github.com/OpenCut-app/OpenCut/issues"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub repository
+ </a>
+ , email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ , or reach out on{" "}
+ <a
+ href="https://x.com/opencutapp"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ X (Twitter)
+ </a>
+ .
+ </p>
+ </section>
+
+ <p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
+ Last updated: July 14, 2025
+ </p>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </main>
+ <Footer />
+ </div>
+ );
+}
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 (
+ <div className="min-h-screen bg-background">
+ <Header />
+ <main className="relative">
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
+ <div className="absolute -top-40 -right-40 w-96 h-96 bg-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
+ <div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
+ </div>
+ <div className="relative container mx-auto px-4 py-16">
+ <div className="max-w-4xl mx-auto">
+ <div className="text-center mb-10">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut"
+ target="_blank"
+ >
+ <Badge variant="secondary" className="gap-2 mb-6">
+ <GithubIcon className="h-3 w-3" />
+ Open Source
+ </Badge>
+ </Link>
+ <h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
+ Terms of Service
+ </h1>
+ <p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
+ Fair and transparent terms for our free, open-source video
+ editor.
+ </p>
+ </div>
+ <Card className="bg-background/80 backdrop-blur-sm border-2 border-muted/30">
+ <CardContent className="p-8 text-base leading-relaxed space-y-8">
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Welcome to OpenCut
+ </h2>
+ <p className="mb-4">
+ 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.
+ </p>
+ <p>
+ <strong>Key principle:</strong> Your content stays on your
+ device. We never claim ownership of your videos or projects.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Your Content, Your Rights
+ </h2>
+ <p className="mb-4">
+ <strong>You own everything you create.</strong> 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.
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ Your videos remain completely private and under your
+ control
+ </li>
+ <li>
+ You retain all intellectual property rights to your
+ content
+ </li>
+ <li>
+ You can export and use your content however you choose
+ </li>
+ <li>
+ No watermarks, no licensing restrictions from OpenCut
+ </li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ How You Can Use OpenCut
+ </h2>
+ <p className="mb-4">
+ OpenCut is free for personal and commercial use. You can:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ Create videos for personal, educational, or commercial
+ purposes
+ </li>
+ <li>Use OpenCut for client work and paid projects</li>
+ <li>Share and distribute videos created with OpenCut</li>
+ <li>
+ Modify and distribute the OpenCut software (under MIT
+ license)
+ </li>
+ </ul>
+ <p>
+ <strong>What we ask:</strong> Don't use OpenCut for illegal
+ activities, harassment, or creating harmful content. Be
+ respectful of others and follow applicable laws.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Account and Service
+ </h2>
+ <p className="mb-4">
+ To use certain features, you may create an account:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>Provide accurate information when signing up</li>
+ <li>
+ Keep your account secure and don't share credentials
+ </li>
+ <li>You're responsible for activity under your account</li>
+ <li>You can delete your account at any time</li>
+ </ul>
+ <p>
+ OpenCut is provided "as is" without warranties. While we
+ strive for reliability, we can't guarantee uninterrupted
+ service.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Open Source Benefits
+ </h2>
+ <p className="mb-4">
+ Because OpenCut is open source, you have additional rights:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ Review our code to see exactly how we handle your data
+ </li>
+ <li>Self-host OpenCut on your own servers</li>
+ <li>Modify the software to suit your needs</li>
+ <li>Contribute improvements back to the community</li>
+ </ul>
+ <p>
+ View our source code and license on{" "}
+ <a
+ href="https://github.com/OpenCut-app/OpenCut"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub
+ </a>
+ .
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Third-Party Content
+ </h2>
+ <p className="mb-4">
+ When using OpenCut, make sure you have the right to use any
+ content you import:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ Only upload content you own or have permission to use
+ </li>
+ <li>
+ Respect copyright, trademarks, and other intellectual
+ property
+ </li>
+ <li>
+ Don't use copyrighted music, images, or videos without
+ permission
+ </li>
+ <li>
+ You're responsible for any claims related to your content
+ </li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Limitations and Liability
+ </h2>
+ <p className="mb-4">
+ OpenCut is provided free of charge. To the extent permitted
+ by law:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>We're not liable for any loss of data or content</li>
+ <li>
+ Projects are stored in your browser and may be lost if you
+ clear browser data
+ </li>
+ <li>We're not responsible for how you use the service</li>
+ <li>
+ Our liability is limited to the maximum extent allowed by
+ law
+ </li>
+ </ul>
+ <p>
+ Since your content stays on your device, we have no way to
+ recover lost projects. Consider exporting important videos
+ when finished editing.
+ </p>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Service Changes
+ </h2>
+ <p className="mb-4">We may update OpenCut and these terms:</p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>
+ We'll notify you of significant changes to these terms
+ </li>
+ <li>Continued use means you accept any updates</li>
+ <li>
+ You can always self-host an older version if you prefer
+ </li>
+ <li>
+ Major changes will be discussed with the community on
+ GitHub
+ </li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">Termination</h2>
+ <p className="mb-4">
+ You can stop using OpenCut at any time:
+ </p>
+ <ul className="list-disc pl-6 mb-4 space-y-2">
+ <li>Delete your account through your profile settings</li>
+ <li>Clear your browser data to remove local projects</li>
+ <li>
+ Your content remains yours even if you stop using OpenCut
+ </li>
+ <li>
+ We may suspend accounts for violations of these terms
+ </li>
+ </ul>
+ </section>
+
+ <section>
+ <h2 className="text-2xl font-semibold mb-4">
+ Contact and Disputes
+ </h2>
+ <p className="mb-4">
+ Questions about these terms or need to report an issue?
+ </p>
+ <p className="mb-4">
+ Contact us through our{" "}
+ <a
+ href="https://github.com/OpenCut-app/OpenCut/issues"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ GitHub repository
+ </a>
+ , email us at{" "}
+ <a
+ href="mailto:oss@opencut.app"
+ className="text-primary hover:underline"
+ >
+ oss@opencut.app
+ </a>
+ , or reach out on{" "}
+ <a
+ href="https://x.com/opencutapp"
+ target="_blank"
+ rel="noopener"
+ className="text-primary hover:underline"
+ >
+ X (Twitter)
+ </a>
+ .
+ </p>
+ <p>
+ These terms are governed by applicable law in your
+ jurisdiction. We prefer to resolve disputes through friendly
+ discussion in our open-source community.
+ </p>
+ </section>
+
+ <p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
+ Last updated: July 14, 2025
+ </p>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </main>
+ <Footer />
+ </div>
+ );
+}
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 (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 (isVideoOrImage) {
+ // For video/image, check if we need a main track or additional media track
+ const mainTrack = getMainTrack(tracks);
- 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);