+ {/* Page header */}
+
+
Notification Center
+
+ {activeTab === "reminders" && (
+
+ )}
+
+
+
+
+ {/* Tabs */}
+
+ {tabs.map((tab) => (
+
+ ))}
+
+
+ {/* Loading state */}
+ {isLoading && (
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+ )}
+
+ {/* Content */}
+ {!isLoading && (
+
+ {/* All / Notifications / Approvals tabs */}
+ {(activeTab === "all" || activeTab === "notifications" || activeTab === "approvals") && (
+ <>
+ {(activeTab === "all" ? allNotifications : activeTab === "notifications" ? notifications : approvals).length === 0 ? (
+
+ {activeTab === "approvals" ? "No pending approvals" : "No notifications"}
+
+ ) : (
+ (activeTab === "all" ? allNotifications : activeTab === "notifications" ? notifications : approvals).map((n) => {
+ const isUnread = n.readAt === null;
+ const isTask = n.category === "TASK" || n.category === "APPROVAL";
+
+ if (isTask && n.taskStatus) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ })
+ )}
+ >
+ )}
+
+ {/* Tasks tab */}
+ {activeTab === "tasks" && (
+ <>
+ {tasks.length === 0 ? (
+
+ No tasks
+
+ ) : (
+ tasks.map((t) => (
+
+ ))
+ )}
+ >
+ )}
+
+ {/* Reminders tab */}
+ {activeTab === "reminders" && (
+ <>
+ {reminders.length === 0 ? (
+
+ No reminders. Create one to get started.
+
+ ) : (
+ reminders.map((r) => (
+
+
+
+
+ {r.title}
+
+ {r.body && (
+
+ {r.body}
+
+ )}
+
+ {r.nextRemindAt && (
+
+ {new Date(r.nextRemindAt).toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+ )}
+ {r.recurrence && (
+
+ {r.recurrence}
+
+ )}
+
+
+
+
+
+
+
+
+ ))
+ )}
+ >
+ )}
+
+ )}
+
+ {/* Reminder Modal */}
+ {reminderModal.open && (
+
setReminderModal({ open: false, reminder: null })}
+ onSuccess={() => setReminderModal({ open: false, reminder: null })}
+ />
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/notifications/ReminderModal.tsx b/apps/web/src/components/notifications/ReminderModal.tsx
new file mode 100644
index 0000000..268e4e0
--- /dev/null
+++ b/apps/web/src/components/notifications/ReminderModal.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { useFocusTrap } from "~/hooks/useFocusTrap.js";
+import { trpc } from "~/lib/trpc/client.js";
+import { DateInput } from "~/components/ui/DateInput.js";
+
+const RECURRENCE_OPTIONS = [
+ { value: "", label: "None" },
+ { value: "daily", label: "Daily" },
+ { value: "weekly", label: "Weekly" },
+ { value: "monthly", label: "Monthly" },
+] as const;
+
+interface ReminderModalProps {
+ reminder?: {
+ id: string;
+ title: string;
+ body?: string | null;
+ remindAt?: string | Date | null;
+ recurrence?: string | null;
+ link?: string | null;
+ } | null;
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+function toDateInputValue(date: Date | string | null | undefined): string {
+ if (!date) return "";
+ const d = typeof date === "string" ? new Date(date) : date;
+ return d.toISOString().split("T")[0] ?? "";
+}
+
+function toTimeInputValue(date: Date | string | null | undefined): string {
+ if (!date) return "09:00";
+ const d = typeof date === "string" ? new Date(date) : date;
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
+}
+
+export function ReminderModal({ reminder, onClose, onSuccess }: ReminderModalProps) {
+ const isEdit = !!reminder;
+
+ const [title, setTitle] = useState(reminder?.title ?? "");
+ const [body, setBody] = useState(reminder?.body ?? "");
+ const [remindDate, setRemindDate] = useState(toDateInputValue(reminder?.remindAt));
+ const [remindTime, setRemindTime] = useState(toTimeInputValue(reminder?.remindAt));
+ const [recurrence, setRecurrence] = useState(reminder?.recurrence ?? "");
+ const [link, setLink] = useState(reminder?.link ?? "");
+ const [serverError, setServerError] = useState