Files
guilan-ace-frontend/src/components/NotificationsBell.tsx

178 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { Bell, CheckCheck, Loader2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useNotifications } from "@/contexts/NotificationsContext";
import type { NotificationSchema } from "@/lib/types";
import { cn, formatJalali } from "@/lib/utils";
const connectionLabels = {
idle: "خاموش",
connecting: "در حال اتصال",
connected: "متصل",
disconnected: "قطع شده",
} as const;
function NotificationItem({
notification,
onOpen,
onDelete,
}: {
notification: NotificationSchema;
onOpen: (notification: NotificationSchema) => Promise<unknown>;
onDelete: (notification: NotificationSchema) => Promise<unknown>;
}) {
return (
<div
className={cn(
"rounded-2xl border border-border/70 bg-background/75 p-3 text-right transition hover:border-primary/30 hover:bg-muted/35",
!notification.is_seen && "border-primary/30 bg-primary/5",
)}
>
<div className="flex items-start justify-between gap-3">
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0 rounded-full text-muted-foreground hover:text-destructive"
onClick={() => void onDelete(notification)}
>
<Trash2 className="h-4 w-4" />
</Button>
<button
type="button"
onClick={() => void onOpen(notification)}
className="min-w-0 flex-1 space-y-1 text-right"
>
<div className="flex items-center justify-end gap-2">
{!notification.is_seen ? (
<span className="h-2.5 w-2.5 rounded-full bg-primary" />
) : null}
<p className="truncate font-semibold">{notification.title}</p>
</div>
<p className="line-clamp-2 text-sm text-muted-foreground">
{notification.message}
</p>
<p className="text-xs text-muted-foreground/80">
{formatJalali(notification.created_at, false)}
</p>
</button>
</div>
</div>
);
}
export default function NotificationsBell() {
const {
notifications,
unreadCount,
totalCount,
hasMore,
isLoading,
isLoadingMore,
connectionStatus,
loadMore,
markAllAsSeen,
deleteNotification,
openNotification,
} = useNotifications();
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="relative h-10 w-10 rounded-full border-0 bg-transparent shadow-none transition hover:bg-background/45 hover:shadow-sm"
aria-label="اعلان‌ها"
>
<Bell className="h-4 w-4" />
{unreadCount > 0 ? (
<span className="absolute -left-1 -top-1 flex min-w-5 items-center justify-center rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground">
{unreadCount > 9 ? "9+" : unreadCount}
</span>
) : null}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[min(92vw,26rem)] rounded-[1.5rem] border border-border/70 bg-background/95 p-0 shadow-xl backdrop-blur-xl" align="end" sideOffset={12}>
<div className="border-b border-border/70 px-4 py-4 text-right">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Badge variant={connectionStatus === "connected" ? "default" : "secondary"}>
{connectionLabels[connectionStatus]}
</Badge>
{unreadCount > 0 ? (
<Button
type="button"
variant="ghost"
size="sm"
className="rounded-full"
onClick={() => void markAllAsSeen()}
>
<CheckCheck className="ml-2 h-4 w-4" />
خواندن همه
</Button>
) : null}
</div>
<div>
<p className="font-semibold">اعلانها</p>
<p className="mt-1 text-xs text-muted-foreground">
{totalCount > 0 ? `${totalCount} مورد ثبت شده` : "هنوز اعلانی ندارید."}
</p>
</div>
</div>
</div>
<ScrollArea className="max-h-[24rem] px-4 py-4">
<div className="space-y-3">
{isLoading ? (
<div className="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
در حال بارگذاری اعلانها...
</div>
) : notifications.length ? (
notifications.map((notification) => (
<NotificationItem
key={notification.id}
notification={notification}
onOpen={openNotification}
onDelete={deleteNotification}
/>
))
) : (
<div className="rounded-2xl border border-dashed border-border/70 bg-muted/20 px-4 py-10 text-center text-sm text-muted-foreground">
اعلان تازهای برای شما ثبت نشده است.
</div>
)}
</div>
</ScrollArea>
{hasMore ? (
<div className="border-t border-border/70 px-4 py-3">
<Button
type="button"
variant="secondary"
className="w-full rounded-2xl"
onClick={() => void loadMore()}
disabled={isLoadingMore}
>
{isLoadingMore ? (
<>
<Loader2 className="ml-2 h-4 w-4 animate-spin" />
در حال بارگذاری...
</>
) : (
"نمایش موارد بیشتر"
)}
</Button>
</div>
) : null}
</PopoverContent>
</Popover>
);
}