Browse Source

feat(08-01): update Toast component for error notifications

- Move toast position from bottom-center to top-right
- Add persistent mode to disable auto-dismiss for errors
- Add expandable details section for full error messages
- Maintain backward compatibility with existing callers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
66254496fc
  1. 67
      src/components/Toast.tsx

67
src/components/Toast.tsx

@ -1,20 +1,24 @@
"use client"; "use client";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { create } from "zustand"; import { create } from "zustand";
interface ToastState { interface ToastState {
message: string | null; message: string | null;
type: "info" | "success" | "warning" | "error"; type: "info" | "success" | "warning" | "error";
show: (message: string, type?: "info" | "success" | "warning" | "error") => void; persistent: boolean;
details: string | null;
show: (message: string, type?: "info" | "success" | "warning" | "error", persistent?: boolean, details?: string | null) => void;
hide: () => void; hide: () => void;
} }
export const useToast = create<ToastState>((set) => ({ export const useToast = create<ToastState>((set) => ({
message: null, message: null,
type: "info", type: "info",
show: (message, type = "info") => set({ message, type }), persistent: false,
hide: () => set({ message: null }), details: null,
show: (message, type = "info", persistent = false, details = null) => set({ message, type, persistent, details }),
hide: () => set({ message: null, persistent: false, details: null }),
})); }));
const typeStyles = { const typeStyles = {
@ -48,34 +52,59 @@ const typeIcons = {
}; };
export function Toast() { export function Toast() {
const { message, type, hide } = useToast(); const { message, type, persistent, details, hide } = useToast();
const [isExpanded, setIsExpanded] = useState(false);
useEffect(() => { useEffect(() => {
if (message) { // Reset expanded state when toast changes
setIsExpanded(false);
}, [message]);
useEffect(() => {
if (message && !persistent) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
hide(); hide();
}, 4000); }, 4000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [message, hide]); }, [message, persistent, hide]);
if (!message) return null; if (!message) return null;
return ( return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] animate-in fade-in slide-in-from-bottom-4 duration-300"> <div className="fixed top-6 right-6 z-[200] animate-in fade-in slide-in-from-top-4 duration-300 max-w-md">
<div <div
className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-xl ${typeStyles[type]}`} className={`flex flex-col rounded-lg border shadow-xl ${typeStyles[type]}`}
> >
{typeIcons[type]} <div className="flex items-center gap-3 px-4 py-3">
<span className="text-sm font-medium">{message}</span> {typeIcons[type]}
<button <span className="text-sm font-medium flex-1">{message}</span>
onClick={hide} <button
className="ml-2 p-1 rounded hover:bg-white/10 transition-colors" onClick={hide}
> className="p-1 rounded hover:bg-white/10 transition-colors"
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> >
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
</svg> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</button> </svg>
</button>
</div>
{details && (
<>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="px-4 py-1 text-xs opacity-70 hover:opacity-100 transition-opacity text-left border-t border-white/10"
>
{isExpanded ? "Hide details" : "Show details"}
</button>
{isExpanded && (
<div className="px-4 pb-3">
<pre className="bg-black/30 rounded p-2 max-h-40 overflow-auto text-xs font-mono whitespace-pre-wrap break-words">
{details}
</pre>
</div>
)}
</>
)}
</div> </div>
</div> </div>
); );

Loading…
Cancel
Save