From 5b2fddee4abd5cd22e89b1146a1ff43ca6a8351e Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 11 Jun 2026 06:59:15 +0330 Subject: [PATCH] UNO home: mode cards + bottom nav bar Rebuild HomeScreen to UNO's home layout: top bar (avatar+coins) + 3 big glossy 3D mode cards in the center (Online[gold,live-count badge] / vs-Computer[teal] / Private Room[violet]) + a bottom icon nav bar (NavRail bottom variant, drops the redundant home item). Speed toggle + language sit in a slim controls row. Online card shows live player count; room card creates a private room then enters it. New menu.room/menu.roomDesc i18n. Co-Authored-By: Claude Opus 4.8 --- src/components/HomeScreen.tsx | 275 +++++++++++++++++------------- src/components/online/NavRail.tsx | 23 ++- src/lib/i18n.tsx | 4 + 3 files changed, 172 insertions(+), 130 deletions(-) diff --git a/src/components/HomeScreen.tsx b/src/components/HomeScreen.tsx index f25ef25..5c2351e 100644 --- a/src/components/HomeScreen.tsx +++ b/src/components/HomeScreen.tsx @@ -1,20 +1,13 @@ "use client"; import { motion } from "framer-motion"; -import { - Bot, - ChevronLeft, - ChevronRight, - Globe, - LogIn, - LogOut, - Play, -} from "lucide-react"; +import { Bot, Globe, Play, Users } from "lucide-react"; import { useEffect, useState } from "react"; import { Zap } from "lucide-react"; import { useGameStore, hasActiveMatch } from "@/lib/game-store"; import { pushNotification } from "@/lib/notification-store"; import { useSessionStore } from "@/lib/session-store"; +import { useOnlineStore } from "@/lib/online-store"; import { useUIStore, type Screen } from "@/lib/ui-store"; import { useI18n } from "@/lib/i18n"; import { getService } from "@/lib/online/service"; @@ -32,7 +25,7 @@ export function HomeScreen() { const go = useUIStore((s) => s.go); const profile = useSessionStore((s) => s.profile); const isAuthed = useSessionStore((s) => s.isAuthed); - const signOut = useSessionStore((s) => s.signOut); + const createRoom = useOnlineStore((s) => s.createRoom); const nav = (screen: Screen) => { sound.init(); @@ -41,6 +34,35 @@ export function HomeScreen() { }; const [speed, setSpeed] = useState(false); + const [online, setOnline] = useState(null); + + // Live online count for the «بازی آنلاین» card badge. + useEffect(() => { + let alive = true; + const tick = async () => { + try { + const n = await getService().getOnlineCount(); + if (alive) setOnline(n); + } catch { + /* ignore */ + } + }; + tick(); + const id = setInterval(tick, 8000); + return () => { + alive = false; + clearInterval(id); + }; + }, []); + + // Private room: create one and jump in (auth required). + const playRoom = async () => { + if (!isAuthed) return nav("auth"); + sound.init(); + sound.play("click"); + await createRoom({ targetScore: 7, stake: 0, ranked: false }); + go("room"); + }; const playVsComputer = () => { // One game at a time: resume the running match instead of starting a new one. @@ -67,139 +89,148 @@ export function HomeScreen() { }; const playOnline = () => nav(isAuthed ? "online" : "auth"); - const Chevron = locale === "fa" ? ChevronLeft : ChevronRight; - return (
{/* content */} -
+
- {/* hero: branding + play actions (stacked portrait, side-by-side landscape) */} -
- {/* branding */} - -
-
- -
-
-

- {t("app.title")} -

-

{t("app.subtitle")}

-
-
-
- -
-
- - {/* play actions */} - - - - - - {/* Normal / Speed mode picker */} -
- - -
-
+ {/* title */} +
+ {t("app.title")} + {t("app.subtitle")}
- {/* footer */} -
- {isAuthed ? ( + {/* mode cards */} + + } + name={t("menu.online")} + desc={t("menu.onlineDesc")} + badge={ + online != null + ? t("home.onlineCount", { n: new Intl.NumberFormat(locale === "fa" ? "fa-IR" : "en-US").format(online) }) + : undefined + } + onClick={playOnline} + /> + } + name={t("menu.vsComputer")} + desc={speed ? t("speed.desc") : t("menu.vsComputerDesc")} + onClick={playVsComputer} + /> + } + name={t("menu.room")} + desc={t("menu.roomDesc")} + onClick={playRoom} + /> + + + {/* controls: speed toggle (center) + language (end) */} +
+
+
- ) : ( - )} - +
+
+ +
+ + {/* bottom nav */} +
- -
); } +const MODE_TONES = { + gold: { bg: "linear-gradient(180deg,#f1da8a,#d4af37)", lo: "#946c0c", fg: "#2a1f04" }, + teal: { bg: "linear-gradient(180deg,#3fd0c2,#0d8a78)", lo: "#0a5a4c", fg: "#04261f" }, + violet: { bg: "linear-gradient(180deg,#c79cff,#7c4ddb)", lo: "#4f2e9e", fg: "#1c0e3a" }, +} as const; + +function ModeCard({ + icon, + name, + desc, + onClick, + tone, + badge, +}: { + icon: React.ReactNode; + name: string; + desc: string; + onClick: () => void; + tone: keyof typeof MODE_TONES; + badge?: string; +}) { + const c = MODE_TONES[tone]; + return ( + + {badge && ( + + {badge} + + )} + + {icon} + + {name} + + {desc} + + + ); +} + function OnlinePlayers() { const { t, locale } = useI18n(); const [count, setCount] = useState(null); diff --git a/src/components/online/NavRail.tsx b/src/components/online/NavRail.tsx index 6a0c001..792fa2f 100644 --- a/src/components/online/NavRail.tsx +++ b/src/components/online/NavRail.tsx @@ -13,13 +13,13 @@ type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean }; * (the app's main orientation) and a bottom tab bar in portrait. Active section * is highlighted gold and pulled forward. */ -export function NavRail() { +export function NavRail({ bottom = false }: { bottom?: boolean }) { const screen = useUIStore((s) => s.screen); const go = useUIStore((s) => s.go); const isAuthed = useSessionStore((s) => s.isAuthed); const { t } = useI18n(); - const items: Item[] = [ + const all: Item[] = [ { key: "home", icon: Home, label: t("nav.home") }, { key: "profile", icon: User, label: t("menu.profile") }, { key: "shop", icon: ShoppingBag, label: t("menu.shop") }, @@ -27,16 +27,22 @@ export function NavRail() { { key: "leaderboard", icon: Trophy, label: t("menu.leaderboard") }, { key: "achievements", icon: Star, label: t("achv.title") }, ]; + // On the home screen (bottom bar) drop the redundant "home" item. + const items = bottom ? all.filter((i) => i.key !== "home") : all; return (