实现 resolvePostMessageTargetOrigin,优化 iframe 消息通信中的目标来源(origin)解析与校验。 更新 IframeBridge:支持定期刷新允许的来源列表,并优化消息事件管理机制。 重构 usePendingWalletReconcile:优化待对账通知的获取与缓存逻辑,提升性能与用户体验。 增强 NotificationsScreen:新增待对账通知内容,并优化界面展示效果。 更新英文、尼泊尔语与中文语言包,新增待对账通知相关翻译文案。
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
"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 { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
|
import { formatLocalDateTime } from "@/lib/format-local-datetime";
|
|
import { formatMinorAsCurrency } from "@/lib/money";
|
|
import {
|
|
pendingReconcileDescriptionKey,
|
|
pendingReconcileTitleKey,
|
|
} from "@/lib/pending-reconcile-notification";
|
|
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">
|
|
<p className="rounded-xl border border-amber-200 bg-amber-50/90 px-3 py-2.5 text-xs leading-relaxed text-amber-900">
|
|
{t("notifications.subtitle")}
|
|
</p>
|
|
|
|
<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-amber-900">
|
|
{t(pendingReconcileTitleKey(item.type))}
|
|
</p>
|
|
<p className="mt-0.5 text-xs text-slate-500">
|
|
{formatLocalDateTime(item.created_at)}
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 flex-col items-end gap-1">
|
|
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-bold text-amber-800">
|
|
{t("notifications.pendingBadge")}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"text-[10px] font-semibold",
|
|
cardRead ? "text-slate-400" : "text-amber-700",
|
|
)}
|
|
>
|
|
{cardRead ? t("notifications.read") : t("notifications.unread")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="mt-2 text-xs leading-relaxed text-amber-950/85">
|
|
{t(pendingReconcileDescriptionKey(item.type))}
|
|
</p>
|
|
|
|
<p className="mt-2 text-sm text-slate-700">
|
|
<span className="text-xs font-medium text-slate-500">
|
|
{t("notifications.amountLabel")}{" "}
|
|
</span>
|
|
{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.viewLogs")}
|
|
</Link>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : null}
|
|
</div>
|
|
</PlayerPanel>
|
|
);
|
|
}
|