Files
ai-learning-platform/frontend/src/app/notifications/page.tsx
T

133 lines
4.4 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import { apiFetch } from '@/lib/auth';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
interface Notification {
id: number;
type: 'like' | 'comment' | 'follow' | 'system';
title: string;
content?: string;
link?: string;
relatedId?: number;
isRead: boolean;
createdAt: string;
}
function NotificationIcon({ type }: { type: string }) {
const icons: Record<string, string> = {
like: '❤️',
comment: '💬',
follow: '👤',
system: '🔔',
};
return <span className="text-lg">{icons[type] || '🔔'}</span>;
}
export default function NotificationsPage() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const res = await apiFetch('/notifications');
if (res.ok) {
const data = await res.json();
setNotifications(data.items || []);
setUnreadCount(data.unread || 0);
}
} catch (e) { console.error(e) }
setLoading(false);
}, []);
useEffect(() => { load(); }, [load]);
async function markRead(id: number) {
try {
await apiFetch(`/notifications/${id}/read`, { method: 'PATCH' });
setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n));
setUnreadCount(prev => Math.max(0, prev - 1));
} catch (e) { console.error(e) }
}
async function markAllRead() {
try {
await apiFetch('/notifications/read-all', { method: 'PATCH' });
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
setUnreadCount(0);
} catch (e) { console.error(e) }
}
if (loading) return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Skeleton className="h-8 w-48 mb-6" />
<Skeleton className="h-4 w-32 mb-8" />
<Skeleton className="h-64 w-full mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</div>
);
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="mt-2 text-muted-foreground">
{unreadCount > 0 ? `你有 ${unreadCount} 条未读通知` : '暂无未读通知'}
</p>
</div>
{unreadCount > 0 && (
<Button variant="outline" size="sm" onClick={markAllRead}>
</Button>
)}
</div>
<div className="space-y-2">
{notifications.map(n => (
<div key={n.id}
className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${
n.isRead
? 'bg-card border-border'
: 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'
}`}>
<div className="mt-1"><NotificationIcon type={n.type} /></div>
<div className="flex-1 min-w-0">
{n.link ? (
<Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }}
className="text-sm font-medium text-foreground hover:text-brand-600">
{n.title}
</Link>
) : (
<p className="text-sm font-medium text-foreground">{n.title}</p>
)}
{n.content && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{n.content}</p>}
<p className="text-xs text-muted-foreground mt-1">
{new Date(n.createdAt).toLocaleString('zh-CN')}
</p>
</div>
{!n.isRead && (
<button onClick={() => markRead(n.id)}
className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">
</button>
)}
</div>
))}
{notifications.length === 0 && (
<div className="text-center py-20 text-muted-foreground">
<p className="text-4xl mb-4">🔔</p>
<p></p>
<p className="text-sm mt-1"></p>
</div>
)}
</div>
</div>
);
}