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
86 changes: 75 additions & 11 deletions app/app/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,106 @@
import { useEffect, useState } from 'react';
'use client';

import { useEffect, useState, useCallback } from 'react';
import { Badge } from '@/components/ui/badge';

interface Notification {
id: string;
message: string;
link?: string;
isRead: boolean;
createdAt: string;
}

export default function NotificationsPage() {
const [notifications, setNotifications] = useState([]);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchNotifications() {
setLoading(true);
const fetchNotifications = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/notifications?page=1&pageSize=50');
const data = await res.json();
setNotifications(data.notifications || []);
} finally {
setLoading(false);
}
fetchNotifications();
}, []);

useEffect(() => {
fetchNotifications();
}, [fetchNotifications]);

const markAsRead = async (id: string) => {
await fetch(`/api/notifications/${id}`, { method: 'PATCH' });
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n)),
);
};

const markAllAsRead = async () => {
const unread = notifications.filter((n) => !n.isRead);
await Promise.all(
unread.map((n) => fetch(`/api/notifications/${n.id}`, { method: 'PATCH' })),
);
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })));
};

const unreadCount = notifications.filter((n) => !n.isRead).length;

return (
<div className="max-w-2xl mx-auto py-8 px-4">
<h1 className="text-2xl font-bold mb-6">Notifications</h1>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">
Notifications
{unreadCount > 0 && (
<Badge variant="destructive" className="ml-2">{unreadCount}</Badge>
)}
</h1>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="text-sm text-blue-600 hover:underline"
>
Mark all as read
</button>
)}
</div>

{loading ? (
<div>Loading...</div>
) : notifications.length === 0 ? (
<div className="text-gray-500">No notifications yet.</div>
) : (
<ul className="space-y-4">
{notifications.map((n) => (
<li key={n.id} className={`p-4 rounded-lg border ${n.isRead ? 'bg-gray-50' : 'bg-orange-50 border-orange-200'}`}>
<li
key={n.id}
className={`p-4 rounded-lg border ${n.isRead ? 'bg-gray-50' : 'bg-orange-50 border-orange-200'}`}
>
<div className="flex items-center justify-between">
<div>
<span className="font-medium">{n.message}</span>
{n.link && (
<a href={n.link} className="ml-2 text-blue-600 underline text-xs">View</a>
<a href={n.link} className="ml-2 text-blue-600 underline text-xs">
View
</a>
)}
</div>
{!n.isRead && <Badge variant="destructive">New</Badge>}
{!n.isRead && (
<div className="flex items-center gap-2">
<Badge variant="destructive">New</Badge>
<button
onClick={() => markAsRead(n.id)}
className="text-xs text-gray-500 hover:text-gray-800 hover:underline"
>
Mark as read
</button>
</div>
)}
</div>
<div className="text-xs text-gray-400 mt-1">
{new Date(n.createdAt).toLocaleString()}
</div>
<div className="text-xs text-gray-400 mt-1">{new Date(n.createdAt).toLocaleString()}</div>
</li>
))}
</ul>
Expand Down
26 changes: 26 additions & 0 deletions app/contexts/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,32 @@ export function AppProvider({ children }: { children: React.ReactNode }) {

const contextValue: AppContextType = {
...state,
refreshPosts: async () => {
try {
const res = await fetch('/api/posts', { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
dispatch({
type: 'SET_POSTS',
payload: Array.isArray(data.data) ? data.data : data.data?.posts ?? [],
});
} catch {
// silently ignore refresh failures
}
},
refreshPostDetail: async (postId: string) => {
try {
const res = await fetch(`/api/posts/${postId}`, { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
const post = data.data ?? data.post ?? null;
if (post) {
dispatch({ type: 'UPDATE_POST', payload: { id: postId, updates: post } });
}
} catch {
// silently ignore refresh failures
}
},
login: async (
user: User,
credentials: {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export interface AppContextType extends AppState {
logout: () => Promise<void>
setCurrentUser: (user: User | null) => void
// Post actions
refreshPosts: () => Promise<void>
refreshPostDetail: (postId: string) => Promise<void>
createPost: (post: Omit<Post, "id" | "createdAt" | "updatedAt" | "author" | "entriesCount" | "shareCount" | "burnCount" | "commentCount" | "likesCount">) => void
updatePost: (postId: string, updates: Partial<Post>) => void
deletePost: (postId: string) => void
Expand Down
Loading