Files
figlet-ui/components/converter/FigletConverter.tsx
Sebastian Krüger 704695f14f 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

150 lines
4.3 KiB
TypeScript

'use client';
import * as React from 'react';
import { TextInput } from './TextInput';
import { FontPreview } from './FontPreview';
import { FontSelector } from './FontSelector';
import { textToAscii } from '@/lib/figlet/figletService';
import { getFontList } from '@/lib/figlet/fontLoader';
import { debounce } from '@/lib/utils/debounce';
import { addRecentFont } from '@/lib/storage/favorites';
import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing';
import { useToast } from '@/components/ui/Toast';
import type { FigletFont } from '@/types/figlet';
export function FigletConverter() {
const [text, setText] = React.useState('Figlet UI');
const [selectedFont, setSelectedFont] = React.useState('Standard');
const [asciiArt, setAsciiArt] = React.useState('');
const [fonts, setFonts] = React.useState<FigletFont[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
const { addToast } = useToast();
// Load fonts and check URL params on mount
React.useEffect(() => {
getFontList().then(setFonts);
// Check for URL parameters
const urlState = decodeFromUrl();
if (urlState) {
if (urlState.text) setText(urlState.text);
if (urlState.font) setSelectedFont(urlState.font);
}
}, []);
// Generate ASCII art
const generateAsciiArt = React.useCallback(
debounce(async (inputText: string, fontName: string) => {
if (!inputText) {
setAsciiArt('');
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const result = await textToAscii(inputText, fontName);
setAsciiArt(result);
} catch (error) {
console.error('Error generating ASCII art:', error);
setAsciiArt('Error generating ASCII art. Please try a different font.');
} finally {
setIsLoading(false);
}
}, 300),
[]
);
// Trigger generation when text or font changes
React.useEffect(() => {
generateAsciiArt(text, selectedFont);
// Track recent fonts
if (selectedFont) {
addRecentFont(selectedFont);
}
// Update URL
updateUrl(text, selectedFont);
}, [text, selectedFont, generateAsciiArt]);
// Copy to clipboard
const handleCopy = async () => {
if (!asciiArt) return;
try {
await navigator.clipboard.writeText(asciiArt);
addToast('Copied to clipboard!', 'success');
} catch (error) {
console.error('Failed to copy:', error);
addToast('Failed to copy', 'error');
}
};
// Download as text file
const handleDownload = () => {
if (!asciiArt) return;
const blob = new Blob([asciiArt], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `figlet-${selectedFont}-${Date.now()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Share (copy URL to clipboard)
const handleShare = async () => {
const shareUrl = getShareableUrl(text, selectedFont);
try {
await navigator.clipboard.writeText(shareUrl);
addToast('Shareable URL copied!', 'success');
} catch (error) {
console.error('Failed to copy URL:', error);
addToast('Failed to copy URL', 'error');
}
};
// Random font
const handleRandomFont = () => {
if (fonts.length === 0) return;
const randomIndex = Math.floor(Math.random() * fonts.length);
setSelectedFont(fonts[randomIndex].name);
addToast(`Random font: ${fonts[randomIndex].name}`, 'info');
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Input and Preview */}
<div className="lg:col-span-2 space-y-6">
<TextInput
value={text}
onChange={setText}
placeholder="Type your text here..."
/>
<FontPreview
text={asciiArt}
font={selectedFont}
isLoading={isLoading}
onCopy={handleCopy}
onDownload={handleDownload}
onShare={handleShare}
/>
</div>
{/* Right Column - Font Selector */}
<div className="lg:col-span-1">
<FontSelector
fonts={fonts}
selectedFont={selectedFont}
onSelectFont={setSelectedFont}
onRandomFont={handleRandomFont}
/>
</div>
</div>
);
}