refactor: Toast 升级为全局 Context,消除 onToast prop 透传
将 useToast 从独立 state hook 改为 Context-based,在 layout 中 挂载 ToastProvider 全局渲染 Toast。QrInviteModal、RoomManageModal、 ShareCardModal 不再需要 onToast prop,直接 useToast() 调用即可。 父组件 TopNav、MatchResult、profile、blindbox 移除了本地 Toast 渲染和 onToast 传递逻辑。
This commit is contained in:
@@ -23,7 +23,6 @@ import {
|
|||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||||
import ShareCardModal from "@/components/ShareCardModal";
|
import ShareCardModal from "@/components/ShareCardModal";
|
||||||
import Toast from "@/components/Toast";
|
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
|
import { BlindboxRoomSkeleton } from "@/components/Skeleton";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
@@ -815,7 +814,6 @@ export default function BlindboxRoomPage() {
|
|||||||
drawer: revealedIdea.drawnBy ?? undefined,
|
drawer: revealedIdea.drawnBy ?? undefined,
|
||||||
roomName: room.name,
|
roomName: room.name,
|
||||||
}}
|
}}
|
||||||
onToast={toast.show}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -850,8 +848,6 @@ export default function BlindboxRoomPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Toast message={toast.message} position="bottom" />
|
|
||||||
|
|
||||||
<div className="h-8 shrink-0" />
|
<div className="h-8 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "./globals.css";
|
|||||||
import GlobalUserBadge from "@/components/GlobalUserBadge";
|
import GlobalUserBadge from "@/components/GlobalUserBadge";
|
||||||
import ServiceWorkerRegistrar from "@/components/ServiceWorkerRegistrar";
|
import ServiceWorkerRegistrar from "@/components/ServiceWorkerRegistrar";
|
||||||
import PageTransition from "@/components/PageTransition";
|
import PageTransition from "@/components/PageTransition";
|
||||||
|
import ToastProvider from "@/components/ToastProvider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -40,8 +41,10 @@ export default function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className={`${geistSans.variable} font-sans antialiased`}>
|
<body className={`${geistSans.variable} font-sans antialiased`}>
|
||||||
<ServiceWorkerRegistrar />
|
<ServiceWorkerRegistrar />
|
||||||
|
<ToastProvider>
|
||||||
<PageTransition>{children}</PageTransition>
|
<PageTransition>{children}</PageTransition>
|
||||||
<GlobalUserBadge />
|
<GlobalUserBadge />
|
||||||
|
</ToastProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import EmptyState from "@/components/EmptyState";
|
import EmptyState from "@/components/EmptyState";
|
||||||
import Toast from "@/components/Toast";
|
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import RestaurantImage from "@/components/RestaurantImage";
|
import RestaurantImage from "@/components/RestaurantImage";
|
||||||
import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton";
|
import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton";
|
||||||
@@ -711,7 +710,6 @@ export default function ProfilePage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toast message={toast.message} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { isRegistered } from "@/lib/userId";
|
|||||||
import ShareCardModal from "@/components/ShareCardModal";
|
import ShareCardModal from "@/components/ShareCardModal";
|
||||||
import RestaurantImage from "@/components/RestaurantImage";
|
import RestaurantImage from "@/components/RestaurantImage";
|
||||||
import AuthModal from "@/components/AuthModal";
|
import AuthModal from "@/components/AuthModal";
|
||||||
import Toast from "@/components/Toast";
|
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
interface MatchResultProps {
|
interface MatchResultProps {
|
||||||
@@ -632,10 +631,7 @@ export default function MatchResult({
|
|||||||
userCount,
|
userCount,
|
||||||
scene,
|
scene,
|
||||||
}}
|
}}
|
||||||
onToast={toast.show}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Toast message={toast.message} />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { X, Copy, Share2, QrCode } from "lucide-react";
|
|||||||
import type { SceneType } from "@/types";
|
import type { SceneType } from "@/types";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
interface QrInviteModalProps {
|
interface QrInviteModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
onToast: (msg: string) => void;
|
|
||||||
scene?: SceneType;
|
scene?: SceneType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ export default function QrInviteModal({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
roomId,
|
roomId,
|
||||||
onToast,
|
|
||||||
scene = "eat",
|
scene = "eat",
|
||||||
}: QrInviteModalProps) {
|
}: QrInviteModalProps) {
|
||||||
|
const toast = useToast();
|
||||||
const sceneConfig = getSceneConfig(scene);
|
const sceneConfig = getSceneConfig(scene);
|
||||||
const inviteUrl =
|
const inviteUrl =
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
@@ -31,11 +31,11 @@ export default function QrInviteModal({
|
|||||||
const handleCopy = useCallback(async () => {
|
const handleCopy = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(inviteUrl);
|
await navigator.clipboard.writeText(inviteUrl);
|
||||||
onToast("邀请链接已复制,快去发给朋友吧!");
|
toast.show("邀请链接已复制,快去发给朋友吧!");
|
||||||
} catch {
|
} catch {
|
||||||
onToast("复制失败,请手动复制链接");
|
toast.show("复制失败,请手动复制链接");
|
||||||
}
|
}
|
||||||
}, [inviteUrl, onToast]);
|
}, [inviteUrl, toast]);
|
||||||
|
|
||||||
const handleShare = useCallback(async () => {
|
const handleShare = useCallback(async () => {
|
||||||
const shareData = {
|
const shareData = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { UserProfile } from "@/types";
|
import { UserProfile } from "@/types";
|
||||||
import { getAvatar, getAvatarBg } from "@/lib/avatars";
|
import { getAvatar, getAvatarBg } from "@/lib/avatars";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
interface RoomManageModalProps {
|
interface RoomManageModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -24,7 +25,6 @@ interface RoomManageModalProps {
|
|||||||
swipeCounts: Record<string, number>;
|
swipeCounts: Record<string, number>;
|
||||||
totalCards: number;
|
totalCards: number;
|
||||||
userProfiles: Record<string, UserProfile>;
|
userProfiles: Record<string, UserProfile>;
|
||||||
onToast: (msg: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RoomManageModal({
|
export default function RoomManageModal({
|
||||||
@@ -37,8 +37,8 @@ export default function RoomManageModal({
|
|||||||
swipeCounts,
|
swipeCounts,
|
||||||
totalCards,
|
totalCards,
|
||||||
userProfiles,
|
userProfiles,
|
||||||
onToast,
|
|
||||||
}: RoomManageModalProps) {
|
}: RoomManageModalProps) {
|
||||||
|
const toast = useToast();
|
||||||
const [loading, setLoading] = useState<string | null>(null);
|
const [loading, setLoading] = useState<string | null>(null);
|
||||||
const [confirmKick, setConfirmKick] = useState<string | null>(null);
|
const [confirmKick, setConfirmKick] = useState<string | null>(null);
|
||||||
const [confirmEnd, setConfirmEnd] = useState(false);
|
const [confirmEnd, setConfirmEnd] = useState(false);
|
||||||
@@ -54,33 +54,33 @@ export default function RoomManageModal({
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
onToast(data.error ?? "操作失败");
|
toast.show(data.error ?? "操作失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "lock":
|
case "lock":
|
||||||
onToast("房间已锁定,其他人无法加入");
|
toast.show("房间已锁定,其他人无法加入");
|
||||||
break;
|
break;
|
||||||
case "unlock":
|
case "unlock":
|
||||||
onToast("房间已解锁");
|
toast.show("房间已解锁");
|
||||||
break;
|
break;
|
||||||
case "kick":
|
case "kick":
|
||||||
onToast("已将该用户移出房间");
|
toast.show("已将该用户移出房间");
|
||||||
setConfirmKick(null);
|
setConfirmKick(null);
|
||||||
break;
|
break;
|
||||||
case "end_voting":
|
case "end_voting":
|
||||||
onToast("已结束投票,正在结算结果");
|
toast.show("已结束投票,正在结算结果");
|
||||||
setConfirmEnd(false);
|
setConfirmEnd(false);
|
||||||
onClose();
|
onClose();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
onToast("操作失败,请重试");
|
toast.show("操作失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(null);
|
setLoading(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[roomId, userId, onToast, onClose],
|
[roomId, userId, toast, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
const otherUsers = users.filter((u) => u !== userId);
|
const otherUsers = users.filter((u) => u !== userId);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { X, Download, Share2, Loader2, Star, MapPin, Zap } from "lucide-react";
|
|||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
import { toPng } from "html-to-image";
|
import { toPng } from "html-to-image";
|
||||||
import type { Restaurant, MatchType, SceneType } from "@/types";
|
import type { Restaurant, MatchType, SceneType } from "@/types";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
type ShareCardData =
|
type ShareCardData =
|
||||||
| {
|
| {
|
||||||
@@ -28,7 +29,6 @@ interface ShareCardModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
data: ShareCardData;
|
data: ShareCardData;
|
||||||
onToast?: (msg: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadImageAsDataUrl(src: string): Promise<string | null> {
|
async function loadImageAsDataUrl(src: string): Promise<string | null> {
|
||||||
@@ -775,8 +775,8 @@ export default function ShareCardModal({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
data,
|
data,
|
||||||
onToast,
|
|
||||||
}: ShareCardModalProps) {
|
}: ShareCardModalProps) {
|
||||||
|
const toast = useToast();
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
const backdropRef = useRef<HTMLDivElement>(null);
|
const backdropRef = useRef<HTMLDivElement>(null);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
@@ -821,7 +821,7 @@ export default function ShareCardModal({
|
|||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
const png = await handleGenerate();
|
const png = await handleGenerate();
|
||||||
if (!png) {
|
if (!png) {
|
||||||
onToast?.("生成图片失败,请重试");
|
toast.show("生成图片失败,请重试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name =
|
const name =
|
||||||
@@ -829,13 +829,13 @@ export default function ShareCardModal({
|
|||||||
? `NoWhatever_${data.restaurant.name}.png`
|
? `NoWhatever_${data.restaurant.name}.png`
|
||||||
: `NoWhatever_周末契约.png`;
|
: `NoWhatever_周末契约.png`;
|
||||||
downloadDataUrl(png, name);
|
downloadDataUrl(png, name);
|
||||||
onToast?.("图片已保存");
|
toast.show("图片已保存");
|
||||||
}, [handleGenerate, onToast, data]);
|
}, [handleGenerate, toast, data]);
|
||||||
|
|
||||||
const handleShare = useCallback(async () => {
|
const handleShare = useCallback(async () => {
|
||||||
const png = await handleGenerate();
|
const png = await handleGenerate();
|
||||||
if (!png) {
|
if (!png) {
|
||||||
onToast?.("生成图片失败,请重试");
|
toast.show("生成图片失败,请重试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,8 +852,8 @@ export default function ShareCardModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadDataUrl(png, "NoWhatever.png");
|
downloadDataUrl(png, "NoWhatever.png");
|
||||||
onToast?.("图片已保存,快去分享吧!");
|
toast.show("图片已保存,快去分享吧!");
|
||||||
}, [handleGenerate, onToast]);
|
}, [handleGenerate, toast]);
|
||||||
|
|
||||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === backdropRef.current) onClose();
|
if (e.target === backdropRef.current) onClose();
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef } from "react";
|
||||||
|
import { ToastContext, type ToastPosition } from "@/hooks/useToast";
|
||||||
|
import Toast from "./Toast";
|
||||||
|
|
||||||
|
export default function ToastProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [position, setPosition] = useState<ToastPosition>("top");
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
|
const show = useCallback((msg: string, pos: ToastPosition = "top") => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
setMessage(msg);
|
||||||
|
setPosition(pos);
|
||||||
|
timerRef.current = setTimeout(() => setMessage(""), 2200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ show }}>
|
||||||
|
{children}
|
||||||
|
<Toast message={message} position={position} />
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,10 +4,8 @@ import { useState } from "react";
|
|||||||
import { QrCode, LogOut, Crown, Lock } from "lucide-react";
|
import { QrCode, LogOut, Crown, Lock } from "lucide-react";
|
||||||
import QrInviteModal from "./QrInviteModal";
|
import QrInviteModal from "./QrInviteModal";
|
||||||
import RoomManageModal from "./RoomManageModal";
|
import RoomManageModal from "./RoomManageModal";
|
||||||
import Toast from "@/components/Toast";
|
|
||||||
import type { UserProfile, SceneType } from "@/types";
|
import type { UserProfile, SceneType } from "@/types";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
import { useToast } from "@/hooks/useToast";
|
|
||||||
|
|
||||||
interface TopNavProps {
|
interface TopNavProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -37,7 +35,6 @@ export default function TopNav({
|
|||||||
scene = "eat",
|
scene = "eat",
|
||||||
}: TopNavProps) {
|
}: TopNavProps) {
|
||||||
const sceneConfig = getSceneConfig(scene);
|
const sceneConfig = getSceneConfig(scene);
|
||||||
const toast = useToast();
|
|
||||||
const [showQr, setShowQr] = useState(false);
|
const [showQr, setShowQr] = useState(false);
|
||||||
const [showManage, setShowManage] = useState(false);
|
const [showManage, setShowManage] = useState(false);
|
||||||
|
|
||||||
@@ -79,13 +76,10 @@ export default function TopNav({
|
|||||||
</h1>
|
</h1>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Toast message={toast.message} />
|
|
||||||
|
|
||||||
<QrInviteModal
|
<QrInviteModal
|
||||||
open={showQr}
|
open={showQr}
|
||||||
onClose={() => setShowQr(false)}
|
onClose={() => setShowQr(false)}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
onToast={toast.show}
|
|
||||||
scene={scene}
|
scene={scene}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -100,7 +94,6 @@ export default function TopNav({
|
|||||||
swipeCounts={swipeCounts}
|
swipeCounts={swipeCounts}
|
||||||
totalCards={totalCards}
|
totalCards={totalCards}
|
||||||
userProfiles={userProfiles}
|
userProfiles={userProfiles}
|
||||||
onToast={toast.show}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+12
-14
@@ -1,17 +1,15 @@
|
|||||||
import { useState, useCallback, useRef } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
export function useToast(duration = 2200) {
|
export type ToastPosition = "top" | "bottom";
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
||||||
|
|
||||||
const show = useCallback(
|
export interface ToastContextValue {
|
||||||
(msg: string) => {
|
show: (msg: string, position?: ToastPosition) => void;
|
||||||
clearTimeout(timerRef.current);
|
}
|
||||||
setMessage(msg);
|
|
||||||
timerRef.current = setTimeout(() => setMessage(""), duration);
|
export const ToastContext = createContext<ToastContextValue | null>(null);
|
||||||
},
|
|
||||||
[duration],
|
export function useToast() {
|
||||||
);
|
const ctx = useContext(ToastContext);
|
||||||
|
if (!ctx) throw new Error("useToast must be used within ToastProvider");
|
||||||
return { message, show } as const;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user