diff --git a/README.md b/README.md index 038a7a5..c6eecd8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A web clone of [DevToys](https://github.com/veler/DevToys) - [x] Add site layout - [x] Add all tools page mock - [ ] Implement tools - - [ ] Converters + - [x] Converters - [ ] Encoders / Decoders - [ ] Formatters - [ ] Generators diff --git a/package.json b/package.json index a7b3bdc..aed4963 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@mui/material": "^5.5.1", "ace-builds": "^1.4.14", "fast-deep-equal": "^3.1.3", + "fp-ts": "^2.11.9", "fuse.js": "^6.5.3", "next": "12.1.0", "react": "17.0.2", diff --git a/src/components/common/TextField.tsx b/src/components/common/TextField.tsx new file mode 100644 index 0000000..89a7ebf --- /dev/null +++ b/src/components/common/TextField.tsx @@ -0,0 +1,18 @@ +import { TextField } from "@mui/material"; +import { ComponentPropsWithoutRef, memo } from "react"; + +type Props = ComponentPropsWithoutRef; + +const StyledComponent = (props: Props) => ( + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index f009121..cd6937b 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -2,5 +2,6 @@ import CodeEditorHalfLoading from "./CodeEditorHalfLoading"; import Configurations from "./Configurations"; import Main from "./Main"; import MainItem from "./MainItem"; +import TextField from "./TextField"; -export { CodeEditorHalfLoading, Configurations, Main, MainItem }; +export { CodeEditorHalfLoading, Configurations, Main, MainItem, TextField }; diff --git a/src/components/layout/Drawer.tsx b/src/components/layout/Drawer.tsx index 6efe1b4..80b3d61 100644 --- a/src/components/layout/Drawer.tsx +++ b/src/components/layout/Drawer.tsx @@ -53,7 +53,12 @@ const toolGroups = [ href: pagesPath.converters.json_yaml.$url(), disabled: false, }, - { icon: , title: "Number Base", href: pagesPath.$url(), disabled: true }, + { + icon: , + title: "Number Base", + href: pagesPath.converters.number_base.$url(), + disabled: false, + }, ], }, { diff --git a/src/components/pages/converters/number-base/Configuration.tsx b/src/components/pages/converters/number-base/Configuration.tsx new file mode 100644 index 0000000..5c19e63 --- /dev/null +++ b/src/components/pages/converters/number-base/Configuration.tsx @@ -0,0 +1,36 @@ +import { AutoFixHigh } from "@mui/icons-material"; +import { FormControlLabel, Switch } from "@mui/material"; +import { SwitchBaseProps } from "@mui/material/internal/SwitchBase"; +import { memo } from "react"; + +import { Configurations } from "@/components/common"; + +type SwitchChecked = NonNullable; +type OnSwitchChange = NonNullable; + +type Props = { + format: SwitchChecked; + onFormatChange: OnSwitchChange; +}; + +const StyledComponent = ({ format, onFormatChange }: Props) => ( + , + title: "Format number", + input: ( + } + /> + ), + }, + ]} + /> +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/pages/converters/number-base/Content.tsx b/src/components/pages/converters/number-base/Content.tsx new file mode 100644 index 0000000..d12c0de --- /dev/null +++ b/src/components/pages/converters/number-base/Content.tsx @@ -0,0 +1,145 @@ +import { Stack } from "@mui/material"; +import { ComponentPropsWithoutRef, memo, useCallback, useState } from "react"; + +import { Main, MainItem, TextField } from "@/components/common"; +import { + formatBinary, + formatDecimal, + formatHexadecimal, + formatOctal, + isBinary, + isDecimal, + isHexadecimal, + isOctal, + unformatBinary, + unformatDecimal, + unformatHexadecimal, + unformatOctal, +} from "@/libs/string"; + +import Configuration from "./Configuration"; + +type TextFieldValue = ComponentPropsWithoutRef["value"]; +type OnTextFieldChange = NonNullable["onChange"]>; + +type Props = { + dec: TextFieldValue; + hex: TextFieldValue; + oct: TextFieldValue; + bin: TextFieldValue; + onDecChange: OnTextFieldChange; + onHexChange: OnTextFieldChange; + onOctChange: OnTextFieldChange; + onBinChange: OnTextFieldChange; +} & ComponentPropsWithoutRef; + +const StyledComponent = ({ + format, + dec, + hex, + oct, + bin, + onFormatChange, + onDecChange, + onHexChange, + onOctChange, + onBinChange, +}: Props) => ( +
+ + + + + + + + + + + + + + + + + +
+); + +export const Component = memo(StyledComponent); + +const Container = () => { + const [format, setFormat] = useState(false); + const [int, setInt] = useState(BigInt(42)); + + const onFormatChange: Props["onFormatChange"] = useCallback((_e, checked) => { + setFormat(checked); + }, []); + + const trySetInt = ( + value: string, + prefix: string, + validate: (x: string) => boolean, + unformat: (x: string) => string + ) => { + if (value === "") { + setInt(undefined); + return; + } + + const unformatted = unformat(value); + + if (!validate(unformatted)) { + return; + } + + const newInt = BigInt(`${prefix}${unformatted}`); + + setInt(newInt); + }; + + const onDecChange: Props["onDecChange"] = useCallback(({ currentTarget: { value } }) => { + trySetInt(value, "", isDecimal, unformatDecimal); + }, []); + + const onHexChange: Props["onHexChange"] = useCallback(({ currentTarget: { value } }) => { + trySetInt(value, "0x", isHexadecimal, unformatHexadecimal); + }, []); + + const onOctChange: Props["onOctChange"] = useCallback(({ currentTarget: { value } }) => { + trySetInt(value, "0o", isOctal, unformatOctal); + }, []); + + const onBinChange: Props["onBinChange"] = useCallback(({ currentTarget: { value } }) => { + trySetInt(value, "0b", isBinary, unformatBinary); + }, []); + + const newDec = int?.toString(10) ?? ""; + const newHex = int?.toString(16).toUpperCase() ?? ""; + const newOct = int?.toString(8) ?? ""; + const newBin = int?.toString(2) ?? ""; + + const dec = format ? formatDecimal(newDec) : newDec; + const hex = format ? formatHexadecimal(newHex) : newHex; + const oct = format ? formatOctal(newOct) : newOct; + const bin = format ? formatBinary(newBin) : newBin; + + return ( + + ); +}; + +export default Container; diff --git a/src/components/pages/converters/number-base/index.ts b/src/components/pages/converters/number-base/index.ts new file mode 100644 index 0000000..ae1b8af --- /dev/null +++ b/src/components/pages/converters/number-base/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 b655369..f5d6b7e 100644 --- a/src/components/pages/home/Content.tsx +++ b/src/components/pages/home/Content.tsx @@ -39,8 +39,8 @@ const tools = [ title: "Number Base Converter", description: "Convert numbers from one base to another", keywords: "number base converter", - href: pagesPath.$url(), - disabled: true, + href: pagesPath.converters.number_base.$url(), + disabled: false, }, { icon: , diff --git a/src/data/mui.ts b/src/data/mui.ts index b75b9f7..c0be15c 100644 --- a/src/data/mui.ts +++ b/src/data/mui.ts @@ -16,5 +16,12 @@ export const theme = createTheme({ disableRipple: true, }, }, + MuiTextField: { + styleOverrides: { + root: { + backgroundColor: "white", + }, + }, + }, }, }); diff --git a/src/libs/$path.ts b/src/libs/$path.ts index 08a7817..fad7389 100644 --- a/src/libs/$path.ts +++ b/src/libs/$path.ts @@ -6,6 +6,12 @@ export const pagesPath = { hash: url?.hash, }), }, + number_base: { + $url: (url?: { hash?: string }) => ({ + pathname: "/converters/number-base" as const, + hash: url?.hash, + }), + }, }, $url: (url?: { hash?: string }) => ({ pathname: "/" as const, hash: url?.hash }), }; diff --git a/src/libs/string.ts b/src/libs/string.ts new file mode 100644 index 0000000..c4df61d --- /dev/null +++ b/src/libs/string.ts @@ -0,0 +1,34 @@ +import { chunksOf, flatten, intersperse, map, reverse } from "fp-ts/Array"; +import { pipe } from "fp-ts/function"; + +const match = (regex: RegExp) => (x: string) => regex.test(x); + +export const isDecimal = match(/^[0-9]*$/); +export const isHexadecimal = match(/^[0-9A-F]*$/i); +export const isOctal = match(/^[0-7]*$/); +export const isBinary = match(/^[0-1]*$/); + +const formatNumber = (digits: number, sep: string) => (x: string) => + pipe( + x, + y => y.split(""), + reverse, + chunksOf(digits), + map(reverse), + intersperse([sep]), + reverse, + flatten, + xs => xs.join("") + ); + +export const formatDecimal = formatNumber(3, ","); +export const formatHexadecimal = formatNumber(4, " "); +export const formatOctal = formatNumber(3, " "); +export const formatBinary = formatNumber(4, " "); + +const unformatNumber = (sep: string) => (x: string) => x.replaceAll(sep, ""); + +export const unformatDecimal = unformatNumber(","); +export const unformatHexadecimal = unformatNumber(" "); +export const unformatOctal = unformatNumber(" "); +export const unformatBinary = unformatNumber(" "); diff --git a/src/pages/converters/number-base.tsx b/src/pages/converters/number-base.tsx new file mode 100644 index 0000000..1d463b4 --- /dev/null +++ b/src/pages/converters/number-base.tsx @@ -0,0 +1,7 @@ +import type { NextPage } from "next"; + +import { Content } from "@/components/pages/converters/number-base"; + +const Page: NextPage = Content; + +export default Page; diff --git a/yarn.lock b/yarn.lock index d43d26f..c28a108 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1285,6 +1285,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +fp-ts@^2.11.9: + version "2.11.9" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.11.9.tgz#bbc204e0932954b59c98a282635754a4b624a05e" + integrity sha512-GhYlNKkCOfdjp71ocdtyaQGoqCswEoWDJLRr+2jClnBBq2dnSOtd6QxmJdALq8UhfqCyZZ0f0lxadU4OhwY9nw== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"