feat: enhance player panel and draw status handling
- Refactored PlayerPanel layout for improved title positioning and responsiveness. - Added new function to check if betting is blocked based on hall status. - Updated HallDrawPanel to utilize the new betting status check and display appropriate messages. - Enhanced i18n support with new notices for review and non-bettable states across multiple languages.
This commit is contained in:
@@ -6,7 +6,6 @@ import type { ReactNode } from "react";
|
||||
import { Bell, ChevronLeft } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { CurrencySwitcher } from "@/components/currency-switcher";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import {
|
||||
playerHeaderControl,
|
||||
@@ -48,10 +47,10 @@ export function PlayerPanel({
|
||||
<header
|
||||
className={cn(
|
||||
playerPageHeader,
|
||||
"sticky top-0 z-50 overflow-visible bg-white/95 backdrop-blur pt-2 pb-2 -mx-3 px-3",
|
||||
"sticky top-0 z-50 bg-white/95 backdrop-blur pt-2 pb-2 -mx-3 px-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 justify-start">
|
||||
<div className="relative z-[1] flex min-w-0 max-w-[38%] shrink-0 justify-start">
|
||||
<Link
|
||||
href={backHref}
|
||||
className={cn(
|
||||
@@ -64,22 +63,13 @@ export function PlayerPanel({
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 max-w-[min(52vw,12rem)] justify-self-center text-center">
|
||||
<h1 className="truncate text-base font-black leading-tight text-[#0b3f96]">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
<h1
|
||||
className="pointer-events-none absolute top-1/2 right-[5.25rem] left-[5.25rem] z-0 -translate-y-1/2 truncate text-center text-base font-black leading-tight text-[#0b3f96]"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div className="flex min-w-0 items-center justify-end 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]",
|
||||
)}
|
||||
/>
|
||||
<div className="relative z-[1] flex min-w-0 max-w-[48%] shrink-0 items-center justify-end gap-0.5">
|
||||
<LanguageSwitcher
|
||||
variant="minimal"
|
||||
menuAlign="end"
|
||||
|
||||
@@ -14,6 +14,11 @@ export function isHallSealedCountdownUi(status: string): boolean {
|
||||
return status === "closing" || status === "closed";
|
||||
}
|
||||
|
||||
/** 不可下注时展示阻断提示条 */
|
||||
export function isHallBlockedForBetting(status: string): boolean {
|
||||
return status !== "open";
|
||||
}
|
||||
|
||||
/** 对齐界面文档 §4.2 状态文案与 PRD 期号状态 */
|
||||
export function drawStatusHud(status: string): DrawStatusHud {
|
||||
switch (status) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { drawStatusHud, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
|
||||
import { drawStatusHud, isHallBlockedForBetting, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
|
||||
import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
|
||||
import { formatSecondsClock } from "@/lib/format-gmt";
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
@@ -64,7 +64,9 @@ function CloseTime({
|
||||
let seconds = 0;
|
||||
let label = t("draw.closesIn");
|
||||
|
||||
if (hud.countdownKind === "close") {
|
||||
if (hud.countdownKind === "none") {
|
||||
label = t(hud.labelKey, { defaultValue: hud.labelKey });
|
||||
} else if (hud.countdownKind === "close") {
|
||||
seconds = Math.max(0, Math.ceil(((payload.close_time ? Date.parse(payload.close_time) : 0) - nowMs) / 1000));
|
||||
} else if (hud.countdownKind === "draw") {
|
||||
seconds = Math.max(0, Math.ceil(((payload.draw_time ? Date.parse(payload.draw_time) : 0) - nowMs) / 1000));
|
||||
@@ -77,7 +79,7 @@ function CloseTime({
|
||||
return (
|
||||
<>
|
||||
<span className="text-lg font-black tabular-nums text-[#ff143d]">
|
||||
{formatSecondsClock(seconds)}
|
||||
{hud.countdownKind === "none" ? "--:--" : formatSecondsClock(seconds)}
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] text-slate-500">{label}</span>
|
||||
</>
|
||||
@@ -127,6 +129,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
|
||||
|
||||
const hud = drawStatusHud(display.status);
|
||||
const sealedUi = isHallSealedCountdownUi(display.status);
|
||||
const blockedUi = isHallBlockedForBetting(display.status);
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
@@ -163,10 +166,14 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{sealedUi ? (
|
||||
{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">
|
||||
<TimerReset className="size-4" aria-hidden />
|
||||
{t("draw.sealedNotice")}
|
||||
<TimerReset className="size-4 shrink-0" aria-hidden />
|
||||
{display.status === "review"
|
||||
? t("draw.reviewNotice")
|
||||
: sealedUi
|
||||
? t("draw.sealedNotice")
|
||||
: t("draw.notBettableNotice")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between border-t border-[#eef3f9] bg-[#fbfdff] px-3 py-1.5 text-[11px] text-slate-500">
|
||||
|
||||
@@ -53,17 +53,6 @@ const resources = {
|
||||
Record<(typeof namespaces)[number], Record<string, unknown>>
|
||||
>;
|
||||
|
||||
function resolveInitialLanguage(): AppLanguage {
|
||||
// SSR 始终使用默认语言,避免首屏渲染依赖浏览器状态
|
||||
if (typeof window === "undefined") {
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
const stored = window.localStorage.getItem("i18nextLng");
|
||||
const preferred = normalizeLanguage(stored ?? window.navigator.language);
|
||||
return preferred;
|
||||
}
|
||||
|
||||
export function syncDocumentLanguage(lang: AppLanguage): void {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
@@ -83,17 +72,15 @@ export function syncPreferredLanguage(): void {
|
||||
}
|
||||
|
||||
if (!i18n.isInitialized) {
|
||||
const initialLng = resolveInitialLanguage();
|
||||
|
||||
void i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
supportedLngs: ["en", "ne", "zh"],
|
||||
defaultNS: "common",
|
||||
ns: [...namespaces],
|
||||
/** 始终先用默认语言完成 SSR / 首次 hydration,避免首屏文本不一致 */
|
||||
/** 与 SSR 一致:首屏固定默认语言,hydration 后再由 syncPreferredLanguage 切换 */
|
||||
load: "languageOnly",
|
||||
lng: initialLng,
|
||||
lng: DEFAULT_LANGUAGE,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"hall": "Betting Hall",
|
||||
"noIssue": "No available issue. Please try again later.",
|
||||
"sealedNotice": "Closed. Please wait for the next issue.",
|
||||
"reviewNotice": "Draw results are pending admin review. The next issue opens after publish.",
|
||||
"notBettableNotice": "Betting is not available right now. Please try again later.",
|
||||
"status": {
|
||||
"pending": "Not started",
|
||||
"open": "Open",
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"hall": "बेटिङ हल",
|
||||
"noIssue": "उपलब्ध इश्यू छैन। कृपया पछि प्रयास गर्नुहोस्।",
|
||||
"sealedNotice": "बन्द भयो। कृपया अर्को इश्यू पर्खनुहोस्।",
|
||||
"reviewNotice": "नतिजा प्रशासक समीक्षामा छ। प्रकाशन पछि अर्को इश्यू खुल्छ।",
|
||||
"notBettableNotice": "अहिले बाजी लगाउन मिल्दैन। कृपया पछि प्रयास गर्नुहोस्।",
|
||||
"status": {
|
||||
"pending": "सुरु भएको छैन",
|
||||
"open": "खुला",
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"hall": "下注大厅",
|
||||
"noIssue": "暂无可用期号,请稍后再试",
|
||||
"sealedNotice": "已封盘,请等待下一期。",
|
||||
"reviewNotice": "开奖结果待后台审核发布,审核通过后将开启下一期。",
|
||||
"notBettableNotice": "当前不可下注,请稍后再试。",
|
||||
"status": {
|
||||
"pending": "未开始",
|
||||
"open": "可下注",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** 玩家端统一紧凑间距(页面壳、区块堆叠、区块间距) */
|
||||
export const playerPageInset = "px-3 pb-6 pt-2";
|
||||
/** 顶栏:左右等宽列 + 居中标题,避免返回键与标题高低不齐 */
|
||||
/** 顶栏:左右分列 + 标题绝对居中,避免右侧控件挤压/遮挡标题 */
|
||||
export const playerPageHeader =
|
||||
"mb-2 grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-x-2";
|
||||
"relative mb-2 flex min-h-9 items-center justify-between gap-2";
|
||||
/** 顶栏左右控件统一高度 */
|
||||
export const playerHeaderControl =
|
||||
"inline-flex h-8 shrink-0 items-center justify-center";
|
||||
|
||||
@@ -28,8 +28,8 @@ interface ErrorState {
|
||||
}
|
||||
|
||||
export const useErrorStore = create<ErrorState>((set, get) => ({
|
||||
// 初始状态
|
||||
isOffline: typeof window !== "undefined" ? !navigator.onLine : false,
|
||||
// 首屏与 SSR 一致为 false;真实离线状态由 useNetworkStatus 在 mount 后同步
|
||||
isOffline: false,
|
||||
isServerError: false,
|
||||
serverErrorMessage: "服务器暂时不可用,请稍后重试",
|
||||
currentErrorType: null,
|
||||
|
||||
Reference in New Issue
Block a user