Files
figlet-ui/components/ui/Toast.tsx

91 lines
2.9 KiB
TypeScript
Raw Normal View History

feat: add toast notifications, random font, and font info display Major UX enhancements for better user feedback and discovery: **Toast Notification System** - Beautiful toast notifications with 3 types (success/error/info) - Auto-dismiss after 3 seconds - Slide-in animation from right - Color-coded by type (green/red/blue) - Dark mode support - Close button for manual dismiss - ToastProvider context for global access - Non-blocking UI overlay **Random Font Discovery** - Shuffle button in font selector - One-click random font selection - Toast notification shows selected font - Perfect for discovering new fonts - Located next to "Select Font" header **Enhanced Font Preview** - Font name badge display - Character count statistics - Line count statistics - Better visual hierarchy - Responsive stat display **Improved Feedback** - Toast on copy: "Copied to clipboard!" - Toast on share: "Shareable URL copied!" - Toast on random: "Random font: FontName" - Error toasts for failed operations - Removed temporary text replacement **Smooth Animations** - Slide-in animation for toasts - Fade-in animation class - Custom keyframe animations - CSS utility classes - Smooth transitions throughout **Technical Improvements** - useToast custom hook - Context-based state management - Auto-cleanup with setTimeout - Unique toast IDs - TypeScript types for toast system - Proper event propagation **Better UX** - No more jarring text replacements - Non-intrusive notifications - Professional feedback system - Discoverable random feature - Informative preview stats The app now feels polished and professional with proper user feedback! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 13:21:13 +01:00
'use client';
import * as React from 'react';
import { X, CheckCircle2, AlertCircle, Info } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
export type ToastType = 'success' | 'error' | 'info';
export interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
toasts: Toast[];
addToast: (message: string, type?: ToastType) => void;
removeToast: (id: string) => void;
}
const ToastContext = React.createContext<ToastContextType | undefined>(undefined);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<Toast[]>([]);
const addToast = React.useCallback((message: string, type: ToastType = 'success') => {
const id = Math.random().toString(36).substring(7);
setToasts((prev) => [...prev, { id, message, type }]);
// Auto remove after 3 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const removeToast = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} />
))}
</div>
</ToastContext.Provider>
);
}
function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) {
const Icon = toast.type === 'success' ? CheckCircle2 : toast.type === 'error' ? AlertCircle : Info;
return (
<div
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg pointer-events-auto',
'animate-in slide-in-from-right-full duration-300',
'min-w-[300px] max-w-[400px]',
{
'bg-green-50 text-green-900 border border-green-200 dark:bg-green-900/20 dark:text-green-100 dark:border-green-800':
toast.type === 'success',
'bg-red-50 text-red-900 border border-red-200 dark:bg-red-900/20 dark:text-red-100 dark:border-red-800':
toast.type === 'error',
'bg-blue-50 text-blue-900 border border-blue-200 dark:bg-blue-900/20 dark:text-blue-100 dark:border-blue-800':
toast.type === 'info',
}
)}
>
<Icon className="h-5 w-5 flex-shrink-0" />
<p className="text-sm font-medium flex-1">{toast.message}</p>
<button
onClick={onClose}
className="flex-shrink-0 opacity-70 hover:opacity-100 transition-opacity"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
export function useToast() {
const context = React.useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
}