2026-02-22 21:35:53 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
import { toPng } from 'html-to-image';
|
2026-02-25 16:00:10 +01:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
2026-02-24 16:20:35 +01:00
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
2026-02-27 19:11:25 +01:00
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from '@/components/ui/tooltip';
|
2026-02-24 16:20:35 +01:00
|
|
|
import {
|
|
|
|
|
Empty,
|
|
|
|
|
EmptyDescription,
|
|
|
|
|
EmptyHeader,
|
|
|
|
|
EmptyMedia,
|
|
|
|
|
EmptyTitle,
|
|
|
|
|
} from "@/components/ui/empty"
|
2026-02-27 19:11:25 +01:00
|
|
|
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type, MessageSquareCode } from 'lucide-react';
|
2026-02-22 21:35:53 +01:00
|
|
|
import { cn } from '@/lib/utils/cn';
|
2026-02-23 02:04:46 +01:00
|
|
|
import { toast } from 'sonner';
|
2026-02-22 21:35:53 +01:00
|
|
|
|
2026-02-27 19:11:25 +01:00
|
|
|
export type CommentStyle = 'none' | '//' | '#' | '--' | ';' | '/* */' | '<!-- -->' | '"""';
|
|
|
|
|
|
|
|
|
|
const COMMENT_STYLES: { value: CommentStyle; label: string }[] = [
|
|
|
|
|
{ value: 'none', label: 'None' },
|
|
|
|
|
{ value: '//', label: '// C, JS, Go' },
|
|
|
|
|
{ value: '#', label: '# Python, Shell' },
|
|
|
|
|
{ value: '--', label: '-- SQL, Lua' },
|
|
|
|
|
{ value: ';', label: '; Lisp, ASM' },
|
|
|
|
|
{ value: '/* */', label: '/* */ Block' },
|
|
|
|
|
{ value: '<!-- -->', label: '<!-- --> HTML' },
|
|
|
|
|
{ value: '"""', label: '""" Docstring' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function applyCommentStyle(text: string, style: CommentStyle): string {
|
|
|
|
|
if (style === 'none' || !text) return text;
|
|
|
|
|
const lines = text.split('\n');
|
|
|
|
|
switch (style) {
|
|
|
|
|
case '//':
|
|
|
|
|
case '#':
|
|
|
|
|
case '--':
|
|
|
|
|
case ';':
|
|
|
|
|
return lines.map(line => `${style} ${line}`).join('\n');
|
|
|
|
|
case '/* */':
|
|
|
|
|
return ['/*', ...lines.map(line => ` * ${line}`), ' */'].join('\n');
|
|
|
|
|
case '<!-- -->':
|
|
|
|
|
return ['<!--', ...lines, '-->'].join('\n');
|
|
|
|
|
case '"""':
|
|
|
|
|
return ['"""', ...lines, '"""'].join('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 21:35:53 +01:00
|
|
|
export interface FontPreviewProps {
|
|
|
|
|
text: string;
|
|
|
|
|
font?: string;
|
|
|
|
|
isLoading?: boolean;
|
|
|
|
|
onCopy?: () => void;
|
|
|
|
|
onDownload?: () => void;
|
|
|
|
|
onShare?: () => void;
|
2026-02-27 19:11:25 +01:00
|
|
|
onCommentedTextChange?: (commentedText: string) => void;
|
2026-02-22 21:35:53 +01:00
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TextAlign = 'left' | 'center' | 'right';
|
|
|
|
|
|
2026-02-27 19:11:25 +01:00
|
|
|
export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare, onCommentedTextChange, className }: FontPreviewProps) {
|
2026-02-22 21:35:53 +01:00
|
|
|
const previewRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
const [textAlign, setTextAlign] = React.useState<TextAlign>('left');
|
|
|
|
|
const [fontSize, setFontSize] = React.useState<'xs' | 'sm' | 'base'>('sm');
|
2026-02-27 19:11:25 +01:00
|
|
|
const [commentStyle, setCommentStyle] = React.useState<CommentStyle>('none');
|
|
|
|
|
|
|
|
|
|
const commentedText = React.useMemo(() => applyCommentStyle(text, commentStyle), [text, commentStyle]);
|
|
|
|
|
const lineCount = commentedText ? commentedText.split('\n').length : 0;
|
|
|
|
|
const charCount = commentedText ? commentedText.length : 0;
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
onCommentedTextChange?.(commentedText);
|
|
|
|
|
}, [commentedText, onCommentedTextChange]);
|
2026-02-22 21:35:53 +01:00
|
|
|
|
|
|
|
|
const handleExportPNG = async () => {
|
|
|
|
|
if (!previewRef.current || !text) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const dataUrl = await toPng(previewRef.current, {
|
|
|
|
|
backgroundColor: getComputedStyle(previewRef.current).backgroundColor,
|
|
|
|
|
pixelRatio: 2,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const link = document.createElement('a');
|
2026-02-26 12:31:10 +01:00
|
|
|
link.download = `ascii-${font || 'export'}-${Date.now()}.png`;
|
2026-02-22 21:35:53 +01:00
|
|
|
link.href = dataUrl;
|
|
|
|
|
link.click();
|
|
|
|
|
|
2026-02-23 02:04:46 +01:00
|
|
|
toast.success('Exported as PNG!');
|
2026-02-22 21:35:53 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to export PNG:', error);
|
2026-02-23 02:04:46 +01:00
|
|
|
toast.error('Failed to export PNG');
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
};
|
2026-02-27 19:11:25 +01:00
|
|
|
|
2026-02-22 21:35:53 +01:00
|
|
|
return (
|
|
|
|
|
<Card className={cn('relative', className)}>
|
2026-02-25 16:09:29 +01:00
|
|
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
2026-02-25 16:00:10 +01:00
|
|
|
<div className="flex items-center gap-2">
|
2026-02-25 16:09:29 +01:00
|
|
|
<CardTitle>Preview</CardTitle>
|
2026-02-25 16:00:10 +01:00
|
|
|
{font && (
|
2026-02-27 19:11:25 +01:00
|
|
|
<Badge className="text-[10px] font-mono">
|
2026-02-25 16:00:10 +01:00
|
|
|
{font}
|
2026-02-27 19:11:25 +01:00
|
|
|
</Badge>
|
2026-02-25 16:00:10 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-27 12:35:02 +01:00
|
|
|
<div className="flex gap-1.5 flex-wrap">
|
2026-02-25 16:00:10 +01:00
|
|
|
{onCopy && (
|
2026-02-27 19:11:25 +01:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="outline" size="xs" onClick={onCopy}>
|
|
|
|
|
<Copy className="h-3 w-3 mr-1" />
|
|
|
|
|
Copy
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Copy to clipboard</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-02-25 16:00:10 +01:00
|
|
|
)}
|
|
|
|
|
{onShare && (
|
2026-02-27 19:11:25 +01:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="outline" size="xs" onClick={onShare}>
|
|
|
|
|
<Share2 className="h-3 w-3 mr-1" />
|
|
|
|
|
Share
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Copy shareable URL</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-02-25 16:00:10 +01:00
|
|
|
)}
|
2026-02-27 19:11:25 +01:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="outline" size="xs" onClick={handleExportPNG}>
|
|
|
|
|
<ImageIcon className="h-3 w-3 mr-1" />
|
|
|
|
|
PNG
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Export as PNG</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-02-25 16:00:10 +01:00
|
|
|
{onDownload && (
|
2026-02-27 19:11:25 +01:00
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button variant="outline" size="xs" onClick={onDownload}>
|
|
|
|
|
<Download className="h-3 w-3 mr-1" />
|
|
|
|
|
TXT
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Download as text file</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-02-25 16:00:10 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
2026-02-27 12:35:02 +01:00
|
|
|
<CardContent className="space-y-3">
|
2026-02-25 16:00:10 +01:00
|
|
|
{/* Controls */}
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
2026-02-27 19:11:25 +01:00
|
|
|
<ToggleGroup
|
|
|
|
|
type="single"
|
|
|
|
|
value={textAlign}
|
|
|
|
|
onValueChange={(v) => v && setTextAlign(v as TextAlign)}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={commentStyle !== 'none'}
|
|
|
|
|
>
|
|
|
|
|
<ToggleGroupItem value="left" aria-label="Align left" className="px-1.5">
|
2026-02-27 12:35:02 +01:00
|
|
|
<AlignLeft className="h-3 w-3" />
|
2026-02-27 19:11:25 +01:00
|
|
|
</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem value="center" aria-label="Align center" className="px-1.5">
|
2026-02-27 12:35:02 +01:00
|
|
|
<AlignCenter className="h-3 w-3" />
|
2026-02-27 19:11:25 +01:00
|
|
|
</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem value="right" aria-label="Align right" className="px-1.5">
|
2026-02-27 12:35:02 +01:00
|
|
|
<AlignRight className="h-3 w-3" />
|
2026-02-27 19:11:25 +01:00
|
|
|
</ToggleGroupItem>
|
|
|
|
|
</ToggleGroup>
|
|
|
|
|
|
|
|
|
|
<ToggleGroup
|
|
|
|
|
type="single"
|
|
|
|
|
value={fontSize}
|
|
|
|
|
onValueChange={(v) => v && setFontSize(v as 'xs' | 'sm' | 'base')}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
>
|
|
|
|
|
<ToggleGroupItem value="xs" aria-label="Extra small font" className="px-1.5 text-[10px] uppercase">
|
|
|
|
|
xs
|
|
|
|
|
</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem value="sm" aria-label="Small font" className="px-1.5 text-[10px] uppercase">
|
|
|
|
|
sm
|
|
|
|
|
</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem value="base" aria-label="Medium font" className="px-1.5 text-[10px] uppercase">
|
|
|
|
|
md
|
|
|
|
|
</ToggleGroupItem>
|
|
|
|
|
</ToggleGroup>
|
|
|
|
|
|
|
|
|
|
<Select value={commentStyle} onValueChange={(v) => setCommentStyle(v as CommentStyle)}>
|
|
|
|
|
<SelectTrigger size="sm" className="h-8 w-auto gap-1 text-xs">
|
|
|
|
|
<MessageSquareCode className="h-3 w-3 text-foreground shrink-0" />
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{COMMENT_STYLES.map((s) => (
|
|
|
|
|
<SelectItem key={s.value} value={s.value}>
|
|
|
|
|
{s.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2026-02-22 21:35:53 +01:00
|
|
|
|
2026-02-27 12:35:02 +01:00
|
|
|
{!isLoading && text && (
|
|
|
|
|
<div className="flex gap-2 text-[10px] text-muted-foreground ml-auto">
|
|
|
|
|
<span>{lineCount} lines</span>
|
|
|
|
|
<span>{charCount} chars</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-22 21:35:53 +01:00
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
ref={previewRef}
|
|
|
|
|
className={cn(
|
|
|
|
|
'relative min-h-[200px] bg-muted/50 rounded-lg p-4 overflow-x-auto',
|
2026-02-27 19:11:25 +01:00
|
|
|
commentStyle === 'none' && textAlign === 'center' && 'text-center',
|
|
|
|
|
commentStyle === 'none' && textAlign === 'right' && 'text-right'
|
2026-02-22 21:35:53 +01:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<Skeleton className="h-6 w-3/4" />
|
|
|
|
|
<Skeleton className="h-6 w-full" />
|
|
|
|
|
<Skeleton className="h-6 w-5/6" />
|
|
|
|
|
<Skeleton className="h-6 w-2/3" />
|
|
|
|
|
<Skeleton className="h-6 w-full" />
|
|
|
|
|
<Skeleton className="h-6 w-4/5" />
|
|
|
|
|
</div>
|
|
|
|
|
) : text ? (
|
|
|
|
|
<pre className={cn(
|
|
|
|
|
'font-mono whitespace-pre overflow-x-auto animate-in',
|
|
|
|
|
fontSize === 'xs' && 'text-[10px]',
|
|
|
|
|
fontSize === 'sm' && 'text-xs sm:text-sm',
|
|
|
|
|
fontSize === 'base' && 'text-sm sm:text-base'
|
|
|
|
|
)}>
|
2026-02-27 19:11:25 +01:00
|
|
|
{commentedText}
|
2026-02-22 21:35:53 +01:00
|
|
|
</pre>
|
|
|
|
|
) : (
|
2026-02-24 16:20:35 +01:00
|
|
|
<Empty>
|
|
|
|
|
<EmptyHeader>
|
|
|
|
|
<EmptyMedia variant="icon">
|
|
|
|
|
<Type />
|
|
|
|
|
</EmptyMedia>
|
|
|
|
|
<EmptyTitle>Start typing to see your ASCII art</EmptyTitle>
|
|
|
|
|
<EmptyDescription>Enter text in the input field above to generate ASCII art with the selected font</EmptyDescription>
|
|
|
|
|
</EmptyHeader>
|
|
|
|
|
</Empty>
|
2026-02-22 21:35:53 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-25 16:00:10 +01:00
|
|
|
</CardContent>
|
2026-02-22 21:35:53 +01:00
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|