Midday / apps /website /src /components /homepage /customer-statement-animation.tsx
Jules
Final deployment with all fixes and verified content
c09f67c
"use client";
import { motion } from "motion/react";
import Image from "next/image";
import { useEffect, useState } from "react";
import { usePlayOnceOnVisible } from "@/hooks/use-play-once-on-visible";
import { MaterialIcon } from "./icon-mapping";
interface Invoice {
id: string;
invoiceNo: string;
date: string;
dueDate: string;
amount: string;
status: "unpaid" | "overdue" | "paid";
}
const initialInvoices: Invoice[] = [
{
id: "1",
invoiceNo: "INV-0042",
date: "Mar 15",
dueDate: "Apr 15",
amount: "12 450,00 €",
status: "unpaid",
},
{
id: "2",
invoiceNo: "INV-0038",
date: "Feb 10",
dueDate: "Mar 10",
amount: "18 750,00 €",
status: "overdue",
},
{
id: "3",
invoiceNo: "INV-0035",
date: "Jan 20",
dueDate: "Feb 20",
amount: "22 300,00 €",
status: "paid",
},
{
id: "4",
invoiceNo: "INV-0032",
date: "Dec 15",
dueDate: "Jan 15",
amount: "15 600,00 €",
status: "paid",
},
{
id: "5",
invoiceNo: "INV-0029",
date: "Nov 20",
dueDate: "Dec 20",
amount: "19 800,00 €",
status: "paid",
},
{
id: "6",
invoiceNo: "INV-0026",
date: "Oct 25",
dueDate: "Nov 25",
amount: "14 200,00 €",
status: "paid",
},
];
export function CustomerStatementAnimation({
onComplete,
}: {
onComplete?: () => void;
}) {
const [showHeader, setShowHeader] = useState(false);
const [showLogo, setShowLogo] = useState(false);
const [showGeneral, setShowGeneral] = useState(false);
const [_showDetails, _setShowDetails] = useState(false);
const [showStatement, setShowStatement] = useState(false);
const [showCards, setShowCards] = useState(false);
const [showTable, setShowTable] = useState(false);
const [invoices, _setInvoices] = useState<Invoice[]>(initialInvoices);
const [containerRef, shouldPlay] = usePlayOnceOnVisible(
() => {
// Callback triggered when element becomes visible
},
{ threshold: 0.5 },
);
useEffect(() => {
if (!shouldPlay) return;
const headerTimer = setTimeout(() => {
setShowHeader(true);
setShowLogo(true);
}, 0);
const generalTimer = setTimeout(() => setShowGeneral(true), 300);
const statementTimer = setTimeout(() => setShowStatement(true), 600);
const cardsTimer = setTimeout(() => setShowCards(true), 900);
const tableTimer = setTimeout(() => setShowTable(true), 1200);
const doneTimer = onComplete
? setTimeout(() => {
onComplete();
}, 10000)
: undefined;
return () => {
clearTimeout(headerTimer);
clearTimeout(generalTimer);
clearTimeout(statementTimer);
clearTimeout(cardsTimer);
clearTimeout(tableTimer);
if (doneTimer) clearTimeout(doneTimer);
};
}, [shouldPlay, onComplete]);
const getStatusColor = (status: string) => {
switch (status) {
case "paid":
return "text-green-500";
case "overdue":
return "text-yellow-500";
default:
return "text-foreground";
}
};
return (
<div
ref={containerRef}
className="w-full h-full flex flex-col relative bg-background min-h-0"
>
{/* Header */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showHeader ? 1 : 0 }}
transition={{ duration: 0.25 }}
className="pt-2 md:pt-3 pb-2 md:pb-3 border-b border-border flex items-center justify-between px-2 md:px-3"
>
<div className="flex items-center gap-2 md:gap-3">
{/* Logo - Supabase */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showLogo ? 1 : 0 }}
transition={{ duration: 0.25 }}
className="w-7 h-7 md:w-9 md:h-9 rounded-full flex items-center justify-center flex-shrink-0 bg-foreground/5 border border-border overflow-hidden"
>
<Image
src="/images/supabase.png"
alt="Supabase"
width={20}
height={20}
className="w-full h-full object-contain"
/>
</motion.div>
<h2 className="text-[16px] md:text-[18px] font-serif text-foreground">
Supabase
</h2>
</div>
<MaterialIcon
name="more_vert"
className="text-sm text-muted-foreground"
size={16}
/>
</motion.div>
{/* General Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showGeneral ? 1 : 0 }}
transition={{ duration: 0.25 }}
className="border-b border-border md:mt-2"
>
<div className="pt-2 md:pt-3 pb-3 md:py-5 flex items-center justify-between px-2 md:px-3">
<h3 className="text-[11px] md:text-[12px] text-foreground">
General
</h3>
<MaterialIcon
name="expand_less"
className="text-sm text-muted-foreground"
size={16}
/>
</div>
{showGeneral && (
<div className="pt-0 pb-3 md:pb-4 space-y-2.5 md:space-y-3 px-2 md:px-3">
<div className="text-[10px] md:text-[11px] text-muted-foreground">
<span className="text-foreground">Contact person:</span> Michael
Thompson
</div>
<div className="text-[10px] md:text-[11px] text-muted-foreground">
<span className="text-foreground">Email:</span>{" "}
finance@supabase.com
</div>
<div className="text-[10px] md:text-[11px] text-muted-foreground">
<span className="text-foreground">Website:</span> supabase.com
</div>
</div>
)}
</motion.div>
{/* Details Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showGeneral ? 1 : 0 }}
transition={{ duration: 0.25 }}
className="border-b border-border"
>
<div className="py-2.5 md:py-3.5 flex items-center justify-between px-2 md:px-3">
<h3 className="text-[11px] md:text-[12px] text-foreground">
Details
</h3>
<MaterialIcon
name="expand_more"
className="text-sm text-muted-foreground"
size={16}
/>
</div>
</motion.div>
{/* Statement Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: showStatement ? 1 : 0 }}
transition={{ duration: 0.25 }}
className="flex-1 flex flex-col min-h-0 overflow-hidden"
>
<div className="py-2.5 md:py-3.5 flex items-center justify-between border-b border-border flex-shrink-0 px-2 md:px-3">
<h3 className="text-[11px] md:text-[12px] text-foreground">
Statement
</h3>
<MaterialIcon
name="more_vert"
className="text-sm text-muted-foreground"
size={16}
/>
</div>
{/* Summary Cards */}
{showCards && (
<div className="grid grid-cols-2 gap-3 md:gap-4 pt-4 md:pt-6 pb-4 md:pb-6 flex-shrink-0">
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-background border border-border p-3 md:p-4"
>
<div className="font-serif text-base md:text-lg text-foreground mb-1 md:mb-1.5">
53 500,00 €
</div>
<div className="font-sans text-[10px] md:text-xs text-foreground mb-1 md:mb-1.5">
Total Amount
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="bg-background border border-border p-3 md:p-4"
>
<div className="font-serif text-base md:text-lg text-foreground mb-1 md:mb-1.5">
22 300,00 €
</div>
<div className="font-sans text-[10px] md:text-xs text-foreground mb-1 md:mb-1.5">
Paid
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="bg-background border border-border p-3 md:p-4"
>
<div className="font-serif text-base md:text-lg text-foreground mb-1 md:mb-1.5">
31 200,00 €
</div>
<div className="font-sans text-[10px] md:text-xs text-foreground mb-1 md:mb-1.5">
Outstanding
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
className="bg-background border border-border p-3 md:p-4"
>
<div className="font-serif text-base md:text-lg text-foreground mb-1 md:mb-1.5">
3
</div>
<div className="font-sans text-[10px] md:text-xs text-foreground mb-1 md:mb-1.5">
Invoices
</div>
</motion.div>
</div>
)}
{/* Table */}
{showTable && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.25 }}
className="flex-1 min-h-0 overflow-hidden border border-border bg-background relative"
>
<table
className="w-full border-collapse"
style={{ borderSpacing: 0 }}
>
<thead className="sticky top-0 z-10 bg-secondary border-b border-border">
<tr className="h-[28px] md:h-[32px]">
<th className="w-[90px] md:w-[100px] px-1.5 md:px-2 text-left text-[10px] md:text-[11px] font-medium text-muted-foreground border-r border-border">
Invoice
</th>
<th className="w-[70px] md:w-[80px] px-1.5 md:px-2 text-left text-[10px] md:text-[11px] font-medium text-muted-foreground border-r border-border">
Date
</th>
<th className="w-[80px] md:w-[90px] px-1.5 md:px-2 text-left text-[10px] md:text-[11px] font-medium text-muted-foreground border-r border-border">
Due Date
</th>
<th className="w-[100px] md:w-[110px] px-1.5 md:px-2 text-left text-[10px] md:text-[11px] font-medium text-muted-foreground border-r border-border">
Amount
</th>
<th className="w-[80px] md:w-[90px] px-1.5 md:px-2 text-left text-[10px] md:text-[11px] font-medium text-muted-foreground">
Status
</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice, index) => (
<motion.tr
key={invoice.id}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: showTable ? 1 : 0,
y: showTable ? 0 : 10,
}}
transition={{
duration: 0.3,
delay: 0.5 + index * 0.08,
ease: "easeOut",
}}
className="h-[28px] md:h-[32px] border-b border-border bg-background hover:bg-secondary transition-colors"
>
<td className="w-[90px] md:w-[100px] px-1.5 md:px-2 text-[10px] md:text-[11px] text-foreground border-r border-border">
{invoice.invoiceNo}
</td>
<td className="w-[70px] md:w-[80px] px-1.5 md:px-2 text-[10px] md:text-[11px] text-muted-foreground border-r border-border">
{invoice.date}
</td>
<td className="w-[80px] md:w-[90px] px-1.5 md:px-2 text-[10px] md:text-[11px] text-muted-foreground border-r border-border">
{invoice.dueDate}
</td>
<td className="w-[100px] md:w-[110px] px-1.5 md:px-2 text-[10px] md:text-[11px] text-foreground border-r border-border">
{invoice.amount}
</td>
<td className="w-[80px] md:w-[90px] px-1.5 md:px-2">
<span
className={`text-[10px] md:text-[11px] ${getStatusColor(invoice.status)}`}
>
{invoice.status === "unpaid"
? "Unpaid"
: invoice.status === "overdue"
? "Overdue"
: "Paid"}
</span>
</td>
</motion.tr>
))}
</tbody>
</table>
</motion.div>
)}
</motion.div>
</div>
);
}