diff --git a/src/app/blindbox/page.tsx b/src/app/blindbox/page.tsx index 8353535..82f786d 100644 --- a/src/app/blindbox/page.tsx +++ b/src/app/blindbox/page.tsx @@ -15,6 +15,7 @@ import { import { getCachedProfile, isRegistered } from "@/lib/userId"; import AuthModal from "@/components/AuthModal"; import Button from "@/components/Button"; +import Input from "@/components/Input"; import { BlindboxListSkeleton } from "@/components/Skeleton"; import type { UserProfile } from "@/types"; @@ -244,7 +245,7 @@ export default function BlindboxLobbyPage() { {/* Inline create form */}
- { if (e.key === "Enter") handleCreate(); }} maxLength={30} - className="h-11 flex-1 rounded-xl border-none bg-surface px-4 text-sm text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600" + size="xl" + variant="purple" + className="flex-1" />
); @@ -295,11 +297,7 @@ export default function ProfilePage() {
{/* Profile card */} - +

密码

- { @@ -171,7 +173,8 @@ export default function AuthModal({ open, onClose, onAuth, defaultTab = "login" setError(""); }} placeholder={tab === "register" ? "至少 6 个字符" : "请输入密码"} - className="h-11 w-full rounded-xl border-none bg-elevated px-4 pr-10 text-sm text-heading outline-none ring-1 ring-border transition-colors placeholder:text-dim focus:ring-2 focus:ring-accent/50" + size="xl" + className="pr-10" />
)} diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 0000000..67695f4 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,37 @@ +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + animated?: boolean; + delay?: number; +} + +const fadeUp = { + initial: { y: 10, opacity: 0 }, + animate: { y: 0, opacity: 1 }, +} as const; + +export default function Card({ + children, + className = "", + animated = false, + delay, +}: CardProps) { + const cls = `rounded-2xl bg-surface p-4 ring-1 ring-border ${className}`; + + if (animated) { + return ( + + {children} + + ); + } + + return
{children}
; +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..585cbb5 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,31 @@ +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +const sizeStyles = { + sm: "h-8 rounded-lg px-2", + md: "h-9 rounded-lg px-3", + lg: "h-10 rounded-xl px-3", + xl: "h-11 rounded-xl px-4", +} as const; + +const variantStyles = { + default: "bg-elevated text-heading focus:ring-accent/50", + purple: "bg-surface text-foreground focus:ring-purple-600", +} as const; + +interface InputProps extends Omit, "size"> { + size?: keyof typeof sizeStyles; + variant?: keyof typeof variantStyles; +} + +const Input = forwardRef( + ({ size = "md", variant = "default", className = "", ...rest }, ref) => ( + + ), +); + +Input.displayName = "Input"; +export default Input; diff --git a/src/components/RoomManageModal.tsx b/src/components/RoomManageModal.tsx index 81efe0a..4ac5de6 100644 --- a/src/components/RoomManageModal.tsx +++ b/src/components/RoomManageModal.tsx @@ -11,7 +11,7 @@ import { Loader2, } from "lucide-react"; import { UserProfile } from "@/types"; -import { getAvatar, getAvatarBg } from "@/lib/avatars"; +import UserAvatar from "@/components/UserAvatar"; import Modal from "@/components/Modal"; import { useToast } from "@/hooks/useToast"; @@ -129,10 +129,7 @@ export default function RoomManageModal({
{users.map((uid) => { - const profile = userProfiles[uid]; - const emoji = profile?.avatar ?? getAvatar(uid).emoji; - const bg = profile ? getAvatarBg(profile.avatar) : getAvatar(uid).bg; - const displayName = profile?.username ?? uid.slice(0, 8); + const displayName = userProfiles[uid]?.username ?? uid.slice(0, 8); const isCreator = uid === userId; const swiped = swipeCounts[uid] ?? 0; const finished = swiped >= totalCards; @@ -142,11 +139,7 @@ export default function RoomManageModal({ key={uid} className="flex items-center gap-2.5 rounded-xl bg-elevated px-3 py-2.5" > - - {emoji} - +
{isCreator && ( diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index 54b6779..28d1a83 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -8,7 +8,7 @@ import MatchResult from "./MatchResult"; import SwipeGuide from "./SwipeGuide"; import { Restaurant, SwipeDirection, MatchType, RunnerUp, UserProfile, SceneType } from "@/types"; import { Heart, Undo2, Check } from "lucide-react"; -import { getAvatar, getAvatarBg } from "@/lib/avatars"; +import UserAvatar from "@/components/UserAvatar"; function UserProgressBar({ userId, @@ -27,17 +27,11 @@ function UserProgressBar({ }) { const others = Object.entries(swipeCounts).filter(([id]) => id !== userId); - const myProfile = userProfiles[userId]; - const myAvatar = myProfile?.avatar ?? getAvatar(userId).emoji; - const myAvatarBg = myProfile ? getAvatarBg(myProfile.avatar) : "bg-emerald-500/20"; - return (
- - {myAvatar} - + {localIndex}/{total} @@ -45,18 +39,13 @@ function UserProgressBar({ {others.map(([id, count]) => { const finished = count >= total; - const profile = userProfiles[id]; - const emoji = profile?.avatar ?? getAvatar(id).emoji; - const bg = profile ? getAvatarBg(profile.avatar) : getAvatar(id).bg; - const label = profile?.username ?? ""; + const label = userProfiles[id]?.username ?? ""; return ( - - {emoji} - + {label && {label}} {count}/{total} @@ -95,17 +84,11 @@ function WaitingProgress({ const others = entries.filter(([id]) => id !== userId); const finishedCount = others.filter(([, c]) => c >= total).length; - const myProfile = userProfiles[userId]; - const myEmoji = myProfile?.avatar ?? getAvatar(userId).emoji; - const myBg = myProfile ? getAvatarBg(myProfile.avatar) : "bg-emerald-100"; - return (
- - {myEmoji} - + 你 {total}/{total} @@ -114,17 +97,17 @@ function WaitingProgress({ {others.map(([id, count]) => { const finished = count >= total; const pct = Math.min((count / total) * 100, 100); - const profile = userProfiles[id]; - const emoji = profile?.avatar ?? getAvatar(id).emoji; - const bg = profile ? getAvatarBg(profile.avatar) : getAvatar(id).bg; - const label = profile?.username ?? ""; + const label = userProfiles[id]?.username ?? ""; return (
- - {emoji} - + {label && {label}} {count}/{total} diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000..75be9dc --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,34 @@ +import { resolveAvatar } from "@/lib/avatars"; + +const sizeStyles = { + xs: "h-4 w-4 text-[10px]", + sm: "h-5 w-5 text-sm", + md: "h-8 w-8 text-base", + lg: "h-11 w-11 text-xl", + xl: "h-14 w-14 text-2xl", +} as const; + +interface UserAvatarProps { + userId: string; + profile?: { avatar: string } | null; + size?: keyof typeof sizeStyles; + bg?: string; + className?: string; +} + +export default function UserAvatar({ + userId, + profile, + size = "md", + bg, + className = "", +}: UserAvatarProps) { + const avatar = resolveAvatar(userId, profile); + return ( + + {avatar.emoji} + + ); +} diff --git a/src/lib/avatars.ts b/src/lib/avatars.ts index c139e5e..88f3cef 100644 --- a/src/lib/avatars.ts +++ b/src/lib/avatars.ts @@ -25,3 +25,14 @@ export function getAvatarBg(emoji: string): string { const found = AVATARS.find((a) => a.emoji === emoji); return found?.bg ?? "bg-zinc-100"; } + +export function resolveAvatar( + userId: string, + profile?: { avatar: string } | null, +): { emoji: string; bg: string } { + if (profile) { + return { emoji: profile.avatar, bg: getAvatarBg(profile.avatar) }; + } + const fallback = getAvatar(userId); + return { emoji: fallback.emoji, bg: fallback.bg }; +}