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

@@ -2,14 +2,11 @@
import { Bell } from "lucide-react";
import Link from "next/link";
import { useState, type ReactElement } from "react";
import { useEffect, type ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { logTypeLabel } from "@/features/wallet/wallet-logs-block";
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
import { playerHeaderControl } from "@/lib/player-spacing";
import { formatMinorAsCurrency } from "@/lib/money";
import { cn } from "@/lib/utils";
type PlayerNotificationBellProps = {
@@ -19,89 +16,26 @@ type PlayerNotificationBellProps = {
/** 顶栏铃铛待对账划转等提醒Popover 右上角展开) */
export function PlayerNotificationBell({ className }: PlayerNotificationBellProps): ReactElement {
const { t } = useTranslation("common");
const { t: tp } = useTranslation("player");
const [open, setOpen] = useState(false);
const { pending, hasPending, loading, refresh } = usePendingWalletReconcile();
const { hasUnread, refresh } = usePendingWalletReconcile();
useEffect(() => {
void refresh();
}, [refresh]);
return (
<Popover
open={open}
onOpenChange={(next) => {
setOpen(next);
if (next) void refresh();
}}
<Link
href="/notifications"
className={cn(
playerHeaderControl,
"relative size-8 rounded-full text-[#1d57b7] hover:bg-[#f4f7fb]",
className,
)}
aria-label={t("navigation.notifications")}
>
<PopoverTrigger
render={
<button
type="button"
className={cn(
playerHeaderControl,
"relative size-8 rounded-full text-[#1d57b7] hover:bg-[#f4f7fb]",
className,
)}
aria-label={t("navigation.notifications")}
>
<Bell className="size-4" aria-hidden />
{hasPending ? (
<span className="absolute right-1.5 top-1.5 size-1.5 rounded-full bg-[#ff143d]" />
) : null}
</button>
}
/>
<PopoverContent
align="end"
sideOffset={8}
className="w-[min(20rem,calc(100vw-1.5rem))] border-[#dce7f7] p-0 shadow-[0_16px_40px_rgba(15,23,42,0.14)]"
>
<div className="border-b border-[#edf2f9] px-3 py-2.5">
<p className="text-sm font-black text-[#0b3f96]">{t("navigation.notifications")}</p>
</div>
{loading && pending.length === 0 ? (
<p className="px-3 py-6 text-center text-xs text-slate-500">{tp("actions.loading")}</p>
) : null}
{!loading && pending.length === 0 ? (
<p className="px-3 py-6 text-center text-xs text-slate-500">
{tp("notifications.empty")}
</p>
) : null}
{pending.length > 0 ? (
<div className="max-h-[min(50vh,320px)] overflow-y-auto p-2">
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3">
<p className="text-sm font-black text-amber-700">{tp("wallet.pendingTitle")}</p>
<p className="mt-1 text-xs leading-relaxed text-amber-800/90">
{tp("wallet.pendingDescription")}
</p>
<ul className="mt-2 space-y-2">
{pending.map((p) => (
<li
key={p.transfer_no}
className="flex flex-wrap items-baseline justify-between gap-1 border-b border-dashed border-amber-200/80 py-2 last:border-0"
>
<span className="text-sm text-amber-950">
{logTypeLabel(p.type, tp)}{" "}
{formatMinorAsCurrency(p.amount, p.currency_code)}
</span>
<span className="text-xs font-semibold text-amber-700">
{tp("wallet.pendingStatus")}
</span>
</li>
))}
</ul>
</div>
<Link
href="/wallet/logs"
className="mt-2 block rounded-lg px-2 py-2 text-center text-xs font-bold text-[#0b56b7] hover:bg-[#f8fbff]"
onClick={() => setOpen(false)}
>
{tp("wallet.logs", { defaultValue: "查看流水" })}
</Link>
</div>
) : null}
</PopoverContent>
</Popover>
<Bell className="size-4" aria-hidden />
{hasUnread ? (
<span className="absolute right-1.5 top-1.5 size-1.5 rounded-full bg-[#ff143d]" />
) : null}
</Link>
);
}