d0e6912017
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
143 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|