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
169 changes: 106 additions & 63 deletions frontend/src/components/IncomingStreams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ const ClaimableAmount: React.FC<{ stream: Stream }> = ({ stream }) => {
);
};

/**
* Shown when the current filter returns no results.
* Distinguished from the global empty-state (no streams at all), which is
* handled one level up in dashboard-view.tsx.
*/
const FilterEmptyState: React.FC<{ filter: string; onClearFilter: () => void }> = ({ filter, onClearFilter }) => (
<div className="p-12 text-center">
<div className="h-14 w-14 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mx-auto mb-4">
<svg className="h-7 w-7 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z" />
</svg>
</div>
<p className="text-gray-600 dark:text-gray-400 font-medium">
No <span className="lowercase">{filter}</span> streams found.
</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-1">
Try a different filter or wait for new streams to arrive.
</p>
<button
type="button"
onClick={onClearFilter}
className="mt-4 text-sm text-accent hover:underline"
>
Show all streams
</button>
</div>
);

const IncomingStreams: React.FC<IncomingStreamsProps> = ({
streams,
onWithdraw,
Expand All @@ -64,7 +93,7 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
try {
await onWithdraw(stream);
toast.success(`Successfully withdrew from stream #${stream.id}`);
} catch (error) {
} catch {
toast.error(`Failed to withdraw from stream #${stream.id}`);
}
};
Expand All @@ -79,7 +108,9 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
</p>
</div>
<div className="flex items-center gap-2">
<label htmlFor="filter" className="text-sm font-medium text-gray-700 dark:text-gray-300">Filter:</label>
<label htmlFor="filter" className="text-sm font-medium text-gray-700 dark:text-gray-300">
Filter:
</label>
<select
id="filter"
value={filter}
Expand All @@ -94,71 +125,83 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
</div>
</div>

<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead className="bg-gray-50/50 dark:bg-gray-800/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Sender</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Token</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Deposited</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Withdrawn</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Claimable</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredStreams.map((stream) => {
const isPaused = stream.status === 'Paused';
return (
<tr key={stream.id} className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${isPaused ? 'bg-gray-50/50 dark:bg-gray-800/50 opacity-75' : ''}`}>
<td className="px-6 py-4 whitespace-nowrap">
<div className={`text-sm font-mono ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{stream.recipient}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Stream #{stream.id}</div>
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{stream.token}</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{formatTokenAmount(stream.deposited)} {stream.token}</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-bold tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{formatTokenAmount(stream.withdrawn)} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<ClaimableAmount stream={stream} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
stream.status === 'Paused' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
{stream.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
disabled={stream.status !== 'Active' || withdrawingStreamId === stream.id}
onClick={() => {
void handleWithdraw(stream);
}}
className={`px-4 py-2 rounded-lg transition-all ${stream.status === 'Active'
? 'bg-accent text-white hover:bg-accent-hover shadow-lg'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
{/* Empty state when the filter matches nothing */}
{filteredStreams.length === 0 ? (
<FilterEmptyState
filter={filter}
onClearFilter={() => setFilter('All')}
/>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead className="bg-gray-50/50 dark:bg-gray-800/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Sender</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Token</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Deposited</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Withdrawn</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Claimable</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredStreams.map((stream) => {
const isPaused = stream.status === 'Paused';
return (
<tr
key={stream.id}
className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${isPaused ? 'bg-gray-50/50 dark:bg-gray-800/50 opacity-75' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className={`text-sm font-mono ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
{stream.recipient}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Stream #{stream.id}</div>
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
{stream.token}
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
{formatTokenAmount(stream.deposited)} {stream.token}
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-bold tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
{formatTokenAmount(stream.withdrawn)} {stream.token}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<ClaimableAmount stream={stream} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
stream.status === 'Paused' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
{stream.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
disabled={stream.status !== 'Active' || withdrawingStreamId === stream.id}
onClick={() => { void handleWithdraw(stream); }}
className={`px-4 py-2 rounded-lg transition-all ${
stream.status === 'Active'
? 'bg-accent text-white hover:bg-accent-hover shadow-lg'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
}`}
>
{withdrawingStreamId === stream.id ? 'Withdrawing...' : 'Withdraw'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>

{filteredStreams.length === 0 && (
<div className="p-12 text-center">
<p className="text-gray-500 dark:text-gray-400">No incoming streams found matching the filter.</p>
>
{withdrawingStreamId === stream.id ? 'Withdrawing...' : 'Withdraw'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};

export default IncomingStreams;
export default IncomingStreams;
Loading
Loading