mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-17 12:32:56 +00:00
feat: add json formatter
This commit is contained in:
@@ -9,7 +9,7 @@ A web clone of [DevToys](https://github.com/veler/DevToys)
|
|||||||
- [ ] Implement tools
|
- [ ] Implement tools
|
||||||
- [x] Converters
|
- [x] Converters
|
||||||
- [x] Encoders / Decoders
|
- [x] Encoders / Decoders
|
||||||
- [ ] Formatters
|
- [x] Formatters
|
||||||
- [ ] Generators
|
- [ ] Generators
|
||||||
- [ ] Text
|
- [ ] Text
|
||||||
- [ ] Graphic
|
- [ ] Graphic
|
||||||
|
|||||||
50
src/components/pages/formatters/json/Configuration.tsx
Normal file
50
src/components/pages/formatters/json/Configuration.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { SpaceBar } from "@mui/icons-material";
|
||||||
|
import { FormControl, MenuItem, Select } from "@mui/material";
|
||||||
|
import { SelectInputProps } from "@mui/material/Select/SelectInput";
|
||||||
|
import { css } from "@mui/material/styles";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
import { Configurations } from "@/components/common";
|
||||||
|
|
||||||
|
const spacesArray = [2, 4, 0] as const;
|
||||||
|
export type Spaces = typeof spacesArray[number];
|
||||||
|
export const isSpaces = (x: number): x is Spaces => spacesArray.includes(x as Spaces);
|
||||||
|
|
||||||
|
type OnSelectChange<T> = NonNullable<SelectInputProps<T>["onChange"]>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
spaces: Spaces;
|
||||||
|
onSpacesChange: OnSelectChange<Spaces>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const select = css`
|
||||||
|
& .MuiSelect-select:focus {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledComponent = ({ spaces, onSpacesChange }: Props) => (
|
||||||
|
<Configurations
|
||||||
|
configurations={[
|
||||||
|
{
|
||||||
|
icon: <SpaceBar />,
|
||||||
|
title: "Indentation",
|
||||||
|
input: (
|
||||||
|
<FormControl variant="standard">
|
||||||
|
<Select value={spaces} onChange={onSpacesChange} css={select}>
|
||||||
|
{spacesArray.map(value => (
|
||||||
|
<MenuItem key={value} {...{ value }}>
|
||||||
|
{value === 0 ? "minified" : `${value} spaces`}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Component = memo(StyledComponent);
|
||||||
|
|
||||||
|
export default Component;
|
||||||
88
src/components/pages/formatters/json/Content.tsx
Normal file
88
src/components/pages/formatters/json/Content.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { ComponentPropsWithoutRef, memo, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { CodeEditorHalfLoading, Main, MainItem } from "@/components/common";
|
||||||
|
|
||||||
|
import Configuration, { isSpaces, Spaces } from "./Configuration";
|
||||||
|
|
||||||
|
// https://github.com/securingsincity/react-ace/issues/27
|
||||||
|
const CodeEditorHalf = dynamic(
|
||||||
|
async () => {
|
||||||
|
const ace = await import("@/components/common/CodeEditorHalf");
|
||||||
|
await import("ace-builds/src-noconflict/mode-json");
|
||||||
|
return ace;
|
||||||
|
},
|
||||||
|
{ ssr: false, loading: () => <CodeEditorHalfLoading /> }
|
||||||
|
);
|
||||||
|
|
||||||
|
type CodeValue = NonNullable<ComponentPropsWithoutRef<typeof CodeEditorHalf>["value"]>;
|
||||||
|
type OnCodeChange = NonNullable<ComponentPropsWithoutRef<typeof CodeEditorHalf>["onChange"]>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
json: CodeValue;
|
||||||
|
formatted: CodeValue;
|
||||||
|
onJsonChange: OnCodeChange;
|
||||||
|
} & ComponentPropsWithoutRef<typeof Configuration>;
|
||||||
|
|
||||||
|
const StyledComponent = ({ json, formatted, spaces, onJsonChange, onSpacesChange }: Props) => (
|
||||||
|
<Main title="Json Formatter">
|
||||||
|
<MainItem title="Configuration">
|
||||||
|
<Configuration {...{ spaces, onSpacesChange }} />
|
||||||
|
</MainItem>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<MainItem title="Input">
|
||||||
|
<CodeEditorHalf name="json" mode="json" value={json} tabSize={2} onChange={onJsonChange} />
|
||||||
|
</MainItem>
|
||||||
|
<MainItem title="Output">
|
||||||
|
<CodeEditorHalf name="formatted" mode="json" value={formatted} tabSize={spaces} readOnly />
|
||||||
|
</MainItem>
|
||||||
|
</Stack>
|
||||||
|
</Main>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Component = memo(StyledComponent);
|
||||||
|
|
||||||
|
const Container = () => {
|
||||||
|
const [json, setJson] = useState('{"foo":"bar"}');
|
||||||
|
const [formatted, setFormatted] = useState('{\n "foo": "bar"\n}');
|
||||||
|
const [spaces, setSpaces] = useState<Spaces>(2);
|
||||||
|
|
||||||
|
const onJsonChange: Props["onJsonChange"] = useCallback(
|
||||||
|
value => {
|
||||||
|
setJson(value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as unknown;
|
||||||
|
setFormatted(JSON.stringify(parsed, null, spaces));
|
||||||
|
} catch {
|
||||||
|
setFormatted("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[spaces]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpacesChange: Props["onSpacesChange"] = useCallback(
|
||||||
|
({ target: { value } }) => {
|
||||||
|
const newSpaces = Number(value);
|
||||||
|
|
||||||
|
if (!isSpaces(newSpaces)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpaces(newSpaces);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(json) as unknown;
|
||||||
|
setFormatted(JSON.stringify(parsed, null, newSpaces));
|
||||||
|
} catch {
|
||||||
|
setFormatted("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[json]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Component {...{ json, formatted, spaces, onJsonChange, onSpacesChange }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Container;
|
||||||
3
src/components/pages/formatters/json/index.ts
Normal file
3
src/components/pages/formatters/json/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Content from "./Content";
|
||||||
|
|
||||||
|
export { Content };
|
||||||
@@ -97,8 +97,8 @@ const toolGroups = [
|
|||||||
longTitle: "JSON Formatter",
|
longTitle: "JSON Formatter",
|
||||||
description: "Indent or minify JSON data",
|
description: "Indent or minify JSON data",
|
||||||
keywords: "json formatter",
|
keywords: "json formatter",
|
||||||
href: pagesPath.$url(),
|
href: pagesPath.formatters.json.$url(),
|
||||||
disabled: true,
|
disabled: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ export const pagesPath = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
formatters: {
|
||||||
|
json: {
|
||||||
|
$url: (url?: { hash?: string }) => ({
|
||||||
|
pathname: "/formatters/json" as const,
|
||||||
|
hash: url?.hash,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
$url: (url?: { hash?: string }) => ({ pathname: "/search" as const, hash: url?.hash }),
|
$url: (url?: { hash?: string }) => ({ pathname: "/search" as const, hash: url?.hash }),
|
||||||
},
|
},
|
||||||
|
|||||||
7
src/pages/formatters/json.tsx
Normal file
7
src/pages/formatters/json.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextPage } from "next";
|
||||||
|
|
||||||
|
import { Content } from "@/components/pages/formatters/json";
|
||||||
|
|
||||||
|
const Page: NextPage = Content;
|
||||||
|
|
||||||
|
export default Page;
|
||||||
Reference in New Issue
Block a user