recreate project by using https://github.com/shadcn/next-template

App:
- support dark mode
- add toggle theme button
- add clear search button
- add search button
- add current page indicator
- add tool group pages
- add settings tool
- add 1 tab format option to Json format tool
- add paste button to some tools
- add file button to some tools
- add copy button to some tools
- add clear button to some tools
- change favicon
- change search hit rate
- change each page title
- change icons from Material Icons to Lucide
- change sidebar scroll area
- change editor from Ace to Monaco
- change parsable separators of number base converter
- change default value of format option of number base converter
- change default values of some tool forms
- change some styles
- remove disabled tools
- remove real-time search
- fix uri encoding tool

Dev:
- MUI + Emotion -> Radix UI + Tailwind CSS
- Next.js 12 Pages -> Next.js 13 App Router
- React 17 -> React 18
- many other packages upgraded
- use useState instead of recoil
- use Next.js typedRoutes instead of pathpida
- clean npm scripts
- format import statements by Prettier
- no component separations between container and presenter
- effective component memoizations
- add vscode settings
- many refactors
This commit is contained in:
rusconn
2023-05-14 12:44:34 +00:00
parent a7001213f8
commit f1c3bd2971
167 changed files with 5931 additions and 3853 deletions

BIN
app/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.converters.tools.jsonYaml.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,174 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import YAML from "yaml";
import { toolGroups } from "@/config/tools";
import { Editor, EditorProps } from "@/components/ui/editor";
import {
Select,
SelectContent,
SelectItem,
SelectProps,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { ControlMenu } from "@/components/control-menu";
import { icons } from "@/components/icons";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
const two = " ";
const four = " ";
export default function Page() {
const [indentation, setIndentation] = useState(two);
const [json, setJson] = useState('{\n "foo": "bar"\n}');
const [yaml, setYaml] = useState("foo: bar");
const setJsonReactively = useCallback(
(text: string) => {
setJson(text);
try {
const parsed = JSON.parse(text) as unknown;
setYaml(YAML.stringify(parsed, { indent: indentation.length, simpleKeys: true }));
} catch {
setYaml("");
}
},
[indentation.length]
);
const setYamlReactively = useCallback(
(text: string) => {
setYaml(text);
try {
const parsed = YAML.parse(text, { merge: true }) as unknown;
setJson(JSON.stringify(parsed, null, indentation));
} catch {
setJson("");
}
},
[indentation]
);
const clearBoth = useCallback(() => {
setJson("");
setYaml("");
}, []);
const onIndentationChange: SelectProps["onValueChange"] = value => {
setIndentation(value);
try {
const parsed = JSON.parse(json) as unknown;
setJson(JSON.stringify(parsed, null, value));
setYaml(YAML.stringify(parsed, { indent: value.length, simpleKeys: true }));
} catch {
clearBoth();
}
};
const onJsonChange: EditorProps["onChange"] = value => setJsonReactively(value ?? "");
const onYamlChange: EditorProps["onChange"] = value => setYamlReactively(value ?? "");
const indentationIcon = useMemo(() => <icons.Space size={24} className="-translate-y-1.5" />, []);
const indentationConfig = (
<Configuration
icon={indentationIcon}
title="Indentation"
control={
<Select value={indentation} onValueChange={onIndentationChange}>
<SelectTrigger
className="w-28"
aria-label="toggle open/close state of indentation selection"
>
<SelectValue placeholder={indentation} />
</SelectTrigger>
<SelectContent>
<SelectItem value={two}>2 spaces</SelectItem>
<SelectItem value={four}>4 spaces</SelectItem>
</SelectContent>
</Select>
}
/>
);
const jsonPasteButton = useMemo(
() => <PasteButton onClipboardRead={setJsonReactively} />,
[setJsonReactively]
);
const yamlPasteButton = useMemo(
() => <PasteButton onClipboardRead={setYamlReactively} />,
[setYamlReactively]
);
const jsonFileButton = useMemo(
() => (
<FileButton
accept=".json"
onFileRead={setJsonReactively}
iconOnly
aria-label="load a json file"
/>
),
[setJsonReactively]
);
const yamlFileButton = useMemo(
() => (
<FileButton
accept=".yml,.yaml"
onFileRead={setYamlReactively}
iconOnly
aria-label="load a yaml file"
/>
),
[setYamlReactively]
);
const jsonCopyButton = <CopyButton text={json} />;
const yamlCopyButton = <CopyButton text={yaml} />;
const clearButton = useMemo(
() => <ClearButton onClick={clearBoth} iconOnly aria-label="clear json and yaml" />,
[clearBoth]
);
const jsonControl = (
<ControlMenu list={[jsonPasteButton, jsonFileButton, jsonCopyButton, clearButton]} />
);
const yamlControl = (
<ControlMenu list={[yamlPasteButton, yamlFileButton, yamlCopyButton, clearButton]} />
);
return (
<PageRootSection
className="flex h-full flex-col"
title={toolGroups.converters.tools.jsonYaml.longTitle}
>
<PageSection className="mb-6 mt-0" title="Configuration">
<Configurations list={[indentationConfig]} />
</PageSection>
<div className="flex flex-1 flex-col gap-x-4 gap-y-5 lg:flex-row">
<PageSection className="mt-0 min-h-[200px] flex-1" title="Json" control={jsonControl}>
<Editor language="json" value={json} onChange={onJsonChange} />
</PageSection>
<PageSection className="mt-0 min-h-[200px] flex-1" title="Yaml" control={yamlControl}>
<Editor language="yaml" value={yaml} onChange={onYamlChange} />
</PageSection>
</div>
</PageRootSection>
);
}

11
app/converters/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.converters.title,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.converters.tools.numberBase.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,111 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { toolGroups } from "@/config/tools";
import * as baselib from "@/lib/base";
import { Input, InputProps } from "@/components/ui/input";
import { PasteButton } from "@/components/buttons/paste";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { ControlMenu } from "@/components/control-menu";
import { icons } from "@/components/icons";
import { LabeledSwitch } from "@/components/labeled-switch";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
const baseConfig = {
10: { prefix: "", validate: baselib.isDecimal },
16: { prefix: "0x", validate: baselib.isHexadecimal },
8: { prefix: "0o", validate: baselib.isOctal },
2: { prefix: "0b", validate: baselib.isBinary },
} as const;
export default function Page() {
const [format, setFormat] = useState(true);
const [int, setInt] = useState<bigint | undefined>(BigInt(42));
const newDec = int?.toString(10) ?? "";
const newHex = int?.toString(16).toUpperCase() ?? "";
const newOct = int?.toString(8) ?? "";
const newBin = int?.toString(2) ?? "";
const dec = format ? baselib.formatDecimal(newDec) : newDec;
const hex = format ? baselib.formatHexadecimal(newHex) : newHex;
const oct = format ? baselib.formatOctal(newOct) : newOct;
const bin = format ? baselib.formatBinary(newBin) : newBin;
const trySetInt = (base: 10 | 16 | 8 | 2) => (value: string) => {
if (value === "") {
return setInt(undefined);
}
const { prefix, validate } = baseConfig[base];
const unformatted = baselib.unformatNumber(value);
if (validate(unformatted)) {
setInt(BigInt(`${prefix}${unformatted}`));
}
};
const trySetDec = useCallback((value: string) => trySetInt(10)(value), []);
const trySetHex = useCallback((value: string) => trySetInt(16)(value), []);
const trySetOct = useCallback((value: string) => trySetInt(8)(value), []);
const trySetBin = useCallback((value: string) => trySetInt(2)(value), []);
const onDecChange: InputProps["onChange"] = ({ currentTarget: { value } }) => trySetDec(value);
const onHexChange: InputProps["onChange"] = ({ currentTarget: { value } }) => trySetHex(value);
const onOctChange: InputProps["onChange"] = ({ currentTarget: { value } }) => trySetOct(value);
const onBinChange: InputProps["onChange"] = ({ currentTarget: { value } }) => trySetBin(value);
const formatNumberIcon = useMemo(() => <icons.CaseSensitive size={24} />, []);
const formatNumberConfig = useMemo(
() => (
<Configuration
icon={formatNumberIcon}
title="Format number"
control={
<LabeledSwitch
id="format-number-switch"
label={format ? "On" : "Off"}
checked={format}
onCheckedChange={setFormat}
aria-label="toggle whether to format numbers"
/>
}
/>
),
[format, formatNumberIcon]
);
const decPasteButton = useMemo(() => <PasteButton onClipboardRead={trySetDec} />, [trySetDec]);
const hexPasteButton = useMemo(() => <PasteButton onClipboardRead={trySetHex} />, [trySetHex]);
const octPasteButton = useMemo(() => <PasteButton onClipboardRead={trySetOct} />, [trySetOct]);
const binPasteButton = useMemo(() => <PasteButton onClipboardRead={trySetBin} />, [trySetBin]);
const decControl = <ControlMenu list={[decPasteButton]} />;
const hexControl = <ControlMenu list={[hexPasteButton]} />;
const octControl = <ControlMenu list={[octPasteButton]} />;
const binControl = <ControlMenu list={[binPasteButton]} />;
return (
<PageRootSection title={toolGroups.converters.tools.numberBase.longTitle}>
<PageSection className="mb-6" title="Configuration">
<Configurations list={[formatNumberConfig]} />
</PageSection>
<PageSection title="Decimal" control={decControl}>
<Input value={dec} onChange={onDecChange} />
</PageSection>
<PageSection title="Hexadecimal" control={hexControl}>
<Input value={hex} onChange={onHexChange} />
</PageSection>
<PageSection title="Octal" control={octControl}>
<Input value={oct} onChange={onOctChange} />
</PageSection>
<PageSection title="Binary" control={binControl}>
<Input value={bin} onChange={onBinChange} />
</PageSection>
</PageRootSection>
);
}

11
app/converters/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { toolGroups } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
return (
<PageRootSection title={toolGroups.converters.title}>
<ToolCards tools={Object.values(toolGroups.converters.tools)} />
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.encodersDecoders.tools.base64.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,98 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { decode, encode, isValid } from "js-base64";
import { toolGroups } from "@/config/tools";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { ControlMenu } from "@/components/control-menu";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const [decoded, setDecoded] = useState("😀😂🤣");
const [encoded, setEncoded] = useState("8J+YgPCfmILwn6Sj");
const setDecodedReactively = useCallback((text: string) => {
setDecoded(text);
setEncoded(encode(text));
}, []);
const setEncodedReactively = useCallback((text: string) => {
setEncoded(text);
const newDecoded = decode(text);
if (isValid(text) && !newDecoded.includes("<22>")) {
setDecoded(newDecoded);
} else {
setDecoded("");
}
}, []);
const clearBoth = useCallback(() => {
setDecoded("");
setEncoded("");
}, []);
const onDecodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setDecodedReactively(value);
const onEncodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setEncodedReactively(value);
const decodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setDecodedReactively} />,
[setDecodedReactively]
);
const encodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setEncodedReactively} />,
[setEncodedReactively]
);
const decodedFileButton = useMemo(
() => (
<FileButton onFileRead={setDecodedReactively} iconOnly aria-label="load a decoded file" />
),
[setDecodedReactively]
);
const encodedFileButton = useMemo(
() => (
<FileButton onFileRead={setEncodedReactively} iconOnly aria-label="load a encoded file" />
),
[setEncodedReactively]
);
const decodedCopyButton = useMemo(() => <CopyButton text={decoded} />, [decoded]);
const encodedCopyButton = useMemo(() => <CopyButton text={encoded} />, [encoded]);
const clearButton = useMemo(
() => <ClearButton onClick={clearBoth} iconOnly aria-label="clear decoded and encoded" />,
[clearBoth]
);
const decodedControl = (
<ControlMenu list={[decodedPasteButton, decodedFileButton, decodedCopyButton, clearButton]} />
);
const encodedControl = (
<ControlMenu list={[encodedPasteButton, encodedFileButton, encodedCopyButton, clearButton]} />
);
return (
<PageRootSection title={toolGroups.encodersDecoders.tools.base64.longTitle}>
<PageSection title="Decoded" control={decodedControl}>
<Textarea value={decoded} onChange={onDecodedChange} rows={10} />
</PageSection>
<PageSection title="Encoded" control={encodedControl}>
<Textarea value={encoded} onChange={onEncodedChange} rows={10} />
</PageSection>
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.encodersDecoders.tools.html.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,91 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { decode, encode } from "html-entities";
import { toolGroups } from "@/config/tools";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { ControlMenu } from "@/components/control-menu";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const [decoded, setDecoded] = useState('> It\'s "HTML escaping".');
const [encoded, setEncoded] = useState("&gt; It&apos;s &quot;HTML escaping&quot;.");
const setDecodedReactively = useCallback((text: string) => {
setDecoded(text);
setEncoded(encode(text));
}, []);
const setEncodedReactively = useCallback((text: string) => {
setEncoded(text);
setDecoded(decode(text));
}, []);
const clearBoth = useCallback(() => {
setDecoded("");
setEncoded("");
}, []);
const onDecodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setDecodedReactively(value);
const onEncodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setEncodedReactively(value);
const decodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setDecodedReactively} />,
[setDecodedReactively]
);
const encodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setEncodedReactively} />,
[setEncodedReactively]
);
const decodedFileButton = useMemo(
() => (
<FileButton onFileRead={setDecodedReactively} iconOnly aria-label="load a decoded file" />
),
[setDecodedReactively]
);
const encodedFileButton = useMemo(
() => (
<FileButton onFileRead={setEncodedReactively} iconOnly aria-label="load a encoded file" />
),
[setEncodedReactively]
);
const decodedCopyButton = useMemo(() => <CopyButton text={decoded} />, [decoded]);
const encodedCopyButton = useMemo(() => <CopyButton text={encoded} />, [encoded]);
const clearButton = useMemo(
() => <ClearButton onClick={clearBoth} iconOnly aria-label="clear decoded and encoded" />,
[clearBoth]
);
const decodedControl = (
<ControlMenu list={[decodedPasteButton, decodedFileButton, decodedCopyButton, clearButton]} />
);
const encodedControl = (
<ControlMenu list={[encodedPasteButton, encodedFileButton, encodedCopyButton, clearButton]} />
);
return (
<PageRootSection title={toolGroups.encodersDecoders.tools.html.longTitle}>
<PageSection title="Decoded" control={decodedControl}>
<Textarea value={decoded} onChange={onDecodedChange} rows={10} />
</PageSection>
<PageSection title="Encoded" control={encodedControl}>
<Textarea value={encoded} onChange={onEncodedChange} rows={10} />
</PageSection>
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.encodersDecoders.tools.jwt.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,65 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { toolGroups } from "@/config/tools";
import { decode } from "@/lib/jwt";
import { Editor } from "@/components/ui/editor";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { ControlMenu } from "@/components/control-menu";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const [jwt, setJwt] = useState(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
);
const { headerObj, payloadObj } = decode(jwt);
const header = JSON.stringify(headerObj, null, 2) ?? "";
const payload = JSON.stringify(payloadObj, null, 2) ?? "";
const clearJwt = useCallback(() => setJwt(""), []);
const onJwtChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) => setJwt(value);
const jwtTokenPasteButton = useMemo(() => <PasteButton onClipboardRead={setJwt} />, [setJwt]);
const jwtTokenFileButton = useMemo(
() => <FileButton onFileRead={setJwt} iconOnly aria-label="load a token file" />,
[setJwt]
);
const jwtTokenClearButton = useMemo(
() => <ClearButton onClick={clearJwt} iconOnly aria-label="clear token" />,
[clearJwt]
);
const heaederCopyButton = useMemo(() => <CopyButton text={header} />, [header]);
const payloadCopyButton = useMemo(() => <CopyButton text={payload} />, [payload]);
const jwtTokenControl = (
<ControlMenu list={[jwtTokenPasteButton, jwtTokenFileButton, jwtTokenClearButton]} />
);
const heaederControl = <ControlMenu list={[heaederCopyButton]} />;
const payloadControl = <ControlMenu list={[payloadCopyButton]} />;
return (
<PageRootSection title={toolGroups.encodersDecoders.tools.jwt.longTitle}>
<PageSection title="Jwt Token" control={jwtTokenControl}>
<Textarea value={jwt} onChange={onJwtChange} rows={3} />
</PageSection>
<PageSection title="Header" control={heaederControl}>
<Editor height={180} language="json" value={header} options={{ readOnly: true }} />
</PageSection>
<PageSection title="Payload" control={payloadControl}>
<Editor height={180} language="json" value={payload} options={{ readOnly: true }} />
</PageSection>
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.encodersDecoders.title,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,11 @@
import { toolGroups } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
return (
<PageRootSection title={toolGroups.encodersDecoders.title}>
<ToolCards tools={Object.values(toolGroups.encodersDecoders.tools)} />
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.encodersDecoders.tools.url.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,100 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { toolGroups } from "@/config/tools";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { ControlMenu } from "@/components/control-menu";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const [decoded, setDecoded] = useState('> It\'s "URL encoding"?');
const [encoded, setEncoded] = useState("%3E%20It's%20%22URL%20encoding%22%3F");
const setDecodedReactively = useCallback((text: string) => {
setDecoded(text);
try {
setEncoded(encodeURIComponent(text));
} catch {
setEncoded("");
}
}, []);
const setEncodedReactively = useCallback((text: string) => {
setEncoded(text);
try {
setDecoded(decodeURIComponent(text));
} catch {
setDecoded("");
}
}, []);
const clearBoth = useCallback(() => {
setDecoded("");
setEncoded("");
}, []);
const onDecodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setDecodedReactively(value);
const onEncodedChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setEncodedReactively(value);
const decodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setDecodedReactively} />,
[setDecodedReactively]
);
const encodedPasteButton = useMemo(
() => <PasteButton onClipboardRead={setEncodedReactively} />,
[setEncodedReactively]
);
const decodedFileButton = useMemo(
() => (
<FileButton onFileRead={setDecodedReactively} iconOnly aria-label="load a decoded file" />
),
[setDecodedReactively]
);
const encodedFileButton = useMemo(
() => (
<FileButton onFileRead={setEncodedReactively} iconOnly aria-label="load a encoded file" />
),
[setEncodedReactively]
);
const decodedCopyButton = useMemo(() => <CopyButton text={decoded} />, [decoded]);
const encodedCopyButton = useMemo(() => <CopyButton text={encoded} />, [encoded]);
const clearButton = useMemo(
() => <ClearButton onClick={clearBoth} iconOnly aria-label="clear decoded and encoded" />,
[clearBoth]
);
const decodedControl = (
<ControlMenu list={[decodedPasteButton, decodedFileButton, decodedCopyButton, clearButton]} />
);
const encodedControl = (
<ControlMenu list={[encodedPasteButton, encodedFileButton, encodedCopyButton, clearButton]} />
);
return (
<PageRootSection title={toolGroups.encodersDecoders.tools.url.longTitle}>
<PageSection title="Decoded" control={decodedControl}>
<Textarea value={decoded} onChange={onDecodedChange} rows={10} />
</PageSection>
<PageSection title="Encoded" control={encodedControl}>
<Textarea value={encoded} onChange={onEncodedChange} rows={10} />
</PageSection>
</PageRootSection>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.formatters.tools.json.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,111 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { toolGroups } from "@/config/tools";
import { Editor, EditorProps } from "@/components/ui/editor";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { ControlMenu } from "@/components/control-menu";
import { icons } from "@/components/icons";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
const two = " ";
const four = " ";
const zero = "";
const tab = "\t";
export default function Page() {
const [indentation, setIndentation] = useState(two);
const [input, setInput] = useState('{\n"foo":"bar"\n}');
let output: string;
try {
const parsed = JSON.parse(input) as unknown;
output = JSON.stringify(parsed, null, indentation);
} catch {
output = "";
}
const clearInput = useCallback(() => setInput(""), []);
const onJsonChange: EditorProps["onChange"] = value => setInput(value ?? "");
const indentationIcon = useMemo(() => <icons.Space size={24} className="-translate-y-1.5" />, []);
const indentationConfig = useMemo(
() => (
<Configuration
icon={indentationIcon}
title="Indentation"
control={
<Select value={indentation} onValueChange={setIndentation}>
<SelectTrigger
className="w-28"
aria-label="toggle open/close state of indentation selection"
>
<SelectValue placeholder={indentation} />
</SelectTrigger>
<SelectContent>
<SelectItem value={two}>2 spaces</SelectItem>
<SelectItem value={four}>4 spaces</SelectItem>
<SelectItem value={tab}>1 tab</SelectItem>
<SelectItem value={zero}>minified</SelectItem>
</SelectContent>
</Select>
}
/>
),
[indentation, indentationIcon]
);
const inputPasteButton = useMemo(() => <PasteButton onClipboardRead={setInput} />, []);
const inputFileButton = useMemo(
() => (
<FileButton accept=".json" onFileRead={setInput} iconOnly aria-label="load a json file" />
),
[]
);
const inputClearButton = useMemo(
() => <ClearButton onClick={clearInput} iconOnly aria-label="clear json" />,
[clearInput]
);
const outputCopyButton = useMemo(() => <CopyButton text={output} />, [output]);
const inputControl = <ControlMenu list={[inputPasteButton, inputFileButton, inputClearButton]} />;
const outputControl = <ControlMenu list={[outputCopyButton]} />;
return (
<PageRootSection
className="flex h-full flex-col"
title={toolGroups.formatters.tools.json.longTitle}
>
<PageSection className="mt-0" title="Configuration">
<Configurations list={[indentationConfig]} />
</PageSection>
<div className="flex flex-1 flex-col gap-x-4 gap-y-5 lg:flex-row">
<PageSection className="min-h-[200px] flex-1" title="Input" control={inputControl}>
<Editor language="json" value={input} onChange={onJsonChange} />
</PageSection>
<PageSection className="min-h-[200px] flex-1" title="Output" control={outputControl}>
<Editor language="json" value={output} options={{ readOnly: true }} />
</PageSection>
</div>
</PageRootSection>
);
}

11
app/formatters/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.formatters.title,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

11
app/formatters/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { toolGroups } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
return (
<PageRootSection title={toolGroups.formatters.title}>
<ToolCards tools={Object.values(toolGroups.formatters.tools)} />
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.generators.tools.hash.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,129 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import createHash from "create-hash";
import { toolGroups } from "@/config/tools";
import { Input } from "@/components/ui/input";
import { Textarea, TextareaProps } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { FileButton } from "@/components/buttons/file";
import { PasteButton } from "@/components/buttons/paste";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { ControlMenu } from "@/components/control-menu";
import { icons } from "@/components/icons";
import { LabeledSwitch } from "@/components/labeled-switch";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const [uppercase, setUppercase] = useState(false);
const [input, setInput] = useState("Hello there !");
const newMd5 = createHash("md5").update(input).digest("hex");
const newSha1 = createHash("sha1").update(input).digest("hex");
const newSha256 = createHash("sha256").update(input).digest("hex");
const newSha512 = createHash("sha512").update(input).digest("hex");
const md5 = uppercase ? newMd5.toUpperCase() : newMd5;
const sha1 = uppercase ? newSha1.toUpperCase() : newSha1;
const sha256 = uppercase ? newSha256.toUpperCase() : newSha256;
const sha512 = uppercase ? newSha512.toUpperCase() : newSha512;
const clearInput = useCallback(() => setInput(""), []);
const onInputChange: TextareaProps["onChange"] = ({ currentTarget: { value } }) =>
setInput(value);
const uppercaseIcon = useMemo(() => <icons.CaseSensitive size={24} />, []);
const uppercaseConfig = useMemo(
() => (
<Configuration
icon={uppercaseIcon}
title="Uppercase"
control={
<LabeledSwitch
id="uppercase-switch"
label={uppercase ? "On" : "Off"}
checked={uppercase}
onCheckedChange={setUppercase}
aria-label="toggle whether to generate in uppercase"
/>
}
/>
),
[uppercase, uppercaseIcon]
);
const inputPasteButton = useMemo(() => <PasteButton onClipboardRead={setInput} />, []);
const inputFileButton = useMemo(
() => <FileButton onFileRead={setInput} iconOnly aria-label="load a file" />,
[]
);
const inputClearButton = useMemo(
() => <ClearButton onClick={clearInput} iconOnly aria-label="clear input" />,
[clearInput]
);
const inputControl = <ControlMenu list={[inputPasteButton, inputFileButton, inputClearButton]} />;
const md5CopyButton = useMemo(
() => <CopyButton text={md5} iconOnly aria-label="copy generated md5" />,
[md5]
);
const sha1CopyButton = useMemo(
() => <CopyButton text={sha1} iconOnly aria-label="copy generated sha1" />,
[sha1]
);
const sha256CopyButton = useMemo(
() => <CopyButton text={sha256} iconOnly aria-label="copy generated sha256" />,
[sha256]
);
const sha512CopyButton = useMemo(
() => <CopyButton text={sha512} iconOnly aria-label="copy generated sha512" />,
[sha512]
);
return (
<PageRootSection title={toolGroups.generators.tools.hash.longTitle}>
<PageSection title="Configuration">
<Configurations list={[uppercaseConfig]} />
</PageSection>
<PageSection className="my-4" title="Input" control={inputControl}>
<Textarea value={input} onChange={onInputChange} rows={5} />
</PageSection>
<PageSection title="MD5">
<div className="flex gap-2">
<Input className="flex-1" value={md5} readOnly />
<ControlMenu list={[md5CopyButton]} />
</div>
</PageSection>
<PageSection title="SHA1">
<div className="flex gap-2">
<Input className="flex-1" value={sha1} readOnly />
<ControlMenu list={[sha1CopyButton]} />
</div>
</PageSection>
<PageSection title="SHA256">
<div className="flex gap-2">
<Input className="flex-1" value={sha256} readOnly />
<ControlMenu list={[sha256CopyButton]} />
</div>
</PageSection>
<PageSection title="SHA512">
<div className="flex gap-2">
<Input className="flex-1" value={sha512} readOnly />
<ControlMenu list={[sha512CopyButton]} />
</div>
</PageSection>
</PageRootSection>
);
}

11
app/generators/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.generators.title,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

11
app/generators/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { toolGroups } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
return (
<PageRootSection title={toolGroups.generators.title}>
<ToolCards tools={Object.values(toolGroups.generators.tools)} />
</PageRootSection>
);
}

View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { toolGroups } from "@/config/tools";
export const metadata: Metadata = {
title: toolGroups.generators.tools.uuid.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -0,0 +1,171 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { range } from "fp-ts/NonEmptyArray";
import * as t from "io-ts";
import { toolGroups } from "@/config/tools";
import { uuid } from "@/lib/uuid";
import { useScrollFollow } from "@/hooks/useScrollFollow";
import { Button } from "@/components/ui/button";
import { Input, InputProps } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectProps,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ClearButton } from "@/components/buttons/clear";
import { CopyButton } from "@/components/buttons/copy";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { ControlMenu } from "@/components/control-menu";
import { icons } from "@/components/icons";
import { LabeledSwitch } from "@/components/labeled-switch";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
const v1 = "1";
const v4 = "4";
const uuidVersions = t.keyof({ [v1]: null, [v4]: null });
type UuidVersion = t.TypeOf<typeof uuidVersions>;
export default function Page() {
const [hyphens, setHyphens] = useState(true);
const [uppercase, setUppercase] = useState(false);
const [uuidVersion, setUuidVersion] = useState<UuidVersion>("4");
const [generates, setGenerates] = useState(1);
const [uuids, setUuids] = useState<string[]>([]);
const ref = useScrollFollow<HTMLTextAreaElement>([uuids]);
const uuidsString = uuids.join("\n");
const clearUuids = useCallback(() => setUuids([]), []);
const onUuidVersionChange: SelectProps["onValueChange"] = value => {
if (uuidVersions.is(value)) {
setUuidVersion(value);
}
};
const onGeneratesChange: InputProps["onChange"] = ({ currentTarget: { value } }) => {
const newGenerates = Number(value);
if (newGenerates >= 1 && newGenerates <= 1000) {
setGenerates(newGenerates);
}
};
const onGenerateClick = () => {
const newUuids = range(1, generates).map(_ => uuid(uuidVersion, hyphens, uppercase));
setUuids([...uuids, ...newUuids]);
};
const hyphensConfig = useMemo(
() => (
<Configuration
icon={<icons.Minus size={24} />}
title="Hyphens"
control={
<LabeledSwitch
id="hyphens-switch"
label={hyphens ? "On" : "Off"}
checked={hyphens}
onCheckedChange={setHyphens}
aria-label="toggle whether to add hyphens"
/>
}
/>
),
[hyphens]
);
const uppercaseConfig = useMemo(
() => (
<Configuration
icon={<icons.CaseSensitive size={24} />}
title="Uppercase"
control={
<LabeledSwitch
id="uppercase-switch"
label={uppercase ? "On" : "Off"}
checked={uppercase}
onCheckedChange={setUppercase}
aria-label="toggle whether to generate in uppercase"
/>
}
/>
),
[uppercase]
);
const uuidVersionConfig = useMemo(
() => (
<Configuration
icon={<icons.Settings2 size={24} />}
title="UUID version"
description="Choose the version of UUID to generate"
control={
<Select value={uuidVersion} onValueChange={onUuidVersionChange}>
<SelectTrigger
className="w-28"
aria-label="toggle open/close state of uuid version selection"
>
<SelectValue placeholder={uuidVersion} />
</SelectTrigger>
<SelectContent>
<SelectItem value={v1}>1</SelectItem>
<SelectItem value={v4}>4 (GUID)</SelectItem>
</SelectContent>
</Select>
}
/>
),
[uuidVersion]
);
const generatesInput = useMemo(
() => (
<Input
className="w-24 font-sans"
type="number"
value={generates}
onChange={onGeneratesChange}
/>
),
[generates]
);
const uuidsCopyButton = useMemo(() => <CopyButton text={uuidsString} />, [uuidsString]);
const uuidsClearButton = useMemo(
() => <ClearButton onClick={clearUuids} iconOnly aria-label="clear uuids" />,
[clearUuids]
);
const uuidsControl = <ControlMenu list={[uuidsCopyButton, uuidsClearButton]} />;
return (
<PageRootSection title={toolGroups.generators.tools.uuid.longTitle}>
<PageSection title="Configuration">
<Configurations list={[hyphensConfig, uppercaseConfig, uuidVersionConfig]} />
</PageSection>
<PageSection className="mt-6" title="Generate">
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={onGenerateClick}>
Generate UUID(s)
</Button>
<span>×</span>
{generatesInput}
</div>
</PageSection>
<PageSection title="UUID(s)" control={uuidsControl}>
<Textarea {...{ ref }} value={uuidsString} rows={10} readOnly />
</PageSection>
</PageRootSection>
);
}

9
app/icon.svg Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="180px" height="180px" viewBox="-40.96 -40.96 593.92 593.92" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)">
<rect x="-40.96" y="-40.96" width="593.92" height="593.92" rx="83.1488" fill="#6f32ac" strokewidth="0"/>

After

Width:  |  Height:  |  Size: 1.4 KiB

61
app/layout.tsx Normal file
View File

@@ -0,0 +1,61 @@
import "@/styles/globals.css";
import { Metadata } from "next";
import { siteConfig } from "@/config/site";
import { fontMono, fontSans } from "@/lib/fonts";
import { cn } from "@/lib/style";
import { Sidebar } from "@/components/sidebar";
import { SiteHeader } from "@/components/site-header";
import { TailwindIndicator } from "@/components/tailwind-indicator";
import { ThemeProvider } from "@/components/theme-provider";
export const metadata: Metadata = {
generator: "Next.js",
applicationName: siteConfig.name,
referrer: "origin-when-cross-origin",
title: {
default: siteConfig.name,
template: `%s - ${siteConfig.name}`,
},
description: siteConfig.description,
openGraph: {
title: siteConfig.name,
siteName: siteConfig.name,
type: "website",
},
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
type RootLayoutProps = {
children: React.ReactNode;
};
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
"h-screen bg-background font-sans text-sm font-medium text-foreground antialiased",
fontSans.variable,
fontMono.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" disableTransitionOnChange>
<div className="relative flex h-full flex-col">
<SiteHeader />
<div className="flex flex-1 overflow-y-hidden">
<Sidebar />
<main className="h-full flex-1 overflow-y-auto rounded-tl-md border bg-page p-12">
{children}
</main>
</div>
</div>
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
);
}

9
app/not-found.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { PageRootSection } from "@/components/page-root-section";
export default function NotFound() {
return (
<PageRootSection title="Not Found">
<p>Could not find requested resource</p>
</PageRootSection>
);
}

11
app/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { homeTools, singleTools } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
return (
<PageRootSection title={singleTools.allTools.longTitle}>
<ToolCards tools={homeTools} />
</PageRootSection>
);
}

10
app/search/layout.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { Metadata } from "next";
// TODO: use query param
export const metadata: Metadata = {
title: "Search results",
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

26
app/search/page.tsx Normal file
View File

@@ -0,0 +1,26 @@
"use client";
import { useSearchParams } from "next/navigation";
import Fuse from "fuse.js";
import { homeTools } from "@/config/tools";
import { PageRootSection } from "@/components/page-root-section";
import { ToolCards } from "@/components/tool-cards";
export default function Page() {
const params = useSearchParams();
const q = params.get("q")?.trim() ?? "";
const fuse = new Fuse(homeTools, { keys: ["keywords"], threshold: 0.45 });
const keyWordsOptions = q.split(" ").map(word => ({ keywords: word }));
const result = fuse.search({ $or: keyWordsOptions });
const tools = result.map(({ item }) => item);
const [title, child] =
tools.length === 0
? ["No results found", null]
: [`Search results for "${q}"`, <ToolCards {...{ tools }} />];
return <PageRootSection {...{ title }}>{child}</PageRootSection>;
}

11
app/settings/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Metadata } from "next";
import { singleTools } from "@/config/tools";
export const metadata: Metadata = {
title: singleTools.settings.longTitle,
};
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}

58
app/settings/page.tsx Normal file
View File

@@ -0,0 +1,58 @@
"use client";
import { useMemo } from "react";
import { useTheme } from "next-themes";
import { singleTools } from "@/config/tools";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Configuration } from "@/components/configuration";
import { Configurations } from "@/components/configurations";
import { icons } from "@/components/icons";
import { PageRootSection } from "@/components/page-root-section";
import { PageSection } from "@/components/page-section";
export default function Page() {
const { theme = "system", setTheme } = useTheme();
const appThemeIcon = useMemo(() => <icons.Paintbrush size={24} />, []);
const appThemeConfig = useMemo(
() => (
<Configuration
icon={appThemeIcon}
title="App theme"
description="Select which app theme to display"
control={
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger
className="w-28"
aria-label="toggle open/close state of app theme selection"
>
<SelectValue placeholder={theme} />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
}
/>
),
[appThemeIcon, setTheme, theme]
);
return (
<PageRootSection title={singleTools.settings.longTitle}>
<PageSection>
<Configurations list={[appThemeConfig]} />
</PageSection>
</PageRootSection>
);
}