refactor toast system to use react-hot-toast
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-11 21:09:45 +01:00
parent 03384c1bda
commit cff416dd83
12 changed files with 151 additions and 132 deletions
@@ -39,11 +39,6 @@ export function FloatingPreferences({
/>
<LocaleToggle
locale={locale}
localeChangedLabels={{
de: t("localeChangedDe"),
en: t("localeChangedEn"),
ar: t("localeChangedAr"),
}}
className="h-9 w-9 rounded-pill border border-transparent bg-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground"
/>
</div>
+12 -4
View File
@@ -19,14 +19,18 @@ type LocaleToggleProps = {
locale: string;
className?: string;
showLabel?: boolean;
localeChangedLabels?: Partial<Record<AppLocale, string>>;
};
const localeChangedMessages: Record<AppLocale, string> = {
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({
<a
href={getLocalizedPath(targetLocale, currentPath)}
onClick={() => {
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"
+1 -11
View File
@@ -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({
<div className={desktopControlRailClassName}>
<LocaleToggle
locale={locale}
localeChangedLabels={{
de: t("localeChangedDe"),
en: t("localeChangedEn"),
ar: t("localeChangedAr"),
}}
className={desktopControlButtonClassName}
/>
<SoundToggle
@@ -449,11 +444,6 @@ export function SiteHeader({
<div className="flex items-center justify-between gap-2">
<LocaleToggle
locale={locale}
localeChangedLabels={{
de: t("localeChangedDe"),
en: t("localeChangedEn"),
ar: t("localeChangedAr"),
}}
className={mobileControlButtonClassName}
/>
<div className="flex items-center gap-2">
+7 -5
View File
@@ -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);
+1 -2
View File
@@ -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 = {
+1 -2
View File
@@ -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 = {
-89
View File
@@ -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 (
<Sonner
{...props}
dir={props.dir ?? "ltr"}
theme={resolvedTheme as ToasterProps["theme"]}
position={props.position ?? "top-center"}
duration={props.duration ?? 1800}
closeButton={props.closeButton ?? false}
offset={props.offset ?? "16px"}
mobileOffset={props.mobileOffset ?? "16px"}
className={cn("toaster group", props.className)}
style={toasterStyle}
toastOptions={{
...props.toastOptions,
style: {
...(props.toastOptions?.style ?? {}),
borderRadius: "16px",
padding: "10px 12px",
minWidth: "0",
maxWidth: "min(calc(100vw - 2rem), 32rem)",
background: "hsl(var(--card))",
color: "hsl(var(--card-foreground))",
border: "1px solid hsl(var(--border))",
direction: isArabic ? "rtl" : "ltr",
textAlign: isArabic ? "right" : "left",
},
classNames: {
...props.toastOptions?.classNames,
toast: cn(
"group toast group-[.toaster]:min-w-0 group-[.toaster]:w-fit group-[.toaster]:max-w-[min(calc(100vw-2rem),32rem)] group-[.toaster]:rounded-[16px] group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:bg-card group-[.toaster]:px-3 group-[.toaster]:py-2 group-[.toaster]:text-card-foreground group-[.toaster]:shadow-panel",
toastFontClassName,
props.toastOptions?.classNames?.toast,
),
title: cn(
"text-[13px] font-medium leading-5",
isArabic ? "text-right" : "text-left",
props.toastOptions?.classNames?.title,
),
description: cn(
"text-[13px] leading-5 text-muted-foreground",
isArabic ? "text-right" : "text-left",
props.toastOptions?.classNames?.description,
),
default: cn(
"border-border bg-card text-card-foreground",
props.toastOptions?.classNames?.default,
),
success: cn(
"border-status-success/30 bg-status-success-soft text-status-success dark:border-status-success/40 dark:bg-status-success-soft dark:text-status-success",
props.toastOptions?.classNames?.success,
),
error: cn(
"border-destructive/30 bg-destructive/10 text-destructive dark:border-destructive/40 dark:bg-destructive/20 dark:text-destructive-foreground",
props.toastOptions?.classNames?.error,
),
closeButton: "hidden",
},
}}
/>
);
}
+33
View File
@@ -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 (
<HotToaster
key={locale}
{...props}
position={props.position ?? (isArabic ? "top-right" : "top-left")}
reverseOrder={props.reverseOrder ?? false}
toastOptions={{
...props.toastOptions,
className:
props.toastOptions?.className ??
toastClassName,
style: {
borderRadius: "36px",
...(props.toastOptions?.style ?? {}),
},
}}
/>
);
}