fix: notifications page — wire to real API, make Mark all read functional
- Replaced hardcoded mock data with real call to /api/notifications?childId= - Read/unread state stored in localStorage (tia_read_notifications) since notifications are computed on-the-fly from vaccination schedule, no DB row - Tapping a notification marks it read individually - Mark all read button appears in header only when there are unread items - Unread count badge shown in header - Empty state now says "All caught up!" instead of generic "No notifications" - Shows 🚨 for overdue vaccines, 💉 for due-today - Added due date display in IST-formatted date Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7a332cacd
commit
ee4bcc4498
1 changed files with 117 additions and 28 deletions
|
|
@ -1,58 +1,147 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useFamily } from "@/app/FamilyProvider";
|
||||||
|
import { fmtDate } from "@/lib/date-ist";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
time: string;
|
dueDate?: string;
|
||||||
read: boolean;
|
status?: string;
|
||||||
|
childName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_KEY = "tia_read_notifications";
|
||||||
|
|
||||||
|
function getReadSet(): Set<string> {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(READ_KEY);
|
||||||
|
return new Set(raw ? JSON.parse(raw) : []);
|
||||||
|
} catch { return new Set(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveReadSet(ids: Set<string>) {
|
||||||
|
localStorage.setItem(READ_KEY, JSON.stringify([...ids]));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [notifications] = useState<Notification[]>([
|
const { childId } = useFamily();
|
||||||
{ id: "1", title: "Reminder", message: "Time to log today's feed", time: "2 hours ago", read: false },
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
{ id: "2", title: "Growth Update", message: "New growth data saved", time: "Yesterday", read: true },
|
const [readIds, setReadIds] = useState<Set<string>>(new Set());
|
||||||
]);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setReadIds(getReadSet());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!childId) return;
|
||||||
|
fetch(`/api/notifications?childId=${childId}`)
|
||||||
|
.then(r => r.ok ? r.json() : { notifications: [] })
|
||||||
|
.then(d => setNotifications(d.notifications || []))
|
||||||
|
.catch(() => setNotifications([]))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [childId]);
|
||||||
|
|
||||||
|
const markRead = (id: string) => {
|
||||||
|
setReadIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(id);
|
||||||
|
saveReadSet(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllRead = () => {
|
||||||
|
const allIds = new Set(notifications.map(n => n.id));
|
||||||
|
setReadIds(allIds);
|
||||||
|
saveReadSet(allIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => !readIds.has(n.id)).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
||||||
<div className="p-4 flex items-center gap-4">
|
{/* Header */}
|
||||||
<button onClick={() => router.back()} className="p-2">←</button>
|
<div className="p-4 flex items-center gap-3">
|
||||||
|
<button onClick={() => router.back()} className="p-2 rounded-xl bg-white dark:bg-gray-800 shadow-sm text-xl">←</button>
|
||||||
<h1 className="text-xl font-bold">Notifications</h1>
|
<h1 className="text-xl font-bold">Notifications</h1>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="ml-1 px-2 py-0.5 bg-rose-400 text-white text-xs font-bold rounded-full">
|
||||||
|
{unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={markAllRead}
|
||||||
|
className="ml-auto text-sm text-rose-500 font-medium"
|
||||||
|
>
|
||||||
|
Mark all read
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 space-y-2">
|
<div className="px-4 space-y-2">
|
||||||
{notifications.length === 0 ? (
|
{loading ? (
|
||||||
|
<div className="text-center py-20 text-gray-400">
|
||||||
|
<div className="text-4xl mb-3 animate-pulse">🔔</div>
|
||||||
|
<p className="text-sm">Loading…</p>
|
||||||
|
</div>
|
||||||
|
) : notifications.length === 0 ? (
|
||||||
<div className="text-center py-20 text-gray-400">
|
<div className="text-center py-20 text-gray-400">
|
||||||
<div className="text-6xl mb-4">🔔</div>
|
<div className="text-6xl mb-4">🔔</div>
|
||||||
<p>No notifications</p>
|
<p className="font-medium">All caught up!</p>
|
||||||
|
<p className="text-sm mt-1">No overdue or due-today vaccinations</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
notifications.map((notif) => (
|
notifications.map(notif => {
|
||||||
<div
|
const isRead = readIds.has(notif.id);
|
||||||
|
const isOverdue = notif.status === "overdue";
|
||||||
|
return (
|
||||||
|
<button
|
||||||
key={notif.id}
|
key={notif.id}
|
||||||
className={`p-4 rounded-xl ${notif.read ? "bg-gray-50 dark:bg-gray-800" : "bg-white dark:bg-gray-700"}`}
|
onClick={() => markRead(notif.id)}
|
||||||
|
className={`w-full text-left p-4 rounded-xl transition-colors ${
|
||||||
|
isRead
|
||||||
|
? "bg-gray-50 dark:bg-gray-800/60"
|
||||||
|
: "bg-white dark:bg-gray-800 shadow-sm"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex-1">
|
<span className="text-xl mt-0.5">{isOverdue ? "🚨" : "💉"}</span>
|
||||||
<div className="font-medium">{notif.title}</div>
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm text-gray-500">{notif.message}</div>
|
<div className={`font-medium text-sm ${isRead ? "text-gray-500 dark:text-gray-400" : "text-gray-900 dark:text-white"}`}>
|
||||||
<div className="text-xs text-gray-400 mt-1">{notif.time}</div>
|
{notif.title}
|
||||||
</div>
|
</div>
|
||||||
{!notif.read && <div className="w-2 h-2 bg-rose-400 rounded-full mt-2" />}
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{notif.message}
|
||||||
</div>
|
</div>
|
||||||
|
{notif.dueDate && (
|
||||||
|
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
|
Due: {fmtDate(notif.dueDate + "T00:00:00Z", { day: "numeric", month: "short", year: "numeric" })}
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
|
</div>
|
||||||
|
{!isRead && (
|
||||||
|
<div className="w-2 h-2 bg-rose-400 rounded-full mt-2 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notifications.length > 0 && (
|
{notifications.length > 0 && (
|
||||||
<div className="px-4 mt-6">
|
<div className="px-4 mt-6 pb-8">
|
||||||
<button className="text-sm text-gray-500">Mark all as read</button>
|
<p className="text-center text-xs text-gray-400">
|
||||||
|
Showing overdue & due-today IAP vaccinations for {notifications[0]?.childName || "your child"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue