Files
backerup-website/frontend/app/page.tsx
T
Anders Böttcher 984b282a95 feat: add admin links management and updaterup page
- Implemented AdminLinksPage for managing link entries with admin role check.
- Created API routes for fetching, updating, and deleting link entries.
- Added ProductTabs component for navigation between Backerup and Updaterup.
- Developed UpdaterupPage with detailed features, setup instructions, and statistics.
- Introduced links database functionality for runtime-editable link overrides.
2026-05-13 10:53:21 +02:00

701 lines
38 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";
import { getLinks } from "@/lib/links-db";
/* ── 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.",
},
];
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: "Open source", 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: "source available" },
{ num: 500, suffix: " MB", label: "Max file size", sub: "per upload" },
{ num: 4, suffix: "", label: "Backup strategies", sub: "fully configurable" },
{ num: 0, suffix: "", label: "Cloud dependencies", sub: "runs on your infra" },
];
/* ── Page ───────────────────────────────────────────────────── */
export default async function LandingPage() {
const links = await getLinks();
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.gitea}
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 />
Gitea
</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 links={{ dockerImage: links.dockerImage, uiPort: links.uiPort }} />
</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">
Setup with docker-compose
</h2>
<p className="text-slate-400 mb-10">
The easiest way to get Backerup running is with docker-compose for declarative, reproducible deployments.
</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">docker-compose setup</span>
</div>
<div className="p-6">
<TerminalTyper mode="docker-compose" links={{ dockerImage: links.dockerImage, uiPort: links.uiPort }} />
</div>
</div>
</div>
</AnimateIn>
</section>
{/* ═══ More tools ═════════════════════════════════════════ */}
<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-violet-500/30 bg-violet-500/10 px-4 py-1 text-xs font-semibold uppercase tracking-widest text-violet-400">
More tools
</span>
<h2 className="mt-5 text-4xl font-bold tracking-tight text-white">
Built for Docker operators
</h2>
<p className="mt-4 text-slate-400 leading-relaxed">
Backerup is part of a growing suite of self-hosted Docker management tools all
open-source, dark-mode-first, and designed to run alongside your containers.
</p>
</div>
</AnimateIn>
<div className="mt-14 grid gap-6 lg:grid-cols-2">
{/* Backerup card */}
<AnimateIn variant="fade-up" delay={0}>
<SpotlightCard className="rounded-2xl border border-cyan-500/20 bg-gradient-to-br from-cyan-500/10 to-blue-600/10 p-8 h-full card-hover flex flex-col">
<div className="flex items-start justify-between gap-4">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-cyan-500/10 text-cyan-400 shrink-0">
<CubeIcon />
</div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 px-3 py-1 text-xs font-semibold text-cyan-400">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400" />
Current
</span>
</div>
<h3 className="mt-5 text-xl font-bold text-white">Backerup</h3>
<p className="mt-2 text-sm text-slate-400 leading-relaxed">
Full Docker container backup manager. Archive volumes, bind mounts, and config
into a single portable <code className="font-mono text-cyan-300 text-xs">.backerup</code> bundle.
Restore, migrate, or share all from a self-hosted UI.
</p>
<ul className="mt-5 space-y-2 text-sm text-slate-400 flex-1">
{[
"Full volume + config archives",
"Config-only export for fast migrations",
"One-click restore or container recreation",
"Cron-based schedules with retention policies",
].map((f) => (
<li key={f} className="flex items-center gap-2.5">
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0 text-cyan-500" 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>
{f}
</li>
))}
</ul>
<div className="mt-7 flex gap-3">
<a
href={links.gitea}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-5 py-2.5 text-xs font-semibold text-slate-300 hover:bg-white/10 hover:text-white transition-all"
>
<GitHubIcon />
Gitea
</a>
</div>
</SpotlightCard>
</AnimateIn>
{/* Updaterup card */}
<AnimateIn variant="fade-up" delay={100}>
<SpotlightCard className="rounded-2xl border border-violet-500/20 bg-gradient-to-br from-violet-500/10 to-purple-600/10 p-8 h-full card-hover flex flex-col">
<div className="flex items-start justify-between gap-4">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-violet-500/10 text-violet-400 shrink-0">
<RefreshIcon />
</div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-violet-500/30 bg-violet-500/10 px-3 py-1 text-xs font-semibold text-violet-400">
<span className="w-1.5 h-1.5 rounded-full bg-violet-400" />
New
</span>
</div>
<h3 className="mt-5 text-xl font-bold text-white">Updaterup</h3>
<p className="mt-2 text-sm text-slate-400 leading-relaxed">
Docker container update manager. Continuously checks your running containers for
new image versions and lets you update them with a single click or automatically
on a cron schedule.
</p>
<ul className="mt-5 space-y-2 text-sm text-slate-400 flex-1">
{[
"Digest-based update detection",
"One-click update — preserves all original config",
"Batch check & update all containers",
"Named cron schedules with full audit history",
"Live log viewer with level filtering",
].map((f) => (
<li key={f} className="flex items-center gap-2.5">
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0 text-violet-500" 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>
{f}
</li>
))}
</ul>
<div className="mt-7 flex gap-3">
<Link
href="/updaterup"
className="flex items-center gap-2 rounded-xl px-5 py-2.5 text-xs font-bold text-white transition-all hover:brightness-110"
style={{ background: "linear-gradient(135deg, #a78bfa, #7c3aed)" }}
>
Explore Updaterup
</Link>
<a
href={links.updaterupGitea}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-5 py-2.5 text-xs font-semibold text-slate-300 hover:bg-white/10 hover:text-white transition-all"
>
<GitHubIcon />
Gitea
</a>
</div>
</SpotlightCard>
</AnimateIn>
</div>
</div>
</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.gitea}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/3 text-xs text-slate-400 hover:text-white hover:border-white/20 transition-all"
>
<GitHubIcon />
Star on Gitea
</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>
</>
);
}