diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-06-28 12:38:31 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-06-28 12:38:31 +0200 |
| commit | 5a6872537e526ed4b5d0c428c31e28f1d60ac471 (patch) | |
| tree | 5ed2c65aef8421c26877fcf70f76a8d36ae4a22c | |
| parent | 8fe09c83b1abd67161c880bf630d0e6d46c70cf4 (diff) | |
| download | opencut-5a6872537e526ed4b5d0c428c31e28f1d60ac471.tar.gz | |
feat: dedicated footer
| -rw-r--r-- | apps/web/src/app/page.tsx | 52 | ||||
| -rw-r--r-- | apps/web/src/components/footer.tsx | 124 | ||||
| -rw-r--r-- | apps/web/src/components/landing/hero.tsx | 350 | ||||
| -rw-r--r-- | apps/web/tailwind.config.ts | 2 |
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": { |
