diff --git a/src/app/api/room/[id]/undo/route.ts b/src/app/api/room/[id]/undo/route.ts new file mode 100644 index 0000000..eb4636e --- /dev/null +++ b/src/app/api/room/[id]/undo/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from "next/server"; +import { atomicUpdateRoom } from "@/lib/store"; + +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + + try { + const { userId, restaurantId } = await req.json(); + + if (!userId || restaurantId == null) { + return NextResponse.json( + { error: "userId and restaurantId are required" }, + { status: 400 }, + ); + } + + const rid = String(restaurantId); + + const updated = await atomicUpdateRoom(id, (data) => { + if (data.likes[rid]) { + data.likes[rid] = data.likes[rid].filter((u) => u !== userId); + if (data.likes[rid].length === 0) { + delete data.likes[rid]; + } + } + + if (data.match === rid) { + data.match = null; + } + + const count = data.swipeCounts[userId] ?? 0; + if (count > 0) { + data.swipeCounts[userId] = count - 1; + } + + return data; + }); + + if (!updated) { + return NextResponse.json( + { error: "房间不存在或已过期" }, + { status: 404 }, + ); + } + + return NextResponse.json({ ok: true }); + } catch (e) { + console.error("Failed to undo swipe:", e); + return NextResponse.json( + { error: "撤回失败" }, + { status: 500 }, + ); + } +} diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index 7970ec5..97a41db 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -7,7 +7,7 @@ import ActionButtons from "./ActionButtons"; import MatchResult from "./MatchResult"; import SwipeGuide from "./SwipeGuide"; import { Restaurant, SwipeDirection, MatchType } from "@/types"; -import { Heart } from "lucide-react"; +import { Heart, Undo2 } from "lucide-react"; interface SwipeDeckProps { restaurants: Restaurant[]; @@ -38,6 +38,7 @@ export default function SwipeDeck({ const [resetting, setResetting] = useState(false); const [bubble, setBubble] = useState(""); const [guideVisible, setGuideVisible] = useState(true); + const [swipeHistory, setSwipeHistory] = useState([]); const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null); const swipingRef = useRef(false); const prevLikeCounts = useRef>({}); @@ -104,6 +105,7 @@ export default function SwipeDeck({ const action = direction === "right" ? "like" : "nope"; sendSwipe(current.id, action); + setSwipeHistory((h) => [...h, current.id]); const nextIndex = currentIndex + 1; setCurrentIndex(nextIndex); swipeFnRef.current = null; @@ -122,6 +124,27 @@ export default function SwipeDeck({ [], ); + const handleUndo = useCallback(async () => { + if (swipeHistory.length === 0 || currentIndex === 0) return; + + const lastRid = swipeHistory[swipeHistory.length - 1]; + + try { + await fetch(`/api/room/${roomId}/undo`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId, restaurantId: lastRid }), + }); + } catch { + // Best-effort + } + + setSwipeHistory((h) => h.slice(0, -1)); + setCurrentIndex((i) => i - 1); + setLocalMatchId(null); + swipeFnRef.current = null; + }, [swipeHistory, currentIndex, roomId, userId]); + const handleReset = useCallback(async () => { setResetting(true); try { @@ -129,6 +152,7 @@ export default function SwipeDeck({ setCurrentIndex(0); setShowMatch(false); setLocalMatchId(null); + setSwipeHistory([]); prevLikeCounts.current = {}; } finally { setResetting(false); @@ -147,8 +171,8 @@ export default function SwipeDeck({ return ( <> {!allSwiped && !resolvedMatchId && ( -
-
+
+
{currentIndex}/{restaurants.length} +
)}