mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat(renderer): AsciiGeneratorDialog (figlet text-to-ASCII-art)
This commit is contained in:
Generated
+33
@@ -47,6 +47,7 @@
|
||||
"dompurify": "^3.3.1",
|
||||
"electron-store": "^10.1.0",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"figlet": "^1.11.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2pdf.js": "^0.14.0",
|
||||
"immer": "^11.1.8",
|
||||
@@ -76,6 +77,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -5737,6 +5739,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/figlet": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz",
|
||||
"integrity": "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
@@ -10241,6 +10250,30 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/figlet": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.11.0.tgz",
|
||||
"integrity": "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"figlet": "bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/figlet/node_modules/commander": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -118,6 +119,7 @@
|
||||
"dompurify": "^3.3.1",
|
||||
"electron-store": "^10.1.0",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"figlet": "^1.11.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2pdf.js": "^0.14.0",
|
||||
"immer": "^11.1.8",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useAppStore } from '@/stores/app-store';
|
||||
import { toast } from '@/lib/toast';
|
||||
import { figletText, FIGLET_FONTS, type FigletFont } from '@/lib/figlet';
|
||||
|
||||
export function AsciiGeneratorDialog() {
|
||||
const closeModal = useAppStore((s) => s.closeModal);
|
||||
const [text, setText] = useState('Hello');
|
||||
const [font, setFont] = useState<FigletFont>('Standard');
|
||||
const [output, setOutput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
figletText(text || ' ', font)
|
||||
.then((result) => { if (!cancelled) setOutput(result); })
|
||||
.catch(() => { if (!cancelled) setOutput('(render error)'); });
|
||||
return () => { cancelled = true; };
|
||||
}, [text, font]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(output);
|
||||
toast.success('Copied to clipboard');
|
||||
} catch {
|
||||
toast.error('Failed to copy');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="ascii-desc" className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>ASCII generator</DialogTitle>
|
||||
<DialogDescription id="ascii-desc">Type text, pick a font, see ASCII art</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="ascii-text">Text</Label>
|
||||
<Textarea
|
||||
id="ascii-text"
|
||||
aria-label="Text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ascii-font">Font</Label>
|
||||
<Select value={font} onValueChange={(v) => setFont(v as FigletFont)}>
|
||||
<SelectTrigger id="ascii-font" aria-label="Font"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{FIGLET_FONTS.map((f) => (
|
||||
<SelectItem key={f} value={f}>{f}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Output</Label>
|
||||
<pre className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs" data-testid="ascii-output">
|
||||
{output}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={closeModal}>Close</Button>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import figlet from 'figlet';
|
||||
import type { Fonts } from 'figlet';
|
||||
|
||||
export const FIGLET_FONTS = ['Standard', 'Big', 'Small', 'Banner', 'Doom', 'Slant', 'Block'] as const;
|
||||
export type FigletFont = typeof FIGLET_FONTS[number];
|
||||
|
||||
export function figletText(text: string, font: FigletFont): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
figlet.text(text, { font }, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result ?? '');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { AsciiGeneratorDialog } from '@/components/modals/AsciiGeneratorDialog';
|
||||
|
||||
vi.mock('@/lib/figlet', () => ({
|
||||
figletText: vi.fn().mockResolvedValue(' MOCKED ASCII '),
|
||||
FIGLET_FONTS: ['Standard', 'Big', 'Small', 'Banner', 'Doom', 'Slant', 'Block'],
|
||||
}));
|
||||
|
||||
describe('AsciiGeneratorDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders with an input textarea and font selector', () => {
|
||||
render(<AsciiGeneratorDialog />);
|
||||
expect(screen.getByText(/ascii generator/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox', { name: /font/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /text/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the ASCII output as the user types', async () => {
|
||||
render(<AsciiGeneratorDialog />);
|
||||
const input = screen.getByRole('textbox', { name: /text/i });
|
||||
await userEvent.type(input, 'hi');
|
||||
const pre = await screen.findByTestId('ascii-output');
|
||||
expect(pre).toHaveTextContent('MOCKED ASCII');
|
||||
});
|
||||
|
||||
it('shows a copy button next to the output', () => {
|
||||
render(<AsciiGeneratorDialog />);
|
||||
expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user