diff --git a/src/app/notifications/page.tsx b/src/app/notifications/page.tsx index f657073..ee0451d 100644 --- a/src/app/notifications/page.tsx +++ b/src/app/notifications/page.tsx @@ -1,8 +1,102 @@ 'use client'; +import { useState, useEffect } from 'react'; import { BellIcon } from '@/components/Icons'; +import Link from 'next/link'; +import Image from 'next/image'; + +interface NotificationActor { + id: string; + handle: string; + displayName: string | null; + avatarUrl: string | null; +} + +interface NotificationPost { + id: string; + content: string; +} + +interface Notification { + id: string; + type: 'follow' | 'like' | 'repost' | 'mention'; + createdAt: string; + readAt: string | null; + actor: NotificationActor | null; + post: NotificationPost | null; +} export default function NotificationsPage() { + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchNotifications(); + }, []); + + const fetchNotifications = async () => { + try { + const res = await fetch('/api/notifications'); + if (!res.ok) { + if (res.status === 401) { + setError('Please log in to view notifications'); + return; + } + throw new Error('Failed to fetch notifications'); + } + const data = await res.json(); + setNotifications(data.notifications || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load notifications'); + } finally { + setLoading(false); + } + }; + + const markAllRead = async () => { + try { + await fetch('/api/notifications', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ all: true }), + }); + setNotifications(prev => prev.map(n => ({ ...n, readAt: new Date().toISOString() }))); + } catch (err) { + console.error('Failed to mark notifications as read:', err); + } + }; + + const getNotificationText = (notification: Notification) => { + switch (notification.type) { + case 'follow': + return 'followed you'; + case 'like': + return 'liked your post'; + case 'repost': + return 'reposted your post'; + case 'mention': + return 'mentioned you'; + default: + return 'interacted with you'; + } + }; + + const formatTime = (dateStr: string) => { + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m`; + if (diffHours < 24) return `${diffHours}h`; + if (diffDays < 7) return `${diffDays}d`; + return date.toLocaleDateString(); + }; + return (

Notifications

+ {notifications.length > 0 && ( + + )}
-
-
-
- -
+ {loading ? ( +
+ Loading...
-

No notifications yet

-

When you get interactions, they'll show up here.

-
+ ) : error ? ( +
+ {error} +
+ ) : notifications.length === 0 ? ( +
+
+
+ +
+
+

No notifications yet

+

When you get interactions, they'll show up here.

+
+ ) : ( +
+ {notifications.map((notification) => ( + + ))} +
+ )} +
+ ); +} + +function NotificationItem({ + notification, + getNotificationText, + formatTime, +}: { + notification: Notification; + getNotificationText: (n: Notification) => string; + formatTime: (d: string) => string; +}) { + const isUnread = !notification.readAt; + const actor = notification.actor; + + return ( +
+ + {actor?.avatarUrl ? ( + {actor.displayName + ) : ( +
+ {(actor?.displayName || actor?.handle || '?')[0].toUpperCase()} +
+ )} + + +
+
+ + {actor?.displayName || actor?.handle || 'Someone'} + + + {getNotificationText(notification)} + + + ยท {formatTime(notification.createdAt)} + +
+ + {notification.post && ( + + {notification.post.content} + + )} +
+ + {isUnread && ( +
+ )}
); }