mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-17 04:59:23 +00:00
feat: add number base converter
This commit is contained in:
@@ -7,7 +7,7 @@ A web clone of [DevToys](https://github.com/veler/DevToys)
|
|||||||
- [x] Add site layout
|
- [x] Add site layout
|
||||||
- [x] Add all tools page mock
|
- [x] Add all tools page mock
|
||||||
- [ ] Implement tools
|
- [ ] Implement tools
|
||||||
- [ ] Converters
|
- [x] Converters
|
||||||
- [ ] Encoders / Decoders
|
- [ ] Encoders / Decoders
|
||||||
- [ ] Formatters
|
- [ ] Formatters
|
||||||
- [ ] Generators
|
- [ ] Generators
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@mui/material": "^5.5.1",
|
"@mui/material": "^5.5.1",
|
||||||
"ace-builds": "^1.4.14",
|
"ace-builds": "^1.4.14",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fp-ts": "^2.11.9",
|
||||||
"fuse.js": "^6.5.3",
|
"fuse.js": "^6.5.3",
|
||||||
"next": "12.1.0",
|
"next": "12.1.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
|||||||
18
src/components/common/TextField.tsx
Normal file
18
src/components/common/TextField.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { TextField } from "@mui/material";
|
||||||
|
import { ComponentPropsWithoutRef, memo } from "react";
|
||||||
|
|
||||||
|
type Props = ComponentPropsWithoutRef<typeof TextField>;
|
||||||
|
|
||||||
|
const StyledComponent = (props: Props) => (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
hiddenLabel
|
||||||
|
size="small"
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Component = memo(StyledComponent);
|
||||||
|
|
||||||
|
export default Component;
|
||||||
@@ -2,5 +2,6 @@ import CodeEditorHalfLoading from "./CodeEditorHalfLoading";
|
|||||||
import Configurations from "./Configurations";
|
import Configurations from "./Configurations";
|
||||||
import Main from "./Main";
|
import Main from "./Main";
|
||||||
import MainItem from "./MainItem";
|
import MainItem from "./MainItem";
|
||||||
|
import TextField from "./TextField";
|
||||||
|
|
||||||
export { CodeEditorHalfLoading, Configurations, Main, MainItem };
|
export { CodeEditorHalfLoading, Configurations, Main, MainItem, TextField };
|
||||||
|
|||||||
@@ -53,7 +53,12 @@ const toolGroups = [
|
|||||||
href: pagesPath.converters.json_yaml.$url(),
|
href: pagesPath.converters.json_yaml.$url(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{ icon: <Numbers />, title: "Number Base", href: pagesPath.$url(), disabled: true },
|
{
|
||||||
|
icon: <Numbers />,
|
||||||
|
title: "Number Base",
|
||||||
|
href: pagesPath.converters.number_base.$url(),
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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<SwitchBaseProps["checked"]>;
|
||||||
|
type OnSwitchChange = NonNullable<SwitchBaseProps["onChange"]>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
format: SwitchChecked;
|
||||||
|
onFormatChange: OnSwitchChange;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledComponent = ({ format, onFormatChange }: Props) => (
|
||||||
|
<Configurations
|
||||||
|
configurations={[
|
||||||
|
{
|
||||||
|
icon: <AutoFixHigh />,
|
||||||
|
title: "Format number",
|
||||||
|
input: (
|
||||||
|
<FormControlLabel
|
||||||
|
labelPlacement="start"
|
||||||
|
label={format ? "On" : "Off"}
|
||||||
|
control={<Switch checked={format} onChange={onFormatChange} />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Component = memo(StyledComponent);
|
||||||
|
|
||||||
|
export default Component;
|
||||||
145
src/components/pages/converters/number-base/Content.tsx
Normal file
145
src/components/pages/converters/number-base/Content.tsx
Normal file
@@ -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<typeof TextField>["value"];
|
||||||
|
type OnTextFieldChange = NonNullable<ComponentPropsWithoutRef<typeof TextField>["onChange"]>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dec: TextFieldValue;
|
||||||
|
hex: TextFieldValue;
|
||||||
|
oct: TextFieldValue;
|
||||||
|
bin: TextFieldValue;
|
||||||
|
onDecChange: OnTextFieldChange;
|
||||||
|
onHexChange: OnTextFieldChange;
|
||||||
|
onOctChange: OnTextFieldChange;
|
||||||
|
onBinChange: OnTextFieldChange;
|
||||||
|
} & ComponentPropsWithoutRef<typeof Configuration>;
|
||||||
|
|
||||||
|
const StyledComponent = ({
|
||||||
|
format,
|
||||||
|
dec,
|
||||||
|
hex,
|
||||||
|
oct,
|
||||||
|
bin,
|
||||||
|
onFormatChange,
|
||||||
|
onDecChange,
|
||||||
|
onHexChange,
|
||||||
|
onOctChange,
|
||||||
|
onBinChange,
|
||||||
|
}: Props) => (
|
||||||
|
<Main title="Number Base Converter">
|
||||||
|
<MainItem title="Configuration">
|
||||||
|
<Configuration {...{ format, onFormatChange }} />
|
||||||
|
</MainItem>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<MainItem title="Decimal">
|
||||||
|
<TextField value={dec} onChange={onDecChange} />
|
||||||
|
</MainItem>
|
||||||
|
<MainItem title="Hexadecimal">
|
||||||
|
<TextField value={hex} onChange={onHexChange} />
|
||||||
|
</MainItem>
|
||||||
|
<MainItem title="Octal">
|
||||||
|
<TextField value={oct} onChange={onOctChange} />
|
||||||
|
</MainItem>
|
||||||
|
<MainItem title="Binary">
|
||||||
|
<TextField value={bin} onChange={onBinChange} />
|
||||||
|
</MainItem>
|
||||||
|
</Stack>
|
||||||
|
</Main>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Component = memo(StyledComponent);
|
||||||
|
|
||||||
|
const Container = () => {
|
||||||
|
const [format, setFormat] = useState(false);
|
||||||
|
const [int, setInt] = useState<bigint | undefined>(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 (
|
||||||
|
<Component
|
||||||
|
{...{
|
||||||
|
format,
|
||||||
|
dec,
|
||||||
|
hex,
|
||||||
|
oct,
|
||||||
|
bin,
|
||||||
|
onFormatChange,
|
||||||
|
onDecChange,
|
||||||
|
onHexChange,
|
||||||
|
onOctChange,
|
||||||
|
onBinChange,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Container;
|
||||||
3
src/components/pages/converters/number-base/index.ts
Normal file
3
src/components/pages/converters/number-base/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import Content from "./Content";
|
||||||
|
|
||||||
|
export { Content };
|
||||||
@@ -39,8 +39,8 @@ const tools = [
|
|||||||
title: "Number Base Converter",
|
title: "Number Base Converter",
|
||||||
description: "Convert numbers from one base to another",
|
description: "Convert numbers from one base to another",
|
||||||
keywords: "number base converter",
|
keywords: "number base converter",
|
||||||
href: pagesPath.$url(),
|
href: pagesPath.converters.number_base.$url(),
|
||||||
disabled: true,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Code />,
|
icon: <Code />,
|
||||||
|
|||||||
@@ -16,5 +16,12 @@ export const theme = createTheme({
|
|||||||
disableRipple: true,
|
disableRipple: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export const pagesPath = {
|
|||||||
hash: url?.hash,
|
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 }),
|
$url: (url?: { hash?: string }) => ({ pathname: "/" as const, hash: url?.hash }),
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/libs/string.ts
Normal file
34
src/libs/string.ts
Normal file
@@ -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(" ");
|
||||||
7
src/pages/converters/number-base.tsx
Normal file
7
src/pages/converters/number-base.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextPage } from "next";
|
||||||
|
|
||||||
|
import { Content } from "@/components/pages/converters/number-base";
|
||||||
|
|
||||||
|
const Page: NextPage = Content;
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -1285,6 +1285,11 @@ flatted@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
|
||||||
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
|
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:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
|
|||||||
Reference in New Issue
Block a user