From cb9f4a3d0f465985fcc24914b418af4b4b29ae6f Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 19:34:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=86=E5=88=86"=E5=86=8D=E6=9D=A5?= =?UTF-8?q?=E4=B8=80=E8=BD=AE"=E4=B8=BA=20Top=20N=20=E5=86=B3=E8=B5=9B?= =?UTF-8?q?=E5=92=8C=E6=8D=A2=E4=B8=80=E6=89=B9=E9=A4=90=E5=8E=85=E4=B8=A4?= =?UTF-8?q?=E4=B8=AA=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/room/[id]/reset/route.ts | 16 ++++++- src/app/room/[id]/page.tsx | 10 +++++ src/components/MatchResult.tsx | 63 ++++++++++++++++++++++++---- src/components/SwipeDeck.tsx | 29 ++++++++++--- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/app/api/room/[id]/reset/route.ts b/src/app/api/room/[id]/reset/route.ts index 9609cc1..ff84a7b 100644 --- a/src/app/api/room/[id]/reset/route.ts +++ b/src/app/api/room/[id]/reset/route.ts @@ -2,13 +2,27 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; export async function POST( - _req: Request, + req: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; + let restaurantIds: string[] | undefined; + try { + const body = await req.json().catch(() => null); + if (body?.restaurantIds && Array.isArray(body.restaurantIds)) { + restaurantIds = body.restaurantIds; + } + } catch { + // No body or invalid JSON — plain reset + } + try { const updated = await atomicUpdateRoom(id, (data) => { + if (restaurantIds && restaurantIds.length > 0) { + const idSet = new Set(restaurantIds); + data.restaurants = data.restaurants.filter((r) => idSet.has(r.id)); + } data.likes = {}; data.swipeCounts = {}; data.match = null; diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index 8cd2163..e2aac04 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -34,6 +34,15 @@ export default function RoomPage() { 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]); + const initialIndex = swipeCounts[userId] ?? 0; const ready = joined && userId && restaurants.length > 0; @@ -62,6 +71,7 @@ export default function RoomPage() { swipeCounts={swipeCounts} userCount={userCount} onReset={handleReset} + onNarrow={handleNarrow} /> ); diff --git a/src/components/MatchResult.tsx b/src/components/MatchResult.tsx index d11c712..243a0ee 100644 --- a/src/components/MatchResult.tsx +++ b/src/components/MatchResult.tsx @@ -15,6 +15,8 @@ import { SearchX, Home, ChevronDown, + Swords, + RefreshCw, } from "lucide-react"; import { Restaurant, MatchType, RunnerUp } from "@/types"; @@ -26,6 +28,7 @@ interface MatchResultProps { allRestaurants: Restaurant[]; userCount: number; onReset: () => Promise; + onNarrow: (restaurantIds: string[]) => Promise; resetting: boolean; } @@ -163,8 +166,10 @@ export default function MatchResult({ allRestaurants, userCount, onReset, + onNarrow, resetting, }: MatchResultProps) { + const router = useRouter(); const [showRunnerUps, setShowRunnerUps] = useState(false); if (matchType === "no_match") { @@ -180,6 +185,11 @@ export default function MatchResult({ }) .filter((x): x is { restaurant: Restaurant; likes: number } => x !== null); + const canNarrow = !isUnanimous && runnerUpRestaurants.length > 0; + const narrowIds = canNarrow + ? [restaurant.id, ...runnerUps.map((ru) => ru.id)] + : []; + return ( )} - - - {resetting ? "重置中..." : "再来一轮"} - + {canNarrow ? ( + <> + onNarrow(narrowIds)} + disabled={resetting} + className="flex w-full items-center justify-center gap-2 rounded-full bg-white/20 px-8 py-3 text-sm font-bold text-white backdrop-blur-sm transition-colors hover:bg-white/30 disabled:opacity-50" + whileTap={{ scale: 0.95 }} + > + + {resetting ? "加载中..." : `Top ${narrowIds.length} 决赛`} + + router.push("/")} + className="flex items-center gap-1.5 text-sm font-medium text-amber-200 underline underline-offset-2 hover:text-white" + > + + 换一批餐厅 + + + ) : ( + <> + + + {resetting ? "重置中..." : "再来一轮"} + + router.push("/")} + className={`flex items-center gap-1.5 text-sm font-medium underline underline-offset-2 hover:text-white ${ + isUnanimous ? "text-emerald-200" : "text-amber-200" + }`} + > + + 换一批餐厅 + + + )} + ); diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index b3f240e..edeb687 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -149,6 +149,7 @@ interface SwipeDeckProps { swipeCounts: Record; userCount: number; onReset: () => Promise; + onNarrow: (restaurantIds: string[]) => Promise; } export default function SwipeDeck({ @@ -164,6 +165,7 @@ export default function SwipeDeck({ swipeCounts, userCount, onReset, + onNarrow, }: SwipeDeckProps) { const [currentIndex, setCurrentIndex] = useState(initialIndex); const [showMatch, setShowMatch] = useState(false); @@ -280,19 +282,33 @@ export default function SwipeDeck({ swipeFnRef.current = null; }, [swipeHistory, currentIndex, roomId, userId]); + const clearLocalState = useCallback(() => { + setCurrentIndex(0); + setShowMatch(false); + setLocalMatchId(null); + setSwipeHistory([]); + prevLikeCounts.current = {}; + }, []); + const handleReset = useCallback(async () => { setResetting(true); try { await onReset(); - setCurrentIndex(0); - setShowMatch(false); - setLocalMatchId(null); - setSwipeHistory([]); - prevLikeCounts.current = {}; + clearLocalState(); } finally { setResetting(false); } - }, [onReset]); + }, [onReset, clearLocalState]); + + const handleNarrow = useCallback(async (restaurantIds: string[]) => { + setResetting(true); + try { + await onNarrow(restaurantIds); + clearLocalState(); + } finally { + setResetting(false); + } + }, [onNarrow, clearLocalState]); const allSwiped = currentIndex >= restaurants.length; const isDone = allSwiped || resolvedMatchId != null; @@ -404,6 +420,7 @@ export default function SwipeDeck({ allRestaurants={restaurants} userCount={userCount} onReset={handleReset} + onNarrow={handleNarrow} resetting={resetting} /> )}