feat: enhance notification system and integrate DOMPurify for HTML sanitization

- Added `dompurify` for improved HTML sanitization in the play rules.
- Updated `PlayerNotificationBell` component to handle unread notifications and improve user interaction.
- Enhanced `usePendingWalletReconcile` hook to manage unread notifications and mark them as read.
- Added new translations for notification-related texts in English, Nepali, and Chinese to support multilingual users.
This commit is contained in:
2026-06-01 11:32:18 +08:00
parent 10bee1b857
commit aeaba5eea3
10 changed files with 269 additions and 98 deletions

View File

@@ -0,0 +1,119 @@
"use client";
import Link from "next/link";
import { BellRing, CheckCheck } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { PlayerPanel } from "@/components/layout/player-panel";
import { logTypeLabel } from "@/features/wallet/wallet-logs-block";
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
import { formatLocalDateTime } from "@/lib/format-local-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { cn } from "@/lib/utils";
export function NotificationsScreen() {
const { t } = useTranslation("player");
const { pending, unreadPending, unreadCount, loading, markAsRead, markAllAsRead } =
usePendingWalletReconcile();
const unreadSet = new Set(unreadPending.map((item) => item.transfer_no));
return (
<PlayerPanel title={t("notifications.title")} backHref="/hall">
<div className="space-y-3">
<div className="flex items-center justify-between rounded-xl border border-[#dce7f7] bg-[#f8fbff] px-3 py-2.5">
<p className="text-sm font-semibold text-[#0b3f96]">
{t("notifications.unreadCount", { count: unreadCount })}
</p>
<Button
type="button"
size="sm"
variant="ghost"
className="h-8 px-2 text-xs font-bold text-[#0b56b7] hover:bg-[#ebf2ff]"
onClick={markAllAsRead}
disabled={pending.length === 0 || unreadCount === 0}
>
<CheckCheck className="mr-1 size-3.5" aria-hidden />
{t("notifications.markAllRead")}
</Button>
</div>
{loading && pending.length === 0 ? (
<div className="rounded-xl border border-[#dce7f7] bg-white px-4 py-8 text-center text-sm text-slate-500">
{t("actions.loading")}
</div>
) : null}
{!loading && pending.length === 0 ? (
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-white px-4 py-10 text-center">
<BellRing className="mx-auto size-5 text-slate-400" aria-hidden />
<p className="mt-2 text-sm text-slate-500">{t("notifications.empty")}</p>
</div>
) : null}
{pending.length > 0 ? (
<ul className="space-y-2">
{pending.map((item) => {
const cardRead = !unreadSet.has(item.transfer_no);
return (
<li
key={item.transfer_no}
className={cn(
"rounded-xl border px-3 py-3 transition-colors",
cardRead
? "border-[#e4eaf4] bg-white"
: "border-amber-200 bg-amber-50/80",
)}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="text-sm font-bold text-[#0b3f96]">
{logTypeLabel(item.type, t)}
</p>
<p className="mt-0.5 text-xs text-slate-500">
{formatLocalDateTime(item.created_at)}
</p>
</div>
<span
className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-[11px] font-bold",
cardRead
? "bg-slate-100 text-slate-500"
: "bg-amber-100 text-amber-700",
)}
>
{cardRead ? t("notifications.read") : t("notifications.unread")}
</span>
</div>
<p className="mt-2 text-sm text-slate-700">
{formatMinorAsCurrency(item.amount, item.currency_code)}
</p>
<div className="mt-3 flex items-center gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="h-8 rounded-full border-[#dce7f7] px-3 text-xs font-semibold text-[#0b56b7]"
onClick={() => markAsRead(item.transfer_no)}
>
{t("notifications.markRead")}
</Button>
<Link
href="/wallet/logs"
className="inline-flex h-8 items-center rounded-full bg-[#07459f] px-3 text-xs font-semibold text-white hover:bg-[#063b88]"
onClick={() => markAsRead(item.transfer_no)}
>
{t("notifications.open")}
</Link>
</div>
</li>
);
})}
</ul>
) : null}
</div>
</PlayerPanel>
);
}