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"
},