Skip to content

Commit c18eafa

Browse files
authored
Diff & sql formatter (#3)
* Diff * Sql formatter
1 parent 0e0d10a commit c18eafa

File tree

11 files changed

+276
-17
lines changed

11 files changed

+276
-17
lines changed

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,21 @@
164164
"@teamsupercell/typings-for-css-modules-loader": "^2.4.0",
165165
"@testing-library/jest-dom": "^5.11.6",
166166
"@testing-library/react": "^11.2.2",
167+
"@types/diff": "^5.0.1",
167168
"@types/enzyme": "^3.10.5",
168169
"@types/enzyme-adapter-react-16": "^1.0.6",
169170
"@types/history": "4.7.6",
170171
"@types/jest": "^26.0.15",
171172
"@types/marked": "^2.0.4",
172173
"@types/node": "14.14.10",
174+
"@types/qrcode": "^1.4.1",
173175
"@types/react": "^16.9.44",
174176
"@types/react-dom": "^16.9.9",
175177
"@types/react-router-dom": "^5.1.6",
176178
"@types/react-test-renderer": "^16.9.3",
177179
"@types/webpack-env": "^1.15.2",
178180
"@typescript-eslint/eslint-plugin": "^4.8.1",
179181
"@typescript-eslint/parser": "^4.8.1",
180-
"@types/qrcode": "^1.4.1",
181182
"autoprefixer": "^10.3.1",
182183
"babel-eslint": "^10.1.0",
183184
"babel-jest": "^26.1.0",
@@ -250,6 +251,7 @@
250251
"@tailwindcss/typography": "^0.4.1",
251252
"caniuse-lite": "^1.0.30001246",
252253
"dayjs": "^1.10.6",
254+
"diff": "^5.0.0",
253255
"electron-debug": "^3.1.0",
254256
"electron-log": "^4.2.4",
255257
"electron-updater": "^4.3.4",
@@ -260,7 +262,8 @@
260262
"react-dom": "^17.0.1",
261263
"react-router-dom": "^5.2.0",
262264
"regenerator-runtime": "^0.13.5",
263-
"source-map-support": "^0.5.19"
265+
"source-map-support": "^0.5.19",
266+
"sql-formatter": "^4.0.2"
264267
},
265268
"devEngines": {
266269
"node": ">=10.x",

src/components/Main.tsx

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import React from 'react';
22
import { NavLink, Route } from 'react-router-dom';
33
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4-
import MarkdownToHtml from './md-to-html/MarkdownToHtml';
5-
import UnixTimestamp from './unix-timestamp/UnixTimestamp';
6-
import HtmlPreview from './html-preview/HtmlPreview';
7-
import QrCode from './qrcode/QrCode';
4+
import MarkdownToHtml from './markdown/MarkdownToHtml';
5+
import UnixTimestamp from './timestamp/UnixTimestamp';
6+
import HtmlPreview from './html/HtmlPreview';
7+
import QrCodeGenerator from './qrcode/QrCodeGenerator';
88
import Base64 from './base64/Base64';
9+
import DiffText from './diff/TextDiff';
10+
import SqlFormatter from './sql/SqlFormatter';
911

1012
const Main = () => {
1113
const routes = [
1214
{
1315
icon: <FontAwesomeIcon icon="clock" />,
14-
path: '/unix',
16+
path: '/unix-converter',
1517
name: 'Unix Time Converter',
1618
Component: UnixTimestamp,
1719
},
1820
{
1921
icon: <FontAwesomeIcon icon={['fab', 'markdown']} />,
20-
path: '/md-to-html',
22+
path: '/markdown-to-html',
2123
name: 'Markdown to HTML',
2224
Component: MarkdownToHtml,
2325
},
@@ -31,14 +33,26 @@ const Main = () => {
3133
icon: <FontAwesomeIcon icon="qrcode" />,
3234
path: '/qrcode-generator',
3335
name: 'QRCode Generator',
34-
Component: QrCode,
36+
Component: QrCodeGenerator,
3537
},
3638
{
3739
icon: <FontAwesomeIcon icon="code" />,
38-
path: '/base64',
40+
path: '/base64-encoder',
3941
name: 'Base64 Encoder',
4042
Component: Base64,
4143
},
44+
{
45+
icon: <FontAwesomeIcon icon="exchange-alt" />,
46+
path: '/text-diff',
47+
name: 'Text Diff',
48+
Component: DiffText,
49+
},
50+
{
51+
icon: <FontAwesomeIcon icon="database" />,
52+
path: '/sql-formatter',
53+
name: 'SQL Formatter',
54+
Component: SqlFormatter,
55+
},
4256
];
4357

4458
return (

src/components/diff/TextDiff.tsx

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* eslint-disable react/no-danger */
2+
import React, { useEffect, useState } from 'react';
3+
import { clipboard } from 'electron';
4+
import { Change } from 'diff';
5+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6+
import { camelToText } from '../../helpers/stringUtils';
7+
8+
const DiffLib = require('diff');
9+
10+
const TextDiff = () => {
11+
const [left, setLeft] = useState('Left text');
12+
const [right, setRight] = useState('Right text');
13+
const [diff, setDiff] = useState('');
14+
const [diffCount, setDiffCount] = useState(0);
15+
const [diffType, setDiffType] = useState('diffChars');
16+
17+
const handleChangeLeft = (evt: { target: { value: string } }) =>
18+
setLeft(evt.target.value);
19+
20+
const handleChangeRight = (evt: { target: { value: string } }) =>
21+
setRight(evt.target.value);
22+
23+
const handleClipboardLeft = () => {
24+
setLeft(clipboard.readText());
25+
};
26+
27+
const handleClipboardRight = () => {
28+
setRight(clipboard.readText());
29+
};
30+
31+
const handleSwap = () => {
32+
setRight(left);
33+
setLeft(right);
34+
};
35+
36+
const diffTypes = ['diffChars', 'diffWords', 'diffLines'];
37+
38+
useEffect(() => {
39+
const diffFunc = DiffLib[diffType];
40+
const changes = diffFunc(left, right);
41+
let c = 0;
42+
43+
const d = changes
44+
.map((part: Change) => {
45+
// eslint-disable-next-line no-nested-ternary
46+
const cl = part.added
47+
? 'text-green-600'
48+
: part.removed
49+
? 'text-red-600'
50+
: 'text-grey-600';
51+
c += part.added || part.removed ? 1 : 0;
52+
return `<span class="${cl}">${part.value}</span>`;
53+
})
54+
.join('');
55+
56+
setDiffCount(c);
57+
setDiff(d);
58+
}, [left, right, diffType]);
59+
60+
return (
61+
<div className="min-h-full flex flex-col">
62+
<div className="flex justify-between mb-1">
63+
<span className="flex space-x-2">
64+
<button type="button" className="btn" onClick={handleClipboardLeft}>
65+
Clipboard
66+
</button>
67+
</span>
68+
<span className="flex space-x-2">
69+
<button type="button" className="btn" onClick={handleSwap}>
70+
<FontAwesomeIcon icon="exchange-alt" />
71+
</button>
72+
</span>
73+
<span className="flex space-x-2">
74+
<button type="button" className="btn" onClick={handleClipboardRight}>
75+
Clipboard
76+
</button>
77+
</span>
78+
</div>
79+
<section className="flex flex-1 flex-col space-y-2 min-h-full">
80+
<div className="flex min-h-full flex-1 space-x-2">
81+
<textarea
82+
onChange={handleChangeLeft}
83+
className="flex-1 min-h-full bg-white p-4 rounded-md"
84+
value={left}
85+
/>
86+
<textarea
87+
onChange={handleChangeRight}
88+
className="flex-1 min-h-full bg-white p-4 rounded-md"
89+
value={right}
90+
/>
91+
</div>
92+
93+
<div className="flex flex-0 space-x-4 items-center justify-between">
94+
<div className="flex flex-0 space-x-4 items-center">
95+
{diffTypes.map((dt) => (
96+
<label
97+
htmlFor={dt}
98+
className="flex items-center space-x-1"
99+
key={dt}
100+
>
101+
<input
102+
type="radio"
103+
className="btn"
104+
name="diffType"
105+
id={dt}
106+
checked={diffType === dt}
107+
onChange={() => setDiffType(dt)}
108+
/>
109+
<p>{camelToText(dt)}</p>
110+
</label>
111+
))}
112+
</div>
113+
<p>
114+
{diffCount} change{diffCount === 1 ? '' : 's'}
115+
</p>
116+
</div>
117+
118+
<section
119+
className="w-full min-h-full bg-gray-100 p-4 flex-1 rounded-md"
120+
dangerouslySetInnerHTML={{ __html: diff }}
121+
/>
122+
</section>
123+
</div>
124+
);
125+
};
126+
127+
export default TextDiff;

src/components/html-preview/HtmlPreview.tsx renamed to src/components/html/HtmlPreview.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ const HtmlPreview = () => {
3838
Open...
3939
</button>
4040
</div>
41-
<div className="flex flex-1 min-h-full">
41+
<div className="flex flex-1 min-h-full space-x-2">
4242
<textarea
4343
onChange={handleChange}
4444
className="flex-1 min-h-full bg-white p-4 rounded-md"
4545
value={html}
4646
disabled={opening}
4747
/>
48-
<div className="mx-1" />
4948
<section
5049
className="flex-1 min-h-full bg-gray-100 p-4 prose rounded-md"
5150
dangerouslySetInnerHTML={{ __html: html }}

src/components/md-to-html/MarkdownToHtml.tsx renamed to src/components/markdown/MarkdownToHtml.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,13 @@ const Md2Html = () => {
6666
</button>
6767
</span>
6868
</div>
69-
<div className="flex min-h-full flex-1">
69+
<div className="flex min-h-full flex-1 space-x-2">
7070
<textarea
7171
onChange={handleChange}
7272
className="flex-1 min-h-full bg-white p-4 rounded-md"
7373
value={md}
7474
disabled={opening}
7575
/>
76-
<div className="mx-1" />
7776
{preview ? (
7877
<section
7978
className="flex-1 min-h-full bg-gray-100 p-4 prose w-full rounded-md"

src/components/qrcode/QrCode.tsx renamed to src/components/qrcode/QrCodeGenerator.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,12 @@ const HtmlPreview = () => {
7070
Save...
7171
</button>
7272
</div>
73-
<div className="flex flex-1 min-h-full">
73+
<div className="flex flex-1 min-h-full space-x-2">
7474
<textarea
7575
onChange={handleChange}
7676
className="flex-1 min-h-full bg-white p-4 rounded-md"
7777
value={content}
7878
/>
79-
<div className="mx-1" />
8079
<section className="flex-1 min-h-full flex items-center p-4 prose bg-gray-100 rounded-md">
8180
{qrCode && <img src={qrCode} alt={content} />}
8281
</section>

src/components/sql/SqlFormatter.tsx

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { format } from 'sql-formatter';
3+
import { ipcRenderer, clipboard } from 'electron';
4+
5+
const SqlFormatter = () => {
6+
const [input, setInput] = useState('SELECT * FROM tbl');
7+
const [output, setOutput] = useState('');
8+
const [opening, setOpening] = useState(false);
9+
const [copied, setCopied] = useState(false);
10+
11+
const handleChangeInput = (evt: { target: { value: string } }) =>
12+
setInput(evt.target.value);
13+
14+
const handleOpenInput = async () => {
15+
setOpening(true);
16+
const content = await ipcRenderer.invoke('open-file', []);
17+
setInput(Buffer.from(content).toString());
18+
setOpening(false);
19+
};
20+
21+
const handleClipboardInput = () => {
22+
setInput(clipboard.readText());
23+
};
24+
25+
const handleCopyOutput = () => {
26+
setCopied(true);
27+
clipboard.write({ text: output });
28+
setTimeout(() => setCopied(false), 500);
29+
};
30+
31+
useEffect(() => {
32+
setOutput(format(input));
33+
}, [input]);
34+
35+
return (
36+
<div className="min-h-full flex flex-col">
37+
<div className="flex justify-between mb-1">
38+
<span className="flex space-x-2">
39+
<button type="button" className="btn" onClick={handleClipboardInput}>
40+
Clipboard
41+
</button>
42+
<button
43+
type="button"
44+
className="btn"
45+
onClick={handleOpenInput}
46+
disabled={opening}
47+
>
48+
Open...
49+
</button>
50+
</span>
51+
<span className="flex space-x-4">
52+
<button
53+
type="button"
54+
className="btn"
55+
onClick={handleCopyOutput}
56+
disabled={copied}
57+
>
58+
{copied ? 'Copied' : 'Copy'}
59+
</button>
60+
</span>
61+
</div>
62+
<div className="flex min-h-full flex-1">
63+
<textarea
64+
onChange={handleChangeInput}
65+
className="flex-1 min-h-full bg-white p-4 rounded-md"
66+
value={input}
67+
disabled={opening}
68+
/>
69+
<div className="mx-1" />
70+
<textarea
71+
className="flex-1 min-h-full bg-gray-100 p-4 rounded-md"
72+
value={output}
73+
readOnly
74+
/>
75+
</div>
76+
</div>
77+
);
78+
};
79+
80+
export default SqlFormatter;

src/helpers/fontAwesome.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ import {
44
faClock,
55
faCode,
66
faCopy,
7+
faDatabase,
8+
faExchangeAlt,
79
faQrcode,
810
} from '@fortawesome/free-solid-svg-icons';
911

10-
library.add(fab, faMarkdown, faClock, faHtml5, faQrcode, faCopy, faCode);
12+
library.add(
13+
fab,
14+
faMarkdown,
15+
faClock,
16+
faHtml5,
17+
faQrcode,
18+
faCopy,
19+
faCode,
20+
faExchangeAlt,
21+
faDatabase
22+
);

src/helpers/stringUtils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const camelToText = (camel: string) =>
2+
camel
3+
.replace(/([A-Z])/g, ' $1')
4+
.toLowerCase()
5+
.replace(/^./, (str) => {
6+
return str.toUpperCase();
7+
});
8+
9+
export default { camelToText };

0 commit comments

Comments
 (0)