Implemented Phases 5-7 of the implementation plan with major UX enhancements:
**Dark Mode (Phase 9)**
- Added ThemeToggle component with localStorage persistence
- System preference detection
- Smooth theme transitions
- Moon/Sun icon toggle in header
**Fuzzy Search with Fuse.js (Phase 5)**
- Integrated Fuse.js for intelligent font search
- 30% threshold for flexible matching
- Search by font name and filename
- Clear button for search input
- Much better than simple string matching
**Favorites & Recent Fonts System (Phase 7)**
- localStorage-based favorites with heart icon toggle
- Auto-tracking of recently used fonts (max 10)
- Filter tabs: All / Favorites / Recent
- Favorite hearts visible on hover
- Red filled heart for favorited fonts
- Stats showing favorite and recent counts
**Shareable URLs (Phase 6)**
- Encode text + font in URL parameters
- Auto-load from URL on page load
- Share button copies URL to clipboard
- Clean URL updates without page reload
- Perfect for sharing ASCII art creations
**Enhanced Font Selector**
- 3-tab filter system (All/Favorites/Recent)
- Visual feedback for selected fonts
- Empty states for each filter
- Font count statistics
- Heart icon for quick favoriting
- Recent fonts sorted by usage order
**UX Improvements**
- Copy feedback ("Copied to clipboard! ✓")
- Share feedback ("URL copied to clipboard! ✓")
- Responsive button layout
- Better empty states
- Improved accessibility with aria-labels
**Tech Highlights**
- Client-side localStorage management
- URL encoding/decoding utilities
- React hooks for state management
- Fuse.js fuzzy search integration
- Theme persistence across sessions
The app now has professional-grade features rivaling any modern web app!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
4.1 KiB
TypeScript
140 lines
4.1 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 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 [isCopied, setIsCopied] = React.useState(false);
|
|
const [isShared, setIsShared] = React.useState(false);
|
|
|
|
// 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);
|
|
setIsCopied(true);
|
|
setTimeout(() => setIsCopied(false), 2000);
|
|
} catch (error) {
|
|
console.error('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);
|
|
setIsShared(true);
|
|
setTimeout(() => setIsShared(false), 2000);
|
|
} catch (error) {
|
|
console.error('Failed to copy URL:', error);
|
|
}
|
|
};
|
|
|
|
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={isCopied ? 'Copied to clipboard! ✓' : isShared ? 'URL copied to clipboard! ✓' : asciiArt}
|
|
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}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|