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 && (
@@ -43,6 +53,14 @@ export function FontPreview({ text, isLoading, onCopy, onDownload, onShare, clas
+ {!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 && (
+
+
+ Random
+
+ )}
+
{/* 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;
+}