refactor: 提取 useToast hook + Toast 组件,消除 4 处重复的通知逻辑
将 state + setTimeout 自动消失逻辑封装为 useToast hook, Toast UI 统一为组件支持 top/bottom 两种位置,净减约 12 行。
This commit is contained in:
@@ -34,6 +34,8 @@ import { isRegistered } from "@/lib/userId";
|
||||
import ShareCardModal from "@/components/ShareCardModal";
|
||||
import RestaurantImage from "@/components/RestaurantImage";
|
||||
import AuthModal from "@/components/AuthModal";
|
||||
import Toast from "@/components/Toast";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
|
||||
interface MatchResultProps {
|
||||
restaurant: Restaurant;
|
||||
@@ -194,7 +196,7 @@ export default function MatchResult({
|
||||
const router = useRouter();
|
||||
const [showRunnerUps, setShowRunnerUps] = useState(false);
|
||||
const [showShareCard, setShowShareCard] = useState(false);
|
||||
const [toast, setToast] = useState("");
|
||||
const toast = useToast();
|
||||
const celebratedRef = useRef(false);
|
||||
const historySavedRef = useRef(false);
|
||||
const isSolo = userCount <= 1;
|
||||
@@ -204,11 +206,6 @@ export default function MatchResult({
|
||||
const [registered, setRegistered] = useState(() => isRegistered());
|
||||
const [showAuth, setShowAuth] = useState(false);
|
||||
|
||||
const showToast = useCallback((msg: string) => {
|
||||
setToast(msg);
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnanimous && !celebratedRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -247,9 +244,9 @@ export default function MatchResult({
|
||||
(profile: UserProfile) => {
|
||||
setRegistered(true);
|
||||
setShowAuth(false);
|
||||
showToast(`欢迎,${profile.username}!记录已保存`);
|
||||
toast.show(`欢迎,${profile.username}!记录已保存`);
|
||||
},
|
||||
[showToast],
|
||||
[toast],
|
||||
);
|
||||
|
||||
const handleFavorite = useCallback(async () => {
|
||||
@@ -263,13 +260,13 @@ export default function MatchResult({
|
||||
});
|
||||
if (res.ok) {
|
||||
setFavorited(true);
|
||||
showToast("已收藏");
|
||||
toast.show("已收藏");
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setFavLoading(false);
|
||||
}, [registered, userId, restaurant, favorited, favLoading, showToast]);
|
||||
}, [registered, userId, restaurant, favorited, favLoading, toast]);
|
||||
|
||||
if (matchType === "no_match") {
|
||||
return <NoMatchResult onReset={onReset} resetting={resetting} />;
|
||||
@@ -635,22 +632,10 @@ export default function MatchResult({
|
||||
userCount,
|
||||
scene,
|
||||
}}
|
||||
onToast={showToast}
|
||||
onToast={toast.show}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
className="fixed left-1/2 top-10 z-60 -translate-x-1/2 rounded-xl bg-surface px-4 py-2.5 text-xs font-medium text-heading shadow-lg ring-1 ring-border"
|
||||
initial={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
animate={{ opacity: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
>
|
||||
{toast}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Toast message={toast.message} />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
|
||||
const positionClass = {
|
||||
top: "top-10",
|
||||
bottom: "bottom-8",
|
||||
};
|
||||
|
||||
export default function Toast({ message, position = "top" }: ToastProps) {
|
||||
const isTop = position === "top";
|
||||
const y = isTop ? -12 : 12;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{message && (
|
||||
<motion.div
|
||||
className={`fixed left-1/2 z-60 -translate-x-1/2 rounded-xl bg-elevated px-4 py-2.5 text-xs font-medium text-heading shadow-lg ring-1 ring-subtle ${positionClass[position]}`}
|
||||
initial={{ opacity: 0, y, x: "-50%" }}
|
||||
animate={{ opacity: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, y, x: "-50%" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
>
|
||||
{message}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { QrCode, LogOut, Crown, Lock } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import QrInviteModal from "./QrInviteModal";
|
||||
import RoomManageModal from "./RoomManageModal";
|
||||
import Toast from "@/components/Toast";
|
||||
import type { UserProfile, SceneType } from "@/types";
|
||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
|
||||
interface TopNavProps {
|
||||
roomId: string;
|
||||
@@ -36,15 +37,10 @@ export default function TopNav({
|
||||
scene = "eat",
|
||||
}: TopNavProps) {
|
||||
const sceneConfig = getSceneConfig(scene);
|
||||
const [toast, setToast] = useState("");
|
||||
const toast = useToast();
|
||||
const [showQr, setShowQr] = useState(false);
|
||||
const [showManage, setShowManage] = useState(false);
|
||||
|
||||
const showToast = useCallback((msg: string) => {
|
||||
setToast(msg);
|
||||
setTimeout(() => setToast(""), 2200);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="relative z-10 flex h-14 items-center px-4">
|
||||
@@ -83,25 +79,13 @@ export default function TopNav({
|
||||
</h1>
|
||||
</nav>
|
||||
|
||||
<AnimatePresence>
|
||||
{toast && (
|
||||
<motion.div
|
||||
className="fixed left-1/2 top-16 z-50 -translate-x-1/2 rounded-xl bg-elevated px-4 py-2.5 text-xs font-medium text-heading shadow-lg ring-1 ring-subtle"
|
||||
initial={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
animate={{ opacity: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, y: -12, x: "-50%" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
>
|
||||
{toast}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Toast message={toast.message} />
|
||||
|
||||
<QrInviteModal
|
||||
open={showQr}
|
||||
onClose={() => setShowQr(false)}
|
||||
roomId={roomId}
|
||||
onToast={showToast}
|
||||
onToast={toast.show}
|
||||
scene={scene}
|
||||
/>
|
||||
|
||||
@@ -116,7 +100,7 @@ export default function TopNav({
|
||||
swipeCounts={swipeCounts}
|
||||
totalCards={totalCards}
|
||||
userProfiles={userProfiles}
|
||||
onToast={showToast}
|
||||
onToast={toast.show}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user