summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-06-28 12:38:31 +0200
committerMaze Winther <mazewinther@gmail.com>2025-06-28 12:38:31 +0200
commit5a6872537e526ed4b5d0c428c31e28f1d60ac471 (patch)
tree5ed2c65aef8421c26877fcf70f76a8d36ae4a22c /apps
parent8fe09c83b1abd67161c880bf630d0e6d46c70cf4 (diff)
downloadopencut-5a6872537e526ed4b5d0c428c31e28f1d60ac471.tar.gz
feat: dedicated footer
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/app/page.tsx52
-rw-r--r--apps/web/src/components/footer.tsx124
-rw-r--r--apps/web/src/components/landing/hero.tsx350
-rw-r--r--apps/web/tailwind.config.ts2
4 files changed, 305 insertions, 223 deletions
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index e9957e1b..183b6a56 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,25 +1,27 @@
-import { Hero } from "@/components/landing/hero";
-import { Header } from "@/components/header";
-import { getWaitlistCount } from "@/lib/waitlist";
-import Image from "next/image";
-
-// Force dynamic rendering so waitlist count updates in real-time
-export const dynamic = "force-dynamic";
-
-export default async function Home() {
- const signupCount = await getWaitlistCount();
-
- return (
- <div>
- <Image
- className="fixed top-0 left-0 -z-50 size-full object-cover"
- src="/landing-page-bg.png"
- height={1903.5}
- width={1269}
- alt="landing-page.bg"
- />
- <Header />
- <Hero signupCount={signupCount} />
- </div>
- );
-}
+import { Hero } from "@/components/landing/hero";
+import { Header } from "@/components/header";
+import { Footer } from "@/components/footer";
+import { getWaitlistCount } from "@/lib/waitlist";
+import Image from "next/image";
+
+// Force dynamic rendering so waitlist count updates in real-time
+export const dynamic = "force-dynamic";
+
+export default async function Home() {
+ const signupCount = await getWaitlistCount();
+
+ return (
+ <div>
+ <Image
+ className="fixed top-0 left-0 -z-50 size-full object-cover"
+ src="/landing-page-bg.png"
+ height={1903.5}
+ width={1269}
+ alt="landing-page.bg"
+ />
+ <Header />
+ <Hero signupCount={signupCount} />
+ <Footer />
+ </div>
+ );
+}
diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx
new file mode 100644
index 00000000..7cc1440f
--- /dev/null
+++ b/apps/web/src/components/footer.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import { motion } from "motion/react";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { RiGithubLine, RiTwitterXLine } from "react-icons/ri";
+import { getStars } from "@/lib/fetchGhStars";
+import Image from "next/image";
+
+export function Footer() {
+ const [star, setStar] = useState<string>();
+
+ useEffect(() => {
+ const fetchStars = async () => {
+ try {
+ const data = await getStars();
+ setStar(data);
+ } catch (err) {
+ console.error("Failed to fetch GitHub stars", err);
+ }
+ };
+
+ fetchStars();
+ }, []);
+
+ return (
+ <motion.footer
+ className="bg-background/80 backdrop-blur-sm border mt-16 m-6 rounded-sm"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.8, duration: 0.8 }}
+ >
+ <div className="max-w-5xl mx-auto px-4 py-10">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-8">
+ {/* Brand Section */}
+ <div className="md:col-span-1 max-w-sm">
+ <div className="flex items-center gap-2 mb-4">
+ <Image src="/logo.svg" alt="OpenCut" width={24} height={24} />
+ <span className="font-bold text-lg">OpenCut</span>
+ </div>
+ <p className="text-sm text-muted-foreground mb-5">
+ The open source video editor that gets the job done. Simple,
+ powerful, and works on any platform.
+ </p>
+ <div className="flex gap-3">
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiGithubLine className="h-5 w-5" />
+ </Link>
+ <Link
+ href="https://x.com/OpenCutApp"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <RiTwitterXLine className="h-5 w-5" />
+ </Link>
+ </div>
+ </div>
+
+ <div className="flex gap-12 justify-end items-start py-2">
+ <div>
+ <h3 className="font-semibold text-foreground mb-4">Resources</h3>
+ <ul className="space-y-2 text-sm">
+ <li>
+ <Link
+ href="/privacy"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Privacy policy
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/terms"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Terms of use
+ </Link>
+ </li>
+ </ul>
+ </div>
+
+ {/* Company Links */}
+ <div>
+ <h3 className="font-semibold text-foreground mb-4">Company</h3>
+ <ul className="space-y-2 text-sm">
+ <li>
+ <Link
+ href="/contributors"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ Contributors
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="https://github.com/OpenCut-app/OpenCut/blob/main/README.md"
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ About
+ </Link>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+
+ {/* Bottom Section */}
+ <div className="pt-2 flex flex-col md:flex-row justify-between items-center gap-4">
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
+ <span>© 2025 OpenCut, All Rights Reserved</span>
+ </div>
+ </div>
+ </div>
+ </motion.footer>
+ );
+}
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index 7f4a7f2b..1ac9390f 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -1,197 +1,153 @@
-"use client";
-
-import { motion } from "motion/react";
-import { Button } from "../ui/button";
-import { Input } from "../ui/input";
-import { ArrowRight } from "lucide-react";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { useToast } from "@/hooks/use-toast";
-import { getStars } from "@/lib/fetchGhStars";
-import Image from "next/image";
-import { RiGithubLine, RiTwitterXLine } from "react-icons/ri";
-
-interface HeroProps {
- signupCount: number;
-}
-
-export function Hero({ signupCount }: HeroProps) {
- const [star, setStar] = useState<string>();
- const [email, setEmail] = useState("");
- const [isSubmitting, setIsSubmitting] = useState(false);
- const { toast } = useToast();
-
- useEffect(() => {
- const fetchStars = async () => {
- try {
- const data = await getStars();
- setStar(data);
- } catch (err) {
- console.error("Failed to fetch GitHub stars", err);
- }
- };
-
- fetchStars();
- }, []);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!email.trim()) {
- toast({
- title: "Email required",
- description: "Please enter your email address.",
- variant: "destructive",
- });
- return;
- }
-
- setIsSubmitting(true);
-
- try {
- const response = await fetch("/api/waitlist", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ email: email.trim() }),
- });
-
- const data = await response.json();
-
- if (response.ok) {
- toast({
- title: "Welcome to the waitlist! 🎉",
- description: "You'll be notified when we launch.",
- });
- setEmail("");
- } else {
- toast({
- title: "Oops!",
- description: data.error || "Something went wrong. Please try again.",
- variant: "destructive",
- });
- }
- } catch (error) {
- toast({
- title: "Network error",
- description: "Please check your connection and try again.",
- variant: "destructive",
- });
- } finally {
- setIsSubmitting(false);
- }
- };
-
- return (
- <div className="min-h-[calc(100vh-6rem)] supports-[height:100dvh]:min-h-[calc(100dvh-6rem)] flex flex-col justify-between items-center text-center px-4">
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ duration: 1 }}
- className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center"
- >
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.2, duration: 0.8 }}
- className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
- >
- <h1>The Open Source</h1>
- <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
- <div className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2">
- <Image src="/frame.svg" height={79} width={459} alt="frame" />
- <span className="absolute inset-0 flex items-center justify-center">
- Video Editor
- </span>
- </div>
- </div>
- </motion.div>
-
- <motion.p
- className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.4, duration: 0.8 }}
- >
- A simple but powerful video editor that gets the job done. Works on
- any platform.
- </motion.p>
-
- <motion.div
- className="mt-12 flex gap-8 justify-center"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.6, duration: 0.8 }}
- >
- <form
- onSubmit={handleSubmit}
- className="flex gap-3 w-full max-w-lg flex-col sm:flex-row"
- >
- <Input
- type="email"
- placeholder="Enter your email"
- className="h-11 text-base flex-1"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- disabled={isSubmitting}
- required
- />
- <Button
- type="submit"
- size="lg"
- className="px-6 h-11 text-base"
- disabled={isSubmitting}
- >
- <span className="relative z-10">
- {isSubmitting ? "Joining..." : "Join waitlist"}
- </span>
- <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
- </Button>
- </form>
- </motion.div>
-
- {signupCount > 0 && (
- <motion.div
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ delay: 0.8, duration: 0.6 }}
- className="mt-8 inline-flex items-center gap-2 text-sm text-muted-foreground justify-center"
- >
- <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
- <span>{signupCount.toLocaleString()} people already joined</span>
- </motion.div>
- )}
- </motion.div>
-
- <motion.div
- className="mb-8 text-center text-sm text-muted-foreground/60 flex flex-row gap-2 items-center justify-center"
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{ delay: 0.8, duration: 0.8 }}
- >
- Currently in beta • Open source on{" "}
- <Link
- href="https://github.com/OpenCut-app/OpenCut"
- className="text-foreground flex items-center gap-1"
- target="_blank"
- rel="noopener noreferrer"
- >
- Github
- <RiGithubLine className="h-5 w-5 " />
- {star}+
- </Link>
- • Follow us on
- <Link
- href="https://x.com/OpenCutApp"
- className="text-foreground flex items-center gap-1"
- target="_blank"
- rel="noopener noreferrer"
- >
- Twitter
- <RiTwitterXLine className="h-5 w-5 " />
- </Link>
- </motion.div>
- </div>
- );
-}
+"use client";
+
+import { motion } from "motion/react";
+import { Button } from "../ui/button";
+import { Input } from "../ui/input";
+import { ArrowRight } from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+import { useToast } from "@/hooks/use-toast";
+
+import Image from "next/image";
+
+interface HeroProps {
+ signupCount: number;
+}
+
+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",
+ description: "Please enter your email address.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch("/api/waitlist", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: email.trim() }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ toast({
+ title: "Welcome to the waitlist! 🎉",
+ description: "You'll be notified when we launch.",
+ });
+ setEmail("");
+ } else {
+ toast({
+ title: "Oops!",
+ description: data.error || "Something went wrong. Please try again.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ toast({
+ title: "Network error",
+ description: "Please check your connection and try again.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="min-h-[calc(100vh-6rem)] supports-[height:100dvh]:min-h-[calc(100dvh-6rem)] flex flex-col justify-between items-center text-center px-4">
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ duration: 1 }}
+ className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center"
+ >
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2, duration: 0.8 }}
+ className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
+ >
+ <h1>The Open Source</h1>
+ <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
+ <div className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2">
+ <Image src="/frame.svg" height={79} width={459} alt="frame" />
+ <span className="absolute inset-0 flex items-center justify-center">
+ Video Editor
+ </span>
+ </div>
+ </div>
+ </motion.div>
+
+ <motion.p
+ className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.4, duration: 0.8 }}
+ >
+ A simple but powerful video editor that gets the job done. Works on
+ any platform.
+ </motion.p>
+
+ <motion.div
+ className="mt-12 flex gap-8 justify-center"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.6, duration: 0.8 }}
+ >
+ <form
+ onSubmit={handleSubmit}
+ className="flex gap-3 w-full max-w-lg flex-col sm:flex-row"
+ >
+ <Input
+ type="email"
+ placeholder="Enter your email"
+ className="h-11 text-base flex-1"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ disabled={isSubmitting}
+ required
+ />
+ <Button
+ type="submit"
+ size="lg"
+ className="px-6 h-11 text-base"
+ disabled={isSubmitting}
+ >
+ <span className="relative z-10">
+ {isSubmitting ? "Joining..." : "Join waitlist"}
+ </span>
+ <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
+ </Button>
+ </form>
+ </motion.div>
+
+ {signupCount > 0 && (
+ <motion.div
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.8, duration: 0.6 }}
+ className="mt-8 inline-flex items-center gap-2 text-sm text-muted-foreground justify-center"
+ >
+ <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
+ <span>{signupCount.toLocaleString()} people already joined</span>
+ </motion.div>
+ )}
+ </motion.div>
+ </div>
+ );
+}
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
index c35ee1ad..00309f84 100644
--- a/apps/web/tailwind.config.ts
+++ b/apps/web/tailwind.config.ts
@@ -69,7 +69,7 @@ export default {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)",
+ sm: "calc(var(--radius) - 6px)",
},
keyframes: {
"accordion-down": {