feat: dedicated footer

This commit is contained in:
Maze Winther
2025-06-28 12:38:31 +02:00
parent 8fe09c83b1
commit 5a6872537e
4 changed files with 305 additions and 223 deletions

View File

@ -1,25 +1,27 @@
import { Hero } from "@/components/landing/hero"; import { Hero } from "@/components/landing/hero";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { getWaitlistCount } from "@/lib/waitlist"; import { Footer } from "@/components/footer";
import Image from "next/image"; 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"; // Force dynamic rendering so waitlist count updates in real-time
export const dynamic = "force-dynamic";
export default async function Home() {
const signupCount = await getWaitlistCount(); export default async function Home() {
const signupCount = await getWaitlistCount();
return (
<div> return (
<Image <div>
className="fixed top-0 left-0 -z-50 size-full object-cover" <Image
src="/landing-page-bg.png" className="fixed top-0 left-0 -z-50 size-full object-cover"
height={1903.5} src="/landing-page-bg.png"
width={1269} height={1903.5}
alt="landing-page.bg" width={1269}
/> alt="landing-page.bg"
<Header /> />
<Hero signupCount={signupCount} /> <Header />
</div> <Hero signupCount={signupCount} />
); <Footer />
} </div>
);
}

View File

@ -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>
);
}

View File

@ -1,197 +1,153 @@
"use client"; "use client";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { getStars } from "@/lib/fetchGhStars";
import Image from "next/image"; import Image from "next/image";
import { RiGithubLine, RiTwitterXLine } from "react-icons/ri";
interface HeroProps {
interface HeroProps { signupCount: number;
signupCount: number; }
}
export function Hero({ signupCount }: HeroProps) {
export function Hero({ signupCount }: HeroProps) { const [email, setEmail] = useState("");
const [star, setStar] = useState<string>(); const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(""); const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast(); const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
useEffect(() => {
const fetchStars = async () => { if (!email.trim()) {
try { toast({
const data = await getStars(); title: "Email required",
setStar(data); description: "Please enter your email address.",
} catch (err) { variant: "destructive",
console.error("Failed to fetch GitHub stars", err); });
} return;
}; }
fetchStars(); setIsSubmitting(true);
}, []);
try {
const handleSubmit = async (e: React.FormEvent) => { const response = await fetch("/api/waitlist", {
e.preventDefault(); method: "POST",
headers: {
if (!email.trim()) { "Content-Type": "application/json",
toast({ },
title: "Email required", body: JSON.stringify({ email: email.trim() }),
description: "Please enter your email address.", });
variant: "destructive",
}); const data = await response.json();
return;
} if (response.ok) {
toast({
setIsSubmitting(true); title: "Welcome to the waitlist! 🎉",
description: "You'll be notified when we launch.",
try { });
const response = await fetch("/api/waitlist", { setEmail("");
method: "POST", } else {
headers: { toast({
"Content-Type": "application/json", title: "Oops!",
}, description: data.error || "Something went wrong. Please try again.",
body: JSON.stringify({ email: email.trim() }), variant: "destructive",
}); });
}
const data = await response.json(); } catch (error) {
toast({
if (response.ok) { title: "Network error",
toast({ description: "Please check your connection and try again.",
title: "Welcome to the waitlist! 🎉", variant: "destructive",
description: "You'll be notified when we launch.", });
}); } finally {
setEmail(""); setIsSubmitting(false);
} else { }
toast({ };
title: "Oops!",
description: data.error || "Something went wrong. Please try again.", return (
variant: "destructive", <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 }}
} catch (error) { animate={{ opacity: 1 }}
toast({ transition={{ duration: 1 }}
title: "Network error", className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center"
description: "Please check your connection and try again.", >
variant: "destructive", <motion.div
}); initial={{ opacity: 0, y: 20 }}
} finally { animate={{ opacity: 1, y: 0 }}
setIsSubmitting(false); transition={{ delay: 0.2, duration: 0.8 }}
} className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]"
}; >
<h1>The Open Source</h1>
return ( <div className="flex justify-center gap-4 leading-[4rem] mt-0 md:mt-2">
<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"> <div className="relative -rotate-[2.76deg] max-w-[250px] md:max-w-[454px] mt-2">
<motion.div <Image src="/frame.svg" height={79} width={459} alt="frame" />
initial={{ opacity: 0 }} <span className="absolute inset-0 flex items-center justify-center">
animate={{ opacity: 1 }} Video Editor
transition={{ duration: 1 }} </span>
className="max-w-3xl mx-auto w-full flex-1 flex flex-col justify-center" </div>
> </div>
<motion.div </motion.div>
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} <motion.p
transition={{ delay: 0.2, duration: 0.8 }} className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto"
className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]" initial={{ opacity: 0 }}
> animate={{ opacity: 1 }}
<h1>The Open Source</h1> transition={{ delay: 0.4, duration: 0.8 }}
<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"> A simple but powerful video editor that gets the job done. Works on
<Image src="/frame.svg" height={79} width={459} alt="frame" /> any platform.
<span className="absolute inset-0 flex items-center justify-center"> </motion.p>
Video Editor
</span> <motion.div
</div> className="mt-12 flex gap-8 justify-center"
</div> initial={{ opacity: 0 }}
</motion.div> animate={{ opacity: 1 }}
transition={{ delay: 0.6, duration: 0.8 }}
<motion.p >
className="mt-10 text-base sm:text-xl text-muted-foreground font-light tracking-wide max-w-xl mx-auto" <form
initial={{ opacity: 0 }} onSubmit={handleSubmit}
animate={{ opacity: 1 }} className="flex gap-3 w-full max-w-lg flex-col sm:flex-row"
transition={{ delay: 0.4, duration: 0.8 }} >
> <Input
A simple but powerful video editor that gets the job done. Works on type="email"
any platform. placeholder="Enter your email"
</motion.p> className="h-11 text-base flex-1"
value={email}
<motion.div onChange={(e) => setEmail(e.target.value)}
className="mt-12 flex gap-8 justify-center" disabled={isSubmitting}
initial={{ opacity: 0 }} required
animate={{ opacity: 1 }} />
transition={{ delay: 0.6, duration: 0.8 }} <Button
> type="submit"
<form size="lg"
onSubmit={handleSubmit} className="px-6 h-11 text-base"
className="flex gap-3 w-full max-w-lg flex-col sm:flex-row" disabled={isSubmitting}
> >
<Input <span className="relative z-10">
type="email" {isSubmitting ? "Joining..." : "Join waitlist"}
placeholder="Enter your email" </span>
className="h-11 text-base flex-1" <ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" />
value={email} </Button>
onChange={(e) => setEmail(e.target.value)} </form>
disabled={isSubmitting} </motion.div>
required
/> {signupCount > 0 && (
<Button <motion.div
type="submit" initial={{ opacity: 0, y: 10 }}
size="lg" animate={{ opacity: 1, y: 0 }}
className="px-6 h-11 text-base" transition={{ delay: 0.8, duration: 0.6 }}
disabled={isSubmitting} className="mt-8 inline-flex items-center gap-2 text-sm text-muted-foreground justify-center"
> >
<span className="relative z-10"> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
{isSubmitting ? "Joining..." : "Join waitlist"} <span>{signupCount.toLocaleString()} people already joined</span>
</span> </motion.div>
<ArrowRight className="relative z-10 ml-0.5 h-4 w-4 inline-block" /> )}
</Button> </motion.div>
</form> </div>
</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>
);
}

View File

@ -69,7 +69,7 @@ export default {
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 6px)",
}, },
keyframes: { keyframes: {
"accordion-down": { "accordion-down": {