mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-16 20:49:23 +00:00
renewal
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:
BIN
app/apple-icon.png
Normal file
BIN
app/apple-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
11
app/converters/json-yaml/layout.tsx
Normal file
11
app/converters/json-yaml/layout.tsx
Normal 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;
|
||||
}
|
||||
174
app/converters/json-yaml/page.tsx
Normal file
174
app/converters/json-yaml/page.tsx
Normal 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
11
app/converters/layout.tsx
Normal 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;
|
||||
}
|
||||
11
app/converters/number-base/layout.tsx
Normal file
11
app/converters/number-base/layout.tsx
Normal 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;
|
||||
}
|
||||
111
app/converters/number-base/page.tsx
Normal file
111
app/converters/number-base/page.tsx
Normal 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
11
app/converters/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/encoders-decoders/base64/layout.tsx
Normal file
11
app/encoders-decoders/base64/layout.tsx
Normal 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;
|
||||
}
|
||||
98
app/encoders-decoders/base64/page.tsx
Normal file
98
app/encoders-decoders/base64/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/encoders-decoders/html/layout.tsx
Normal file
11
app/encoders-decoders/html/layout.tsx
Normal 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;
|
||||
}
|
||||
91
app/encoders-decoders/html/page.tsx
Normal file
91
app/encoders-decoders/html/page.tsx
Normal 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("> It's "HTML escaping".");
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
11
app/encoders-decoders/jwt/layout.tsx
Normal file
11
app/encoders-decoders/jwt/layout.tsx
Normal 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;
|
||||
}
|
||||
65
app/encoders-decoders/jwt/page.tsx
Normal file
65
app/encoders-decoders/jwt/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/encoders-decoders/layout.tsx
Normal file
11
app/encoders-decoders/layout.tsx
Normal 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;
|
||||
}
|
||||
11
app/encoders-decoders/page.tsx
Normal file
11
app/encoders-decoders/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/encoders-decoders/url/layout.tsx
Normal file
11
app/encoders-decoders/url/layout.tsx
Normal 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;
|
||||
}
|
||||
100
app/encoders-decoders/url/page.tsx
Normal file
100
app/encoders-decoders/url/page.tsx
Normal 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
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
11
app/formatters/json/layout.tsx
Normal file
11
app/formatters/json/layout.tsx
Normal 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;
|
||||
}
|
||||
111
app/formatters/json/page.tsx
Normal file
111
app/formatters/json/page.tsx
Normal 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
11
app/formatters/layout.tsx
Normal 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
11
app/formatters/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/generators/hash/layout.tsx
Normal file
11
app/generators/hash/layout.tsx
Normal 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;
|
||||
}
|
||||
129
app/generators/hash/page.tsx
Normal file
129
app/generators/hash/page.tsx
Normal 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
11
app/generators/layout.tsx
Normal 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
11
app/generators/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
app/generators/uuid/layout.tsx
Normal file
11
app/generators/uuid/layout.tsx
Normal 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;
|
||||
}
|
||||
171
app/generators/uuid/page.tsx
Normal file
171
app/generators/uuid/page.tsx
Normal 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
9
app/icon.svg
Normal 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
61
app/layout.tsx
Normal 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
9
app/not-found.tsx
Normal 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
11
app/page.tsx
Normal 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
10
app/search/layout.tsx
Normal 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
26
app/search/page.tsx
Normal 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
11
app/settings/layout.tsx
Normal 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
58
app/settings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user