feat(frontend): rebuild auth around mobile-first flow
This commit is contained in:
177
src/components/NotificationsBell.tsx
Normal file
177
src/components/NotificationsBell.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
"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="outline"
|
||||
size="icon"
|
||||
className="relative h-10 w-10 rounded-full border-border/70 bg-background/85"
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user