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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user