This commit is contained in:
+1
-1
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 ?? {}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
Generated
+27
-12
@@ -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",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user