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
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user