From cff416dd83c2cd3d956e615c2178c81fb5dde38b Mon Sep 17 00:00:00 2001 From: MOH Date: Wed, 11 Mar 2026 21:09:45 +0100 Subject: [PATCH] refactor toast system to use react-hot-toast --- app/layout.tsx | 2 +- components/layout/floating-preferences.tsx | 5 -- components/layout/locale-toggle.tsx | 16 +++- components/layout/site-header.tsx | 12 +-- components/root/query-toast-bridge.tsx | 12 +-- components/sound-toggle.tsx | 3 +- components/theme-toggle.tsx | 3 +- components/ui/sonner.tsx | 89 ---------------------- components/ui/toaster.tsx | 33 ++++++++ lib/toast.tsx | 67 ++++++++++++++++ package-lock.json | 39 +++++++--- package.json | 2 +- 12 files changed, 151 insertions(+), 132 deletions(-) delete mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 lib/toast.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 03db705..6285085 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,7 +6,7 @@ import { getLocale } from "next-intl/server"; import { QueryToastBridge } from "@/components/root/query-toast-bridge"; import { SoundProvider } from "@/components/sound-provider"; import { ThemeProvider } from "@/components/theme-provider"; -import { Toaster } from "@/components/ui/sonner"; +import { Toaster } from "@/components/ui/toaster"; import { buildAppMetadata } from "@/lib/metadata"; import { getDirection } from "@/lib/locale"; import "./globals.css"; diff --git a/components/layout/floating-preferences.tsx b/components/layout/floating-preferences.tsx index d99166c..7e687bf 100644 --- a/components/layout/floating-preferences.tsx +++ b/components/layout/floating-preferences.tsx @@ -39,11 +39,6 @@ export function FloatingPreferences({ /> diff --git a/components/layout/locale-toggle.tsx b/components/layout/locale-toggle.tsx index 396a929..24030bc 100644 --- a/components/layout/locale-toggle.tsx +++ b/components/layout/locale-toggle.tsx @@ -19,14 +19,18 @@ type LocaleToggleProps = { locale: string; className?: string; showLabel?: boolean; - localeChangedLabels?: Partial>; +}; + +const localeChangedMessages: Record = { + de: "Sprache auf Deutsch gewechselt", + en: "Language changed to English", + ar: "تم تغيير اللغة إلى العربية", }; export function LocaleToggle({ locale, className, showLabel = false, - localeChangedLabels, }: LocaleToggleProps) { const pathname = usePathname(); const locales: AppLocale[] = ["de", "en", "ar"]; @@ -80,7 +84,7 @@ export function LocaleToggle({ { - const message = localeChangedLabels?.[targetLocale]; + const message = localeChangedMessages[targetLocale]; if (!message) { return; @@ -88,7 +92,11 @@ export function LocaleToggle({ window.sessionStorage.setItem( PENDING_TOAST_STORAGE_KEY, - JSON.stringify({ message, type: "success" }), + JSON.stringify({ + type: "success", + message, + targetLocale, + }), ); }} className="group flex h-10 w-10 items-center justify-center rounded-full" diff --git a/components/layout/site-header.tsx b/components/layout/site-header.tsx index e64c9e5..215a81b 100644 --- a/components/layout/site-header.tsx +++ b/components/layout/site-header.tsx @@ -6,7 +6,6 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; import { Container } from "@/components/layout/container"; import { LocaleToggle } from "@/components/layout/locale-toggle"; @@ -15,6 +14,7 @@ import { SoundToggle } from "@/components/sound-toggle"; import { ThemeToggle } from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale"; +import { toast } from "@/lib/toast"; import { cn } from "@/lib/utils"; const navItems = [ @@ -295,11 +295,6 @@ export function SiteHeader({
diff --git a/components/root/query-toast-bridge.tsx b/components/root/query-toast-bridge.tsx index bfc4093..b153ad9 100644 --- a/components/root/query-toast-bridge.tsx +++ b/components/root/query-toast-bridge.tsx @@ -2,7 +2,8 @@ import { useEffect, useRef } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { toast } from "sonner"; + +import { toast } from "@/lib/toast"; const TOAST_PARAM_NAMES = ["success", "error"] as const; const PENDING_TOAST_STORAGE_KEY = "mohfarawati-pending-toast"; @@ -25,17 +26,18 @@ export function QueryToastBridge() { message?: string; type?: "success" | "error"; }; + const localizedMessage = pendingToast.message; - if (!pendingToast.message) { + if (!localizedMessage) { return; } if (pendingToast.type === "error") { - toast.error(pendingToast.message); + toast.error(localizedMessage); } else if (pendingToast.type === "success") { - toast.success(pendingToast.message); + toast.success(localizedMessage); } else { - toast(pendingToast.message); + toast(localizedMessage); } } finally { window.sessionStorage.removeItem(PENDING_TOAST_STORAGE_KEY); diff --git a/components/sound-toggle.tsx b/components/sound-toggle.tsx index b898c64..584e8b0 100644 --- a/components/sound-toggle.tsx +++ b/components/sound-toggle.tsx @@ -1,10 +1,9 @@ "use client"; import { Volume2, VolumeX } from "lucide-react"; -import { toast } from "sonner"; - import { useSound } from "@/components/sound-provider"; import { Button, type ButtonProps } from "@/components/ui/button"; +import { toast } from "@/lib/toast"; import { cn } from "@/lib/utils"; type SoundToggleProps = { diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index f7e7184..ccda40e 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -3,10 +3,9 @@ import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; -import { toast } from "sonner"; - import { useSound } from "@/components/sound-provider"; import { Button, type ButtonProps } from "@/components/ui/button"; +import { toast } from "@/lib/toast"; import { cn } from "@/lib/utils"; type ThemeToggleProps = { diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx deleted file mode 100644 index eb90aa5..0000000 --- a/components/ui/sonner.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import { useTheme } from "next-themes"; -import { useEffect, useState, type CSSProperties } from "react"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; - -import { cn } from "@/lib/utils"; - -export function Toaster(props: ToasterProps) { - const pathname = usePathname(); - const { resolvedTheme = "light" } = useTheme(); - const [documentLang, setDocumentLang] = useState("de"); - - useEffect(() => { - setDocumentLang(document.documentElement.lang || "de"); - }, [pathname]); - - const isArabic = documentLang === "ar"; - const toastFontClassName = isArabic ? "font-arabic text-right tracking-normal" : "font-latin text-left"; - const toasterStyle: CSSProperties & Record<"--width", string> = { - "--width": "max-content", - left: "50%", - right: "auto", - transform: "translateX(-50%)", - ...(props.style ?? {}), - }; - - return ( - - ); -} diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000..1c50924 --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { Toaster as HotToaster, type ToasterProps } from "react-hot-toast"; + +import { resolveLocale } from "@/lib/locale"; + +export function Toaster(props: ToasterProps) { + const pathname = usePathname(); + const locale = resolveLocale(pathname.split("/")[1] || "de"); + const isArabic = locale === "ar"; + + const toastClassName = `${isArabic ? "font-arabic tracking-normal" : "font-latin"} text-[13px] leading-5`; + + return ( + + ); +} diff --git a/lib/toast.tsx b/lib/toast.tsx new file mode 100644 index 0000000..0b97a04 --- /dev/null +++ b/lib/toast.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { + toast as hotToast, + type ToastOptions, + type ToastPosition, +} from "react-hot-toast"; + +import { resolveLocale } from "@/lib/locale"; + +type ToastVariant = "default" | "success" | "error" | "loading"; + +type ToastMessage = string; + +function getCurrentLocale() { + if (typeof window === "undefined") { + return "de" as const; + } + + const pathname = window.location.pathname; + const maybeLocale = pathname.split("/")[1] || "de"; + + return resolveLocale(maybeLocale); +} + +function getToastPosition(isArabic: boolean): ToastPosition { + return isArabic ? "top-right" : "top-left"; +} + +function getToastOptions(): ToastOptions { + const locale = getCurrentLocale(); + const isArabic = locale === "ar"; + + return { + duration: 1800, + position: getToastPosition(isArabic), + }; +} + +function showToast(message: ToastMessage, variant: ToastVariant = "default") { + const options = getToastOptions(); + + if (variant === "success") { + return hotToast.success(message, options); + } + + if (variant === "error") { + return hotToast.error(message, options); + } + + if (variant === "loading") { + return hotToast.loading(message, options); + } + + return hotToast(message, options); +} + +export const toast = Object.assign( + (message: ToastMessage) => showToast(message, "default"), + { + success: (message: ToastMessage) => showToast(message, "success"), + error: (message: ToastMessage) => showToast(message, "error"), + loading: (message: ToastMessage) => showToast(message, "loading"), + dismiss: hotToast.dismiss, + remove: hotToast.remove, + }, +); diff --git a/package-lock.json b/package-lock.json index 5465bb6..d88953f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.71.2", - "sonner": "^2.0.7", + "react-hot-toast": "^2.6.0", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, @@ -4614,7 +4614,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -6106,6 +6105,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8415,6 +8423,23 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9003,16 +9028,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sonner": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", - "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index a342481..9697c64 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.71.2", - "sonner": "^2.0.7", + "react-hot-toast": "^2.6.0", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" },