feat: 新用户滑动引导,首次进入展示手势提示
- 首张卡片叠加半透明引导层,左滑不想去/右滑想去动画提示 - 支持点击消失、拖拽消失、3秒自动消失 - localStorage 记录标记,每个用户只展示一次 - 修复 React Strict Mode 下 effect 双执行导致引导不显示的问题
This commit is contained in:
@@ -5,6 +5,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import SwipeableCard from "./SwipeableCard";
|
import SwipeableCard from "./SwipeableCard";
|
||||||
import ActionButtons from "./ActionButtons";
|
import ActionButtons from "./ActionButtons";
|
||||||
import MatchResult from "./MatchResult";
|
import MatchResult from "./MatchResult";
|
||||||
|
import SwipeGuide from "./SwipeGuide";
|
||||||
import { Restaurant, SwipeDirection, MatchType } from "@/types";
|
import { Restaurant, SwipeDirection, MatchType } from "@/types";
|
||||||
import { Heart } from "lucide-react";
|
import { Heart } from "lucide-react";
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export default function SwipeDeck({
|
|||||||
const [localMatchId, setLocalMatchId] = useState<string | null>(null);
|
const [localMatchId, setLocalMatchId] = useState<string | null>(null);
|
||||||
const [resetting, setResetting] = useState(false);
|
const [resetting, setResetting] = useState(false);
|
||||||
const [bubble, setBubble] = useState("");
|
const [bubble, setBubble] = useState("");
|
||||||
|
const [guideVisible, setGuideVisible] = useState(true);
|
||||||
const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null);
|
const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null);
|
||||||
const swipingRef = useRef(false);
|
const swipingRef = useRef(false);
|
||||||
const prevLikeCounts = useRef<Record<string, number>>({});
|
const prevLikeCounts = useRef<Record<string, number>>({});
|
||||||
@@ -97,6 +99,7 @@ export default function SwipeDeck({
|
|||||||
if (!current) return;
|
if (!current) return;
|
||||||
|
|
||||||
swipingRef.current = false;
|
swipingRef.current = false;
|
||||||
|
if (guideVisible) setGuideVisible(false);
|
||||||
|
|
||||||
const action = direction === "right" ? "like" : "nope";
|
const action = direction === "right" ? "like" : "nope";
|
||||||
sendSwipe(current.id, action);
|
sendSwipe(current.id, action);
|
||||||
@@ -163,6 +166,9 @@ export default function SwipeDeck({
|
|||||||
|
|
||||||
<div className="relative flex flex-1 items-center justify-center px-4">
|
<div className="relative flex flex-1 items-center justify-center px-4">
|
||||||
<div className="relative h-[70vh] w-full max-w-sm">
|
<div className="relative h-[70vh] w-full max-w-sm">
|
||||||
|
{currentIndex === 0 && !resolvedMatchId && guideVisible && (
|
||||||
|
<SwipeGuide onDismiss={() => setGuideVisible(false)} />
|
||||||
|
)}
|
||||||
{!resolvedMatchId && (
|
{!resolvedMatchId && (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{restaurants.map((restaurant, index) => {
|
{restaurants.map((restaurant, index) => {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "nw_guide_v2";
|
||||||
|
|
||||||
|
interface SwipeGuideProps {
|
||||||
|
onDismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwipeGuide({ onDismiss }: SwipeGuideProps) {
|
||||||
|
const [visible, setVisible] = useState(() => {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
return !localStorage.getItem(STORAGE_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => {
|
||||||
|
setVisible(false);
|
||||||
|
localStorage.setItem(STORAGE_KEY, "1");
|
||||||
|
onDismiss();
|
||||||
|
}, [onDismiss]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
onDismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(dismiss, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{visible && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 z-20 flex items-center justify-center rounded-2xl bg-black/50 backdrop-blur-[2px]"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
onClick={dismiss}
|
||||||
|
onTouchStart={dismiss}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between px-8">
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
initial={{ x: 0 }}
|
||||||
|
animate={{ x: [0, -24, 0] }}
|
||||||
|
transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-rose-500/80">
|
||||||
|
<ThumbsDown size={26} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold text-white">不想去</span>
|
||||||
|
<span className="text-[10px] text-white/60">← 左滑</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-1"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="h-8 w-0.5 rounded-full bg-white/30"
|
||||||
|
animate={{ scaleY: [1, 0.6, 1] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] font-medium text-white/40">滑动卡片</span>
|
||||||
|
<motion.div
|
||||||
|
className="h-8 w-0.5 rounded-full bg-white/30"
|
||||||
|
animate={{ scaleY: [0.6, 1, 0.6] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
initial={{ x: 0 }}
|
||||||
|
animate={{ x: [0, 24, 0] }}
|
||||||
|
transition={{ duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-emerald-500/80">
|
||||||
|
<ThumbsUp size={26} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold text-white">想去</span>
|
||||||
|
<span className="text-[10px] text-white/60">右滑 →</span>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user