feat: improve mobile responsiveness across login, entry, and result screens
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Updated HallDrawPanel to use responsive grid layout, switching from 3-column to single-column on mobile with appropriate dividers
- Refactored EntryGate to use useMemo for gateReady state instead of useLayoutEffect, improving initialization logic
- Reduced minimum height of hero sections from 320px to 200px on mobile (sm:320px) in login and entry screens
- Enhanced WinningResultDialog layout with flexbox structure and
This commit is contained in:
2026-06-23 16:50:25 +08:00
parent 15b7cca9f5
commit 588d62a381
4 changed files with 37 additions and 27 deletions

View File

@@ -186,7 +186,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
)} )}
aria-label={t("draw.currentIssue")} aria-label={t("draw.currentIssue")}
> >
<div className="grid grid-cols-[1fr_1.05fr_1fr] divide-x divide-[#e7edf6]"> <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="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>

View File

@@ -16,7 +16,6 @@ import { useRouter, useSearchParams } from "next/navigation";
import { import {
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -124,26 +123,33 @@ export function EntryGate() {
!tokenFromUrl && !tokenFromUrl &&
!(bearerToken ?? "").trim(); !(bearerToken ?? "").trim();
const [gateReady, setGateReady] = useState(false); const gateReady = useMemo(() => {
if (typeof window === "undefined") return false;
useLayoutEffect(() => {
if (typeof window === "undefined") return;
if (!isInIframe()) { if (!isInIframe()) {
if (sessionExpired) { if (sessionExpired) return false;
router.replace("/login?session=expired");
return;
}
const hasToken = tokenFromUrl !== "" || (bearerToken ?? "").trim() !== "";
if (!hasToken) return false;
}
return true;
}, [bearerToken, sessionExpired, tokenFromUrl]);
useEffect(() => {
if (gateReady) return;
if (typeof window === "undefined") return;
if (isInIframe()) return;
if (sessionExpired) {
router.replace("/login?session=expired");
} else {
const hasToken = tokenFromUrl !== "" || (bearerToken ?? "").trim() !== ""; const hasToken = tokenFromUrl !== "" || (bearerToken ?? "").trim() !== "";
if (!hasToken) { if (!hasToken) {
router.replace("/login"); router.replace("/login");
return;
} }
} }
}, [gateReady, router, sessionExpired, tokenFromUrl, bearerToken]);
setGateReady(true);
}, [bearerToken, router, sessionExpired, tokenFromUrl]);
const [phase, setPhase] = useState<Phase>(sessionExpired ? "failed" : "loading"); const [phase, setPhase] = useState<Phase>(sessionExpired ? "failed" : "loading");
const [failureDetails, setFailureDetails] = useState<FailureRow[]>(() => const [failureDetails, setFailureDetails] = useState<FailureRow[]>(() =>
@@ -319,7 +325,9 @@ export function EntryGate() {
const trimmedEffectiveToken = (effectiveToken ?? "").trim(); const trimmedEffectiveToken = (effectiveToken ?? "").trim();
const doEntryRef = useRef(doEntry); const doEntryRef = useRef(doEntry);
doEntryRef.current = doEntry; useEffect(() => {
doEntryRef.current = doEntry;
}, [doEntry]);
useEffect(() => { useEffect(() => {
if (sessionExpired || waitingForEmbeddedToken) return; if (sessionExpired || waitingForEmbeddedToken) return;
@@ -354,7 +362,7 @@ export function EntryGate() {
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={cn("relative h-[45vh] min-h-[320px]", phase === "success" ? "bg-white" : "bg-red-600")}> <div className={cn("relative h-[45vh] min-h-[200px] sm:min-h-[320px]", phase === "success" ? "bg-white" : "bg-red-600")}>
<div className="pointer-events-none absolute inset-0 overflow-hidden"> <div className="pointer-events-none absolute inset-0 overflow-hidden">
<Image <Image
src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"} src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"}

View File

@@ -151,7 +151,7 @@ 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-[320px] shrink-0 overflow-hidden bg-red-600"> <div className="relative h-[45vh] min-h-[200px] shrink-0 overflow-hidden bg-red-600 sm:min-h-[320px]">
<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 className="object-cover object-center" priority />
</div> </div>

View File

@@ -212,17 +212,17 @@ function WinningResultDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
className="max-h-[calc(100dvh-24px)] overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.28)] sm:max-w-md" className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.28)] sm:max-w-md"
> >
<button <div className="relative shrink-0 px-5 pt-4">
type="button" <button
onClick={() => onOpenChange(false)} type="button"
className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100" onClick={() => onOpenChange(false)}
aria-label={t("actions.close")} className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
> aria-label={t("actions.close")}
<XIcon className="size-5" /> >
</button> <XIcon className="size-5" />
<div className="max-h-[calc(100dvh-24px)] overflow-y-auto px-5 pb-5 pt-8"> </button>
<DialogHeader className="items-center text-center"> <DialogHeader className="items-center text-center">
<div className="flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600"> <div className="flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600">
<CheckCircle2 className="size-11" /> <CheckCircle2 className="size-11" />
@@ -236,7 +236,9 @@ function WinningResultDialog({
{t("results.check.ticketNumber")} {t("results.check.ticketNumber")}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 pb-5">
<div className="mx-auto mt-3 w-fit rounded-xl bg-emerald-50 px-8 py-2 font-mono text-lg font-black text-[#0a8f3e]"> <div className="mx-auto mt-3 w-fit rounded-xl bg-emerald-50 px-8 py-2 font-mono text-lg font-black text-[#0a8f3e]">
{query} {query}
</div> </div>