Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 33 additions & 51 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,21 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${sora.variable} ${mono.variable} antialiased`}>
<ThemeProvider
attribute="class"
enableSystem={false}
disableTransitionOnChange
>
<WalletProvider>
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-xl font-bold text-blue-600">
FlowFi
</span>
</div>
<nav className="flex gap-6">
<Link
href="/"
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
>
Outgoing
</Link>
<Link
href="/incoming"
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
>
Incoming
</Link>
</nav>
return (
<html lang="en" suppressHydrationWarning>
<body className={`${sora.variable} ${mono.variable} antialiased`}>
<ThemeProvider
attribute="class"
enableSystem={false}
disableTransitionOnChange
>
<WalletProvider>
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-xl font-bold text-blue-600">
FlowFi
</span>
</div>

<nav className="flex gap-6">
Expand All @@ -81,27 +66,24 @@ export default function RootLayout({
</nav>
</div>
</header>
{children}
</WalletProvider>
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: "#111",
color: "#fff",
border: "1px solid #333",
borderRadius: "12px",
},
}}
/>

</header>
{children}
</WalletProvider>
</ThemeProvider>
{children}

</body>
</html>
);
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: "#111",
color: "#fff",
border: "1px solid #333",
borderRadius: "12px",
},
}}
/>
</WalletProvider>
</ThemeProvider>
</body>
</html>
);
}
142 changes: 142 additions & 0 deletions frontend/app/streams/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@

import { notFound } from "next/navigation";
import LiveCounter from "@/components/LiveCounter";
import ProgressBar from "@/components/ProgressBar";


interface Transaction {
id: string;
date: string;
amount: number;
type: "withdrawal";
}

interface Stream {
id: string;
name: string;
recipient: string;
streamedAmount: number;
totalAmount: number;
transactions: Transaction[];
}

const MOCK_STREAMS: Record<string, Stream> = {
"1": {
id: "1",
name: "Developer Grant — Q1",
recipient: "0xAbC…1234",
streamedAmount: 3200,
totalAmount: 5000,
transactions: [
{ id: "tx1", date: "2026-02-20", amount: 1000, type: "withdrawal" },
{ id: "tx2", date: "2026-02-18", amount: 1200, type: "withdrawal" },
{ id: "tx3", date: "2026-02-15", amount: 1000, type: "withdrawal" },
],
},
"2": {
id: "2",
name: "Marketing Budget Stream",
recipient: "0xDeF…5678",
streamedAmount: 800,
totalAmount: 2000,
transactions: [
{ id: "tx1", date: "2026-02-21", amount: 500, type: "withdrawal" },
{ id: "tx2", date: "2026-02-19", amount: 300, type: "withdrawal" },
],
},
};

interface PageProps {
params: Promise<{ id: string }>;
}

export default async function StreamDetailsPage({ params }: PageProps) {
const { id } = await params;
const stream = MOCK_STREAMS[id];

if (!stream) notFound();

const percentage = Math.round((stream.streamedAmount / stream.totalAmount) * 100);

return (
<main style={{ minHeight: "100vh", padding: "clamp(1rem, 3vw, 2rem)" }}>
<div style={{ maxWidth: 720, margin: "0 auto", display: "grid", gap: "1rem" }}>

{/* Header */}
<div>
<p className="kicker">Stream #{stream.id}</p>
<h1 style={{ margin: "0.4rem 0 0", fontSize: "clamp(1.6rem, 3vw, 2.2rem)", lineHeight: 1.1 }}>
Stream Details
</h1>
</div>

{/* Identity card */}
<div className="dashboard-panel">
<h2 style={{ margin: "0 0 0.4rem", fontSize: "1.15rem" }}>{stream.name}</h2>
<p style={{ margin: 0, color: "var(--text-muted)" }}>
Recipient:{" "}
<code
style={{
background: "rgba(19,38,61,0.07)",
borderRadius: "0.4rem",
padding: "0.15rem 0.45rem",
fontSize: "0.85rem",
fontFamily: "var(--font-mono, monospace)",
}}
>
{stream.recipient}
</code>
</p>
</div>

{/* 1️⃣ Progress bar */}
<div className="dashboard-panel">
<div className="dashboard-panel__header">
<h3>Streamed Progress</h3>
</div>
<ProgressBar
percentage={percentage}
label={`${stream.streamedAmount.toLocaleString()} / ${stream.totalAmount.toLocaleString()} tokens`}
/>
</div>

{/* 3️⃣ Live counter */}
<div className="dashboard-panel">
<div className="dashboard-panel__header">
<h3>Live Balance</h3>
</div>
<LiveCounter initial={stream.streamedAmount} label="Accumulated balance" />
</div>

{/* 2️⃣ Transaction history */}
<div className="dashboard-panel">
<div className="dashboard-panel__header">
<h3>Transaction History</h3>
<span>{stream.transactions.length} withdrawals</span>
</div>

{stream.transactions.length === 0 ? (
<div className="mini-empty-state">
<p>No transactions yet.</p>
</div>
) : (
<ul className="activity-list">
{stream.transactions.map((tx) => (
<li key={tx.id} className="activity-item">
<div>
<strong>Withdrawal</strong>
<p>{tx.date}</p>
</div>
<span className="is-negative">
-{tx.amount.toLocaleString()} tokens
</span>
</li>
))}
</ul>
)}
</div>

</div>
</main>
);
}
Empty file added frontend/app/streams/n
Empty file.
67 changes: 67 additions & 0 deletions frontend/components/Livecounter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import { useEffect, useState } from "react";

interface LiveCounterProps {
initial: number;
label?: string;
}

export default function LiveCounter({ initial, label = "Live Streamed" }: LiveCounterProps) {
const [amount, setAmount] = useState(initial);

useEffect(() => {
const interval = setInterval(() => {
setAmount((prev) => prev + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

return (
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<span
style={{
position: "relative",
display: "inline-flex",
width: "0.75rem",
height: "0.75rem",
}}
>
<span
style={{
position: "absolute",
inset: 0,
borderRadius: "999px",
background: "#10b981",
opacity: 0.75,
animation: "pulse-slow 4s cubic-bezier(0.4, 0, 0.6, 1) infinite",
}}
/>
<span
style={{
position: "relative",
display: "inline-flex",
width: "0.75rem",
height: "0.75rem",
borderRadius: "999px",
background: "#10b981",
}}
/>
</span>

<p style={{ margin: 0, fontSize: "0.92rem", color: "var(--text-muted)" }}>
{label}:{" "}
<strong
style={{
fontSize: "1rem",
color: "var(--text-main)",
fontVariantNumeric: "tabular-nums",
}}
>
{amount.toLocaleString()}
</strong>
</p>
</div>
);
}
52 changes: 52 additions & 0 deletions frontend/components/Progressbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
interface ProgressBarProps {
percentage: number;
label?: string;
}

export default function ProgressBar({ percentage, label }: ProgressBarProps) {
const clamped = Math.min(100, Math.max(0, percentage));

return (
<div style={{ display: "grid", gap: "0.5rem" }}>
{label && (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontSize: "0.88rem",
color: "var(--text-muted)",
}}
>
<span>{label}</span>
<span style={{ fontWeight: 600, color: "var(--text-main)" }}>{clamped}%</span>
</div>
)}

{/* Track */}
<div
role="progressbar"
aria-valuenow={clamped}
aria-valuemin={0}
aria-valuemax={100}
style={{
width: "100%",
height: "0.6rem",
background: "rgba(19,38,61,0.1)",
borderRadius: "999px",
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${clamped}%`,
background: "linear-gradient(90deg, var(--accent-strong), var(--wallet-accent))",
borderRadius: "999px",
transition: "width 500ms ease",
}}
/>
</div>
</div>
);
}
Loading
Loading