Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions frontend/app/dashboard/founder/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function newRow(): RecipientRow {
}

export default function CreateStreamPage() {
const { handleCreateStream, handleReleaseVault } = useStreams();
const { handleCreateStream, handleReleaseVault, walletAddress } = useStreams();
const { user } = useAuth();
const router = useRouter();
// When the program rejects create_stream because a stuck vault PDA exists
Expand Down Expand Up @@ -140,7 +140,7 @@ export default function CreateStreamPage() {
// Founder history entry. Stored as UI float amount (no decimals math
// needed) since this is a display log, not a settlement record.
try {
const key = `nirvana:founder-history:${user?.wallet?.address ?? "unknown"}`;
const key = `nirvana:founder-history:${walletAddress || "unknown"}`;
const prev = JSON.parse(localStorage.getItem(key) ?? "[]");
const entry = {
type: "create" as const,
Expand Down
228 changes: 169 additions & 59 deletions frontend/app/dashboard/founder/history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
"use client";

import { useEffect, useState } from "react";
import { useAuth } from "@/app/providers/privy-provider";
import { formatTokenAmount, formatAddress, formatDate } from "@/lib/utils";
import { ExternalLink, History as HistoryIcon, PlusCircle, Ban } from "lucide-react";
import { useStreams } from "@/hooks/use-streams";
import {
formatTokenAmount,
formatAddress,
formatDate,
} from "@/lib/utils";
import {
ExternalLink,
History as HistoryIcon,
PlusCircle,
Ban,
CheckCircle2,
} from "lucide-react";
import Link from "next/link";
import {
StreamListSkeleton,
StreamsError,
} from "@/app/components/stream-states";

interface FounderHistoryEntry {
type: "create" | "cancel";
signature: string;
recipient: string;
// For "create" this is the UI float string (e.g. "100"); for "cancel" it's
// base-units (refund), serialized as a bigint string.
amount: string;
amountIsBaseUnits?: boolean;
tokenSymbol: string;
Expand All @@ -19,94 +32,191 @@ interface FounderHistoryEntry {
}

export default function FounderHistoryPage() {
const { user } = useAuth();
const address = user?.wallet?.address ?? "";
const {
getCompletedFounderStreams,
walletAddress,
loading,
error,
refresh,
} = useStreams();
const [entries, setEntries] = useState<FounderHistoryEntry[]>([]);

const completedStreams = getCompletedFounderStreams(walletAddress);

useEffect(() => {
if (!address) return;
if (!walletAddress) return;
try {
const raw = localStorage.getItem(`nirvana:founder-history:${address}`);
const raw = localStorage.getItem(
`nirvana:founder-history:${walletAddress}`
);
setEntries(raw ? (JSON.parse(raw) as FounderHistoryEntry[]) : []);
} catch {
setEntries([]);
}
}, [address]);
}, [walletAddress]);

const formatAmount = (e: FounderHistoryEntry) => {
if (e.amountIsBaseUnits) {
return formatTokenAmount(BigInt(e.amount), e.tokenDecimals ?? 9);
}
// UI float already.
const n = parseFloat(e.amount);
return Number.isFinite(n) ? n.toLocaleString() : e.amount;
};

const isEmpty = completedStreams.length === 0 && entries.length === 0;

return (
<div>
<div className="mb-8">
<h1 className="font-headline text-3xl font-bold tracking-tight">Founder History</h1>
<p className="font-mono text-xs text-on-surface-variant mt-2 uppercase tracking-widest">
Every stream you created or cancelled
Completed streams and create/cancel activity
</p>
</div>

{entries.length === 0 ? (
{error && completedStreams.length === 0 && entries.length === 0 ? (
<StreamsError message={error} onRetry={refresh} />
) : loading && isEmpty ? (
<StreamListSkeleton count={2} />
) : isEmpty ? (
<div className="glass-plate rounded-lg p-12 text-center">
<HistoryIcon className="w-8 h-8 text-on-surface-variant/30 mx-auto mb-3" />
<p className="font-mono text-sm text-on-surface-variant">No activity yet</p>
<p className="font-mono text-sm text-on-surface-variant">No history yet</p>
<p className="font-mono text-[10px] text-on-surface-variant/50 uppercase tracking-widest mt-2">
Create or cancel a stream to see it here
Finished streams and transactions appear here
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-3">
{entries.map((e) => {
const isCreate = e.type === "create";
return (
<a
key={e.signature}
href={`https://explorer.solana.com/tx/${e.signature}?cluster=devnet`}
target="_blank"
rel="noreferrer"
className="glass-plate rounded-lg p-5 hover:border-mint/30 hover:bg-mint/[0.02] transition-all group"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-start gap-3">
<span
className={`mt-0.5 w-7 h-7 rounded-sm flex items-center justify-center ${
isCreate
? "bg-mint/10 text-mint"
: "bg-red-400/10 text-red-400"
}`}
<div className="space-y-10">
{completedStreams.length > 0 && (
<section>
<h2 className="font-headline text-xl font-bold tracking-tight mb-4">
Completed Streams
</h2>
<div className="grid grid-cols-1 gap-3">
{completedStreams.map((stream) => {
const totalAmount =
stream.baseAmount +
stream.milestoneAmount +
stream.cliffAmount;
return (
<div
key={stream.id}
className="glass-plate rounded-lg p-5"
>
{isCreate ? <PlusCircle className="w-4 h-4" /> : <Ban className="w-4 h-4" />}
</span>
<div>
<p className="font-headline text-lg font-bold text-on-surface tracking-tight">
{isCreate ? "Created" : "Cancelled"}{" "}
<span className={isCreate ? "text-mint" : "text-red-400"}>
{isCreate ? "+" : "↩"}
{formatAmount(e)} {e.tokenSymbol}
<div className="flex items-start gap-3">
<span
className={`mt-0.5 w-7 h-7 rounded-sm flex items-center justify-center ${
stream.isCancelled
? "bg-red-400/10 text-red-400"
: "bg-mint/10 text-mint"
}`}
>
{stream.isCancelled ? (
<Ban className="w-4 h-4" />
) : (
<CheckCircle2 className="w-4 h-4" />
)}
</span>
</p>
<p className="font-mono text-[10px] text-on-surface-variant/60 mt-1">
{isCreate ? "to" : "refunded from"}{" "}
<span className="text-on-surface-variant">{formatAddress(e.recipient)}</span>
</p>
<p className="font-mono text-[10px] text-on-surface-variant/50 uppercase tracking-widest mt-0.5">
{formatDate(Math.floor(e.timestamp / 1000))}
</p>
<div className="flex-1 min-w-0">
<p className="font-headline text-lg font-bold text-on-surface tracking-tight">
{formatTokenAmount(totalAmount, stream.tokenDecimals)}{" "}
{stream.tokenSymbol}
</p>
<p className="font-mono text-[10px] text-on-surface-variant/60 mt-1">
To {formatAddress(stream.recipient)}
{" · "}
{stream.isCancelled ? (
<span className="text-red-400">Cancelled</span>
) : (
<span className="text-mint">Completed</span>
)}
</p>
<p className="font-mono text-[10px] text-on-surface-variant/50 uppercase tracking-widest mt-0.5">
{formatDate(stream.startTime)} → {formatDate(stream.endTime)}
</p>
<p className="font-mono text-[10px] text-on-surface-variant/40 mt-1">
{stream.id}
</p>
</div>
</div>
</div>
</div>
<ExternalLink className="w-4 h-4 text-on-surface-variant/40 group-hover:text-mint transition-colors shrink-0" />
</div>
<p className="font-mono text-[10px] text-on-surface-variant/40 break-all">
{e.signature}
</p>
</a>
);
})}
);
})}
</div>
</section>
)}

{entries.length > 0 && (
<section>
<h2 className="font-headline text-xl font-bold tracking-tight mb-4">
Transactions
</h2>
<div className="grid grid-cols-1 gap-3">
{entries.map((e) => {
const isCreate = e.type === "create";
return (
<a
key={e.signature}
href={`https://explorer.solana.com/tx/${e.signature}?cluster=devnet`}
target="_blank"
rel="noreferrer"
className="glass-plate rounded-lg p-5 hover:border-mint/30 hover:bg-mint/[0.02] transition-all group"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-start gap-3">
<span
className={`mt-0.5 w-7 h-7 rounded-sm flex items-center justify-center ${
isCreate
? "bg-mint/10 text-mint"
: "bg-red-400/10 text-red-400"
}`}
>
{isCreate ? (
<PlusCircle className="w-4 h-4" />
) : (
<Ban className="w-4 h-4" />
)}
</span>
<div>
<p className="font-headline text-lg font-bold text-on-surface tracking-tight">
{isCreate ? "Created" : "Cancelled"}{" "}
<span className={isCreate ? "text-mint" : "text-red-400"}>
{isCreate ? "+" : "↩"}
{formatAmount(e)} {e.tokenSymbol}
</span>
</p>
<p className="font-mono text-[10px] text-on-surface-variant/60 mt-1">
{isCreate ? "to" : "refunded from"}{" "}
<span className="text-on-surface-variant">
{formatAddress(e.recipient)}
</span>
</p>
<p className="font-mono text-[10px] text-on-surface-variant/50 uppercase tracking-widest mt-0.5">
{formatDate(Math.floor(e.timestamp / 1000))}
</p>
</div>
</div>
<ExternalLink className="w-4 h-4 text-on-surface-variant/40 group-hover:text-mint transition-colors shrink-0" />
</div>
<p className="font-mono text-[10px] text-on-surface-variant/40 break-all">
{e.signature}
</p>
</a>
);
})}
</div>
</section>
)}

{completedStreams.length === 0 && entries.length > 0 && (
<p className="font-mono text-[10px] text-on-surface-variant/50 uppercase tracking-widest">
<Link href="/dashboard/founder/streams" className="text-mint hover:brightness-110">
My Streams
</Link>{" "}
shows streams still in progress or awaiting milestone action.
</p>
)}
</div>
)}
</div>
Expand Down
39 changes: 27 additions & 12 deletions frontend/app/dashboard/founder/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ import {
} from "@/app/components/stream-states";

export default function FounderPage() {
const { getFounderStreams, walletAddress, loading, error, refresh } = useStreams();
const { getFounderStreams, getActiveFounderStreams, walletAddress, loading, error, refresh } = useStreams();
// Only show streams this wallet *created* — recipient-only streams belong
// on the worker view and must never bleed into the founder dashboard.
// Filter by the signing Solana address (walletAddress), NOT user.wallet.address
// — for MetaMask/EVM logins those differ and the dashboard would show nothing.
const founderAddress = walletAddress;
const streams = getFounderStreams(founderAddress);
const activeStreams = getActiveFounderStreams(founderAddress);
// Show skeletons on first load; keep showing data during background refetches.
const showSkeleton = loading && streams.length === 0;
const showError = !!error && streams.length === 0;
const showSkeleton = loading && activeStreams.length === 0 && streams.length === 0;
const showError = !!error && activeStreams.length === 0 && streams.length === 0;

const activeStreams = streams.filter((s) => !s.isCancelled);
const totalAllocated = activeStreams.reduce(
(sum, s) => sum + s.baseAmount + s.milestoneAmount + s.cliffAmount,
BigInt(0)
);
const totalClaimed = streams.reduce((sum, s) => sum + s.claimedAmount, BigInt(0));
const pendingMilestones = activeStreams.filter((s) => !s.milestoneAchieved).length;
const pendingMilestones = activeStreams.filter(
(s) => !s.milestoneAchieved && s.milestoneAmount > BigInt(0)
).length;
const uniqueRecipients = new Set(activeStreams.map((s) => s.recipient)).size;

const recentStreams = activeStreams.slice(0, 5);
Expand Down Expand Up @@ -96,14 +98,27 @@ export default function FounderPage() {
<StreamListSkeleton />
) : activeStreams.length === 0 ? (
<StreamsEmpty
message="No active streams yet"
message={
streams.length === 0
? "No streams created yet"
: "No active streams — see History for completed ones"
}
action={
<Link
href="/dashboard/founder/create"
className="text-mint font-mono text-xs font-bold uppercase tracking-widest hover:brightness-110 transition-colors"
>
Create your first stream
</Link>
streams.length === 0 ? (
<Link
href="/dashboard/founder/create"
className="text-mint font-mono text-xs font-bold uppercase tracking-widest hover:brightness-110 transition-colors"
>
Create your first stream
</Link>
) : (
<Link
href="/dashboard/founder/history"
className="text-mint font-mono text-xs font-bold uppercase tracking-widest hover:brightness-110 transition-colors"
>
View history
</Link>
)
}
/>
) : (
Expand Down
Loading
Loading