Files
IPFS-portal/src/components/Toast.tsx
T
maikrolf c9432a16ba SIWE auth, API proxy fixes, dashboard, login, usage pages
- SIWE auth flow: challenge/login/logout/me API routes + JWT middleware
- Fixed [...path] catch-all: Kubo proxy with matchRoute() + mapToKuboAPI()
- Fixed session_id → nonce field mismatch in AuthProvider
- Fixed dashboard pins crash: Kubo returns {Keys: {...}}, not array
- Added middleware route guard (307 redirect to /login)
- Added login, register, profile, usage, upload pages
- Added dashboard components (StorageGauge, FreeTierProgress)
- Added SWR hooks + AuthGuard + Providers + Toast/ErrorBoundary
- Added payment integration (smart contract ABI, tiers, tokens)
- Fixed IPNS key listing: Name/Id → name/id mapping
- Added PWA support (manifest, service worker, icons)
- Added tests for helpers, limits, search-index, wallet
2026-06-28 18:31:05 +02:00

91 lines
2.9 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
import type { Notification } from '@/lib/notifications';
/* ── Icons ── */
const ICONS: Record<Notification['type'], { icon: typeof CheckCircle; color: string }> = {
success: { icon: CheckCircle, color: 'text-accent-green' },
error: { icon: AlertCircle, color: 'text-accent-rose' },
info: { icon: Info, color: 'text-accent-cyan' },
warning: { icon: AlertTriangle,color: 'text-accent-amber' },
};
/* ════════════════════════════ Toast ════════════════════════════ */
export default function Toast({
notification,
onDismiss,
}: {
notification: Notification;
onDismiss: (id: string) => void;
}) {
const [visible, setVisible] = useState(false);
const [progress, setProgress] = useState(100);
const { icon: Icon, color } = ICONS[notification.type];
const duration = notification.duration ?? 4000;
/* ── Enter animation ── */
useEffect(() => {
requestAnimationFrame(() => setVisible(true));
}, []);
/* ── Progress bar ── */
useEffect(() => {
if (duration <= 0) return;
const start = Date.now();
const frame = () => {
const elapsed = Date.now() - start;
const pct = Math.max(0, 100 - (elapsed / duration) * 100);
setProgress(pct);
if (pct > 0) requestAnimationFrame(frame);
};
const raf = requestAnimationFrame(frame);
return () => cancelAnimationFrame(raf);
}, [duration]);
return (
<div
role="alert"
className={`
flex items-start gap-3 p-4 rounded-xl glass border border-surface-700 shadow-xl
transition-all duration-300 ease-out max-w-sm
${visible ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0'}
`}
>
{/* Icon */}
<div className="shrink-0 mt-0.5">
<Icon className={`w-5 h-5 ${color}`} />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-surface-200">{notification.title}</p>
{notification.message && (
<p className="text-xs text-surface-400 mt-0.5 break-words">{notification.message}</p>
)}
{/* Progress bar */}
{duration > 0 && (
<div className="mt-2 h-0.5 rounded-full bg-surface-700 overflow-hidden">
<div
className={`h-full rounded-full transition-none ${color.replace('text-', 'bg-')}`}
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
{/* Close */}
<button
onClick={() => onDismiss(notification.id)}
className="shrink-0 p-0.5 rounded hover:bg-surface-700 transition-colors"
aria-label="Sluiten"
>
<X className="w-4 h-4 text-surface-500" />
</button>
</div>
);
}