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",
|
"dompurify": "^3.3.1",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"ffmpeg-static": "^5.3.0",
|
"ffmpeg-static": "^5.3.0",
|
||||||
|
"figlet": "^1.11.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html2pdf.js": "^0.14.0",
|
"html2pdf.js": "^0.14.0",
|
||||||
"immer": "^11.1.8",
|
"immer": "^11.1.8",
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/figlet": "^1.7.0",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/react": "^19.2.16",
|
"@types/react": "^19.2.16",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -5737,6 +5739,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/fs-extra": {
|
||||||
"version": "9.0.13",
|
"version": "9.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||||
@@ -10241,6 +10250,30 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"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/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/figlet": "^1.7.0",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/react": "^19.2.16",
|
"@types/react": "^19.2.16",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"ffmpeg-static": "^5.3.0",
|
"ffmpeg-static": "^5.3.0",
|
||||||
|
"figlet": "^1.11.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html2pdf.js": "^0.14.0",
|
"html2pdf.js": "^0.14.0",
|
||||||
"immer": "^11.1.8",
|
"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