144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback, useRef } from "react";
|
||
import { useParams, useRouter } from "next/navigation";
|
||
import TopNav from "@/components/TopNav";
|
||
import SwipeDeck from "@/components/SwipeDeck";
|
||
import LeaveConfirmModal from "@/components/LeaveConfirmModal";
|
||
import { useRoomPolling } from "@/hooks/useRoomPolling";
|
||
import { getUserId } from "@/lib/userId";
|
||
|
||
export default function RoomPage() {
|
||
const params = useParams<{ id: string }>();
|
||
const router = useRouter();
|
||
const roomId = params.id;
|
||
|
||
const [userId, setUserId] = useState("");
|
||
const [joined, setJoined] = useState(false);
|
||
const [joinFailed, setJoinFailed] = useState(false);
|
||
const [showLeaveConfirm, setShowLeaveConfirm] = useState(false);
|
||
const leavingRef = useRef(false);
|
||
|
||
const {
|
||
userCount, match, matchType, matchLikes, runnerUps, likeCounts, swipeCounts, restaurants, notFound, mutate,
|
||
} = useRoomPolling(roomId);
|
||
|
||
useEffect(() => {
|
||
const id = getUserId();
|
||
setUserId(id);
|
||
|
||
fetch(`/api/room/${roomId}/join`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ userId: id }),
|
||
}).then((res) => {
|
||
if (res.ok) setJoined(true);
|
||
else setJoinFailed(true);
|
||
}).catch(() => setJoinFailed(true));
|
||
}, [roomId]);
|
||
|
||
useEffect(() => {
|
||
window.history.pushState({ roomGuard: true }, "");
|
||
|
||
const handlePopState = () => {
|
||
if (leavingRef.current) return;
|
||
window.history.pushState({ roomGuard: true }, "");
|
||
setShowLeaveConfirm(true);
|
||
};
|
||
|
||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||
if (leavingRef.current) return;
|
||
e.preventDefault();
|
||
};
|
||
|
||
window.addEventListener("popstate", handlePopState);
|
||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||
return () => {
|
||
window.removeEventListener("popstate", handlePopState);
|
||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||
};
|
||
}, []);
|
||
|
||
const confirmLeave = useCallback(() => {
|
||
leavingRef.current = true;
|
||
setShowLeaveConfirm(false);
|
||
router.push("/");
|
||
}, [router]);
|
||
|
||
const cancelLeave = useCallback(() => {
|
||
setShowLeaveConfirm(false);
|
||
}, []);
|
||
|
||
const handleExitRequest = useCallback(() => {
|
||
setShowLeaveConfirm(true);
|
||
}, []);
|
||
|
||
const handleReset = useCallback(async () => {
|
||
await fetch(`/api/room/${roomId}/reset`, { method: "POST" });
|
||
await mutate();
|
||
}, [roomId, mutate]);
|
||
|
||
const handleNarrow = useCallback(async (restaurantIds: string[]) => {
|
||
await fetch(`/api/room/${roomId}/reset`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ restaurantIds }),
|
||
});
|
||
await mutate();
|
||
}, [roomId, mutate]);
|
||
|
||
if (notFound || joinFailed) {
|
||
return (
|
||
<div className="flex h-dvh flex-col items-center justify-center gap-4 bg-background px-6">
|
||
<p className="text-4xl">🍜</p>
|
||
<p className="text-base font-semibold text-zinc-700">房间不存在或已过期</p>
|
||
<p className="text-sm text-zinc-400">房间号可能有误,或房间已超过 24 小时</p>
|
||
<button
|
||
onClick={() => router.push("/")}
|
||
className="mt-2 h-10 rounded-xl bg-emerald-500 px-6 text-sm font-bold text-white shadow-sm transition-colors hover:bg-emerald-600"
|
||
>
|
||
返回首页
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const initialIndex = swipeCounts[userId] ?? 0;
|
||
const ready = joined && userId && restaurants.length > 0;
|
||
|
||
if (!ready) {
|
||
return (
|
||
<div className="flex h-dvh flex-col items-center justify-center gap-3 bg-background">
|
||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-zinc-300 border-t-emerald-500" />
|
||
<p className="text-sm text-zinc-400">正在加载餐厅数据...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-dvh flex-col bg-background">
|
||
<TopNav roomId={roomId} userCount={userCount} onExit={handleExitRequest} />
|
||
<SwipeDeck
|
||
restaurants={restaurants}
|
||
roomId={roomId}
|
||
userId={userId}
|
||
initialIndex={initialIndex}
|
||
matchedRestaurantId={match}
|
||
matchType={matchType}
|
||
matchLikes={matchLikes}
|
||
runnerUps={runnerUps}
|
||
likeCounts={likeCounts}
|
||
swipeCounts={swipeCounts}
|
||
userCount={userCount}
|
||
onReset={handleReset}
|
||
onNarrow={handleNarrow}
|
||
/>
|
||
<LeaveConfirmModal
|
||
open={showLeaveConfirm}
|
||
onConfirm={confirmLeave}
|
||
onCancel={cancelLeave}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|