fix(blindbox): 修复房间创建后返回大厅不可见 + 大厅房间列表管理
- 修复房主退出房间时误删整间房的问题,改为返回大厅(房间保留) - 修复大厅页 fetchRooms 时序依赖导致导航回来不刷新的问题 - fetch 添加 cache:no-store + router.refresh() 确保数据始终最新 - 房间列表增加 max-h 滚动 + 底部渐变遮罩防溢出 - 大厅房间卡片支持内联删除/退出(··· 按钮 → 确认栏) - rooms API 返回 creatorId 以区分房主/成员操作
This commit is contained in:
@@ -34,6 +34,7 @@ export const GET = apiHandler(async (req) => {
|
|||||||
id: m.room.id,
|
id: m.room.id,
|
||||||
code: m.room.code,
|
code: m.room.code,
|
||||||
name: m.room.name,
|
name: m.room.name,
|
||||||
|
creatorId: m.room.creatorId,
|
||||||
memberCount: m.room._count.members,
|
memberCount: m.room._count.members,
|
||||||
ideaCount: m.room._count.ideas,
|
ideaCount: m.room._count.ideas,
|
||||||
poolCount: 0,
|
poolCount: 0,
|
||||||
|
|||||||
@@ -590,6 +590,12 @@ export default function BlindboxRoomPage() {
|
|||||||
|
|
||||||
const isCreator = profile?.id === room?.creatorId;
|
const isCreator = profile?.id === room?.creatorId;
|
||||||
|
|
||||||
|
const handleBackToLobby = useCallback(() => {
|
||||||
|
router.push("/blindbox");
|
||||||
|
router.refresh();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
/** Non-creator: leave room (remove membership). Creator: delete room (after confirm). */
|
||||||
const handleLeaveOrDelete = async () => {
|
const handleLeaveOrDelete = async () => {
|
||||||
if (!confirmLeave) {
|
if (!confirmLeave) {
|
||||||
setConfirmLeave(true);
|
setConfirmLeave(true);
|
||||||
@@ -608,7 +614,8 @@ export default function BlindboxRoomPage() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
throw new Error(data.error || "操作失败");
|
throw new Error(data.error || "操作失败");
|
||||||
}
|
}
|
||||||
router.replace("/blindbox");
|
router.push("/blindbox");
|
||||||
|
router.refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.show(e instanceof Error ? e.message : "操作失败");
|
toast.show(e instanceof Error ? e.message : "操作失败");
|
||||||
setConfirmLeave(false);
|
setConfirmLeave(false);
|
||||||
@@ -1130,14 +1137,41 @@ export default function BlindboxRoomPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Leave / Delete — hidden during plan view */}
|
{/* Leave / Back — hidden during plan view. Creator: 返回大厅 (no delete) + optional 删除房间. Non-creator: 退出房间. */}
|
||||||
{isMember && room && phase !== "plan_reveal" && phase !== "planning" && (
|
{isMember && room && phase !== "plan_reveal" && phase !== "planning" && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-12 w-full max-w-sm"
|
className="mt-12 flex w-full max-w-sm flex-col items-center gap-2"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.5 }}
|
||||||
>
|
>
|
||||||
|
{isCreator ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleBackToLobby}
|
||||||
|
className="flex w-full items-center justify-center gap-2 rounded-xl py-2.5 text-xs font-medium text-muted transition-colors hover:text-foreground active:bg-elevated"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={13} />
|
||||||
|
返回大厅
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLeaveOrDelete}
|
||||||
|
disabled={leaving}
|
||||||
|
className={`flex w-full items-center justify-center gap-2 rounded-xl py-2 text-xs font-medium transition-colors ${
|
||||||
|
confirmLeave
|
||||||
|
? "bg-rose-600/15 text-rose-400 ring-1 ring-rose-500/30"
|
||||||
|
: "text-dim hover:text-rose-400/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{leaving ? (
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 size={12} />
|
||||||
|
)}
|
||||||
|
{confirmLeave ? "确认删除房间?所有想法将被清除" : "删除房间"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleLeaveOrDelete}
|
onClick={handleLeaveOrDelete}
|
||||||
disabled={leaving}
|
disabled={leaving}
|
||||||
@@ -1149,15 +1183,12 @@ export default function BlindboxRoomPage() {
|
|||||||
>
|
>
|
||||||
{leaving ? (
|
{leaving ? (
|
||||||
<Loader2 size={13} className="animate-spin" />
|
<Loader2 size={13} className="animate-spin" />
|
||||||
) : isCreator ? (
|
|
||||||
<Trash2 size={13} />
|
|
||||||
) : (
|
) : (
|
||||||
<LogOut size={13} />
|
<LogOut size={13} />
|
||||||
)}
|
)}
|
||||||
{confirmLeave
|
{confirmLeave ? "确认退出房间?" : "退出房间"}
|
||||||
? isCreator ? "确认删除房间?所有想法将被清除" : "确认退出房间?"
|
|
||||||
: isCreator ? "删除房间" : "退出房间"}
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+210
-80
@@ -11,24 +11,158 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
MoreHorizontal,
|
||||||
|
Trash2,
|
||||||
|
LogOut,
|
||||||
|
Loader2,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||||
import AuthModal from "@/components/AuthModal";
|
import AuthModal from "@/components/AuthModal";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import { BlindboxListSkeleton } from "@/components/Skeleton";
|
import { BlindboxListSkeleton } from "@/components/Skeleton";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
|
|
||||||
interface RoomSummary {
|
interface RoomSummary {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
creatorId: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
poolCount: number;
|
poolCount: number;
|
||||||
members: { id: string; username: string; avatar: string }[];
|
members: { id: string; username: string; avatar: string }[];
|
||||||
lastDrawn: { content: string; createdAt: string } | null;
|
lastDrawn: { content: string; createdAt: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Room card with inline delete ────────────────────────── */
|
||||||
|
|
||||||
|
function RoomCard({
|
||||||
|
room,
|
||||||
|
index,
|
||||||
|
isCreator,
|
||||||
|
confirmDelete,
|
||||||
|
deleting,
|
||||||
|
onNavigate,
|
||||||
|
onRequestDelete,
|
||||||
|
onConfirmDelete,
|
||||||
|
onCancelDelete,
|
||||||
|
}: {
|
||||||
|
room: RoomSummary;
|
||||||
|
index: number;
|
||||||
|
isCreator: boolean;
|
||||||
|
confirmDelete: boolean;
|
||||||
|
deleting: boolean;
|
||||||
|
onNavigate: () => void;
|
||||||
|
onRequestDelete: () => void;
|
||||||
|
onConfirmDelete: () => void;
|
||||||
|
onCancelDelete: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, height: 0, marginTop: 0, transition: { duration: 0.25 } }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="shrink-0 rounded-2xl bg-surface ring-1 ring-border"
|
||||||
|
>
|
||||||
|
{/* Main card row */}
|
||||||
|
<button
|
||||||
|
onClick={onNavigate}
|
||||||
|
className="flex w-full items-center gap-3 p-4 text-left transition-colors active:bg-elevated"
|
||||||
|
>
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-purple-600/15 ring-1 ring-purple-500/20">
|
||||||
|
<Package size={20} className="text-purple-400" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm font-bold text-heading">{room.name}</p>
|
||||||
|
<div className="mt-1 flex items-center gap-3 text-[11px] text-muted">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users size={11} />
|
||||||
|
{room.memberCount}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Package size={11} />
|
||||||
|
{room.poolCount} 待抽
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{room.lastDrawn && (
|
||||||
|
<p className="mt-1 truncate text-[11px] text-purple-400/60">
|
||||||
|
最近抽中:{room.lastDrawn.content}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex shrink-0 -space-x-1.5">
|
||||||
|
{room.members.slice(0, 3).map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-full bg-elevated text-xs ring-2 ring-surface"
|
||||||
|
title={m.username}
|
||||||
|
>
|
||||||
|
{m.avatar}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{room.memberCount > 3 && (
|
||||||
|
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-elevated text-[10px] font-bold text-muted ring-2 ring-surface">
|
||||||
|
+{room.memberCount - 3}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ··· menu trigger — stops propagation so card onClick doesn't fire */}
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
confirmDelete ? onCancelDelete() : onRequestDelete();
|
||||||
|
}}
|
||||||
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted/40 transition-colors hover:bg-elevated hover:text-muted active:text-foreground"
|
||||||
|
>
|
||||||
|
{confirmDelete ? <X size={14} /> : <MoreHorizontal size={16} />}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Inline confirm bar */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{confirmDelete && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-t border-border/60 px-4 py-2.5">
|
||||||
|
<span className="text-xs text-muted">
|
||||||
|
{isCreator ? "删除房间?所有想法将被清除" : "确认退出该房间?"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={onConfirmDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
className="flex items-center gap-1.5 rounded-lg bg-rose-500/15 px-3 py-1.5 text-xs font-semibold text-rose-400 transition-colors hover:bg-rose-500/25 active:bg-rose-500/30"
|
||||||
|
>
|
||||||
|
{deleting ? (
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
) : isCreator ? (
|
||||||
|
<Trash2 size={12} />
|
||||||
|
) : (
|
||||||
|
<LogOut size={12} />
|
||||||
|
)}
|
||||||
|
{isCreator ? "删除" : "退出"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function BlindboxLobbyPage() {
|
export default function BlindboxLobbyPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [hydrated, setHydrated] = useState(false);
|
const [hydrated, setHydrated] = useState(false);
|
||||||
@@ -38,6 +172,7 @@ export default function BlindboxLobbyPage() {
|
|||||||
const [rooms, setRooms] = useState<RoomSummary[]>([]);
|
const [rooms, setRooms] = useState<RoomSummary[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
const createNameRef = useRef<HTMLInputElement>(null);
|
const createNameRef = useRef<HTMLInputElement>(null);
|
||||||
const joinCodeRef = useRef<HTMLInputElement>(null);
|
const joinCodeRef = useRef<HTMLInputElement>(null);
|
||||||
const [joinCodeLength, setJoinCodeLength] = useState(0);
|
const [joinCodeLength, setJoinCodeLength] = useState(0);
|
||||||
@@ -46,24 +181,8 @@ export default function BlindboxLobbyPage() {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loadError, setLoadError] = useState(false);
|
const [loadError, setLoadError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||||
const registered = isRegistered();
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
setLoggedIn(registered);
|
|
||||||
if (registered) {
|
|
||||||
setProfile(getCachedProfile());
|
|
||||||
}
|
|
||||||
setHydrated(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = () => {
|
|
||||||
const registered = isRegistered();
|
|
||||||
setLoggedIn(registered);
|
|
||||||
setProfile(registered ? getCachedProfile() : null);
|
|
||||||
};
|
|
||||||
window.addEventListener("nowhatever_auth", handler);
|
|
||||||
return () => window.removeEventListener("nowhatever_auth", handler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchRooms = useCallback(async (silent = false) => {
|
const fetchRooms = useCallback(async (silent = false) => {
|
||||||
const p = getCachedProfile();
|
const p = getCachedProfile();
|
||||||
@@ -73,7 +192,7 @@ export default function BlindboxLobbyPage() {
|
|||||||
setLoadError(false);
|
setLoadError(false);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/blindbox/rooms?userId=${p.id}`);
|
const res = await fetch(`/api/blindbox/rooms?userId=${p.id}`, { cache: "no-store" });
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setRooms(Array.isArray(data.rooms) ? data.rooms : []);
|
setRooms(Array.isArray(data.rooms) ? data.rooms : []);
|
||||||
@@ -86,26 +205,49 @@ export default function BlindboxLobbyPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loggedIn) fetchRooms();
|
const registered = isRegistered();
|
||||||
else setLoading(false);
|
setLoggedIn(registered);
|
||||||
}, [loggedIn, fetchRooms]);
|
if (registered) {
|
||||||
|
setProfile(getCachedProfile());
|
||||||
|
fetchRooms();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
setHydrated(true);
|
||||||
|
}, [fetchRooms]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
const registered = isRegistered();
|
||||||
|
setLoggedIn(registered);
|
||||||
|
setProfile(registered ? getCachedProfile() : null);
|
||||||
|
if (registered) fetchRooms();
|
||||||
|
};
|
||||||
|
window.addEventListener("nowhatever_auth", handler);
|
||||||
|
return () => window.removeEventListener("nowhatever_auth", handler);
|
||||||
|
}, [fetchRooms]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loggedIn) return;
|
if (!loggedIn) return;
|
||||||
const onFocus = () => fetchRooms(true);
|
const refresh = () => fetchRooms(true);
|
||||||
window.addEventListener("focus", onFocus);
|
window.addEventListener("focus", refresh);
|
||||||
return () => window.removeEventListener("focus", onFocus);
|
window.addEventListener("pageshow", refresh);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("focus", refresh);
|
||||||
|
window.removeEventListener("pageshow", refresh);
|
||||||
|
};
|
||||||
}, [loggedIn, fetchRooms]);
|
}, [loggedIn, fetchRooms]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rooms.length > 0) setJoinCodeLength(0);
|
if (rooms.length > 0) setJoinCodeLength(0);
|
||||||
}, [rooms.length]);
|
}, [rooms.length]);
|
||||||
|
|
||||||
const handleAuth = (p: UserProfile) => {
|
const handleAuth = useCallback((p: UserProfile) => {
|
||||||
setProfile(p);
|
setProfile(p);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setShowAuth(false);
|
setShowAuth(false);
|
||||||
};
|
fetchRooms();
|
||||||
|
}, [fetchRooms]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (creating || !profile) return;
|
if (creating || !profile) return;
|
||||||
@@ -149,6 +291,29 @@ export default function BlindboxLobbyPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteRoom = useCallback(async (room: RoomSummary) => {
|
||||||
|
if (deletingId || !profile) return;
|
||||||
|
setDeletingId(room.id);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/blindbox/room/${room.code}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ userId: profile.id }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "操作失败");
|
||||||
|
}
|
||||||
|
setRooms((prev) => prev.filter((r) => r.id !== room.id));
|
||||||
|
setConfirmDeleteId(null);
|
||||||
|
toast.show(room.creatorId === profile.id ? "房间已删除" : "已退出房间");
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof Error ? e.message : "操作失败");
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
}, [deletingId, profile, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-dvh flex-col items-center justify-center bg-background px-6 py-6 overflow-y-auto scrollbar-none">
|
<div className="relative flex min-h-dvh flex-col items-center justify-center bg-background px-6 py-6 overflow-y-auto scrollbar-none">
|
||||||
{/* Ambient */}
|
{/* Ambient */}
|
||||||
@@ -270,7 +435,7 @@ export default function BlindboxLobbyPage() {
|
|||||||
<Package size={36} className="text-purple-400/30" strokeWidth={1.5} />
|
<Package size={36} className="text-purple-400/30" strokeWidth={1.5} />
|
||||||
<p className="text-sm text-muted">加载房间失败</p>
|
<p className="text-sm text-muted">加载房间失败</p>
|
||||||
<button
|
<button
|
||||||
onClick={fetchRooms}
|
onClick={() => fetchRooms()}
|
||||||
className="mt-1 text-xs font-medium text-purple-400 active:text-purple-300"
|
className="mt-1 text-xs font-medium text-purple-400 active:text-purple-300"
|
||||||
>
|
>
|
||||||
点击重试
|
点击重试
|
||||||
@@ -448,64 +613,29 @@ export default function BlindboxLobbyPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Room list */}
|
{/* Room list */}
|
||||||
<div className="mt-5 flex flex-col gap-3">
|
<div className="relative mt-5">
|
||||||
|
<div className="max-h-[40dvh] overflow-y-auto scrollbar-none -mx-2 px-2 flex flex-col gap-3 pt-1 pb-5">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
{rooms.map((room, i) => (
|
{rooms.map((room, i) => (
|
||||||
<motion.button
|
<RoomCard
|
||||||
key={room.id}
|
key={room.id}
|
||||||
onClick={() => router.push(`/blindbox/${room.code}`)}
|
room={room}
|
||||||
className="group flex w-full items-center gap-3 rounded-2xl bg-surface p-4 text-left ring-1 ring-border transition-all hover:bg-elevated hover:ring-purple-500/30"
|
index={i}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
isCreator={room.creatorId === profile?.id}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
confirmDelete={confirmDeleteId === room.id}
|
||||||
transition={{ delay: i * 0.06 }}
|
deleting={deletingId === room.id}
|
||||||
whileTap={{ scale: 0.98 }}
|
onNavigate={() => router.push(`/blindbox/${room.code}`)}
|
||||||
>
|
onRequestDelete={() => setConfirmDeleteId(room.id)}
|
||||||
{/* Icon */}
|
onConfirmDelete={() => handleDeleteRoom(room)}
|
||||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-purple-600/15 ring-1 ring-purple-500/20">
|
onCancelDelete={() => setConfirmDeleteId(null)}
|
||||||
<Package size={20} className="text-purple-400" strokeWidth={1.5} />
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Info */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="truncate text-sm font-bold text-heading">{room.name}</p>
|
|
||||||
<div className="mt-1 flex items-center gap-3 text-[11px] text-muted">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Users size={11} />
|
|
||||||
{room.memberCount}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Package size={11} />
|
|
||||||
{room.poolCount} 待抽
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{room.lastDrawn && (
|
|
||||||
<p className="mt-1 truncate text-[11px] text-purple-400/60">
|
|
||||||
最近抽中:{room.lastDrawn.content}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Members preview */}
|
|
||||||
<div className="flex shrink-0 -space-x-1.5">
|
|
||||||
{room.members.slice(0, 3).map((m) => (
|
|
||||||
<div
|
|
||||||
key={m.id}
|
|
||||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-elevated text-xs ring-2 ring-surface"
|
|
||||||
title={m.username}
|
|
||||||
>
|
|
||||||
{m.avatar}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
{room.memberCount > 3 && (
|
</AnimatePresence>
|
||||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-elevated text-[10px] font-bold text-muted ring-2 ring-surface">
|
|
||||||
+{room.memberCount - 3}
|
|
||||||
</div>
|
</div>
|
||||||
|
{rooms.length > 3 && (
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-linear-to-t from-background to-transparent" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChevronRight size={16} className="shrink-0 text-muted/50 transition-colors group-hover:text-purple-400" />
|
|
||||||
</motion.button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
Reference in New Issue
Block a user