feat: implement global pull-to-refresh functionality and refine UI typography and layout scaling for improved mobile UX.
Some checks failed
lotteryfront CI / build (push) Has been cancelled

This commit is contained in:
2026-06-24 11:46:49 +08:00
parent f46ccaac6e
commit cff6470031
24 changed files with 253 additions and 60 deletions

View File

@@ -265,7 +265,7 @@ export function HallBetPreviewDialog({
</div>
{summary ? (
<div className="grid grid-cols-2 overflow-hidden rounded-xl border border-[#dfe8f6] bg-[#f8fbff] text-center text-xs sm:grid-cols-4">
<div className="grid grid-cols-2 overflow-hidden rounded-xl border border-[#dfe8f6] bg-[#f8fbff] text-center text-xs">
<div className="border-r border-[#dfe8f6] px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.totalBet")}</p>
<p className="mt-1 font-black tabular-nums text-[#0b3f96]">
@@ -319,7 +319,7 @@ export function HallBetPreviewDialog({
</div>
</div>
<div className="grid shrink-0 grid-cols-2 gap-2.5 border-t border-[#e8eef7] bg-white p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] sm:p-4">
<div className="grid shrink-0 grid-cols-2 gap-2.5 border-t border-[#e8eef7] bg-white p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]">
<Button
type="button"
variant="outline"

View File

@@ -174,7 +174,7 @@ export function HallBetResultDialog({
</span>
</div>
<div className="grid gap-2 rounded-lg border border-[#e8eef7] bg-white px-3 py-2.5 text-xs text-slate-600 sm:grid-cols-2">
<div className="grid grid-cols-2 gap-2 rounded-lg border border-[#e8eef7] bg-white px-3 py-2.5 text-xs text-slate-600">
<p>
{t("hall.result.orderNo")}:{" "}
<span className="font-mono font-black text-[#0b3f96]">{data.order_no}</span>
@@ -232,7 +232,7 @@ export function HallBetResultDialog({
<span className="block font-mono text-base font-black text-slate-950">
{item.number}
</span>
<span className="block truncate text-[10px] text-slate-400">
<span className="block truncate text-[11px] text-slate-400">
{item.ticket_no}
</span>
</td>
@@ -287,7 +287,7 @@ export function HallBetResultDialog({
</div>
</div>
<div className="grid shrink-0 grid-cols-2 gap-2.5 border-t border-[#e8eef7] bg-white p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] sm:p-4">
<div className="grid shrink-0 grid-cols-2 gap-2.5 border-t border-[#e8eef7] bg-white p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]">
<Button
type="button"
nativeButton={false}

View File

@@ -1301,7 +1301,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
<button
key={`fav-${number}`}
type="button"
className="inline-flex h-7 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
className="inline-flex h-8 touch-manipulation items-center gap-1 rounded-full border border-[#ffd7db] bg-[#fff3f5] px-2.5 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#ffe9ed]"
onPointerDown={() => {
const current = holdFavoriteRef.current;
current.number = number;
@@ -1333,7 +1333,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
}}
>
{number}
<span className="text-[10px] font-medium opacity-70">
<span className="text-[11px] font-medium opacity-70">
{t("hall.quickFill.tapHold")}
</span>
</button>
@@ -1350,14 +1350,14 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
<button
key={`his-${number}`}
type="button"
className="inline-flex h-7 min-w-10 items-center justify-center rounded-full border border-[#d7e5f8] bg-[#f8fbff] px-3 text-xs font-bold text-[#07459f] transition-colors hover:border-[#b9d0f3] hover:bg-[#eef6ff]"
className="inline-flex h-8 min-w-10 items-center justify-center rounded-full border border-[#d7e5f8] bg-[#f8fbff] px-3 text-xs font-bold text-[#07459f] transition-colors hover:border-[#b9d0f3] hover:bg-[#eef6ff]"
onClick={() => fillCurrentRow(number)}
>
{number}
</button>
))
) : (
<span className="inline-flex h-7 items-center rounded-full border border-dashed border-slate-200 bg-slate-50 px-3 text-xs text-slate-400">
<span className="inline-flex h-8 items-center rounded-full border border-dashed border-slate-200 bg-slate-50 px-3 text-xs text-slate-400">
{t("hall.quickFill.emptyHistory")}
</span>
)}
@@ -1405,7 +1405,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
)}
>
<span className="block text-xs">{t("hall.table.number", { defaultValue: "Number" })}</span>
<span className="mt-0.5 block font-mono text-[10px] font-medium text-[#9aa8bd]">
<span className="mt-0.5 block font-mono text-[11px] font-medium text-[#9aa8bd]">
{numberPlaceholder}
</span>
</th>
@@ -1414,7 +1414,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
key={column.key}
className="w-[4.5rem] min-w-[4.5rem] max-w-[4.5rem] px-0.5 py-2 text-center font-bold"
>
<span className="block whitespace-nowrap text-[10px] leading-tight">
<span className="block whitespace-nowrap text-[11px] leading-tight">
{playColumnHeaderLabel(
column.play,
activeCategory as Exclude<HallCategory, "JACKPOT">,
@@ -1422,7 +1422,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
t,
)}
</span>
<span className="mt-0.5 block text-[9px] font-medium text-[#9aa8bd]">
<span className="mt-0.5 block text-[10px] font-medium text-[#9aa8bd]">
{t("hall.table.amountPlaceholder")}
</span>
</th>
@@ -1519,11 +1519,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
)}
/>
{status === "sold_out" ? (
<p className="mt-0.5 text-center text-[9px] font-bold text-slate-500">
<p className="mt-0.5 text-center text-[10px] font-bold text-slate-500">
{t("hall.table.soldOut")}
</p>
) : status === "warning" ? (
<p className="mt-0.5 text-center text-[9px] font-bold text-amber-700">
<p className="mt-0.5 text-center text-[10px] font-bold text-amber-700">
{t("hall.table.warning")}
</p>
) : null}
@@ -1535,7 +1535,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
type="button"
disabled={tableDisabled || rows.length <= 1}
onClick={() => removeRow(row.id)}
className="inline-flex size-6 items-center justify-center rounded-full text-[#e5002c] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
className="inline-flex size-8 items-center justify-center rounded-full text-[#e5002c] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
aria-label={t("actions.deleteRow", { row: index + 1 })}
>
<Trash2 className="size-3.5" aria-hidden />

View File

@@ -52,7 +52,7 @@ function ScheduleAnchorTime({ payload }: { payload: DrawCurrentPayload }) {
<>
<span className="text-lg font-black tabular-nums text-[#0b3f96]">{time}</span>
<span className="mt-1 text-[11px] text-slate-500">{date}</span>
<span className="mt-0.5 text-[10px] text-slate-400">{t(labelKey)}</span>
<span className="mt-0.5 text-[11px] text-slate-400">{t(labelKey)}</span>
</>
);
}
@@ -186,11 +186,11 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
)}
aria-label={t("draw.currentIssue")}
>
<div className="grid grid-cols-1 divide-y divide-[#e7edf6] sm:grid-cols-[1fr_1.05fr_1fr] sm:divide-x sm:divide-y-0">
<div className="grid grid-cols-[1fr_1.05fr_1fr] divide-x divide-[#e7edf6]">
<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">
<p className="text-[11px] font-semibold text-slate-500">{t("draw.issueNo")}</p>
<p className="whitespace-nowrap text-xs font-black tabular-nums text-[#ff143d] sm:text-sm">
<p className="whitespace-nowrap text-sm font-black tabular-nums text-[#ff143d]">
{display.draw_no}
</p>
</div>
@@ -215,7 +215,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
</div>
</div>
{display.schedule_now ? (
<div className="border-t border-[#eef3f9] bg-[#fbfdff] px-3 py-1.5 text-center text-[10px] text-slate-500">
<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,

View File

@@ -270,7 +270,7 @@ export function HallPlayCatalogPanel() {
return (
<Card>
<CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<CardHeader className="flex flex-col gap-2">
<div className="space-y-1">
<CardTitle className="text-base">{t("hall.playCatalog.title")}</CardTitle>
<CardDescription>

View File

@@ -45,11 +45,11 @@ export function HallScreen() {
alt="Nlotto"
width={243}
height={84}
className="h-7 w-auto max-w-[min(100%,160px)] object-contain object-left sm:h-8 sm:max-w-[min(100%,200px)]"
className="h-7 w-auto max-w-[min(100%,160px)] object-contain object-left"
priority
/>
</div>
<div className="flex shrink-0 items-center gap-0.5 sm:gap-1">
<div className="flex shrink-0 items-center gap-1">
<CurrencySwitcher
variant="minimal"
menuAlign="end"
@@ -76,8 +76,8 @@ export function HallScreen() {
)}
aria-label={tp("nav.rules")}
>
<BookOpen className="size-3.5 shrink-0 sm:hidden" aria-hidden />
<span className="hidden sm:inline">{tp("nav.rules")}</span>
<BookOpen className="size-3.5 shrink-0" aria-hidden />
<span>{tp("nav.rules")}</span>
</Link>
</div>
</header>

View File

@@ -137,7 +137,7 @@ function RollingDigit({
return (
<span
className={cn(
"inline-flex min-w-[2.1rem] items-center justify-center rounded-lg border border-[#e8c96a] bg-gradient-to-b from-[#fff9e8] to-[#fff3d4] px-1 py-1 font-mono text-[2rem] font-black leading-none text-[#b8860b] shadow-[inset_0_1px_0_rgba(255,255,255,0.9),0_2px_8px_rgba(201,162,39,0.12)] sm:min-w-[2.5rem] sm:text-[2.35rem]",
"inline-flex min-w-[2.1rem] items-center justify-center rounded-lg border border-[#e8c96a] bg-gradient-to-b from-[#fff9e8] to-[#fff3d4] px-1 py-1 font-mono text-[2rem] font-black leading-none text-[#b8860b] shadow-[inset_0_1px_0_rgba(255,255,255,0.9),0_2px_8px_rgba(201,162,39,0.12)]",
popping && "animate-[jackpot-digit-pop_0.32s_ease-out]",
)}
>
@@ -289,7 +289,7 @@ function JackpotBurstOverlayContent({
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#e8c96a]/70 to-transparent"
aria-hidden
/>
<div className="flex items-center justify-center gap-2 sm:gap-2.5">
<div className="flex items-center justify-center gap-2">
{display.map((char, i) => (
<RollingDigit key={i} char={char} popping={poppedIndex === i} />
))}

View File

@@ -87,13 +87,13 @@ export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps
</p>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
<p className="text-[11px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
<p className="mt-1 text-sm font-black text-slate-900">
{formatMinorAsCurrency(group.total_bet_amount, cur)}
</p>
</div>
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
<p className="text-[11px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
<p className="mt-1 text-sm font-black text-[#0b3f96]">
{formatMinorAsCurrency(group.actual_deduct_amount, cur)}
</p>

View File

@@ -203,7 +203,7 @@ export function TicketOrdersListScreen() {
return (
<PlayerPanel title={t("orders.title")}>
<div className="space-y-3">
<div className="rounded-2xl border border-[#dfe9f8] bg-white p-3 shadow-[0_10px_28px_rgba(15,23,42,0.05)] sm:p-4">
<div className="rounded-2xl border border-[#dfe9f8] bg-white p-3 shadow-[0_10px_28px_rgba(15,23,42,0.05)]">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-[11px] font-black uppercase tracking-wide text-[#6f86ad]">
@@ -241,7 +241,7 @@ export function TicketOrdersListScreen() {
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 lg:grid-cols-4">
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="flex h-9 min-w-0 items-center rounded-full border border-[#dce7f7] bg-[#fbfdff] px-3">
<Input
value={queryDrawNoInput}
@@ -342,7 +342,7 @@ export function TicketOrdersListScreen() {
<span className="flex min-w-0 items-center gap-1.5">
<span className="shrink-0">{t("orders.status")}</span>
{queryStatuses.length ? (
<span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[#0b56b7] px-1 text-[10px] font-black text-white">
<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">
{queryStatuses.length}
</span>
) : null}
@@ -468,13 +468,13 @@ export function TicketOrdersListScreen() {
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
<p className="text-[11px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
<p className="mt-1 text-sm font-black text-slate-900">
{formatMinorAsCurrency(group.total_bet_amount, cur)}
</p>
</div>
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
<p className="text-[11px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
<p className="mt-1 text-sm font-black text-[#0b3f96]">
{formatMinorAsCurrency(group.actual_deduct_amount, cur)}
</p>

View File

@@ -362,7 +362,7 @@ export function EntryGate() {
return (
<div className="relative flex min-h-dvh flex-col bg-white">
<div className={cn("relative h-[45vh] min-h-[200px] sm:min-h-[320px]", phase === "success" ? "bg-white" : "bg-red-600")}>
<div className={cn("relative h-[45vh] min-h-[200px]", phase === "success" ? "bg-white" : "bg-red-600")}>
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<Image
src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"}

View File

@@ -94,7 +94,7 @@ export function NotificationsScreen() {
</span>
<span
className={cn(
"text-[10px] font-semibold",
"text-[11px] font-semibold",
cardRead ? "text-slate-400" : "text-amber-700",
)}
>

View File

@@ -151,7 +151,7 @@ export function PlayerLoginScreen(): React.ReactElement {
return (
<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 sm:min-h-[320px]">
<div className="relative h-[45vh] min-h-[200px] shrink-0 overflow-hidden bg-red-600">
<div className="pointer-events-none absolute inset-0">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority />
</div>
@@ -202,7 +202,7 @@ export function PlayerLoginScreen(): React.ReactElement {
placeholder={t("login.captchaPlaceholder")}
maxLength={32}
disabled={loading}
className="min-w-0 flex-1 sm:max-w-[12rem]"
className="min-w-0 flex-1"
/>
<button
type="button"

View File

@@ -23,7 +23,7 @@ export function PlayerSessionBar({ className }: { className?: string }) {
return (
<div
className={cn(
"flex min-w-0 max-w-[55%] items-center gap-1.5 sm:max-w-[60%]",
"flex min-w-0 max-w-[55%] items-center gap-1.5",
className,
)}
>
@@ -35,7 +35,7 @@ export function PlayerSessionBar({ className }: { className?: string }) {
{label ?? "…"}
</p>
{activeCurrency ? (
<p className="truncate text-[10px] text-muted-foreground">
<p className="truncate text-[11px] text-muted-foreground">
{activeCurrency}
{profile?.locale ? (
<span className="text-muted-foreground/80">

View File

@@ -224,7 +224,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
key={tier}
className="rounded-lg border border-[#edf2f8] bg-white py-2 shadow-[0_4px_12px_rgba(15,23,42,0.03)]"
>
<p className="text-[10px] font-bold text-[#7890b8]">
<p className="text-[11px] font-bold text-[#7890b8]">
{t(resultsPrizeLabelKey(tier))}
</p>
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">
@@ -276,7 +276,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
href="/results/check"
className={cn(
buttonVariants({ variant: "default", size: "sm" }),
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto",
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028]",
)}
>
{t("results.viewMyWinning")}

View File

@@ -295,7 +295,7 @@ export function DrawResultsListScreen() {
<div className="mt-3 grid grid-cols-3 gap-2 text-center">
{RESULTS_TOP_PRIZE_KEYS.map((tier) => (
<div key={tier} className="rounded-lg border border-[#edf2f8] bg-[#f8fbff] py-2">
<p className="text-[10px] font-bold text-[#7890b8]">
<p className="text-[11px] font-bold text-[#7890b8]">
{t(resultsPrizeLabelKey(tier))}
</p>
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">

View File

@@ -41,7 +41,7 @@ export function PlayRulesScreen() {
>
<div className="space-y-3">
<Card className="border-[#dbe7fb] bg-white shadow-sm overflow-hidden min-h-[300px]">
<CardContent className="p-4 sm:p-6">
<CardContent className="p-4">
{loading ? (
<div className="flex h-[200px] items-center justify-center">
<Loader2 className="size-6 animate-spin text-slate-400" />