2026-03-18 13:02:58 -07:00

283 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { SiteHeader } from "../../components/site-header";
import { SiteFooter } from "../../components/site-footer";
const balances = [18420, 19280, 20540, 19980, 21450, 22890, 22120];
const maxBalance = Math.max(...balances);
const expenses = [
{ label: "Rent & mortgage", value: 2800 },
{ label: "Payroll", value: 9200 },
{ label: "Software & tools", value: 1450 },
{ label: "Vendors", value: 2280 },
];
const aiMessages = [
"Youre on track to finish the month with a $6,920 surplus if spending stays at the current pace.",
"Dining is trending 14% above your usual pattern. Consider capping at $620 to stay on target.",
"You can safely move $1,500 into savings without dropping below your $10k buffer.",
];
export default function DemoPage() {
const [aiIndex, setAiIndex] = useState(0);
const [aiVisible, setAiVisible] = useState(false);
useEffect(() => {
let timeout: NodeJS.Timeout;
let interval: NodeJS.Timeout;
const startLoop = () => {
setAiVisible(false);
timeout = setTimeout(() => {
setAiVisible(true);
}, 1000);
};
startLoop();
interval = setInterval(() => {
setAiIndex((prev) => (prev + 1) % aiMessages.length);
startLoop();
}, 5000);
return () => {
clearTimeout(timeout);
clearInterval(interval);
};
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-foreground flex flex-col">
<SiteHeader />
<main className="flex-1 pt-20 pb-12">
<div className="mx-auto max-w-6xl px-6 lg:px-8 space-y-8">
<header className="space-y-3">
<p className="text-xs font-semibold tracking-[0.25em] text-emerald-400 uppercase">
Demo · LedgerOne
</p>
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-slate-50">
AI-powered cash control dashboard
</h1>
<p className="text-sm sm:text-base text-slate-400 max-w-2xl">
This is a looping, demo-only view designed for screen recordings. All data is fake but
behaves like a live, AI-assisted finance cockpit.
</p>
</header>
<div className="grid gap-6 lg:grid-cols-3">
{/* Left: animated cashflow graph */}
<section className="lg:col-span-2 rounded-3xl border border-slate-800 bg-slate-900/60 px-5 pt-4 pb-6 shadow-[0_30px_120px_rgba(15,23,42,0.9)] backdrop-blur-xl">
<div className="flex items-center justify-between gap-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-slate-400">Projected balance · next 30 days</p>
<p className="text-sm font-semibold text-slate-50">
$22,890 <span className="text-emerald-400 text-xs font-normal"> +$3,410</span>
</p>
</div>
<div className="flex items-center gap-2 text-[11px] text-slate-400">
<div className="flex items-center gap-1">
<span className="h-1.5 w-4 rounded-full bg-emerald-400" />
<span>Balance</span>
</div>
<div className="flex items-center gap-1">
<span className="h-1.5 w-4 rounded-full bg-sky-400" />
<span>Income</span>
</div>
<div className="flex items-center gap-1">
<span className="h-1.5 w-4 rounded-full bg-rose-400" />
<span>Outflows</span>
</div>
</div>
</div>
{/* Animated bar/line combo chart */}
<div className="relative h-60 rounded-2xl bg-gradient-to-b from-slate-900/60 to-slate-950/90 overflow-hidden">
<svg viewBox="0 0 100 40" className="absolute inset-0 opacity-40">
<defs>
<linearGradient id="balanceLine" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22c55e" />
<stop offset="50%" stopColor="#38bdf8" />
<stop offset="100%" stopColor="#a855f7" />
</linearGradient>
</defs>
<motion.path
d="
M 0 30
C 15 28, 25 26, 35 24
S 55 20, 65 18
S 85 16, 100 14
"
fill="none"
stroke="url(#balanceLine)"
strokeWidth="1.5"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
/>
</svg>
<div className="absolute inset-x-6 bottom-4 flex items-end justify-between gap-4">
{balances.map((v, i) => {
const height = (v / maxBalance) * 100;
return (
<motion.div
key={i}
className="flex-1 flex flex-col items-center gap-1"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + i * 0.08 }}
>
<motion.div
className="w-full rounded-t-full bg-gradient-to-t from-slate-800 via-sky-500 to-emerald-400 shadow-[0_0_25px_rgba(56,189,248,0.5)]"
style={{ height: `${height}%` }}
animate={{ scaleY: [0.7, 1, 0.85, 1] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut", delay: i * 0.05 }}
/>
<span className="text-[10px] text-slate-500">D{i + 1}</span>
</motion.div>
);
})}
</div>
<div className="absolute left-1/2 bottom-3 -translate-x-1/2 rounded-full bg-amber-500/15 px-3 py-1.5 text-[11px] text-amber-100 ring-1 ring-amber-400/50 flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-amber-300 animate-pulse" />
<span>Balance dip in 6 days · adjust discretionary by ~12%</span>
</div>
</div>
</section>
{/* Right: budget ring + expense breakdown */}
<section className="space-y-4">
<div className="rounded-3xl border border-slate-800 bg-slate-900/70 p-4 shadow-[0_20px_80px_rgba(15,23,42,0.9)] backdrop-blur-xl">
<div className="mb-3 flex items-center justify-between">
<div>
<p className="text-xs font-medium text-slate-400">Monthly budget</p>
<p className="text-sm font-semibold text-slate-50">
$18,400{" "}
<span className="ml-1 text-[11px] font-normal text-emerald-400">Safe to spend: $4,120</span>
</p>
</div>
<span className="rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-medium text-emerald-300">
Healthy
</span>
</div>
<div className="flex items-center gap-4">
{/* Animated progress ring */}
<div className="relative h-20 w-20">
<svg viewBox="0 0 36 36" className="h-20 w-20 -rotate-90">
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="rgba(30,64,175,0.35)"
strokeWidth="3"
/>
<motion.path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="url(#demoRingGradient)"
strokeWidth="3"
strokeLinecap="round"
animate={{ strokeDasharray: ["55, 100", "78, 100", "65, 100"] }}
transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut" }}
/>
<defs>
<linearGradient id="demoRingGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22c55e" />
<stop offset="50%" stopColor="#38bdf8" />
<stop offset="100%" stopColor="#a855f7" />
</linearGradient>
</defs>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center gap-0.5">
<motion.span
className="text-sm font-semibold text-slate-50"
animate={{ opacity: [0.7, 1, 0.7] }}
transition={{ duration: 2.4, repeat: Infinity }}
>
74%
</motion.span>
<span className="text-[10px] text-slate-400">used</span>
</div>
</div>
<div className="flex-1 space-y-2">
{expenses.map((e) => {
const pct = e.value / 18400;
return (
<div key={e.label} className="space-y-0.5">
<div className="flex items-center justify-between text-[11px] text-slate-300">
<span>{e.label}</span>
<span className="text-slate-400">
${e.value.toLocaleString()}{" "}
<span className="text-slate-500">
· {(pct * 100).toFixed(0)}%
</span>
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-800 overflow-hidden">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-emerald-400 via-sky-400 to-violet-500"
initial={{ width: 0 }}
animate={{ width: `${Math.min(pct * 100, 100)}%` }}
transition={{ duration: 2, repeat: Infinity, repeatType: "reverse", ease: "easeInOut" }}
/>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* AI chat card */}
<div className="rounded-3xl border border-slate-800 bg-slate-900/70 p-4 shadow-[0_20px_80px_rgba(15,23,42,0.9)] backdrop-blur-xl space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/15 text-[11px] text-emerald-300 shadow-[0_0_22px_rgba(34,197,94,0.7)]">
AI
</div>
<div className="text-xs text-slate-300">
<p className="font-medium">LedgerOne Copilot</p>
<p className="text-[11px] text-slate-500">Monitors cash flow in real-time</p>
</div>
</div>
<span className="rounded-full bg-slate-800 px-2 py-0.5 text-[10px] text-slate-300">
Demo mode
</span>
</div>
<div className="space-y-2 text-[11px]">
<div className="w-fit max-w-[90%] rounded-2xl bg-slate-800/90 px-3 py-2 text-slate-100">
How much can we safely move into savings this month?
</div>
<motion.div
key={aiIndex}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: aiVisible ? 1 : 0, y: aiVisible ? 0 : 4 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="ml-auto w-fit max-w-[90%] rounded-2xl bg-emerald-500/10 px-3 py-2 text-emerald-50 ring-1 ring-emerald-400/30"
>
{aiMessages[aiIndex]}
</motion.div>
</div>
</div>
</section>
</div>
</div>
</main>
<SiteFooter />
</div>
);
}