"use client";
/**
* NotificationToast — FASE-07 UX & Microinteractions
*
* Global toast notification component. Renders notifications from the
* notificationStore as stacked toasts in the top-right corner.
*
* Usage: Add to your root layout.
*/
import { useNotificationStore } from "@/store/notificationStore";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
const ICONS = {
success: "✓",
error: "✕",
warning: "⚠",
info: "ℹ",
};
/**
* Coerce a toast title/message to a string. `message`/`title` are typed as
* `string`, but callers occasionally pass a raw API error body (an object) —
* rendering that object directly throws React #31 ("Objects are not valid as a
* React child") and freezes the whole page. This keeps the toast resilient no
* matter what a caller hands it.
*/
export function toToastText(value: unknown): string {
if (typeof value === "string") return value;
if (value == null) return "";
if (typeof value === "object") {
const message = (value as { message?: unknown }).message;
if (typeof message === "string") return message;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
return String(value);
}
const BG_DARK = "rgba(30, 30, 30, 0.95)";
const COLORS = {
success: {
bg: BG_DARK,
border: "rgba(16, 185, 129, 0.6)",
icon: "#10b981",
},
error: {
bg: BG_DARK,
border: "rgba(239, 68, 68, 0.6)",
icon: "#ef4444",
},
warning: {
bg: BG_DARK,
border: "rgba(245, 158, 11, 0.6)",
icon: "#fbbf24",
},
info: {
bg: BG_DARK,
border: "rgba(59, 130, 246, 0.6)",
icon: "#3b82f6",
},
};
function Toast({ notification, onDismiss }) {
const t = useTranslations("common");
const [isExiting, setIsExiting] = useState(false);
const handleDismiss = () => {
setIsExiting(true);
setTimeout(() => onDismiss(notification.id), 200);
};
const color = COLORS[notification.type] || COLORS.info;
const textColors = {
title: "var(--text-primary, #fff)",
message: "var(--text-secondary, #ccc)",
};
return (
{ICONS[notification.type]}
{notification.title && (
{toToastText(notification.title)}
)}
{toToastText(notification.message)}
{notification.dismissible && (
)}
);
}
export default function NotificationToast() {
const { notifications, removeNotification } = useNotificationStore();
if (notifications.length === 0) return null;
return (
<>
{notifications.map((n) => (
))}
>
);
}