"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) => (
))}
); }