diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2b0141 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Multi-stage Dockerfile for Next.js 16 static export + +# Stage 1: Dependencies +FROM node:22-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Stage 2: Builder +FROM node:22-alpine AS builder +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy source code +COPY . . + +# Build the application (static export) +RUN pnpm build + +# Stage 3: Runner (serve static files) +FROM nginx:alpine AS runner + +# Install curl for health check +RUN apk add --no-cache curl + +# Copy custom nginx config +COPY --from=builder /app/nginx.conf /etc/nginx/nginx.conf + +# Copy static files from build +COPY --from=builder /app/out /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index e269111..90bab5e 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -2,7 +2,7 @@ ## Project Overview -A modern, feature-rich web UI for generating ASCII art text using figlet.js with 700+ fonts from xero/figlet-fonts. This project aims to be the best figlet web interface, significantly improving upon existing solutions like TAAG. +A modern, feature-rich web UI for generating ASCII art text using figlet.js with 373 fonts from xero/figlet-fonts (.flf format). This project aims to be the best figlet web interface, significantly improving upon existing solutions like TAAG. **Tech Stack:** - Next.js 16 with static export @@ -17,7 +17,7 @@ A modern, feature-rich web UI for generating ASCII art text using figlet.js with ## Key Features & Improvements Over TAAG ### Superior Font Management -- **700+ fonts** from xero collection (vs ~300 on TAAG) +- **373 fonts** from xero collection (all .flf FIGlet fonts) - Visual font preview cards - Fuzzy search with intelligent matching - Font categories and tags (3D, block, script, retro, etc.) diff --git a/README.md b/README.md index 9822255..3805c95 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Figlet UI -A modern, feature-rich web UI for generating ASCII art text using figlet.js with 700+ fonts from the [xero/figlet-fonts](https://github.com/xero/figlet-fonts) collection. +A modern, feature-rich web UI for generating ASCII art text using figlet.js with 373 fonts from the [xero/figlet-fonts](https://github.com/xero/figlet-fonts) collection. ## Features -- **700+ Figlet Fonts** - Massive library from xero's curated collection +- **373 Figlet Fonts** - All .flf fonts from xero's curated collection - **Live Preview** - Real-time rendering as you type - **Fuzzy Search** - Quickly find fonts by name or style - **Visual Font Previews** - See actual rendering in the selector @@ -84,7 +84,7 @@ figlet-ui/ ## Why Figlet UI is Better Than TAAG -- **10x More Fonts**: 700+ fonts vs ~300 on TAAG +- **More Fonts**: 373 fonts vs ~300 on TAAG - **Modern UI/UX**: Clean, responsive design with animations - **Better Search**: Fuzzy search with visual previews - **More Export Options**: PNG, SVG, code snippets, not just text diff --git a/app/page.tsx b/app/page.tsx index d45248e..fd3d310 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,6 @@ import { FigletConverter } from '@/components/converter/FigletConverter'; import { ThemeToggle } from '@/components/layout/ThemeToggle'; +import { KeyboardShortcutsHelp } from '@/components/ui/KeyboardShortcutsHelp'; export default function Home() { return ( @@ -41,6 +42,8 @@ export default function Home() {

+ + ); } diff --git a/components/converter/FontSelector.tsx b/components/converter/FontSelector.tsx index 8c14179..36ead79 100644 --- a/components/converter/FontSelector.tsx +++ b/components/converter/FontSelector.tsx @@ -23,6 +23,7 @@ export function FontSelector({ fonts, selectedFont, onSelectFont, className }: F const [filter, setFilter] = React.useState('all'); const [favorites, setFavorites] = React.useState([]); const [recentFonts, setRecentFonts] = React.useState([]); + const searchInputRef = React.useRef(null); // Load favorites and recent fonts React.useEffect(() => { @@ -30,6 +31,26 @@ export function FontSelector({ fonts, selectedFont, onSelectFont, className }: F setRecentFonts(getRecentFonts()); }, []); + // Keyboard shortcuts + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // "/" to focus search + if (e.key === '/' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + searchInputRef.current?.focus(); + } + // "Esc" to clear search + if (e.key === 'Escape' && searchQuery) { + e.preventDefault(); + setSearchQuery(''); + searchInputRef.current?.blur(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [searchQuery]); + // Initialize Fuse.js for fuzzy search const fuse = React.useMemo(() => { return new Fuse(fonts, { @@ -118,8 +139,9 @@ export function FontSelector({ fonts, selectedFont, onSelectFont, className }: F
setSearchQuery(e.target.value)} className="pl-9 pr-9" diff --git a/components/layout/ThemeToggle.tsx b/components/layout/ThemeToggle.tsx index a11f7d2..8dcc5f0 100644 --- a/components/layout/ThemeToggle.tsx +++ b/components/layout/ThemeToggle.tsx @@ -7,6 +7,13 @@ import { Button } from '@/components/ui/Button'; export function ThemeToggle() { const [theme, setTheme] = React.useState<'light' | 'dark'>('light'); + const toggleTheme = React.useCallback(() => { + const newTheme = theme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + document.documentElement.classList.toggle('dark', newTheme === 'dark'); + }, [theme]); + React.useEffect(() => { // Check for saved theme preference or default to light const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null; @@ -17,12 +24,18 @@ export function ThemeToggle() { document.documentElement.classList.toggle('dark', initialTheme === 'dark'); }, []); - const toggleTheme = () => { - const newTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(newTheme); - localStorage.setItem('theme', newTheme); - document.documentElement.classList.toggle('dark', newTheme === 'dark'); - }; + // Keyboard shortcut: Ctrl/Cmd + D + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'd' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + toggleTheme(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [toggleTheme]); return ( + ); + } + + return ( +
+ +
+
+

+ + Keyboard Shortcuts +

+ +
+ +
+ {shortcuts.map((shortcut, i) => ( +
+ {shortcut.description} +
+ {shortcut.modifier && ( + + {shortcut.modifier === 'ctrl' ? '⌘/Ctrl' : 'Shift'} + + )} + + {shortcut.key} + +
+
+ ))} +
+
+
+
+ ); +} diff --git a/lib/hooks/useKeyboardShortcuts.ts b/lib/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..4be8d73 --- /dev/null +++ b/lib/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,34 @@ +'use client'; + +import { useEffect } from 'react'; + +export interface KeyboardShortcut { + key: string; + ctrlKey?: boolean; + metaKey?: boolean; + shiftKey?: boolean; + handler: () => void; + description: string; +} + +export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + for (const shortcut of shortcuts) { + const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase(); + const ctrlMatches = shortcut.ctrlKey ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey; + const metaMatches = shortcut.metaKey ? event.metaKey : true; + const shiftMatches = shortcut.shiftKey ? event.shiftKey : !event.shiftKey; + + if (keyMatches && ctrlMatches && shiftMatches) { + event.preventDefault(); + shortcut.handler(); + break; + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [shortcuts]); +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..4ccccd4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,59 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Serve index.html for all routes (SPA) + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +}