feat: refine wallet credit mode detection and improve UI consistency across multiple screens
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Updated wallet screens to check both balance and profile for credit funding mode, ensuring consistent credit player detection
- Removed currency switcher from hall header and schedule timezone display from draw panel for cleaner UI
- Simplified ticket orders status filter to single-select mode with improved visual hierarchy
- Added sizes="100vw" attribute to entry and login background images for proper
This commit is contained in:
2026-06-24 14:35:59 +08:00
parent cff6470031
commit 4d24906d45
8 changed files with 34 additions and 60 deletions

View File

@@ -13,12 +13,7 @@ import {
} from "@/features/draw/draw-status-meta"; } from "@/features/draw/draw-status-meta";
import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live"; import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
import { formatSecondsClock } from "@/lib/format-gmt"; import { formatSecondsClock } from "@/lib/format-gmt";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone"; import { formatPlayerInstant } from "@/lib/player-datetime";
import {
formatPlayerInstant,
formatLotteryScheduleClock,
getBrowserTimeZoneLabel,
} from "@/lib/player-datetime";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { DrawCurrentPayload } from "@/types/api/draw-current"; import type { DrawCurrentPayload } from "@/types/api/draw-current";
@@ -190,7 +185,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
<div className="flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center"> <div className="flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
<div className="min-w-0 max-w-full overflow-x-auto"> <div className="min-w-0 max-w-full overflow-x-auto">
<p className="text-[11px] font-semibold text-slate-500">{t("draw.issueNo")}</p> <p className="text-[11px] font-semibold text-slate-500">{t("draw.issueNo")}</p>
<p className="whitespace-nowrap text-sm font-black tabular-nums text-[#ff143d]"> <p className="whitespace-nowrap text-xs font-black tabular-nums text-[#ff143d] sm:text-sm">
{display.draw_no} {display.draw_no}
</p> </p>
</div> </div>
@@ -214,17 +209,6 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
/> />
</div> </div>
</div> </div>
{display.schedule_now ? (
<div className="border-t border-[#eef3f9] bg-[#fbfdff] px-3 py-1.5 text-center text-[11px] text-slate-500">
{t("draw.scheduleNow", {
now: formatLotteryScheduleClock(
display.schedule_now,
display.schedule_timezone ?? LOTTERY_SCHEDULE_TIMEZONE,
),
tz: getBrowserTimeZoneLabel(),
})}
</div>
) : null}
{blockedUi ? ( {blockedUi ? (
<div className="flex items-center gap-2 border-t border-red-100 bg-red-50 px-3 py-2 text-xs font-medium text-red-600"> <div className="flex items-center gap-2 border-t border-red-100 bg-red-50 px-3 py-2 text-xs font-medium text-red-600">
<TimerReset className="size-4 shrink-0" aria-hidden /> <TimerReset className="size-4 shrink-0" aria-hidden />

View File

@@ -5,7 +5,6 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CurrencySwitcher } from "@/components/currency-switcher";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { HallBettingGrid } from "@/features/hall/hall-betting-grid"; import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style"; import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style";
@@ -35,7 +34,7 @@ export function HallScreen() {
<header <header
className={cn( className={cn(
playerPageHeader, playerPageHeader,
"sticky z-20 mb-2 flex min-h-9 items-center gap-2 overflow-visible bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur", "sticky z-20 mb-2 flex min-h-9 items-center gap-2 overflow-visible bg-white pt-2 pb-2 -mx-3 px-3",
)} )}
style={stickyTopStyle} style={stickyTopStyle}
> >
@@ -50,15 +49,6 @@ export function HallScreen() {
/> />
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<CurrencySwitcher
variant="minimal"
menuAlign="end"
showLabel
className={cn(
playerHeaderControl,
"rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:py-0 [&_button]:text-xs [&_button]:font-bold [&_button]:text-[#0b3f96]",
)}
/>
<LanguageSwitcher <LanguageSwitcher
variant="minimal" variant="minimal"
menuAlign="end" menuAlign="end"

View File

@@ -20,6 +20,7 @@ import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useNetworkConnectionStore } from "@/stores/network-connection-store"; import { useNetworkConnectionStore } from "@/stores/network-connection-store";
import { usePlayerSessionStore } from "@/stores/player-session-store";
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency]; const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
@@ -62,7 +63,8 @@ export function HallWalletStrip() {
}, [isDegraded]); }, [isDegraded]);
const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0); const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0);
const isCreditPlayer = isCreditFundingPlayer(balance); const profile = usePlayerSessionStore((s) => s.profile);
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
const balanceMinor = Number(balance?.balance ?? 0); const balanceMinor = Number(balance?.balance ?? 0);
const headlineMinor = isCreditPlayer ? availableMinor : balanceMinor; const headlineMinor = isCreditPlayer ? availableMinor : balanceMinor;
const transferInLotteryMinor = isCreditPlayer ? availableMinor : balanceMinor; const transferInLotteryMinor = isCreditPlayer ? availableMinor : balanceMinor;

View File

@@ -3,7 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CalendarRange, Check, ChevronDown, Search } from "lucide-react"; import { CalendarRange, ChevronDown, Search } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getDrawCurrent } from "@/api/draw"; import { getDrawCurrent } from "@/api/draw";
@@ -342,8 +342,8 @@ export function TicketOrdersListScreen() {
<span className="flex min-w-0 items-center gap-1.5"> <span className="flex min-w-0 items-center gap-1.5">
<span className="shrink-0">{t("orders.status")}</span> <span className="shrink-0">{t("orders.status")}</span>
{queryStatuses.length ? ( {queryStatuses.length ? (
<span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[#0b56b7] px-1 text-[11px] font-black text-white"> <span className="truncate text-[#0b56b7]">
{queryStatuses.length} {t(`ticketStatus.${queryStatuses[0]}`, { defaultValue: queryStatuses[0] })}
</span> </span>
) : null} ) : null}
</span> </span>
@@ -353,40 +353,33 @@ export function TicketOrdersListScreen() {
</Button> </Button>
} }
/> />
<PopoverContent align="start" className="w-56 border-[#dce7f7] p-2 shadow-[0_16px_40px_rgba(15,23,42,0.14)]"> <PopoverContent align="start" className="w-56 border-[#dce7f7] p-1.5 shadow-[0_16px_40px_rgba(15,23,42,0.14)]">
<div className="space-y-1"> <div className="space-y-0.5">
<p className="px-1 pb-1 text-[11px] font-bold text-[#32518d]"> <button
{t("orders.statusFilter")} type="button"
</p> className={cn(
"flex w-full items-center rounded-md px-2 py-1.5 text-left text-[13px] font-semibold transition-colors",
queryStatuses.length === 0 ? "bg-[#eaf2ff] text-[#0b56b7]" : "text-[#32518d] hover:bg-[#f8fbff]",
)}
onClick={() => setQueryStatuses([])}
>
{t("actions.all", { defaultValue: "全部" })}
</button>
{STATUS_OPTIONS.map((status) => { {STATUS_OPTIONS.map((status) => {
const checked = queryStatuses.includes(status); const checked = queryStatuses[0] === status;
return ( return (
<button <button
key={status} key={status}
type="button" type="button"
className={cn( className={cn(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs font-semibold transition-colors", "flex w-full items-center rounded-md px-2 py-1.5 text-left text-[13px] font-semibold transition-colors",
checked ? "bg-[#eaf2ff] text-[#0b56b7]" : "text-[#32518d] hover:bg-[#f8fbff]", checked ? "bg-[#eaf2ff] text-[#0b56b7]" : "text-[#32518d] hover:bg-[#f8fbff]",
)} )}
onClick={() => { onClick={() => {
setQueryStatuses((current) => setQueryStatuses(checked ? [] : [status]);
current.includes(status) setStatusOpen(false);
? current.filter((s) => s !== status)
: [...current, status],
);
}} }}
> >
<span
className={cn(
"flex size-3.5 shrink-0 items-center justify-center rounded border",
checked
? "border-[#0b56b7] bg-[#0b56b7] text-white"
: "border-slate-300 bg-white",
)}
aria-hidden
>
{checked ? <Check className="size-2.5" strokeWidth={3} /> : null}
</span>
<span className="truncate">{t(`ticketStatus.${status}`, { defaultValue: status })}</span> <span className="truncate">{t(`ticketStatus.${status}`, { defaultValue: status })}</span>
</button> </button>
); );

View File

@@ -368,6 +368,7 @@ export function EntryGate() {
src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"} src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"}
alt={t("header.backgroundAlt")} alt={t("header.backgroundAlt")}
fill fill
sizes="100vw"
className="object-cover object-center" className="object-cover object-center"
priority priority
/> />

View File

@@ -151,9 +151,9 @@ export function PlayerLoginScreen(): React.ReactElement {
return ( return (
<div className="relative flex min-h-dvh flex-col bg-white"> <div className="relative flex min-h-dvh flex-col bg-white">
<div className="relative h-[45vh] min-h-[200px] shrink-0 overflow-hidden bg-red-600"> <div className="relative h-[45vh] min-h-[200px] shrink-0 overflow-hidden bg-[#f8fafc]">
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority /> <Image src="/entry/image1.png" alt="" fill sizes="100vw" className="object-cover object-center" priority />
</div> </div>
<div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3"> <div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3">
<LanguageSwitcher variant="header" showFlag={false} /> <LanguageSwitcher variant="header" showFlag={false} />

View File

@@ -12,6 +12,7 @@ import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode"; import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { formatWalletClientError } from "@/lib/wallet-api-error"; import { formatWalletClientError } from "@/lib/wallet-api-error";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs"; import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
const WALLET_LOGS_PAGE_SIZE = 10; const WALLET_LOGS_PAGE_SIZE = 10;
@@ -25,7 +26,8 @@ export function WalletLogsScreen() {
const [logsLoading, setLogsLoading] = useState(false); const [logsLoading, setLogsLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [creditMode, setCreditMode] = useState(false); const profile = usePlayerSessionStore((s) => s.profile);
const [creditMode, setCreditMode] = useState(() => isCreditFundingPlayer(profile));
const loadMoreRef = useRef<HTMLDivElement | null>(null); const loadMoreRef = useRef<HTMLDivElement | null>(null);
const fetchPassRef = useRef(true); const fetchPassRef = useRef(true);

View File

@@ -22,6 +22,7 @@ import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { formatMinorAsCurrency } from "@/lib/money"; import { formatMinorAsCurrency } from "@/lib/money";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference"; import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { formatWalletClientError } from "@/lib/wallet-api-error"; import { formatWalletClientError } from "@/lib/wallet-api-error";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import type { WalletBalanceData } from "@/types/api/wallet-balance"; import type { WalletBalanceData } from "@/types/api/wallet-balance";
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs"; import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
@@ -163,7 +164,8 @@ export function WalletScreen() {
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false; const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
const isCreditPlayer = isCreditFundingPlayer(balance); const profile = usePlayerSessionStore((s) => s.profile);
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
const displayMinor = isCreditPlayer const displayMinor = isCreditPlayer
? Number(balance?.available_balance ?? 0) ? Number(balance?.available_balance ?? 0)
: Number(balance?.balance ?? 0); : Number(balance?.balance ?? 0);