From a9d0fd844335f02049cfd897231d188fdff2b91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 23 Feb 2026 02:04:46 +0100 Subject: [PATCH] refactor: streamline toast system and harmonize UI across tools - Migrate all toast notifications to sonner and remove custom ToastProvider - Align Card and TextInput styling across Figlet and Pastel (rounded-lg, border-based) - Fix build error by removing non-existent export in lib/units/index.ts - Clean up unused Figlet components and constants --- app/(app)/figlet/page.tsx | 14 +- app/(app)/pastel/batch/page.tsx | 2 +- app/(app)/pastel/layout.tsx | 4 +- app/(app)/units/page.tsx | 14 +- app/globals.css | 6 + components/BackToTop.tsx | 2 +- components/figlet/ComparisonMode.tsx | 124 ----------------- components/figlet/FigletConverter.tsx | 189 ++++---------------------- components/figlet/FontPreview.tsx | 7 +- components/figlet/FontSelector.tsx | 102 +++++--------- components/figlet/HistoryPanel.tsx | 133 ------------------ components/figlet/TextInput.tsx | 2 +- components/figlet/TextTemplates.tsx | 92 ------------- components/layout/AppHeader.tsx | 4 +- components/layout/AppSidebar.tsx | 12 +- components/providers/Providers.tsx | 11 +- components/ui/Button.tsx | 2 +- components/ui/Card.tsx | 2 +- components/ui/Input.tsx | 2 +- components/ui/Select.tsx | 2 +- components/ui/Toast.tsx | 90 ------------ lib/figlet/constants/templates.ts | 38 ------ lib/storage/history.ts | 53 -------- lib/units/index.ts | 1 - tailwind.config.ts | 9 -- 25 files changed, 109 insertions(+), 808 deletions(-) delete mode 100644 components/figlet/ComparisonMode.tsx delete mode 100644 components/figlet/HistoryPanel.tsx delete mode 100644 components/figlet/TextTemplates.tsx delete mode 100644 components/ui/Toast.tsx delete mode 100644 lib/figlet/constants/templates.ts delete mode 100644 lib/storage/history.ts delete mode 100644 tailwind.config.ts diff --git a/app/(app)/figlet/page.tsx b/app/(app)/figlet/page.tsx index 64d807a..443ec15 100644 --- a/app/(app)/figlet/page.tsx +++ b/app/(app)/figlet/page.tsx @@ -2,12 +2,16 @@ import { FigletConverter } from '@/components/figlet/FigletConverter'; export default function FigletPage() { return ( -
-
-

Figlet UI

-

ASCII Art Text Generator with 373 Fonts

+
+
+
+

Figlet ASCII

+

+ ASCII Art Text Generator with 373 Fonts +

+
+
-
); } diff --git a/app/(app)/pastel/batch/page.tsx b/app/(app)/pastel/batch/page.tsx index fc96210..7a3ff77 100644 --- a/app/(app)/pastel/batch/page.tsx +++ b/app/(app)/pastel/batch/page.tsx @@ -106,7 +106,7 @@ export default function BatchPage() { value={inputColors} onChange={(e) => setInputColors(e.target.value)} placeholder="#ff0099, #00ff99, #9900ff #ff5533 #3355ff" - className="w-full h-48 p-3 border rounded-lg bg-background font-mono text-sm" + className="w-full h-48 p-3 border border-border rounded-xl bg-input font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary/50 transition-all duration-200" />

diff --git a/app/(app)/pastel/layout.tsx b/app/(app)/pastel/layout.tsx index 8d5a1ac..ba481ed 100644 --- a/app/(app)/pastel/layout.tsx +++ b/app/(app)/pastel/layout.tsx @@ -4,8 +4,8 @@ export default function PastelLayout({ children: React.ReactNode; }>) { return ( -

+ <> {children} -
+ ); } diff --git a/app/(app)/units/page.tsx b/app/(app)/units/page.tsx index d2cc692..179f5ce 100644 --- a/app/(app)/units/page.tsx +++ b/app/(app)/units/page.tsx @@ -2,12 +2,16 @@ import MainConverter from '@/components/units/converter/MainConverter'; export default function UnitsPage() { return ( -
-
-

Units Converter

-

Smart unit converter with 187 units across 23 categories

+
+
+
+

Units Converter

+

+ Smart unit converter with 187 units across 23 categories +

+
+
-
); } diff --git a/app/globals.css b/app/globals.css index 442415b..cdd252b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -110,6 +110,7 @@ } :root, .dark { + color-scheme: dark; /* CORPORATE DARK THEME (The Standard) */ --background: #0a0a0f; --foreground: #ffffff; @@ -134,6 +135,7 @@ } .light { + color-scheme: light; /* LIGHT ADAPTATION (Keeping the "Glass" look) */ --background: oklch(98% 0.005 255); --foreground: oklch(20% 0.04 255); @@ -166,6 +168,10 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + /* Fix native select dropdown styling */ + select option { + @apply bg-popover text-popover-foreground; + } } html { diff --git a/components/BackToTop.tsx b/components/BackToTop.tsx index 4112bae..4fb954e 100644 --- a/components/BackToTop.tsx +++ b/components/BackToTop.tsx @@ -44,7 +44,7 @@ export default function BackToTop() { {isVisible && ( ; - onRemoveFont: (fontName: string) => void; - onCopyFont: (fontName: string, result: string) => void; - onDownloadFont: (fontName: string, result: string) => void; - className?: string; -} - -export function ComparisonMode({ - text, - selectedFonts, - fontResults, - onRemoveFont, - onCopyFont, - onDownloadFont, - className, -}: ComparisonModeProps) { - return ( -
-
-

Font Comparison

- - {selectedFonts.length} font{selectedFonts.length !== 1 ? 's' : ''} selected - -
- - {selectedFonts.length === 0 ? ( - - - - ) : ( -
- {selectedFonts.map((fontName, index) => ( - -
- {/* Font Header */} -
-
- - {fontName} - -
-
- - - -
-
- - {/* ASCII Art Preview */} -
-
-                    
-                      {fontResults[fontName] || 'Loading...'}
-                    
-                  
-
- - {/* Stats */} - {fontResults[fontName] && ( -
- - {fontResults[fontName].split('\n').length} lines - - - {Math.max( - ...fontResults[fontName].split('\n').map((line) => line.length) - )} chars wide - -
- )} -
-
- ))} -
- )} -
- ); -} diff --git a/components/figlet/FigletConverter.tsx b/components/figlet/FigletConverter.tsx index 6647b53..c893171 100644 --- a/components/figlet/FigletConverter.tsx +++ b/components/figlet/FigletConverter.tsx @@ -4,20 +4,12 @@ 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 { toast } from 'sonner'; import type { FigletFont } from '@/types/figlet'; export function FigletConverter() { @@ -26,10 +18,6 @@ export function FigletConverter() { const [asciiArt, setAsciiArt] = React.useState(''); const [fonts, setFonts] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); - const [isComparisonMode, setIsComparisonMode] = React.useState(false); - const [comparisonFonts, setComparisonFonts] = React.useState([]); - const [comparisonResults, setComparisonResults] = React.useState>({}); - const { addToast } = useToast(); // Load fonts and check URL params on mount React.useEffect(() => { @@ -83,11 +71,10 @@ export function FigletConverter() { try { await navigator.clipboard.writeText(asciiArt); - addToHistory(text, selectedFont, asciiArt); - addToast('Copied to clipboard!', 'success'); + toast.success('Copied to clipboard!'); } catch (error) { console.error('Failed to copy:', error); - addToast('Failed to copy', 'error'); + toast.error('Failed to copy'); } }; @@ -112,10 +99,10 @@ export function FigletConverter() { try { await navigator.clipboard.writeText(shareUrl); - addToast('Shareable URL copied!', 'success'); + toast.success('Shareable URL copied!'); } catch (error) { console.error('Failed to copy URL:', error); - addToast('Failed to copy URL', 'error'); + toast.error('Failed to copy URL'); } }; @@ -124,164 +111,40 @@ export function FigletConverter() { if (fonts.length === 0) return; const randomIndex = Math.floor(Math.random() * fonts.length); setSelectedFont(fonts[randomIndex].name); - addToast(`Random font: ${fonts[randomIndex].name}`, 'info'); + toast.info(`Random font: ${fonts[randomIndex].name}`); }; - 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 = {}; - 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 ( -
+
{/* Left Column - Input and Preview */} -
- {/* Comparison Mode Toggle */} - -
-
- - Comparison Mode - {isComparisonMode && comparisonFonts.length > 0 && ( - - {comparisonFonts.length} {comparisonFonts.length === 1 ? 'font' : 'fonts'} - - )} -
- -
-
- - - - - +
- {isComparisonMode ? ( - - ) : ( - - )} +
{/* Right Column - Font Selector */} -
- +
+
+ +
); diff --git a/components/figlet/FontPreview.tsx b/components/figlet/FontPreview.tsx index a4b6ad0..b0338ee 100644 --- a/components/figlet/FontPreview.tsx +++ b/components/figlet/FontPreview.tsx @@ -8,7 +8,7 @@ import { Skeleton } from '@/components/ui/Skeleton'; import { EmptyState } from '@/components/ui/EmptyState'; import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; -import { useToast } from '@/components/ui/Toast'; +import { toast } from 'sonner'; export interface FontPreviewProps { text: string; @@ -28,7 +28,6 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare const previewRef = React.useRef(null); const [textAlign, setTextAlign] = React.useState('left'); const [fontSize, setFontSize] = React.useState<'xs' | 'sm' | 'base'>('sm'); - const { addToast } = useToast(); const handleExportPNG = async () => { if (!previewRef.current || !text) return; @@ -44,10 +43,10 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare link.href = dataUrl; link.click(); - addToast('Exported as PNG!', 'success'); + toast.success('Exported as PNG!'); } catch (error) { console.error('Failed to export PNG:', error); - addToast('Failed to export PNG', 'error'); + toast.error('Failed to export PNG'); } }; return ( diff --git a/components/figlet/FontSelector.tsx b/components/figlet/FontSelector.tsx index c9b04c6..604de0f 100644 --- a/components/figlet/FontSelector.tsx +++ b/components/figlet/FontSelector.tsx @@ -5,7 +5,7 @@ import Fuse from 'fuse.js'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; import { EmptyState } from '@/components/ui/EmptyState'; -import { Search, X, Heart, Clock, List, Shuffle, Plus, Check } from 'lucide-react'; +import { Search, X, Heart, Clock, List, Shuffle } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; import { Button } from '@/components/ui/Button'; import type { FigletFont } from '@/types/figlet'; @@ -16,9 +16,6 @@ export interface FontSelectorProps { selectedFont: string; onSelectFont: (fontName: string) => void; onRandomFont?: () => void; - isComparisonMode?: boolean; - comparisonFonts?: string[]; - onAddToComparison?: (fontName: string) => void; className?: string; } @@ -29,9 +26,6 @@ export function FontSelector({ selectedFont, onSelectFont, onRandomFont, - isComparisonMode = false, - comparisonFonts = [], - onAddToComparison, className }: FontSelectorProps) { const [searchQuery, setSearchQuery] = React.useState(''); @@ -112,9 +106,9 @@ export function FontSelector({ }; return ( - -
-
+ +
+

Select Font

{onRandomFont && ( )}
{/* Filter Tabs */} -
+
{/* Search Input */} -
+
{/* Font List */} -
+
{filteredFonts.length === 0 ? ( ) : ( - filteredFonts.map((font) => { - const isInComparison = comparisonFonts.includes(font.name); - return ( -
( +
+ - {isComparisonMode && onAddToComparison && ( - - )} - + -
- ); - }) + /> + +
+ )) )}
{/* Stats */} -
+
{filteredFonts.length} font{filteredFonts.length !== 1 ? 's' : ''} {filter === 'favorites' && ` • ${favorites.length} total favorites`} {filter === 'recent' && ` • ${recentFonts.length} recent`} diff --git a/components/figlet/HistoryPanel.tsx b/components/figlet/HistoryPanel.tsx deleted file mode 100644 index b9ad1da..0000000 --- a/components/figlet/HistoryPanel.tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { EmptyState } from '@/components/ui/EmptyState'; -import { History, X, Trash2, ChevronDown, ChevronUp, Clock } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; -import { getHistory, clearHistory, removeHistoryItem, type HistoryItem } from '@/lib/storage/history'; - -export interface HistoryPanelProps { - onSelectHistory: (item: HistoryItem) => void; - className?: string; -} - -export function HistoryPanel({ onSelectHistory, className }: HistoryPanelProps) { - const [isExpanded, setIsExpanded] = React.useState(false); - const [history, setHistory] = React.useState([]); - - const loadHistory = React.useCallback(() => { - setHistory(getHistory()); - }, []); - - React.useEffect(() => { - loadHistory(); - // Refresh history every 2 seconds when expanded - if (isExpanded) { - const interval = setInterval(loadHistory, 2000); - return () => clearInterval(interval); - } - }, [isExpanded, loadHistory]); - - const handleClearAll = () => { - clearHistory(); - loadHistory(); - }; - - const handleRemove = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); - removeHistoryItem(id); - loadHistory(); - }; - - const formatTime = (timestamp: number) => { - const now = Date.now(); - const diff = now - timestamp; - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(diff / 3600000); - - if (minutes < 1) return 'Just now'; - if (minutes < 60) return `${minutes}m ago`; - if (hours < 24) return `${hours}h ago`; - return new Date(timestamp).toLocaleDateString(); - }; - - return ( - -
- - - {isExpanded && ( -
- {history.length === 0 ? ( - - ) : ( - <> -
- -
- -
- {history.map((item) => ( -
onSelectHistory(item)} - className="group relative p-3 bg-muted/50 hover:bg-accent hover:scale-[1.02] rounded-md cursor-pointer transition-all" - > -
-
-
- - {item.font} - - - {formatTime(item.timestamp)} - -
-

{item.text}

-
- -
-
- ))} -
- - )} -
- )} -
-
- ); -} diff --git a/components/figlet/TextInput.tsx b/components/figlet/TextInput.tsx index a8b6a26..ab38dcb 100644 --- a/components/figlet/TextInput.tsx +++ b/components/figlet/TextInput.tsx @@ -17,7 +17,7 @@ export function TextInput({ value, onChange, placeholder, className }: TextInput value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder || 'Type something...'} - className="w-full h-32 px-4 py-3 text-base border border-input rounded-lg bg-background resize-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 placeholder:text-muted-foreground" + className="w-full h-32 px-4 py-3 text-base border border-border rounded-lg bg-input resize-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring placeholder:text-muted-foreground transition-all duration-200" maxLength={100} />
diff --git a/components/figlet/TextTemplates.tsx b/components/figlet/TextTemplates.tsx deleted file mode 100644 index 4696527..0000000 --- a/components/figlet/TextTemplates.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { Sparkles, ChevronDown, ChevronUp } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; -import { TEXT_TEMPLATES, TEMPLATE_CATEGORIES } from '@/lib/figlet/constants/templates'; - -export interface TextTemplatesProps { - onSelectTemplate: (text: string) => void; - className?: string; -} - -export function TextTemplates({ onSelectTemplate, className }: TextTemplatesProps) { - const [isExpanded, setIsExpanded] = React.useState(false); - const [selectedCategory, setSelectedCategory] = React.useState('all'); - - const filteredTemplates = React.useMemo(() => { - if (selectedCategory === 'all') return TEXT_TEMPLATES; - return TEXT_TEMPLATES.filter(t => t.category === selectedCategory); - }, [selectedCategory]); - - return ( - -
- - - {isExpanded && ( -
- {/* Category Filter */} -
- - {TEMPLATE_CATEGORIES.map((cat) => ( - - ))} -
- - {/* Templates Grid */} -
- {filteredTemplates.map((template) => ( - - ))} -
-
- )} -
-
- ); -} diff --git a/components/layout/AppHeader.tsx b/components/layout/AppHeader.tsx index c426909..b0c324a 100644 --- a/components/layout/AppHeader.tsx +++ b/components/layout/AppHeader.tsx @@ -17,7 +17,7 @@ export function AppHeader() { const pathSegments = pathname.split('/').filter(Boolean); return ( -
+