Files
figlet-ui/components/converter/FigletConverter.tsx
Sebastian Krüger a09d2c3eef feat: add templates, history, comparison mode, animations, and empty states
- Add text templates with 16 pre-made options across 4 categories (greeting, tech, fun, seasonal)
- Add copy history panel tracking last 10 copied items with restore functionality
- Add font comparison mode to view multiple fonts side-by-side (up to 6 fonts)
- Add smooth animations: slide-down, slide-up, scale-in, fade-in, pulse, and shimmer
- Add loading skeletons for better perceived performance
- Add EmptyState component with contextual messages and icons
- Add hover effects and transitions throughout the UI
- Improve visual feedback with animated badges and shadows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 14:10:08 +01:00

289 lines
9.4 KiB
TypeScript

'use client';
import * as React from 'react';
import { TextInput } from './TextInput';
import { FontPreview } from './FontPreview';
import { FontSelector } from './FontSelector';
import { TextTemplates } from './TextTemplates';
import { HistoryPanel } from './HistoryPanel';
import { ComparisonMode } from './ComparisonMode';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { GitCompare } from 'lucide-react';
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 { addToHistory, type HistoryItem } from '@/lib/storage/history';
import { decodeFromUrl, updateUrl, getShareableUrl } from '@/lib/utils/urlSharing';
import { useToast } from '@/components/ui/Toast';
import { cn } from '@/lib/utils/cn';
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 [isComparisonMode, setIsComparisonMode] = React.useState(false);
const [comparisonFonts, setComparisonFonts] = React.useState<string[]>([]);
const [comparisonResults, setComparisonResults] = React.useState<Record<string, string>>({});
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);
addToHistory(text, selectedFont, 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');
};
const handleSelectTemplate = (templateText: string) => {
setText(templateText);
addToast(`Template applied: ${templateText}`, 'info');
};
const handleSelectHistory = (item: HistoryItem) => {
setText(item.text);
setSelectedFont(item.font);
addToast(`Restored from history`, 'info');
};
// Comparison mode handlers
const handleToggleComparisonMode = () => {
const newMode = !isComparisonMode;
setIsComparisonMode(newMode);
if (newMode && comparisonFonts.length === 0) {
// Initialize with current font
setComparisonFonts([selectedFont]);
}
addToast(newMode ? 'Comparison mode enabled' : 'Comparison mode disabled', 'info');
};
const handleAddToComparison = (fontName: string) => {
if (comparisonFonts.includes(fontName)) {
addToast('Font already in comparison', 'info');
return;
}
if (comparisonFonts.length >= 6) {
addToast('Maximum 6 fonts for comparison', 'info');
return;
}
setComparisonFonts([...comparisonFonts, fontName]);
addToast(`Added ${fontName} to comparison`, 'success');
};
const handleRemoveFromComparison = (fontName: string) => {
setComparisonFonts(comparisonFonts.filter((f) => f !== fontName));
addToast(`Removed ${fontName} from comparison`, 'info');
};
const handleCopyComparisonFont = async (fontName: string, result: string) => {
try {
await navigator.clipboard.writeText(result);
addToHistory(text, fontName, result);
addToast(`Copied ${fontName} to clipboard!`, 'success');
} catch (error) {
console.error('Failed to copy:', error);
addToast('Failed to copy', 'error');
}
};
const handleDownloadComparisonFont = (fontName: string, result: string) => {
const blob = new Blob([result], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `figlet-${fontName}-${Date.now()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Generate comparison results
React.useEffect(() => {
if (!isComparisonMode || comparisonFonts.length === 0 || !text) return;
const generateComparisons = async () => {
const results: Record<string, string> = {};
for (const fontName of comparisonFonts) {
try {
results[fontName] = await textToAscii(text, fontName);
} catch (error) {
console.error(`Error generating ASCII art for ${fontName}:`, error);
results[fontName] = 'Error generating ASCII art';
}
}
setComparisonResults(results);
};
generateComparisons();
}, [isComparisonMode, comparisonFonts, text]);
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">
{/* Comparison Mode Toggle */}
<Card className="scale-in">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<GitCompare className={cn(
"h-4 w-4",
isComparisonMode ? "text-primary" : "text-muted-foreground"
)} />
<span className="text-sm font-medium">Comparison Mode</span>
{isComparisonMode && comparisonFonts.length > 0 && (
<span className="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full font-medium slide-down">
{comparisonFonts.length} {comparisonFonts.length === 1 ? 'font' : 'fonts'}
</span>
)}
</div>
<Button
variant={isComparisonMode ? 'default' : 'outline'}
size="sm"
onClick={handleToggleComparisonMode}
className={cn(isComparisonMode && "shadow-lg")}
>
{isComparisonMode ? 'Disable' : 'Enable'}
</Button>
</div>
</Card>
<TextTemplates onSelectTemplate={handleSelectTemplate} />
<HistoryPanel onSelectHistory={handleSelectHistory} />
<TextInput
value={text}
onChange={setText}
placeholder="Type your text here..."
/>
{isComparisonMode ? (
<ComparisonMode
text={text}
selectedFonts={comparisonFonts}
fontResults={comparisonResults}
onRemoveFont={handleRemoveFromComparison}
onCopyFont={handleCopyComparisonFont}
onDownloadFont={handleDownloadComparisonFont}
/>
) : (
<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}
isComparisonMode={isComparisonMode}
comparisonFonts={comparisonFonts}
onAddToComparison={handleAddToComparison}
/>
</div>
</div>
);
}