Files
kit-ui/components/ascii/FontPreview.tsx

195 lines
6.6 KiB
TypeScript

'use client';
import * as React from 'react';
import { toPng } from 'html-to-image';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import { toast } from 'sonner';
export interface FontPreviewProps {
text: string;
font?: string;
isLoading?: boolean;
onCopy?: () => void;
onDownload?: () => void;
onShare?: () => void;
className?: string;
}
type TextAlign = 'left' | 'center' | 'right';
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;
const previewRef = React.useRef<HTMLDivElement>(null);
const [textAlign, setTextAlign] = React.useState<TextAlign>('left');
const [fontSize, setFontSize] = React.useState<'xs' | 'sm' | 'base'>('sm');
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');
link.download = `ascii-${font || 'export'}-${Date.now()}.png`;
link.href = dataUrl;
link.click();
toast.success('Exported as PNG!');
} catch (error) {
console.error('Failed to export PNG:', error);
toast.error('Failed to export PNG');
}
};
return (
<Card className={cn('relative', className)}>
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
<CardTitle>Preview</CardTitle>
{font && (
<span className="text-[10px] px-1.5 py-0.5 bg-primary/10 text-primary rounded font-mono">
{font}
</span>
)}
</div>
<div className="flex gap-1.5 flex-wrap">
{onCopy && (
<Button variant="outline" size="xs" onClick={onCopy}>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
)}
{onShare && (
<Button variant="outline" size="xs" onClick={onShare} title="Copy shareable URL">
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
)}
<Button variant="outline" size="xs" onClick={handleExportPNG} title="Export as PNG">
<ImageIcon className="h-3 w-3 mr-1" />
PNG
</Button>
{onDownload && (
<Button variant="outline" size="xs" onClick={onDownload}>
<Download className="h-3 w-3 mr-1" />
TXT
</Button>
)}
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Controls */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center border rounded-md p-0.5">
<button
onClick={() => setTextAlign('left')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'left' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align left"
>
<AlignLeft className="h-3 w-3" />
</button>
<button
onClick={() => setTextAlign('center')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'center' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align center"
>
<AlignCenter className="h-3 w-3" />
</button>
<button
onClick={() => setTextAlign('right')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'right' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align right"
>
<AlignRight className="h-3 w-3" />
</button>
</div>
<div className="flex items-center border rounded-md p-0.5">
{(['xs', 'sm', 'base'] as const).map((s) => (
<button
key={s}
onClick={() => setFontSize(s)}
className={cn(
'px-1.5 py-0.5 text-[10px] rounded transition-colors uppercase',
fontSize === s ? 'bg-accent' : 'hover:bg-accent/50'
)}
>
{s === 'base' ? 'md' : s}
</button>
))}
</div>
{!isLoading && text && (
<div className="flex gap-2 text-[10px] text-muted-foreground ml-auto">
<span>{lineCount} lines</span>
<span>{charCount} chars</span>
</div>
)}
</div>
<div
ref={previewRef}
className={cn(
'relative min-h-[200px] bg-muted/50 rounded-lg p-4 overflow-x-auto',
textAlign === 'center' && 'text-center',
textAlign === 'right' && 'text-right'
)}
>
{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'
)}>
{text}
</pre>
) : (
<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>
)}
</div>
</CardContent>
</Card>
);
}