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>
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
'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;
|
|
}
|