From 20df0b8c196e0de8a61820686b6c03ba41406a18 Mon Sep 17 00:00:00 2001 From: rusconn Date: Sat, 26 Mar 2022 03:18:44 +0000 Subject: [PATCH] feat: add Json <> Yaml converter --- package.json | 5 +- src/components/common/CodeEditor.tsx | 32 +++++ src/components/common/CodeEditorHalf.tsx | 26 ++++ .../common/CodeEditorHalfLoading.tsx | 26 ++++ src/components/common/Configuration.tsx | 30 ++++ src/components/common/Configurations.tsx | 21 +++ src/components/common/MainItem.tsx | 19 +++ src/components/common/index.ts | 5 +- src/components/layout/Drawer.tsx | 7 +- .../converters/json-yaml/Configuration.tsx | 50 +++++++ .../pages/converters/json-yaml/Content.tsx | 128 ++++++++++++++++++ .../pages/converters/json-yaml/index.ts | 3 + src/components/pages/home/Content.tsx | 4 +- src/libs/$path.ts | 8 ++ src/pages/converters/json-yaml.tsx | 7 + yarn.lock | 33 ++++- 16 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 src/components/common/CodeEditor.tsx create mode 100644 src/components/common/CodeEditorHalf.tsx create mode 100644 src/components/common/CodeEditorHalfLoading.tsx create mode 100644 src/components/common/Configuration.tsx create mode 100644 src/components/common/Configurations.tsx create mode 100644 src/components/common/MainItem.tsx create mode 100644 src/components/pages/converters/json-yaml/Configuration.tsx create mode 100644 src/components/pages/converters/json-yaml/Content.tsx create mode 100644 src/components/pages/converters/json-yaml/index.ts create mode 100644 src/pages/converters/json-yaml.tsx diff --git a/package.json b/package.json index b874373..a7b3bdc 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,15 @@ "@fontsource/roboto": "^4.5.3", "@mui/icons-material": "^5.5.1", "@mui/material": "^5.5.1", + "ace-builds": "^1.4.14", "fast-deep-equal": "^3.1.3", "fuse.js": "^6.5.3", "next": "12.1.0", "react": "17.0.2", + "react-ace": "^9.5.0", "react-dom": "17.0.2", - "recoil": "^0.6.1" + "recoil": "^0.6.1", + "yaml": "^1.10.2" }, "devDependencies": { "@emotion/babel-plugin": "^11.7.2", diff --git a/src/components/common/CodeEditor.tsx b/src/components/common/CodeEditor.tsx new file mode 100644 index 0000000..575e742 --- /dev/null +++ b/src/components/common/CodeEditor.tsx @@ -0,0 +1,32 @@ +import { Paper } from "@mui/material"; +import { config } from "ace-builds"; +import { ComponentPropsWithoutRef, memo } from "react"; +import AceEditor from "react-ace"; + +// https://github.com/securingsincity/react-ace/issues/725 +config.set("basePath", "https://cdn.jsdelivr.net/npm/ace-builds@1.4.14/src-min-noconflict/"); +config.setModuleUrl( + "ace/mode/javascript_worker", + "https://cdn.jsdelivr.net/npm/ace-builds@1.4.14/src-min-noconflict/worker-javascript.js" +); + +type Props = ComponentPropsWithoutRef; + +const StyledComponent = (props: Props) => ( + + + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/CodeEditorHalf.tsx b/src/components/common/CodeEditorHalf.tsx new file mode 100644 index 0000000..9ff9911 --- /dev/null +++ b/src/components/common/CodeEditorHalf.tsx @@ -0,0 +1,26 @@ +import { useTheme } from "@mui/material/styles"; +import { ComponentPropsWithoutRef, memo } from "react"; + +import { drawerWidth } from "@/components/layout/Drawer"; +import { headerHeight } from "@/components/layout/Header"; + +import CodeEditor from "./CodeEditor"; + +type Props = Omit, "width">; + +const StyledComponent = (props: Props) => { + const theme = useTheme(); + + return ( + + ); +}; + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/CodeEditorHalfLoading.tsx b/src/components/common/CodeEditorHalfLoading.tsx new file mode 100644 index 0000000..ca78dd5 --- /dev/null +++ b/src/components/common/CodeEditorHalfLoading.tsx @@ -0,0 +1,26 @@ +import { Skeleton } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { ComponentPropsWithoutRef, memo } from "react"; + +import { drawerWidth } from "@/components/layout/Drawer"; +import { headerHeight } from "@/components/layout/Header"; + +type Props = Omit, "width">; + +const StyledComponent = (props: Props) => { + const theme = useTheme(); + + return ( + + ); +}; + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/Configuration.tsx b/src/components/common/Configuration.tsx new file mode 100644 index 0000000..e6b243d --- /dev/null +++ b/src/components/common/Configuration.tsx @@ -0,0 +1,30 @@ +import { Box, Paper, Stack, Typography } from "@mui/material"; +import { css, Theme } from "@mui/material/styles"; +import { memo, ReactNode } from "react"; + +type Props = { + icon: ReactNode; + title: string; + input: ReactNode; +}; + +const paper = (theme: Theme) => css` + padding: ${theme.spacing(2)}; + height: ${theme.spacing(8)}; +`; + +const StyledComponent = ({ icon, title, input }: Props) => ( + + theme.spacing(4)}> + + {icon} + {title} + + {input} + + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/Configurations.tsx b/src/components/common/Configurations.tsx new file mode 100644 index 0000000..6c80624 --- /dev/null +++ b/src/components/common/Configurations.tsx @@ -0,0 +1,21 @@ +import { Stack } from "@mui/material"; +import equal from "fast-deep-equal"; +import { ComponentPropsWithoutRef, memo } from "react"; + +import Configuration from "./Configuration"; + +type Props = { + configurations: ComponentPropsWithoutRef[]; +}; + +const StyledComponent = ({ configurations }: Props) => ( + + {configurations.map(({ icon, title, input }) => ( + + ))} + +); + +export const Component = memo(StyledComponent, equal); + +export default Component; diff --git a/src/components/common/MainItem.tsx b/src/components/common/MainItem.tsx new file mode 100644 index 0000000..501958c --- /dev/null +++ b/src/components/common/MainItem.tsx @@ -0,0 +1,19 @@ +import { Box, Typography } from "@mui/material"; +import { memo, PropsWithChildren } from "react"; + +type Props = PropsWithChildren<{ + title: string; +}>; + +const StyledComponent = ({ children, title }: Props) => ( + + + {title} + + {children} + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index c5caf67..f009121 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,3 +1,6 @@ +import CodeEditorHalfLoading from "./CodeEditorHalfLoading"; +import Configurations from "./Configurations"; import Main from "./Main"; +import MainItem from "./MainItem"; -export { Main }; +export { CodeEditorHalfLoading, Configurations, Main, MainItem }; diff --git a/src/components/layout/Drawer.tsx b/src/components/layout/Drawer.tsx index 7d4d54a..6efe1b4 100644 --- a/src/components/layout/Drawer.tsx +++ b/src/components/layout/Drawer.tsx @@ -47,7 +47,12 @@ const toolGroups = [ icon: , title: "Converters", tools: [ - { icon: , title: "Json <> Yaml", href: pagesPath.$url(), disabled: true }, + { + icon: , + title: "Json <> Yaml", + href: pagesPath.converters.json_yaml.$url(), + disabled: false, + }, { icon: , title: "Number Base", href: pagesPath.$url(), disabled: true }, ], }, diff --git a/src/components/pages/converters/json-yaml/Configuration.tsx b/src/components/pages/converters/json-yaml/Configuration.tsx new file mode 100644 index 0000000..9e4771d --- /dev/null +++ b/src/components/pages/converters/json-yaml/Configuration.tsx @@ -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] as const; +export type Spaces = typeof spacesArray[number]; +export const isSpaces = (x: number): x is Spaces => spacesArray.includes(x as Spaces); + +type OnSelectChange = NonNullable["onChange"]>; + +type Props = { + spaces: Spaces; + onSpacesChange: OnSelectChange; +}; + +const select = css` + & .MuiSelect-select:focus { + background-color: transparent; + } +`; + +const StyledComponent = ({ spaces, onSpacesChange }: Props) => ( + , + title: "Indentation", + input: ( + + + + ), + }, + ]} + /> +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/pages/converters/json-yaml/Content.tsx b/src/components/pages/converters/json-yaml/Content.tsx new file mode 100644 index 0000000..cd019ce --- /dev/null +++ b/src/components/pages/converters/json-yaml/Content.tsx @@ -0,0 +1,128 @@ +import { Stack } from "@mui/material"; +import dynamic from "next/dynamic"; +import { ComponentPropsWithoutRef, memo, useCallback, useState } from "react"; +import YAML from "yaml"; + +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 Promise.all([ + import("ace-builds/src-noconflict/mode-json"), + import("ace-builds/src-noconflict/mode-yaml"), + ]); + return ace; + }, + { ssr: false, loading: () => } +); + +type CodeValue = NonNullable["value"]>; +type OnCodeChange = NonNullable["onChange"]>; + +type Props = { + json: CodeValue; + yaml: CodeValue; + onJsonChange: OnCodeChange; + onYamlChange: OnCodeChange; +} & ComponentPropsWithoutRef; + +const StyledComponent = ({ + json, + yaml, + spaces, + onJsonChange, + onYamlChange, + onSpacesChange, +}: Props) => ( +
+ + + + + + + + + + + +
+); + +export const Component = memo(StyledComponent); + +const Container = () => { + const [json, setJson] = useState('{\n "foo": "bar"\n}'); + const [yaml, setYaml] = useState("foo: bar"); + const [spaces, setSpaces] = useState(2); + + const onJsonChange: Props["onJsonChange"] = useCallback( + value => { + setJson(value); + + try { + const parsed = JSON.parse(value) as unknown; + setYaml(YAML.stringify(parsed, { indent: spaces, simpleKeys: true })); + } catch { + setYaml(""); + } + }, + [spaces] + ); + + const onYamlChange: Props["onYamlChange"] = useCallback( + value => { + setYaml(value); + + try { + const parsed = YAML.parse(value, { merge: true }) as unknown; + setJson(JSON.stringify(parsed, null, spaces)); + } catch { + setJson(""); + } + }, + [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; + setJson(JSON.stringify(parsed, null, newSpaces)); + setYaml(YAML.stringify(parsed, { indent: newSpaces, simpleKeys: true })); + } catch { + setJson(""); + setYaml(""); + } + }, + [json] + ); + + return ; +}; + +export default Container; diff --git a/src/components/pages/converters/json-yaml/index.ts b/src/components/pages/converters/json-yaml/index.ts new file mode 100644 index 0000000..ae1b8af --- /dev/null +++ b/src/components/pages/converters/json-yaml/index.ts @@ -0,0 +1,3 @@ +import Content from "./Content"; + +export { Content }; diff --git a/src/components/pages/home/Content.tsx b/src/components/pages/home/Content.tsx index 36b74df..b655369 100644 --- a/src/components/pages/home/Content.tsx +++ b/src/components/pages/home/Content.tsx @@ -31,8 +31,8 @@ const tools = [ title: "Json <> Yaml Converter", description: "Convert Json data to Yaml and vice versa", keywords: "json yaml converter", - href: pagesPath.$url(), - disabled: true, + href: pagesPath.converters.json_yaml.$url(), + disabled: false, }, { icon: , diff --git a/src/libs/$path.ts b/src/libs/$path.ts index f87eac9..08a7817 100644 --- a/src/libs/$path.ts +++ b/src/libs/$path.ts @@ -1,4 +1,12 @@ export const pagesPath = { + converters: { + json_yaml: { + $url: (url?: { hash?: string }) => ({ + pathname: "/converters/json-yaml" as const, + hash: url?.hash, + }), + }, + }, $url: (url?: { hash?: string }) => ({ pathname: "/" as const, hash: url?.hash }), }; diff --git a/src/pages/converters/json-yaml.tsx b/src/pages/converters/json-yaml.tsx new file mode 100644 index 0000000..f46b640 --- /dev/null +++ b/src/pages/converters/json-yaml.tsx @@ -0,0 +1,7 @@ +import type { NextPage } from "next"; + +import { Content } from "@/components/pages/converters/json-yaml"; + +const Page: NextPage = Content; + +export default Page; diff --git a/yarn.lock b/yarn.lock index 25f547b..d43d26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -527,6 +527,11 @@ "@typescript-eslint/types" "5.15.0" eslint-visitor-keys "^3.0.0" +ace-builds@^1.4.13, ace-builds@^1.4.14: + version "1.4.14" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.14.tgz#2c41ccbccdd09e665d3489f161a20baeb3a3c852" + integrity sha512-NBOQlm9+7RBqRqZwimpgquaLeTJFayqb9UEPtTkpC3TkkwDnlsT/TwsCC0svjt9kEZ6G9mH5AEOHSz6Q/HrzQQ== + acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -863,6 +868,11 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +diff-match-patch@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1695,6 +1705,16 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -2072,6 +2092,17 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-ace@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.5.0.tgz#b6c32b70d404dd821a7e01accc2d76da667ff1f7" + integrity sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg== + dependencies: + ace-builds "^1.4.13" + diff-match-patch "^1.0.5" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + react-dom@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -2578,7 +2609,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==