UNO refactor (stage 2): responsive list/grid screens + chat
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 46s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 51s

Make all menu screens use the width on desktop/landscape and the UNO panels:
- Shop item grid 3→up to 6 cols; BuyCoins packs 2→4 cols on lg.
- Lobby: panel league pick (2-col) + 2-col CTA buttons.
- Achievements / Notifications / Leaderboard / Friends lists → responsive
  grids (1 col mobile, 2 cols on lg); glass→panel on section containers.
- Chat: centered max-w-3xl column on desktop, green send button.
All responsive for mobile + desktop. tsc + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 10:35:56 +03:30
parent 5c00f44fdc
commit ac05a7b679
8 changed files with 32 additions and 22 deletions
@@ -36,7 +36,7 @@ export function AchievementsScreen() {
<ScreenHeader title={t("achv.title")} /> <ScreenHeader title={t("achv.title")} />
{/* summary */} {/* summary */}
<div className="glass rounded-2xl p-4 mb-4 flex items-center justify-around text-center"> <div className="panel rounded-2xl p-4 mb-4 flex items-center justify-around text-center">
<Stat value={`${unlockedCount}/${ACHIEVEMENTS.length}`} label={t("achv.unlocked")} /> <Stat value={`${unlockedCount}/${ACHIEVEMENTS.length}`} label={t("achv.unlocked")} />
<div className="h-8 w-px bg-cream/10" /> <div className="h-8 w-px bg-cream/10" />
<Stat <Stat
@@ -74,7 +74,7 @@ export function AchievementsScreen() {
</div> </div>
{/* achievement list */} {/* achievement list */}
<div className="space-y-2 mb-6"> <div className="grid gap-2 lg:grid-cols-2 mb-6">
{list.map((a, i) => { {list.map((a, i) => {
const prog = achievementProgress(a, stats, profile.rating, profile.level); const prog = achievementProgress(a, stats, profile.rating, profile.level);
const unlocked = profile.unlocked.includes(a.id) || prog >= a.goal; const unlocked = profile.unlocked.includes(a.id) || prog >= a.goal;
+1 -1
View File
@@ -128,7 +128,7 @@ export function BuyCoinsScreen() {
<div className="mb-4 text-center text-cream/80 text-sm glass rounded-xl py-2">{msg}</div> <div className="mb-4 text-center text-cream/80 text-sm glass rounded-xl py-2">{msg}</div>
)} )}
<div className="grid grid-cols-2 gap-3 pb-6"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 pb-6">
{packs.map((p) => ( {packs.map((p) => (
<button <button
key={p.id} key={p.id}
+4 -2
View File
@@ -50,7 +50,8 @@ export function ChatScreen() {
}; };
return ( return (
<main className="persian-pattern relative h-dvh w-full flex flex-col"> <main className="persian-pattern relative h-dvh w-full flex justify-center">
<div className="w-full max-w-3xl flex flex-col h-full">
{/* header */} {/* header */}
<header className="glass flex items-center gap-2 p-3 shrink-0 z-10 safe-top"> <header className="glass flex items-center gap-2 p-3 shrink-0 z-10 safe-top">
<button <button
@@ -115,12 +116,13 @@ export function ChatScreen() {
/> />
<button <button
onClick={send} onClick={send}
className="btn-gold tap grid place-items-center rounded-full shrink-0" className="btn-green tap grid place-items-center rounded-full shrink-0"
aria-label={t("chat.send")} aria-label={t("chat.send")}
> >
<Send className="size-4 rtl:-scale-x-100" /> <Send className="size-4 rtl:-scale-x-100" />
</button> </button>
</footer> </footer>
</div>
</main> </main>
); );
} }
+15 -7
View File
@@ -54,7 +54,7 @@ export function FriendsScreen() {
<ScreenHeader title={t("social.title")} /> <ScreenHeader title={t("social.title")} />
{/* tabs */} {/* tabs */}
<div className="glass rounded-2xl p-1 flex gap-1 mb-4"> <div className="panel rounded-2xl p-1 flex gap-1 mb-4">
<TabButton active={tab === "friends"} onClick={() => setTab("friends")} icon={<Users className="size-4" />} label={t("social.tabFriends")} badge={requests.length} /> <TabButton active={tab === "friends"} onClick={() => setTab("friends")} icon={<Users className="size-4" />} label={t("social.tabFriends")} badge={requests.length} />
<TabButton active={tab === "discover"} onClick={() => setTab("discover")} icon={<Search className="size-4" />} label={t("social.tabDiscover")} /> <TabButton active={tab === "discover"} onClick={() => setTab("discover")} icon={<Search className="size-4" />} label={t("social.tabDiscover")} />
<TabButton active={tab === "messages"} onClick={() => setTab("messages")} icon={<MessageCircle className="size-4" />} label={t("social.tabMessages")} /> <TabButton active={tab === "messages"} onClick={() => setTab("messages")} icon={<MessageCircle className="size-4" />} label={t("social.tabMessages")} />
@@ -123,7 +123,7 @@ function FriendsTab() {
{requests.length > 0 && ( {requests.length > 0 && (
<div className="mb-4"> <div className="mb-4">
<h3 className="text-xs text-cream/55 mb-2">{t("friends.requests")}</h3> <h3 className="text-xs text-cream/55 mb-2">{t("friends.requests")}</h3>
<div className="space-y-2"> <div className="grid gap-2 lg:grid-cols-2">
{requests.map((r) => ( {requests.map((r) => (
<div key={r.id} className="glass rounded-xl p-2.5 flex items-center gap-3"> <div key={r.id} className="glass rounded-xl p-2.5 flex items-center gap-3">
<button onClick={() => viewProfile(r.from.id)} className="text-2xl active:scale-95 transition"> <button onClick={() => viewProfile(r.from.id)} className="text-2xl active:scale-95 transition">
@@ -142,8 +142,12 @@ function FriendsTab() {
</div> </div>
)} )}
<div className="space-y-2 pb-6"> <div className="grid gap-2 lg:grid-cols-2 pb-6">
{friends.length === 0 && <EmptyState icon={<Users className="size-7 text-gold-400/70" />} text={t("friends.empty")} />} {friends.length === 0 && (
<div className="lg:col-span-2">
<EmptyState icon={<Users className="size-7 text-gold-400/70" />} text={t("friends.empty")} />
</div>
)}
{friends.map((f: Friend) => ( {friends.map((f: Friend) => (
<div key={f.id} className="glass rounded-xl p-2.5 flex items-center gap-3"> <div key={f.id} className="glass rounded-xl p-2.5 flex items-center gap-3">
<button onClick={() => viewProfile(f.id)} className="flex items-center gap-3 flex-1 min-w-0 text-start active:scale-[0.99] transition"> <button onClick={() => viewProfile(f.id)} className="flex items-center gap-3 flex-1 min-w-0 text-start active:scale-[0.99] transition">
@@ -249,7 +253,7 @@ function DiscoverTab() {
<EmptyState icon={<Search className="size-7 text-gold-400/70" />} text={t("discover.noResults")} /> <EmptyState icon={<Search className="size-7 text-gold-400/70" />} text={t("discover.noResults")} />
)} )}
<div className="space-y-2"> <div className="grid gap-2 lg:grid-cols-2">
{!loading && list?.map((p) => <DiscoverRow key={p.id} player={p} />)} {!loading && list?.map((p) => <DiscoverRow key={p.id} player={p} />)}
</div> </div>
</div> </div>
@@ -356,8 +360,12 @@ function MessagesTab() {
if (convs == null) return <div className="grid place-items-center py-10"><Loader2 className="size-6 text-gold-400 animate-spin" /></div>; if (convs == null) return <div className="grid place-items-center py-10"><Loader2 className="size-6 text-gold-400 animate-spin" /></div>;
return ( return (
<div className="space-y-2 pb-6"> <div className="grid gap-2 lg:grid-cols-2 pb-6">
{convs.length === 0 && <EmptyState icon={<MessageCircle className="size-7 text-gold-400/70" />} text={t("messages.empty")} />} {convs.length === 0 && (
<div className="lg:col-span-2">
<EmptyState icon={<MessageCircle className="size-7 text-gold-400/70" />} text={t("messages.empty")} />
</div>
)}
{convs.map((c) => ( {convs.map((c) => (
<button key={c.friend.id} onClick={() => open(c)} className="w-full glass rounded-xl p-2.5 flex items-center gap-3 text-start active:scale-[0.99] transition"> <button key={c.friend.id} onClick={() => open(c)} className="w-full glass rounded-xl p-2.5 flex items-center gap-3 text-start active:scale-[0.99] transition">
<div className="relative shrink-0"> <div className="relative shrink-0">
+2 -2
View File
@@ -26,7 +26,7 @@ export function LeaderboardScreen() {
<ScreenHeader title={t("lead.title")} /> <ScreenHeader title={t("lead.title")} />
{leaderboard.length === 0 && ( {leaderboard.length === 0 && (
<div className="space-y-1.5 pb-6"> <div className="grid gap-1.5 lg:grid-cols-2 pb-6">
{Array.from({ length: 8 }).map((_, i) => ( {Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="glass rounded-xl p-2.5 flex items-center gap-2.5 animate-pulse"> <div key={i} className="glass rounded-xl p-2.5 flex items-center gap-2.5 animate-pulse">
<span className="size-6 rounded bg-navy-800/80" /> <span className="size-6 rounded bg-navy-800/80" />
@@ -41,7 +41,7 @@ export function LeaderboardScreen() {
</div> </div>
)} )}
<div className="space-y-1.5 pb-6"> <div className="grid gap-1.5 lg:grid-cols-2 pb-6">
{leaderboard.map((e) => ( {leaderboard.map((e) => (
<button <button
key={e.id} key={e.id}
@@ -59,7 +59,7 @@ export function NotificationsScreen() {
</p> </p>
)} )}
<div className="space-y-2 pb-6"> <div className="grid gap-2 lg:grid-cols-2 pb-6">
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{items.map((n) => ( {items.map((n) => (
<NotifRow <NotifRow
+5 -5
View File
@@ -67,12 +67,12 @@ export function OnlineLobbyScreen() {
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} /> <ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
{/* league pick (only for ranked) */} {/* league pick (only for ranked) */}
<div className="glass rounded-2xl p-4 mb-4"> <div className="panel rounded-2xl p-4 mb-4">
<div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5"> <div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5">
<Trophy className="size-4 text-gold-400" /> <Trophy className="size-4 text-gold-400" />
{t("lobby.chooseLeague")} {t("lobby.chooseLeague")}
</div> </div>
<div className="space-y-2"> <div className="grid gap-2 sm:grid-cols-2">
{MATCH_LEAGUES.map((l) => { {MATCH_LEAGUES.map((l) => {
const locked = level < l.minLevel; const locked = level < l.minLevel;
const active = l.id === leagueId; const active = l.id === leagueId;
@@ -123,11 +123,11 @@ export function OnlineLobbyScreen() {
)} )}
</div> </div>
<div className="space-y-3"> <div className="grid gap-3 sm:grid-cols-2">
<motion.button <motion.button
whileTap={{ scale: 0.985 }} whileTap={{ scale: 0.985 }}
onClick={onRandom} onClick={onRandom}
className="press-3d btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start" className="btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
> >
<span className="grid size-12 place-items-center rounded-2xl bg-black/15 text-[#2a1f04]"> <span className="grid size-12 place-items-center rounded-2xl bg-black/15 text-[#2a1f04]">
<Trophy className="size-6" /> <Trophy className="size-6" />
@@ -145,7 +145,7 @@ export function OnlineLobbyScreen() {
<motion.button <motion.button
whileTap={{ scale: 0.985 }} whileTap={{ scale: 0.985 }}
onClick={onCreate} onClick={onCreate}
className="press-3d glass w-full rounded-3xl p-5 flex items-center gap-4 text-start" className="press-3d panel w-full rounded-3xl p-5 flex items-center gap-4 text-start"
> >
<span className="grid size-12 place-items-center rounded-2xl bg-teal-500/15 text-teal-300"> <span className="grid size-12 place-items-center rounded-2xl bg-teal-500/15 text-teal-300">
<Users className="size-6" /> <Users className="size-6" />
+2 -2
View File
@@ -173,7 +173,7 @@ export function ShopScreen() {
{Array.from({ length: 2 }).map((_, s) => ( {Array.from({ length: 2 }).map((_, s) => (
<div key={s}> <div key={s}>
<div className="h-4 w-24 rounded bg-navy-800/80 animate-pulse mb-3" /> <div className="h-4 w-24 rounded bg-navy-800/80 animate-pulse mb-3" />
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="glass rounded-2xl p-3 flex flex-col items-center gap-2 animate-pulse"> <div key={i} className="glass rounded-2xl p-3 flex flex-col items-center gap-2 animate-pulse">
<div className="size-12 rounded-xl bg-navy-800/80" /> <div className="size-12 rounded-xl bg-navy-800/80" />
@@ -192,7 +192,7 @@ export function ShopScreen() {
if (!list.length) return null; if (!list.length) return null;
return ( return (
<Section key={sec.kind} title={sec.title} hint={sec.hint}> <Section key={sec.kind} title={sec.title} hint={sec.hint}>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
{list.map((item) => ( {list.map((item) => (
<ItemCard key={item.id} item={item} owned={owns(item)} reqLabel={lockLabel(item)} onOpen={() => setDetail(item)} /> <ItemCard key={item.id} item={item} owned={owns(item)} reqLabel={lockLabel(item)} onOpen={() => setDetail(item)} />
))} ))}