feat: 撤回滑动功能,按钮移至进度条旁
- 新增 POST /api/room/[id]/undo 端点,撤回 like 和 swipeCount - 进度条右侧显示"↩ 撤回"按钮,可一直撤回到第一张 - ActionButtons 恢复干净的两按钮布局,避免误触
This commit is contained in:
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<string[]>([]);
|
||||
const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null);
|
||||
const swipingRef = useRef(false);
|
||||
const prevLikeCounts = useRef<Record<string, number>>({});
|
||||
@@ -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 && (
|
||||
<div className="flex items-center justify-center gap-2 px-4 pb-1">
|
||||
<div className="h-1 flex-1 max-w-sm overflow-hidden rounded-full bg-zinc-100">
|
||||
<div className="mx-auto flex w-full max-w-sm items-center gap-2 px-4 pb-1">
|
||||
<div className="h-1 flex-1 overflow-hidden rounded-full bg-zinc-100">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-emerald-400"
|
||||
initial={{ width: 0 }}
|
||||
@@ -161,6 +185,14 @@ export default function SwipeDeck({
|
||||
<span className="shrink-0 text-[11px] tabular-nums text-zinc-400">
|
||||
{currentIndex}/{restaurants.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
disabled={currentIndex === 0}
|
||||
className="flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[11px] font-medium text-amber-500 transition-colors active:bg-amber-50 disabled:opacity-0"
|
||||
>
|
||||
<Undo2 size={12} />
|
||||
撤回
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user