Files
backerup-website/frontend/app/page.tsx
T
Anders Böttcher 0d58fafe4e feat: implement user authentication and admin management
- Add user management API endpoints for listing and creating users.
- Implement login and logout functionality with rate limiting.
- Create UI components for login form, logout button, and animated elements.
- Add session management with JWT and secure cookie handling.
- Seed initial admin user if no users exist.
- Introduce utility functions for password hashing and user authentication.
- Set up proxy middleware for route protection and security headers.
- Create a JSON file for user data storage.
- Add links configuration for external resources.
2026-05-12 14:58:11 +02:00

606 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import TerminalTyper from "./components/TerminalTyper";
import AnimateIn from "./components/AnimateIn";
import CountUp from "./components/CountUp";
import SpotlightCard from "./components/SpotlightCard";
import { LINKS } from "@/lib/links";
/* ── SVG Icons ─────────────────────────────────────────────── */
const CubeIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
);
const FileIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
);
const RefreshIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
);
const ClockIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
const ShareIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
</svg>
);
const ShieldIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-6 h-6" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
);
const DownloadIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
);
const CogIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
const ArchiveIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
</svg>
);
const PlayIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
);
const GitHubIcon = () => (
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
);
/* ── Data ───────────────────────────────────────────────────── */
const features = [
{
icon: <CubeIcon />,
gradient: "from-cyan-500/15 to-blue-600/15",
border: "border-cyan-500/20",
iconBg: "bg-cyan-500/10",
iconColor: "text-cyan-400",
title: "Full container backups",
body: "Archive volumes, bind mounts, and Docker inspect metadata into a single .backerup bundle — everything needed to fully restore or clone a container.",
},
{
icon: <FileIcon />,
gradient: "from-violet-500/15 to-purple-600/15",
border: "border-violet-500/20",
iconBg: "bg-violet-500/10",
iconColor: "text-violet-400",
title: "Config-only export",
body: "Export a lightweight config.json (raw docker inspect output) to recreate a container on any host — no volume data needed.",
},
{
icon: <RefreshIcon />,
gradient: "from-emerald-500/15 to-teal-600/15",
border: "border-emerald-500/20",
iconBg: "bg-emerald-500/10",
iconColor: "text-emerald-400",
title: "One-click restore",
body: "Restore volumes in-place, rebuild config without touching data, or spin up a brand-new container from any backup — your choice.",
},
{
icon: <ClockIcon />,
gradient: "from-amber-500/15 to-orange-600/15",
border: "border-amber-500/20",
iconBg: "bg-amber-500/10",
iconColor: "text-amber-400",
title: "Scheduled backups",
body: "Define cron-based backup jobs per container with configurable retention: keep latest N, keep every N-th, or purge older than N days.",
},
{
icon: <ShareIcon />,
gradient: "from-pink-500/15 to-rose-600/15",
border: "border-pink-500/20",
iconBg: "bg-pink-500/10",
iconColor: "text-pink-400",
title: "Community sharing",
body: "Publish your container configs so others can spin up the same setup in seconds. Browse configs shared by the community right here.",
},
{
icon: <ShieldIcon />,
gradient: "from-slate-400/10 to-slate-500/10",
border: "border-slate-600/30",
iconBg: "bg-slate-500/10",
iconColor: "text-slate-300",
title: "Self-hosted & private",
body: "Runs entirely inside your Docker environment. No cloud dependencies, no data leaves your server unless you choose to share.",
},
];
const steps = [
{
n: "01",
icon: <DownloadIcon />,
color: "bg-cyan-500/20 text-cyan-400",
title: "Install",
body: "Run Backerup as a Docker container with access to your Docker socket. Up and running in under 30 seconds.",
},
{
n: "02",
icon: <CogIcon />,
color: "bg-violet-500/20 text-violet-400",
title: "Configure",
body: "Add containers, choose a backup strategy (full archive or config-only), and set a cron schedule.",
},
{
n: "03",
icon: <ArchiveIcon />,
color: "bg-amber-500/20 text-amber-400",
title: "Backup",
body: "Backerup archives your volumes and metadata into a portable .backerup file on your schedule.",
},
{
n: "04",
icon: <PlayIcon />,
color: "bg-emerald-500/20 text-emerald-400",
title: "Restore",
body: "One click to restore in-place, rebuild config, or spin up a fresh container from any backup.",
},
];
const pills = [
{ label: "MIT Licensed", color: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20" },
{ label: "Zero cloud deps", color: "text-cyan-400 bg-cyan-500/10 border-cyan-500/20" },
{ label: "Self-hosted", color: "text-violet-400 bg-violet-500/10 border-violet-500/20" },
{ label: "Docker native", color: "text-blue-400 bg-blue-500/10 border-blue-500/20" },
];
const stats = [
{ num: 100, suffix: "%", label: "Open source", sub: "MIT licensed" },
{ num: 500, suffix: " MB", label: "Max file size", sub: "per upload" },
{ num: 6, suffix: "+", label: "Backup strategies", sub: "fully configurable" },
{ num: 0, suffix: "", label: "Cloud dependencies", sub: "runs on your infra" },
];
/* ── Page ───────────────────────────────────────────────────── */
export default function LandingPage() {
return (
<>
{/* ═══ Hero ═══════════════════════════════════════════════ */}
<section className="relative overflow-hidden px-6 pt-28 pb-20">
{/* ── Animated aurora blobs ── */}
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<div
className="absolute -top-[35%] -left-[18%] w-[750px] h-[750px] rounded-full"
style={{
background: "radial-gradient(circle, rgba(6,182,212,0.13) 0%, transparent 60%)",
filter: "blur(55px)",
animation: "aurora1 18s ease-in-out infinite alternate",
}}
/>
<div
className="absolute -top-[25%] right-[-5%] w-[650px] h-[650px] rounded-full"
style={{
background: "radial-gradient(circle, rgba(139,92,246,0.11) 0%, transparent 60%)",
filter: "blur(55px)",
animation: "aurora2 23s ease-in-out infinite alternate-reverse",
}}
/>
<div
className="absolute top-[28%] left-[22%] w-[520px] h-[520px] rounded-full"
style={{
background: "radial-gradient(circle, rgba(59,130,246,0.09) 0%, transparent 60%)",
filter: "blur(55px)",
animation: "aurora3 15s ease-in-out infinite alternate",
}}
/>
</div>
{/* ── Hero text — above the fold, animate on page load ── */}
<div className="relative mx-auto max-w-4xl text-center">
{/* Pills */}
<div
className="mb-8 flex flex-wrap items-center justify-center gap-2"
style={{ animation: "slideUp 0.7s cubic-bezier(0.22,1,0.36,1) 80ms both" }}
>
{pills.map((p) => (
<span
key={p.label}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold ${p.color}`}
>
<span className="w-1 h-1 rounded-full bg-current opacity-70" />
{p.label}
</span>
))}
</div>
{/* Headline */}
<h1
className="text-5xl font-extrabold leading-[1.1] tracking-tight text-white sm:text-7xl"
style={{ animation: "slideUp 0.75s cubic-bezier(0.22,1,0.36,1) 220ms both" }}
>
Docker backups,
<br />
<span className="gradient-text">done right.</span>
</h1>
{/* Subtitle */}
<p
className="mx-auto mt-7 max-w-2xl text-lg leading-relaxed text-slate-400"
style={{ animation: "slideUp 0.75s cubic-bezier(0.22,1,0.36,1) 380ms both" }}
>
Backerup bundles your container volumes, bind mounts, and config into a single portable
file. Restore in seconds, share configs with your team, or migrate containers between
hosts all from a self-hosted UI.
</p>
{/* CTA buttons */}
<div
className="mt-10 flex flex-wrap justify-center gap-4"
style={{ animation: "slideUp 0.75s cubic-bezier(0.22,1,0.36,1) 520ms both" }}
>
<Link
href="/share"
className="group flex items-center gap-2 rounded-xl px-7 py-3.5 text-sm font-bold text-slate-900 transition-all hover:brightness-110 hover:scale-105 active:scale-95 shadow-lg animate-border-glow"
style={{ background: "linear-gradient(135deg, #22d3ee, #06b6d4)" }}
>
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
Share a config
</Link>
<Link
href="/browse"
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-7 py-3.5 text-sm font-semibold text-slate-200 transition-all hover:bg-white/10 hover:border-white/20 hover:scale-105 active:scale-95"
>
Browse configs
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</Link>
<a
href={LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-xl border border-white/8 bg-white/3 px-7 py-3.5 text-sm font-semibold text-slate-300 transition-all hover:bg-white/8 hover:text-white hover:scale-105"
>
<GitHubIcon />
GitHub
</a>
</div>
</div>
{/* ── Terminal card ── */}
<div
className="relative mx-auto mt-20 max-w-2xl"
style={{ animation: "slideUp 0.8s cubic-bezier(0.22,1,0.36,1) 680ms both" }}
>
{/* Glow behind terminal */}
<div
className="absolute inset-0 rounded-2xl blur-xl opacity-40"
style={{ background: "radial-gradient(ellipse, rgba(6,182,212,0.2) 0%, transparent 70%)" }}
/>
<div className="relative rounded-2xl border border-white/8 glass overflow-hidden shadow-2xl terminal-scan">
<div className="flex items-center gap-2 border-b border-white/6 px-5 py-3.5 bg-white/2">
<span className="h-3 w-3 rounded-full bg-red-500/70" />
<span className="h-3 w-3 rounded-full bg-yellow-500/70" />
<span className="h-3 w-3 rounded-full bg-emerald-500/70" />
<span className="ml-3 text-xs text-slate-500 font-mono tracking-wide">backerup · quick start</span>
<div className="ml-auto flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs text-slate-600 font-mono">live</span>
</div>
</div>
<div className="p-6 min-h-[260px]">
<TerminalTyper />
</div>
</div>
</div>
</section>
{/* ═══ Stats bar ══════════════════════════════════════════ */}
<section className="px-6 py-12">
<div className="mx-auto max-w-4xl">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{stats.map((s, i) => (
<AnimateIn key={s.label} variant="scale-up" delay={i * 90}>
<div className="rounded-2xl border border-white/5 glass p-5 text-center">
<div className="text-2xl font-extrabold text-white tracking-tight">
<CountUp to={s.num} suffix={s.suffix} duration={1800} />
</div>
<div className="mt-1 text-sm font-medium text-slate-300">{s.label}</div>
<div className="mt-0.5 text-xs text-slate-600">{s.sub}</div>
</div>
</AnimateIn>
))}
</div>
</div>
</section>
{/* ═══ Features ═══════════════════════════════════════════ */}
<section className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<AnimateIn variant="fade-up">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-block rounded-full border border-cyan-500/30 bg-cyan-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-widest text-cyan-400">
Features
</span>
<h2 className="mt-5 text-4xl font-bold tracking-tight text-white">
Everything you need
</h2>
<p className="mt-4 text-slate-400 leading-relaxed">
A complete Docker backup lifecycle from scheduled snapshots to one-click restores,
all from a single self-hosted UI.
</p>
</div>
</AnimateIn>
<div className="mt-16 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{features.map((f, i) => (
<AnimateIn key={f.title} variant="fade-up" delay={i * 65}>
<SpotlightCard
className={`rounded-2xl border ${f.border} bg-gradient-to-br ${f.gradient} p-6 h-full card-hover`}
>
<div className={`inline-flex items-center justify-center w-11 h-11 rounded-xl ${f.iconBg} ${f.iconColor} mb-4`}>
{f.icon}
</div>
<h3 className="font-semibold text-white text-lg leading-snug">{f.title}</h3>
<p className="mt-2.5 text-sm leading-relaxed text-slate-400">{f.body}</p>
</SpotlightCard>
</AnimateIn>
))}
</div>
</div>
</section>
{/* ═══ How it works ════════════════════════════════════════ */}
<section className="px-6 py-24">
<div className="mx-auto max-w-5xl">
<AnimateIn variant="fade-up">
<div className="mx-auto max-w-2xl text-center mb-16">
<span className="inline-block rounded-full border border-violet-500/30 bg-violet-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-widest text-violet-400">
How it works
</span>
<h2 className="mt-5 text-4xl font-bold tracking-tight text-white">
Up and running in minutes
</h2>
</div>
</AnimateIn>
<div className="relative grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
{/* Connector line between steps */}
<div
className="hidden lg:block absolute top-8 left-[12.5%] right-[12.5%] h-px"
style={{ background: "linear-gradient(90deg, rgba(6,182,212,0.5), rgba(139,92,246,0.5))" }}
/>
{steps.map((s, i) => (
<AnimateIn key={s.n} variant="fade-up" delay={i * 110}>
<div className="relative flex flex-col items-center text-center gap-4">
<div className="relative z-10 flex h-16 w-16 items-center justify-center rounded-2xl border border-white/10 glass">
<div className={`flex items-center justify-center w-10 h-10 rounded-xl ${s.color}`}>
{s.icon}
</div>
</div>
<div>
<div className="text-xs font-mono text-slate-600 mb-1">{s.n}</div>
<h3 className="font-bold text-white text-lg">{s.title}</h3>
<p className="mt-2 text-sm text-slate-400 leading-relaxed max-w-[200px] mx-auto">{s.body}</p>
</div>
</div>
</AnimateIn>
))}
</div>
</div>
</section>
{/* ═══ .backerup format ════════════════════════════════════ */}
<section className="px-6 py-24">
<div className="mx-auto max-w-5xl">
<AnimateIn variant="fade-up">
<div className="rounded-3xl border border-white/6 glass overflow-hidden">
<div className="grid lg:grid-cols-2">
{/* Left: text */}
<div className="p-10 lg:p-14 flex flex-col justify-center">
<span className="inline-block rounded-full border border-cyan-500/30 bg-cyan-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-widest text-cyan-400 mb-6 self-start">
File format
</span>
<h2 className="text-3xl font-bold text-white leading-snug">
The{" "}
<code className="font-mono text-cyan-400 text-2xl">.backerup</code>{" "}
format
</h2>
<p className="mt-4 text-slate-400 leading-relaxed">
A single gzipped-tar file containing everything needed to restore or clone a
container. Portable, transparent, and restorable{" "}
<span className="text-slate-300">without Backerup itself</span>.
</p>
<ul className="mt-6 space-y-3 text-sm text-slate-400">
{[
{ name: "config.json", desc: "Raw docker inspect output" },
{ name: "manifest.json", desc: "Backup metadata & checksums" },
{ name: "volumes/*.tar.gz", desc: "One archive per volume" },
].map((item) => (
<li key={item.name} className="flex items-start gap-3">
<span className="mt-0.5 text-cyan-500 shrink-0">
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</span>
<span>
<code className="font-mono text-slate-200 text-xs">{item.name}</code>
<span className="ml-2 text-slate-500"> {item.desc}</span>
</span>
</li>
))}
</ul>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/share"
className="rounded-xl px-6 py-2.5 text-sm font-bold text-slate-900 transition-all hover:brightness-110"
style={{ background: "linear-gradient(135deg, #22d3ee, #06b6d4)" }}
>
Upload a backup
</Link>
<Link
href="/browse"
className="rounded-xl border border-white/10 bg-white/5 px-6 py-2.5 text-sm font-semibold text-slate-300 hover:bg-white/10 transition-all"
>
Browse shared files
</Link>
</div>
</div>
{/* Right: file tree */}
<div className="border-t border-white/5 lg:border-t-0 lg:border-l border-white/5 p-10 lg:p-14 flex items-center">
<div className="font-mono text-sm w-full">
<div className="flex items-center gap-2.5 text-slate-300 mb-4">
<span className="text-amber-400">📦</span>
<span>my-postgres.backerup</span>
<span className="text-slate-600 text-xs ml-auto">142 MB</span>
</div>
<div className="ml-4 space-y-3 text-slate-500">
<div className="flex items-center gap-2.5">
<span className="text-slate-700"></span>
<span className="text-violet-400">📄</span>
<span>config.json</span>
<span className="text-slate-700 text-xs ml-auto">8 KB</span>
</div>
<div className="flex items-center gap-2.5">
<span className="text-slate-700"></span>
<span className="text-blue-400">📄</span>
<span>manifest.json</span>
<span className="text-slate-700 text-xs ml-auto">2 KB</span>
</div>
<div className="flex items-center gap-2.5">
<span className="text-slate-700"></span>
<span className="text-amber-400">📁</span>
<span className="text-slate-300">volumes/</span>
</div>
<div className="ml-6 space-y-3">
<div className="flex items-center gap-2.5">
<span className="text-slate-700"></span>
<span className="text-cyan-400">🗜</span>
<span>postgres-data.tar.gz</span>
<span className="text-slate-700 text-xs ml-auto">139 MB</span>
</div>
<div className="flex items-center gap-2.5">
<span className="text-slate-700"></span>
<span className="text-cyan-400">🗜</span>
<span>postgres-config.tar.gz</span>
<span className="text-slate-700 text-xs ml-auto">512 B</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AnimateIn>
</div>
</section>
{/* ═══ Quick start ════════════════════════════════════════ */}
<section className="px-6 py-20">
<AnimateIn variant="fade-up">
<div className="mx-auto max-w-4xl text-center">
<span className="inline-block rounded-full border border-amber-500/30 bg-amber-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-widest text-amber-400 mb-6">
Quick start
</span>
<h2 className="text-4xl font-bold tracking-tight text-white mb-4">
One command to start
</h2>
<p className="text-slate-400 mb-10">
Backerup runs as a Docker container. Give it access to your Docker socket and you're done.
</p>
<div className="rounded-2xl border border-white/6 glass overflow-hidden text-left">
<div className="flex items-center gap-2 border-b border-white/6 px-5 py-3 bg-white/2">
<span className="h-2.5 w-2.5 rounded-full bg-red-500/70" />
<span className="h-2.5 w-2.5 rounded-full bg-yellow-500/70" />
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500/70" />
<span className="ml-3 text-xs font-mono text-slate-500">terminal</span>
</div>
<pre className="overflow-x-auto p-6 text-sm font-mono leading-7 text-slate-300">
<span className="text-slate-600 select-none"># Pull &amp; run replace /path/to/backups with your backup destination</span>{"\n"}
<span className="text-cyan-500 select-none">$ </span>
<span>{"docker run -d \\"}</span>{"\n"}
<span className="text-slate-500 select-none">{" "}</span>
<span>{"--name backerup \\"}</span>{"\n"}
<span className="text-slate-500 select-none">{" "}</span>
<span>{`-p ${LINKS.uiPort}:${LINKS.uiPort} \\`}</span>{"\n"}
<span className="text-slate-500 select-none">{" "}</span>
<span>{"-v /var/run/docker.sock:/var/run/docker.sock \\"}</span>{"\n"}
<span className="text-slate-500 select-none">{" "}</span>
<span>{"-v /path/to/backups:/data \\"}</span>{"\n"}
<span className="text-slate-500 select-none">{" "}</span>
<span className="text-slate-200">{LINKS.dockerImage}</span>{"\n"}
<span className="text-emerald-400">{`✓ Open http://localhost:${LINKS.uiPort} to get started`}</span>
</pre>
</div>
</div>
</AnimateIn>
</section>
{/* ═══ Final CTA ══════════════════════════════════════════ */}
<section className="px-6 py-24">
<div className="mx-auto max-w-4xl">
<AnimateIn variant="scale-up">
<div
className="relative overflow-hidden rounded-3xl p-px"
style={{ background: "linear-gradient(135deg, rgba(6,182,212,0.4), rgba(139,92,246,0.4), rgba(6,182,212,0.4))" }}
>
<div className="relative rounded-3xl px-10 py-16 text-center" style={{ background: "#060810" }}>
{/* Glow */}
<div
className="pointer-events-none absolute inset-0 opacity-20"
style={{ background: "radial-gradient(ellipse at center top, rgba(6,182,212,0.4), transparent 60%)" }}
/>
<div className="relative">
<h2 className="text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
Start backing up today.
</h2>
<p className="mx-auto mt-5 max-w-xl text-lg text-slate-400">
Open source, self-hosted, and free forever. Share your container configs with the
community or keep everything private on your own infrastructure.
</p>
<div className="mt-10 flex flex-wrap justify-center gap-4">
<a
href={LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-7 py-3.5 text-sm font-semibold text-white transition-all hover:bg-white/10 hover:scale-105"
>
<GitHubIcon />
Star on GitHub
</a>
<Link
href="/share"
className="rounded-xl px-7 py-3.5 text-sm font-bold text-slate-900 transition-all hover:brightness-110 hover:scale-105 active:scale-95"
style={{ background: "linear-gradient(135deg, #22d3ee, #06b6d4)" }}
>
Share a config
</Link>
<Link
href="/browse"
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-7 py-3.5 text-sm font-semibold text-slate-200 transition-all hover:bg-white/10 hover:scale-105"
>
Browse configs
</Link>
</div>
</div>
</div>
</div>
</AnimateIn>
</div>
</section>
</>
);
}