diff --git a/app/globals.css b/app/globals.css index b43467c..06e7907 100644 --- a/app/globals.css +++ b/app/globals.css @@ -107,3 +107,24 @@ @apply bg-muted-foreground/20 rounded-lg hover:bg-muted-foreground/30; } } + +@layer utilities { + .animate-in { + animation: fadeIn 0.2s ease-in; + } + + .slide-in-from-right-full { + animation: slideInFromRight 0.3s ease-out; + } +} + +@keyframes slideInFromRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 5eac401..2cbc4bf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,14 +1,15 @@ import type { Metadata } from 'next'; import './globals.css'; +import { ToastProvider } from '@/components/ui/Toast'; export const metadata: Metadata = { title: 'Figlet UI - ASCII Art Text Generator', - description: 'A modern web UI for generating ASCII art text with 700+ figlet fonts. Preview custom text in any figlet font, export to multiple formats, and share your creations.', + description: 'A modern web UI for generating ASCII art text with 373 figlet fonts. Preview custom text in any figlet font, export to multiple formats, and share your creations.', keywords: ['figlet', 'ascii art', 'text generator', 'banner', 'ascii', 'text art'], authors: [{ name: 'Valknar' }], openGraph: { title: 'Figlet UI - ASCII Art Text Generator', - description: 'Generate beautiful ASCII art text with 700+ fonts', + description: 'Generate beautiful ASCII art text with 373 fonts', type: 'website', }, }; @@ -21,7 +22,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/components/converter/FigletConverter.tsx b/components/converter/FigletConverter.tsx index 4d12233..a272576 100644 --- a/components/converter/FigletConverter.tsx +++ b/components/converter/FigletConverter.tsx @@ -9,6 +9,7 @@ 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() { @@ -17,8 +18,7 @@ export function FigletConverter() { const [asciiArt, setAsciiArt] = React.useState(''); const [fonts, setFonts] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); - const [isCopied, setIsCopied] = React.useState(false); - const [isShared, setIsShared] = React.useState(false); + const { addToast } = useToast(); // Load fonts and check URL params on mount React.useEffect(() => { @@ -72,10 +72,10 @@ export function FigletConverter() { try { await navigator.clipboard.writeText(asciiArt); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); + addToast('Copied to clipboard!', 'success'); } catch (error) { console.error('Failed to copy:', error); + addToast('Failed to copy', 'error'); } }; @@ -100,13 +100,21 @@ export function FigletConverter() { try { await navigator.clipboard.writeText(shareUrl); - setIsShared(true); - setTimeout(() => setIsShared(false), 2000); + 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 (
{/* Left Column - Input and Preview */} @@ -118,7 +126,8 @@ export function FigletConverter() { />
diff --git a/components/converter/FontPreview.tsx b/components/converter/FontPreview.tsx index b69347a..b8724da 100644 --- a/components/converter/FontPreview.tsx +++ b/components/converter/FontPreview.tsx @@ -8,6 +8,7 @@ import { cn } from '@/lib/utils/cn'; export interface FontPreviewProps { text: string; + font?: string; isLoading?: boolean; onCopy?: () => void; onDownload?: () => void; @@ -15,13 +16,22 @@ export interface FontPreviewProps { className?: string; } -export function FontPreview({ text, isLoading, onCopy, onDownload, onShare, className }: FontPreviewProps) { +export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare, className }: FontPreviewProps) { + const lineCount = text ? text.split('\n').length : 0; + const charCount = text ? text.length : 0; return (
-
-

Preview

-
+
+
+

Preview

+ {font && ( + + {font} + + )} +
+
{onCopy && (
+ {!isLoading && text && ( +
+ {lineCount} lines + + {charCount} chars +
+ )} +
{isLoading ? (
diff --git a/components/converter/FontSelector.tsx b/components/converter/FontSelector.tsx index 36ead79..ff0c723 100644 --- a/components/converter/FontSelector.tsx +++ b/components/converter/FontSelector.tsx @@ -4,8 +4,9 @@ import * as React from 'react'; import Fuse from 'fuse.js'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; -import { Search, X, Heart, Clock, List } 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'; import { getFavorites, getRecentFonts, toggleFavorite, isFavorite } from '@/lib/storage/favorites'; @@ -13,12 +14,13 @@ export interface FontSelectorProps { fonts: FigletFont[]; selectedFont: string; onSelectFont: (fontName: string) => void; + onRandomFont?: () => void; className?: string; } type FilterType = 'all' | 'favorites' | 'recent'; -export function FontSelector({ fonts, selectedFont, onSelectFont, className }: FontSelectorProps) { +export function FontSelector({ fonts, selectedFont, onSelectFont, onRandomFont, className }: FontSelectorProps) { const [searchQuery, setSearchQuery] = React.useState(''); const [filter, setFilter] = React.useState('all'); const [favorites, setFavorites] = React.useState([]); @@ -99,7 +101,20 @@ export function FontSelector({ fonts, selectedFont, onSelectFont, className }: F return (
-

Select Font

+
+

Select Font

+ {onRandomFont && ( + + )} +
{/* Filter Tabs */}
diff --git a/components/ui/Toast.tsx b/components/ui/Toast.tsx new file mode 100644 index 0000000..7f7d114 --- /dev/null +++ b/components/ui/Toast.tsx @@ -0,0 +1,90 @@ +'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(undefined); + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = React.useState([]); + + 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 ( + + {children} +
+ {toasts.map((toast) => ( + removeToast(toast.id)} /> + ))} +
+
+ ); +} + +function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) { + const Icon = toast.type === 'success' ? CheckCircle2 : toast.type === 'error' ? AlertCircle : Info; + + return ( +
+ +

{toast.message}

+ +
+ ); +} + +export function useToast() { + const context = React.useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within ToastProvider'); + } + return context; +}