diff --git a/src/app/api/room/[id]/route.ts b/src/app/api/room/[id]/route.ts index 4e1c018..b55f057 100644 --- a/src/app/api/room/[id]/route.ts +++ b/src/app/api/room/[id]/route.ts @@ -26,18 +26,20 @@ export async function GET( let match = data.match; let matchType: MatchType = null; let matchLikes = 0; + let runnerUps: { id: string; likes: number }[] = []; if (match) { matchType = "unanimous"; matchLikes = data.users.length; } else if (allFinished && data.restaurants.length > 0) { - const best = findBestMatch(data.likes, data.restaurants); - if (best.likes > 0) { - match = best.id; + const ranked = rankRestaurants(data.likes, data.restaurants); + if (ranked[0].likes > 0) { + match = ranked[0].id; matchType = "best"; - matchLikes = best.likes; + matchLikes = ranked[0].likes; + runnerUps = ranked.slice(1, 3).filter((r) => r.likes > 0); } else { - match = best.id; + match = ranked[0].id; matchType = "no_match"; matchLikes = 0; } @@ -56,6 +58,7 @@ export async function GET( match, matchType, matchLikes, + runnerUps, likeCounts, swipeCounts: data.swipeCounts, restaurants: data.restaurants, @@ -69,26 +72,12 @@ export async function GET( } } -function findBestMatch( +function rankRestaurants( likes: Record, restaurants: { id: string; rating: number }[], -): { id: string; likes: number } { - let bestId = restaurants[0].id; - let bestLikes = 0; - let bestRating = restaurants[0].rating; - - for (const r of restaurants) { - const count = likes[r.id]?.length ?? 0; - - if ( - count > bestLikes || - (count === bestLikes && r.rating > bestRating) - ) { - bestId = r.id; - bestLikes = count; - bestRating = r.rating; - } - } - - return { id: bestId, likes: bestLikes }; +): { id: string; likes: number }[] { + return restaurants + .map((r) => ({ id: r.id, likes: likes[r.id]?.length ?? 0, rating: r.rating })) + .sort((a, b) => b.likes - a.likes || b.rating - a.rating) + .map(({ id, likes: l }) => ({ id, likes: l })); } diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index 94a067d..8cd2163 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -15,7 +15,7 @@ export default function RoomPage() { const [joined, setJoined] = useState(false); const { - userCount, match, matchType, matchLikes, likeCounts, swipeCounts, restaurants, mutate, + userCount, match, matchType, matchLikes, runnerUps, likeCounts, swipeCounts, restaurants, mutate, } = useRoomPolling(roomId); useEffect(() => { @@ -57,6 +57,7 @@ export default function RoomPage() { matchedRestaurantId={match} matchType={matchType} matchLikes={matchLikes} + runnerUps={runnerUps} likeCounts={likeCounts} swipeCounts={swipeCounts} userCount={userCount} diff --git a/src/components/MatchResult.tsx b/src/components/MatchResult.tsx index 10da8d5..d11c712 100644 --- a/src/components/MatchResult.tsx +++ b/src/components/MatchResult.tsx @@ -1,6 +1,7 @@ "use client"; -import { motion } from "framer-motion"; +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { useRouter } from "next/navigation"; import { MapPin, @@ -13,13 +14,16 @@ import { RotateCcw, SearchX, Home, + ChevronDown, } from "lucide-react"; -import { Restaurant, MatchType } from "@/types"; +import { Restaurant, MatchType, RunnerUp } from "@/types"; interface MatchResultProps { restaurant: Restaurant; matchType: MatchType; matchLikes: number; + runnerUps: RunnerUp[]; + allRestaurants: Restaurant[]; userCount: number; onReset: () => Promise; resetting: boolean; @@ -104,23 +108,81 @@ function NoMatchResult({ ); } +function RunnerUpCard({ + restaurant, + likes, + userCount, +}: { + restaurant: Restaurant; + likes: number; + userCount: number; +}) { + return ( + + {restaurant.name} +
+

+ {restaurant.name} +

+
+ + + {restaurant.rating} + + {restaurant.price} + {restaurant.distance && ( + + + {restaurant.distance} + + )} +
+

+ {likes}/{userCount} 人想去 +

+
+
+ ); +} + export default function MatchResult({ restaurant, matchType, matchLikes, + runnerUps, + allRestaurants, userCount, onReset, resetting, }: MatchResultProps) { + const [showRunnerUps, setShowRunnerUps] = useState(false); + if (matchType === "no_match") { return ; } const isUnanimous = matchType === "unanimous"; + const runnerUpRestaurants = runnerUps + .map((ru) => { + const r = allRestaurants.find((rest) => rest.id === ru.id); + return r ? { restaurant: r, likes: ru.likes } : null; + }) + .filter((x): x is { restaurant: Restaurant; likes: number } => x !== null); + return ( - - {isUnanimous ? ( - - ) : ( - - )} - - - - 就去这了! - - - - {isUnanimous - ? "大家一拍即合!" - : `${matchLikes}/${userCount} 人想去这家`} - - - - {restaurant.name} -
-
-

- {restaurant.name} -

- {restaurant.category && ( - - {restaurant.category} - - )} -
- -
- - - {restaurant.rating} - - - {restaurant.price} - - {restaurant.distance && ( - - - {restaurant.distance} - - )} -
- - {restaurant.address && ( -

- {restaurant.address} -

- )} - - {restaurant.openTime && ( -
- - {restaurant.openTime} -
- )} - - {restaurant.tag && ( -
- {restaurant.tag - .split(",") - .slice(0, 4) - .map((t) => ( - - {t.trim()} - - ))} -
- )} -
-
- - - + - - 导航过去 - + {isUnanimous ? ( + + ) : ( + + )} + - {restaurant.tel && ( + + 就去这了! + + + + {isUnanimous + ? "大家一拍即合!" + : `${matchLikes}/${userCount} 人想去这家`} + + + + {restaurant.name} +
+
+

+ {restaurant.name} +

+ {restaurant.category && ( + + {restaurant.category} + + )} +
+ +
+ + + {restaurant.rating} + + + {restaurant.price} + + {restaurant.distance && ( + + + {restaurant.distance} + + )} +
+ + {restaurant.address && ( +

+ {restaurant.address} +

+ )} + + {restaurant.openTime && ( +
+ + {restaurant.openTime} +
+ )} + + {restaurant.tag && ( +
+ {restaurant.tag + .split(",") + .slice(0, 4) + .map((t) => ( + + {t.trim()} + + ))} +
+ )} +
+
+ + - - 打电话订位 + + 导航过去 - )} - - - - {resetting ? "重置中..." : "再来一轮"} - + {restaurant.tel && ( + + + 打电话订位 + + )} +
+ + {!isUnanimous && runnerUpRestaurants.length > 0 && ( + + + + {showRunnerUps && ( + + {runnerUpRestaurants.map(({ restaurant: r, likes }) => ( + + ))} + + )} + + + )} + + + + {resetting ? "重置中..." : "再来一轮"} + + ); } diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index 75060bc..b3f240e 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -6,7 +6,7 @@ import SwipeableCard from "./SwipeableCard"; import ActionButtons from "./ActionButtons"; import MatchResult from "./MatchResult"; import SwipeGuide from "./SwipeGuide"; -import { Restaurant, SwipeDirection, MatchType } from "@/types"; +import { Restaurant, SwipeDirection, MatchType, RunnerUp } from "@/types"; import { Heart, Undo2, Check } from "lucide-react"; const AVATARS = [ @@ -144,6 +144,7 @@ interface SwipeDeckProps { matchedRestaurantId: string | null; matchType: MatchType; matchLikes: number; + runnerUps: RunnerUp[]; likeCounts: Record; swipeCounts: Record; userCount: number; @@ -158,6 +159,7 @@ export default function SwipeDeck({ matchedRestaurantId, matchType, matchLikes, + runnerUps, likeCounts, swipeCounts, userCount, @@ -398,6 +400,8 @@ export default function SwipeDeck({ restaurant={matchRestaurant} matchType={matchType ?? "unanimous"} matchLikes={matchLikes} + runnerUps={runnerUps} + allRestaurants={restaurants} userCount={userCount} onReset={handleReset} resetting={resetting} diff --git a/src/hooks/useRoomPolling.ts b/src/hooks/useRoomPolling.ts index 23d49d8..6c6a0ac 100644 --- a/src/hooks/useRoomPolling.ts +++ b/src/hooks/useRoomPolling.ts @@ -29,6 +29,7 @@ export function useRoomPolling(roomId: string) { match: data?.match ?? null, matchType: data?.matchType ?? null, matchLikes: data?.matchLikes ?? 0, + runnerUps: data?.runnerUps ?? [], likeCounts: data?.likeCounts ?? {}, swipeCounts: data?.swipeCounts ?? {}, restaurants: data?.restaurants ?? [], diff --git a/src/types/index.ts b/src/types/index.ts index de4d485..e37ad97 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,12 +17,18 @@ export type SwipeDirection = "left" | "right"; export type MatchType = "unanimous" | "best" | "no_match" | null; +export interface RunnerUp { + id: string; + likes: number; +} + export interface RoomStatus { roomId: string; userCount: number; match: string | null; matchType: MatchType; matchLikes: number; + runnerUps: RunnerUp[]; likeCounts: Record; swipeCounts: Record; restaurants: Restaurant[];