Files
no-whatever/src/components/RoomManageModal.tsx
T
kurihada d4c6da57a1 refactor: Toast 升级为全局 Context,消除 onToast prop 透传
将 useToast 从独立 state hook 改为 Context-based,在 layout 中
挂载 ToastProvider 全局渲染 Toast。QrInviteModal、RoomManageModal、
ShareCardModal 不再需要 onToast prop,直接 useToast() 调用即可。
父组件 TopNav、MatchResult、profile、blindbox 移除了本地 Toast
渲染和 onToast 传递逻辑。
2026-02-26 17:57:34 +08:00

253 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useCallback } from "react";
import {
X,
Lock,
Unlock,
UserX,
Flag,
Crown,
Loader2,
} from "lucide-react";
import { UserProfile } from "@/types";
import { getAvatar, getAvatarBg } from "@/lib/avatars";
import Modal from "@/components/Modal";
import { useToast } from "@/hooks/useToast";
interface RoomManageModalProps {
open: boolean;
onClose: () => void;
roomId: string;
userId: string;
users: string[];
locked: boolean;
swipeCounts: Record<string, number>;
totalCards: number;
userProfiles: Record<string, UserProfile>;
}
export default function RoomManageModal({
open,
onClose,
roomId,
userId,
users,
locked,
swipeCounts,
totalCards,
userProfiles,
}: RoomManageModalProps) {
const toast = useToast();
const [loading, setLoading] = useState<string | null>(null);
const [confirmKick, setConfirmKick] = useState<string | null>(null);
const [confirmEnd, setConfirmEnd] = useState(false);
const manage = useCallback(
async (action: string, targetUserId?: string) => {
setLoading(action + (targetUserId ?? ""));
try {
const res = await fetch(`/api/room/${roomId}/manage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, action, targetUserId }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
toast.show(data.error ?? "操作失败");
return;
}
switch (action) {
case "lock":
toast.show("房间已锁定,其他人无法加入");
break;
case "unlock":
toast.show("房间已解锁");
break;
case "kick":
toast.show("已将该用户移出房间");
setConfirmKick(null);
break;
case "end_voting":
toast.show("已结束投票,正在结算结果");
setConfirmEnd(false);
onClose();
break;
}
} catch {
toast.show("操作失败,请重试");
} finally {
setLoading(null);
}
},
[roomId, userId, toast, onClose],
);
const otherUsers = users.filter((u) => u !== userId);
return (
<Modal open={open} onClose={onClose}>
<button
onClick={onClose}
className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full bg-elevated text-muted transition-colors active:bg-subtle"
>
<X size={16} />
</button>
<div className="flex items-center gap-2">
<Crown size={18} className="text-amber-400" />
<h2 className="text-lg font-bold text-heading"></h2>
</div>
<p className="mt-1 text-xs text-muted">
{roomId}
</p>
<div className="mt-5">
<button
onClick={() => manage(locked ? "unlock" : "lock")}
disabled={loading !== null}
className={`flex h-11 w-full items-center justify-center gap-2 rounded-xl text-sm font-semibold transition-colors disabled:opacity-50 ${
locked
? "bg-accent/15 text-accent ring-1 ring-accent/30 active:bg-accent/25"
: "bg-elevated text-secondary ring-1 ring-border active:bg-subtle"
}`}
>
{loading === "lock" || loading === "unlock" ? (
<Loader2 size={15} className="animate-spin" />
) : locked ? (
<Unlock size={15} />
) : (
<Lock size={15} />
)}
{locked ? "解锁房间(允许新人加入)" : "锁定房间(阻止新人加入)"}
</button>
</div>
<div className="mt-5">
<h3 className="text-xs font-semibold text-muted">
{users.length}
</h3>
<div className="mt-2 flex flex-col gap-1.5">
{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 isCreator = uid === userId;
const swiped = swipeCounts[uid] ?? 0;
const finished = swiped >= totalCards;
return (
<div
key={uid}
className="flex items-center gap-2.5 rounded-xl bg-elevated px-3 py-2.5"
>
<span
className={`inline-flex h-8 w-8 items-center justify-center rounded-full text-base ${bg}`}
>
{emoji}
</span>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-center gap-1.5">
{isCreator && (
<span className="flex items-center gap-0.5 text-[10px] font-bold text-amber-400">
<Crown size={10} />
</span>
)}
<span className="truncate text-xs font-medium text-tertiary">
{displayName}
</span>
</div>
<span
className={`text-[11px] ${finished ? "text-accent" : "text-muted"}`}
>
{swiped}/{totalCards}
{finished ? " 已完成" : " 进行中"}
</span>
</div>
{!isCreator && (
<>
{confirmKick === uid ? (
<div className="flex items-center gap-1">
<button
onClick={() => manage("kick", uid)}
disabled={loading !== null}
className="rounded-lg bg-rose-500 px-2.5 py-1 text-[11px] font-semibold text-white transition-colors active:bg-rose-600 disabled:opacity-50"
>
{loading === "kick" + uid ? (
<Loader2
size={12}
className="animate-spin"
/>
) : (
"确认"
)}
</button>
<button
onClick={() => setConfirmKick(null)}
className="rounded-lg bg-subtle px-2.5 py-1 text-[11px] font-semibold text-tertiary transition-colors active:bg-elevated"
>
</button>
</div>
) : (
<button
onClick={() => setConfirmKick(uid)}
className="flex items-center gap-0.5 rounded-lg px-2 py-1 text-[11px] font-medium text-muted transition-colors active:bg-subtle active:text-rose-400"
>
<UserX size={13} />
</button>
)}
</>
)}
</div>
);
})}
</div>
</div>
<div className="mt-5">
{confirmEnd ? (
<div className="flex flex-col gap-2 rounded-xl bg-amber-500/10 p-3 ring-1 ring-amber-500/30">
<p className="text-xs font-medium text-amber-300">
</p>
<div className="flex gap-2">
<button
onClick={() => manage("end_voting")}
disabled={loading !== null}
className="flex h-9 flex-1 items-center justify-center gap-1.5 rounded-lg bg-amber-500 text-xs font-semibold text-white transition-colors active:bg-amber-600 disabled:opacity-50"
>
{loading === "end_voting" ? (
<Loader2 size={13} className="animate-spin" />
) : (
<Flag size={13} />
)}
</button>
<button
onClick={() => setConfirmEnd(false)}
className="flex h-9 flex-1 items-center justify-center rounded-lg bg-elevated text-xs font-semibold text-tertiary transition-colors active:bg-subtle"
>
</button>
</div>
</div>
) : (
<button
onClick={() => setConfirmEnd(true)}
disabled={loading !== null}
className="flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-amber-500/10 text-sm font-semibold text-amber-400 ring-1 ring-amber-500/30 transition-colors active:bg-amber-500/20 disabled:opacity-50"
>
<Flag size={15} />
</button>
)}
</div>
</Modal>
);
}