UNO refactor (stage 2): responsive list/grid screens + chat
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:
@@ -36,7 +36,7 @@ export function AchievementsScreen() {
|
||||
<ScreenHeader title={t("achv.title")} />
|
||||
|
||||
{/* 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")} />
|
||||
<div className="h-8 w-px bg-cream/10" />
|
||||
<Stat
|
||||
@@ -74,7 +74,7 @@ export function AchievementsScreen() {
|
||||
</div>
|
||||
|
||||
{/* achievement list */}
|
||||
<div className="space-y-2 mb-6">
|
||||
<div className="grid gap-2 lg:grid-cols-2 mb-6">
|
||||
{list.map((a, i) => {
|
||||
const prog = achievementProgress(a, stats, profile.rating, profile.level);
|
||||
const unlocked = profile.unlocked.includes(a.id) || prog >= a.goal;
|
||||
|
||||
@@ -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="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) => (
|
||||
<button
|
||||
key={p.id}
|
||||
|
||||
@@ -50,7 +50,8 @@ export function ChatScreen() {
|
||||
};
|
||||
|
||||
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 className="glass flex items-center gap-2 p-3 shrink-0 z-10 safe-top">
|
||||
<button
|
||||
@@ -115,12 +116,13 @@ export function ChatScreen() {
|
||||
/>
|
||||
<button
|
||||
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")}
|
||||
>
|
||||
<Send className="size-4 rtl:-scale-x-100" />
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function FriendsScreen() {
|
||||
<ScreenHeader title={t("social.title")} />
|
||||
|
||||
{/* 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 === "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")} />
|
||||
@@ -123,7 +123,7 @@ function FriendsTab() {
|
||||
{requests.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<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) => (
|
||||
<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">
|
||||
@@ -142,8 +142,12 @@ function FriendsTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 pb-6">
|
||||
{friends.length === 0 && <EmptyState icon={<Users className="size-7 text-gold-400/70" />} text={t("friends.empty")} />}
|
||||
<div className="grid gap-2 lg:grid-cols-2 pb-6">
|
||||
{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) => (
|
||||
<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">
|
||||
@@ -249,7 +253,7 @@ function DiscoverTab() {
|
||||
<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} />)}
|
||||
</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>;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pb-6">
|
||||
{convs.length === 0 && <EmptyState icon={<MessageCircle className="size-7 text-gold-400/70" />} text={t("messages.empty")} />}
|
||||
<div className="grid gap-2 lg:grid-cols-2 pb-6">
|
||||
{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) => (
|
||||
<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">
|
||||
|
||||
@@ -26,7 +26,7 @@ export function LeaderboardScreen() {
|
||||
<ScreenHeader title={t("lead.title")} />
|
||||
|
||||
{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) => (
|
||||
<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" />
|
||||
@@ -41,7 +41,7 @@ export function LeaderboardScreen() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5 pb-6">
|
||||
<div className="grid gap-1.5 lg:grid-cols-2 pb-6">
|
||||
{leaderboard.map((e) => (
|
||||
<button
|
||||
key={e.id}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function NotificationsScreen() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 pb-6">
|
||||
<div className="grid gap-2 lg:grid-cols-2 pb-6">
|
||||
<AnimatePresence initial={false}>
|
||||
{items.map((n) => (
|
||||
<NotifRow
|
||||
|
||||
@@ -67,12 +67,12 @@ export function OnlineLobbyScreen() {
|
||||
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
|
||||
|
||||
{/* 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">
|
||||
<Trophy className="size-4 text-gold-400" />
|
||||
{t("lobby.chooseLeague")}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{MATCH_LEAGUES.map((l) => {
|
||||
const locked = level < l.minLevel;
|
||||
const active = l.id === leagueId;
|
||||
@@ -123,11 +123,11 @@ export function OnlineLobbyScreen() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.985 }}
|
||||
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]">
|
||||
<Trophy className="size-6" />
|
||||
@@ -145,7 +145,7 @@ export function OnlineLobbyScreen() {
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.985 }}
|
||||
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">
|
||||
<Users className="size-6" />
|
||||
|
||||
@@ -173,7 +173,7 @@ export function ShopScreen() {
|
||||
{Array.from({ length: 2 }).map((_, s) => (
|
||||
<div key={s}>
|
||||
<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) => (
|
||||
<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" />
|
||||
@@ -192,7 +192,7 @@ export function ShopScreen() {
|
||||
if (!list.length) return null;
|
||||
return (
|
||||
<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) => (
|
||||
<ItemCard key={item.id} item={item} owned={owns(item)} reqLabel={lockLabel(item)} onOpen={() => setDetail(item)} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user