Files
TX/artifacts/tx-os/src/pages/notifications.tsx
T
riyadhafraa d0e6912017 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:52:09 +00:00

143 lines
4.8 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import {
useListNotifications,
getListNotificationsQueryKey,
useMarkNotificationRead,
useMarkAllNotificationsRead,
} from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { ArrowRight, ArrowLeft, Bell, CheckCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { formatDateTime } from "@/lib/i18n-format";
import { useAuth } from "@/contexts/AuthContext";
export default function NotificationsPage() {
const { t, i18n } = useTranslation();
const [, setLocation] = useLocation();
const { user } = useAuth();
const lang = i18n.language;
const isRtl = lang === "ar";
const queryClient = useQueryClient();
const { data: notifications, isLoading } = useListNotifications({
query: { queryKey: getListNotificationsQueryKey() },
});
const markRead = useMarkNotificationRead();
const markAllRead = useMarkAllNotificationsRead();
const BackIcon = isRtl ? ArrowRight : ArrowLeft;
const handleMarkAll = () => {
markAllRead.mutate(undefined, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getListNotificationsQueryKey() });
},
});
};
const handleMarkOne = (id: number) => {
markRead.mutate(
{ id },
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getListNotificationsQueryKey() });
},
},
);
};
const handleNotificationClick = (n: {
id: number;
isRead: boolean;
relatedType?: string | null;
relatedId?: number | null;
}) => {
if (!n.isRead) {
handleMarkOne(n.id);
}
if (n.relatedType === "conversation" && n.relatedId != null) {
setLocation(`/chat?c=${n.relatedId}`);
}
};
const unreadCount = notifications?.filter((n) => !n.isRead).length ?? 0;
return (
<div className="min-h-screen os-bg flex flex-col">
{/* Header */}
<div className="glass-panel border-b border-slate-200/70 px-4 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-3">
<button
onClick={() => setLocation("/")}
className="p-2 rounded-xl hover:bg-slate-100 text-muted-foreground hover:text-foreground transition-colors"
>
<BackIcon size={20} />
</button>
<div className="flex items-center gap-2">
<Bell size={20} className="text-violet-400" />
<h1 className="text-lg font-semibold text-foreground">{t("notifications.title")}</h1>
{unreadCount > 0 && (
<Badge className="bg-destructive/20 text-destructive border-destructive/30">
{unreadCount}
</Badge>
)}
</div>
</div>
{unreadCount > 0 && (
<Button
size="sm"
variant="ghost"
onClick={handleMarkAll}
className="text-xs text-muted-foreground hover:text-foreground"
>
<CheckCheck size={14} className="me-1" />
{t("notifications.markAllRead")}
</Button>
)}
</div>
<div className="flex-1 p-4 pb-8 space-y-3">
{isLoading ? (
<div className="flex items-center justify-center py-20 text-muted-foreground">
{t("common.loading")}
</div>
) : !notifications?.length ? (
<div className="flex flex-col items-center justify-center py-20 gap-3 text-muted-foreground">
<Bell size={40} className="opacity-30" />
<span>{t("notifications.noNotifications")}</span>
</div>
) : (
notifications.map((n) => {
const title = lang === "ar" ? n.titleAr : n.titleEn;
const body = lang === "ar" ? n.bodyAr : n.bodyEn;
return (
<div
key={n.id}
onClick={() => handleNotificationClick(n)}
className={`glass-panel rounded-2xl p-4 cursor-pointer transition-opacity ${n.isRead ? "opacity-60" : ""}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className="font-medium text-foreground text-sm">{title}</p>
{body && <p className="text-muted-foreground text-xs mt-1">{body}</p>}
</div>
{!n.isRead && (
<span className="w-2 h-2 rounded-full bg-primary shrink-0 mt-1.5" />
)}
</div>
<div className="text-xs text-muted-foreground mt-2">
{formatDateTime(n.createdAt, lang, user?.clockHour12 ?? false)}
</div>
</div>
);
})
)}
</div>
</div>
);
}