UNO-style UX rollout: Lobby, Matchmaking, Profile
- Lobby: the two hero actions (random / create room) are now tactile press-3d rounded-3xl with tinted icon chips. - Matchmaking: seat slots use the Avatar frame with a gold border when filled (dashed placeholder when empty + spring pop-in); cancel/start/upgrade buttons are tactile. - Profile: added a level badge on the avatar (casual-game style). Verified: tsc + next build clean; web rebuilt :1500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,13 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import { Crown, Loader2 } from "lucide-react";
|
import { Crown, Loader2 } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { ScreenShell } from "@/components/online/ScreenHeader";
|
import { ScreenShell } from "@/components/online/ScreenHeader";
|
||||||
|
import { Avatar } from "@/components/online/Avatar";
|
||||||
import { useGameStore } from "@/lib/game-store";
|
import { useGameStore } from "@/lib/game-store";
|
||||||
import { useOnlineStore } from "@/lib/online-store";
|
import { useOnlineStore } from "@/lib/online-store";
|
||||||
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 { getService } from "@/lib/online/service";
|
import { getService } from "@/lib/online/service";
|
||||||
import { avatarEmoji } from "@/lib/online/types";
|
|
||||||
|
|
||||||
export function MatchmakingScreen() {
|
export function MatchmakingScreen() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -70,7 +70,7 @@ export function MatchmakingScreen() {
|
|||||||
<div className="mt-9 flex flex-col items-center gap-3 w-full max-w-xs">
|
<div className="mt-9 flex flex-col items-center gap-3 w-full max-w-xs">
|
||||||
<button
|
<button
|
||||||
onClick={() => upgradePlan()}
|
onClick={() => upgradePlan()}
|
||||||
className="btn-gold w-full rounded-xl py-3 flex items-center justify-center gap-2"
|
className="press-3d btn-gold w-full rounded-2xl py-3 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<Crown className="size-4" />
|
<Crown className="size-4" />
|
||||||
{t("queue.upgrade")}
|
{t("queue.upgrade")}
|
||||||
@@ -113,7 +113,10 @@ export function MatchmakingScreen() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="w-16 h-20 rounded-2xl glass flex flex-col items-center justify-center gap-1"
|
className={
|
||||||
|
"w-16 h-20 rounded-2xl flex flex-col items-center justify-center gap-1 transition " +
|
||||||
|
(p ? "glass gold-border" : "border border-dashed border-navy-700/60 bg-navy-900/30")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{p ? (
|
{p ? (
|
||||||
@@ -121,13 +124,16 @@ export function MatchmakingScreen() {
|
|||||||
key={p.id}
|
key={p.id}
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 260, damping: 18 }}
|
||||||
className="flex flex-col items-center gap-0.5"
|
className="flex flex-col items-center gap-0.5"
|
||||||
>
|
>
|
||||||
<span className="text-2xl">{avatarEmoji(p.avatar)}</span>
|
<span className="grid size-9 place-items-center rounded-xl bg-navy-900 overflow-hidden">
|
||||||
<span className="text-[9px] text-cream/70 max-w-14 truncate">
|
<Avatar id={p.avatar} size={24} />
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-cream/80 max-w-14 truncate">
|
||||||
{p.displayName}
|
{p.displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[8px] text-gold-400/70">
|
<span className="text-[8px] text-gold-400/80">
|
||||||
{t("common.level")} {p.level}
|
{t("common.level")} {p.level}
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -148,7 +154,7 @@ export function MatchmakingScreen() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 flex gap-3">
|
<div className="mt-10 flex gap-3">
|
||||||
<button onClick={cancel} className="glass rounded-xl px-6 py-3 text-cream/70 hover:text-cream">
|
<button onClick={cancel} className="press-3d glass rounded-2xl px-6 py-3 text-cream/70">
|
||||||
{t("mm.cancel")}
|
{t("mm.cancel")}
|
||||||
</button>
|
</button>
|
||||||
{ready && (
|
{ready && (
|
||||||
@@ -156,7 +162,7 @@ export function MatchmakingScreen() {
|
|||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
onClick={enter}
|
onClick={enter}
|
||||||
className="btn-gold rounded-xl px-8 py-3 text-lg"
|
className="press-3d btn-gold rounded-2xl px-8 py-3 text-lg"
|
||||||
>
|
>
|
||||||
{t("mm.start")}
|
{t("mm.start")}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|||||||
@@ -112,12 +112,11 @@ export function OnlineLobbyScreen() {
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ y: -2 }}
|
whileTap={{ scale: 0.985 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
onClick={onRandom}
|
onClick={onRandom}
|
||||||
className="btn-gold w-full rounded-2xl p-5 flex items-center gap-4 text-start"
|
className="press-3d btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
|
||||||
>
|
>
|
||||||
<span className="size-12 rounded-xl bg-black/15 flex items-center justify-center 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" />
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
@@ -131,12 +130,11 @@ export function OnlineLobbyScreen() {
|
|||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ y: -2 }}
|
whileTap={{ scale: 0.985 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
className="glass w-full rounded-2xl p-5 flex items-center gap-4 text-start hover:bg-navy-800/80 transition"
|
className="press-3d glass w-full rounded-3xl p-5 flex items-center gap-4 text-start"
|
||||||
>
|
>
|
||||||
<span className="size-12 rounded-xl bg-navy-900 gold-border flex items-center justify-center text-gold-400">
|
<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" />
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ export function ProfileScreen() {
|
|||||||
<div className="size-20 rounded-2xl bg-navy-900 gold-border flex items-center justify-center overflow-hidden">
|
<div className="size-20 rounded-2xl bg-navy-900 gold-border flex items-center justify-center overflow-hidden">
|
||||||
<Avatar id={profile.avatar} image={profile.avatarImage} size={profile.avatarImage ? 80 : 56} />
|
<Avatar id={profile.avatar} image={profile.avatarImage} size={profile.avatarImage ? 80 : 56} />
|
||||||
</div>
|
</div>
|
||||||
|
{/* level badge */}
|
||||||
|
<span className="absolute -top-2 ltr:-left-2 rtl:-right-2 rounded-full bg-navy-950 gold-border px-2 py-0.5 text-[10px] font-black text-gold-300 shadow-lg">
|
||||||
|
{t("common.level")} {profile.level}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => (canUpload ? fileRef.current?.click() : undefined)}
|
onClick={() => (canUpload ? fileRef.current?.click() : undefined)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
Reference in New Issue
Block a user