Auth: email/Google "coming soon"; mobile polish pass on the table
- AuthScreen: phone+OTP is the only active method; email & Google shown as disabled "coming soon" buttons (until SMTP/Gmail are configured) - GameTable responsive: compact scoreboard, smaller seat avatars, and turn-indicator/timer positions tuned for small screens Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,7 +142,7 @@ function Scoreboard() {
|
|||||||
const game = useGameStore((s) => s.game);
|
const game = useGameStore((s) => s.game);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<div className="glass rounded-2xl px-4 py-2.5 flex items-center gap-4">
|
<div className="glass rounded-2xl px-2.5 py-1.5 gap-2.5 sm:px-4 sm:py-2.5 sm:gap-4 flex items-center">
|
||||||
<ScoreCol
|
<ScoreCol
|
||||||
label={t("team.us")}
|
label={t("team.us")}
|
||||||
tricks={game.roundTricks[0]}
|
tricks={game.roundTricks[0]}
|
||||||
@@ -177,9 +177,9 @@ function ScoreCol({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<div className="text-center min-w-14">
|
<div className="text-center min-w-10 sm:min-w-14">
|
||||||
<div className={cn("text-xs font-semibold", accent)}>{label}</div>
|
<div className={cn("text-[11px] sm:text-xs font-semibold", accent)}>{label}</div>
|
||||||
<div className="text-2xl font-black leading-none">{score}</div>
|
<div className="text-lg sm:text-2xl font-black leading-none">{score}</div>
|
||||||
<div className="text-[10px] text-cream/45 mt-0.5">
|
<div className="text-[10px] text-cream/45 mt-0.5">
|
||||||
{t("score.tricks")}: {tricks}
|
{t("score.tricks")}: {tricks}
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +235,7 @@ function SeatAvatar({ seat, className }: { seat: Seat; className?: string }) {
|
|||||||
: { boxShadow: "0 0 0 1px rgba(212,175,55,0.2)" }
|
: { boxShadow: "0 0 0 1px rgba(212,175,55,0.2)" }
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative size-12 rounded-full flex items-center justify-center font-bold text-xl",
|
"relative size-10 sm:size-12 rounded-full flex items-center justify-center font-bold text-lg sm:text-xl",
|
||||||
team === 0 ? "bg-teal-700/80 text-teal-100" : "bg-rose-900/70 text-rose-100"
|
team === 0 ? "bg-teal-700/80 text-teal-100" : "bg-rose-900/70 text-rose-100"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -454,7 +454,7 @@ function TurnIndicator() {
|
|||||||
initial={{ opacity: 0, y: 8 }}
|
initial={{ opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="absolute bottom-[150px] left-1/2 -translate-x-1/2 z-30"
|
className="absolute bottom-[120px] sm:bottom-[150px] left-1/2 -translate-x-1/2 z-30"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -480,7 +480,7 @@ function TurnTimer() {
|
|||||||
const pct = Math.max(0, Math.min(1, (deadline - Date.now()) / TURN_MS));
|
const pct = Math.max(0, Math.min(1, (deadline - Date.now()) / TURN_MS));
|
||||||
const danger = secs <= 5;
|
const danger = secs <= 5;
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-[190px] left-1/2 -translate-x-1/2 z-30 w-40 text-center">
|
<div className="absolute bottom-[156px] sm:bottom-[190px] left-1/2 -translate-x-1/2 z-30 w-36 sm:w-40 text-center">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"block text-sm font-black tabular-nums mb-1",
|
"block text-sm font-black tabular-nums mb-1",
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Mail, Phone } from "lucide-react";
|
import { Mail } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
|
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
|
||||||
import { useSessionStore } from "@/lib/session-store";
|
import { useSessionStore } from "@/lib/session-store";
|
||||||
import { useUIStore } from "@/lib/ui-store";
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
|
|
||||||
type Tab = "phone" | "email";
|
|
||||||
|
|
||||||
export function AuthScreen() {
|
export function AuthScreen() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const go = useUIStore((s) => s.go);
|
const go = useUIStore((s) => s.go);
|
||||||
const s = useSessionStore();
|
|
||||||
const [tab, setTab] = useState<Tab>("phone");
|
|
||||||
|
|
||||||
const done = () => go("online");
|
const done = () => go("online");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,51 +19,39 @@ export function AuthScreen() {
|
|||||||
<div className="glass rounded-3xl p-6 max-w-md mx-auto">
|
<div className="glass rounded-3xl p-6 max-w-md mx-auto">
|
||||||
<p className="text-center text-cream/60 text-sm mb-5">{t("auth.subtitle")}</p>
|
<p className="text-center text-cream/60 text-sm mb-5">{t("auth.subtitle")}</p>
|
||||||
|
|
||||||
<div className="flex gap-2 p-1 rounded-xl bg-navy-900/70 mb-5">
|
{/* Active method: phone OTP */}
|
||||||
<TabBtn active={tab === "phone"} onClick={() => setTab("phone")} icon={<Phone className="size-4" />} label={t("auth.phone")} />
|
<PhoneForm onDone={done} />
|
||||||
<TabBtn active={tab === "email"} onClick={() => setTab("email")} icon={<Mail className="size-4" />} label={t("auth.email")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tab === "phone" ? <PhoneForm onDone={done} /> : <EmailForm onDone={done} />}
|
{/* Coming soon (until email SMTP / Google are configured) */}
|
||||||
|
<div className="mt-6 pt-5 border-t border-gold-500/15 space-y-2">
|
||||||
<div className="mt-5 pt-5 border-t border-gold-500/15">
|
<p className="text-center text-[11px] text-cream/40 mb-1">{t("auth.otherSoon")}</p>
|
||||||
<button
|
<SoonButton icon={<Mail className="size-4" />} label={t("auth.email")} soon={t("common.soon")} />
|
||||||
onClick={async () => {
|
<SoonButton icon={<GoogleIcon />} label={t("auth.google")} soon={t("common.soon")} />
|
||||||
await s.signInGoogle();
|
|
||||||
done();
|
|
||||||
}}
|
|
||||||
className="w-full rounded-xl bg-white text-slate-800 font-bold py-3 flex items-center justify-center gap-2 hover:bg-white/90 transition"
|
|
||||||
>
|
|
||||||
<GoogleIcon />
|
|
||||||
{t("auth.google")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScreenShell>
|
</ScreenShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabBtn({
|
function SoonButton({
|
||||||
active,
|
|
||||||
onClick,
|
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
|
soon,
|
||||||
}: {
|
}: {
|
||||||
active: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
soon: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
disabled
|
||||||
className={cn(
|
className="w-full rounded-xl bg-navy-900/50 border border-gold-500/15 text-cream/40 py-3 flex items-center justify-center gap-2 cursor-not-allowed"
|
||||||
"flex-1 rounded-lg py-2 text-sm font-bold flex items-center justify-center gap-1.5 transition",
|
|
||||||
active ? "btn-gold" : "text-cream/60 hover:text-cream"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{icon}
|
<span className="opacity-50">{icon}</span>
|
||||||
{label}
|
{label}
|
||||||
|
<span className="text-[10px] rounded-full bg-gold-500/15 text-gold-300/80 px-2 py-0.5">
|
||||||
|
{soon}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,6 +87,7 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.phoneLabel")}</label>
|
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.phoneLabel")}</label>
|
||||||
<input
|
<input
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
inputMode="numeric"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
placeholder={t("auth.phonePlaceholder")}
|
placeholder={t("auth.phonePlaceholder")}
|
||||||
@@ -125,6 +108,7 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.codeLabel")}</label>
|
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.codeLabel")}</label>
|
||||||
<input
|
<input
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
inputMode="numeric"
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
placeholder={t("auth.codePlaceholder")}
|
placeholder={t("auth.codePlaceholder")}
|
||||||
@@ -142,67 +126,6 @@ function PhoneForm({ onDone }: { onDone: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmailForm({ onDone }: { onDone: () => void }) {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const signInEmail = useSessionStore((s) => s.signInEmail);
|
|
||||||
const signUpEmail = useSessionStore((s) => s.signUpEmail);
|
|
||||||
const [mode, setMode] = useState<"in" | "up">("in");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!email.trim() || !password.trim()) return;
|
|
||||||
if (mode === "in") await signInEmail(email.trim(), password);
|
|
||||||
else await signUpEmail(email.trim(), password, name.trim());
|
|
||||||
onDone();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{mode === "up" && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.nameLabel")}</label>
|
|
||||||
<input
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
className="w-full rounded-xl bg-navy-900/70 gold-border px-4 py-3 text-cream outline-none focus:ring-2 focus:ring-gold-500/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.emailLabel")}</label>
|
|
||||||
<input
|
|
||||||
dir="ltr"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="w-full rounded-xl bg-navy-900/70 gold-border px-4 py-3 text-cream outline-none focus:ring-2 focus:ring-gold-500/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-cream/55 mb-1.5">{t("auth.passLabel")}</label>
|
|
||||||
<input
|
|
||||||
dir="ltr"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="w-full rounded-xl bg-navy-900/70 gold-border px-4 py-3 text-cream outline-none focus:ring-2 focus:ring-gold-500/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button onClick={submit} className="btn-gold w-full rounded-xl py-3">
|
|
||||||
{mode === "in" ? t("auth.signIn") : t("auth.signUp")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setMode(mode === "in" ? "up" : "in")}
|
|
||||||
className="w-full text-center text-sm text-cream/55 hover:text-cream"
|
|
||||||
>
|
|
||||||
{mode === "in" ? t("auth.toggleSignup") : t("auth.toggleSignin")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GoogleIcon() {
|
function GoogleIcon() {
|
||||||
return (
|
return (
|
||||||
<svg className="size-4" viewBox="0 0 24 24">
|
<svg className="size-4" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ const fa: Dict = {
|
|||||||
"auth.toggleSignup": "حساب ندارید؟ ثبتنام کنید",
|
"auth.toggleSignup": "حساب ندارید؟ ثبتنام کنید",
|
||||||
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
|
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
|
||||||
"auth.invalidCode": "کد نادرست است",
|
"auth.invalidCode": "کد نادرست است",
|
||||||
|
"auth.otherSoon": "سایر روشهای ورود بهزودی فعال میشوند",
|
||||||
|
|
||||||
"reward.title": "پاداش بازی",
|
"reward.title": "پاداش بازی",
|
||||||
"reward.rating": "امتیاز رتبهای",
|
"reward.rating": "امتیاز رتبهای",
|
||||||
@@ -403,6 +404,7 @@ const en: Dict = {
|
|||||||
"auth.toggleSignup": "No account? Sign up",
|
"auth.toggleSignup": "No account? Sign up",
|
||||||
"auth.toggleSignin": "Have an account? Sign in",
|
"auth.toggleSignin": "Have an account? Sign in",
|
||||||
"auth.invalidCode": "Invalid code",
|
"auth.invalidCode": "Invalid code",
|
||||||
|
"auth.otherSoon": "Other sign-in methods coming soon",
|
||||||
|
|
||||||
"reward.title": "Match rewards",
|
"reward.title": "Match rewards",
|
||||||
"reward.rating": "Rating",
|
"reward.rating": "Rating",
|
||||||
|
|||||||
Reference in New Issue
Block a user