mirror of
https://gitlab.w-hs.de/an14051/backerup-website.git
synced 2026-07-27 17:35:29 +00:00
0d58fafe4e
- 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.
606 lines
32 KiB
TypeScript
606 lines
32 KiB
TypeScript
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 & 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>
|
||
</>
|
||
);
|
||
}
|
||
|