diff --git a/.babelrc b/.babelrc deleted file mode 100644 index ad3e93c..0000000 --- a/.babelrc +++ /dev/null @@ -1,43 +0,0 @@ -// https://emotion.sh/docs/css-prop##babel-preset -// https://mui.com/guides/minimizing-bundle-size/#option-2 -// https://github.com/vercel/next.js/discussions/17822 -{ - "presets": [ - [ - "next/babel", - { - "preset-react": { - "runtime": "automatic", - "importSource": "@emotion/react" - }, - "preset-env": { - "debug": false, - "targets": { - "browsers": ">1%, not ie 11, not op_mini all" - } - } - } - ] - ], - "plugins": [ - [ - "babel-plugin-import", - { - "libraryName": "@mui/material", - "libraryDirectory": "", - "camel2DashComponentName": false - }, - "core" - ], - [ - "babel-plugin-import", - { - "libraryName": "@mui/icons-material", - "libraryDirectory": "", - "camel2DashComponentName": false - }, - "icons" - ], - ["@emotion/babel-plugin"] - ] -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9076768..b218fc5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,7 @@ "ms-azuretools.vscode-docker", "esbenp.prettier-vscode", "artdiniz.quitcontrol-vscode", - "styled-components.vscode-styled-components" + "bradlc.vscode-tailwindcss" ], "runServices": ["app"], "shutdownAction": "stopCompose" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ae10a5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore deleted file mode 120000 index 3e4e48b..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -.gitignore \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..dc0f9d8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +dist/* +.cache +public +node_modules +*.esm.js diff --git a/.eslintrc.json b/.eslintrc.json index 669d3db..071a369 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,49 +1,49 @@ { + "$schema": "https://json.schemastore.org/eslintrc", "root": true, - "parserOptions": { - "project": "./tsconfig.json" - }, - "parser": "@typescript-eslint/parser", "extends": [ "airbnb", "airbnb-typescript", "airbnb/hooks", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:tailwindcss/recommended", "next/core-web-vitals", "prettier" ], + "plugins": ["tailwindcss"], "rules": { - "no-console": "off", - "no-else-return": "off", - "no-underscore-dangle": ["error", { "allow": ["_slug"] }], - "import/prefer-default-export": "off", + "@next/next/no-html-link-for-pages": "off", "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], - "react/require-default-props": "off", - "react/function-component-definition": "off", - "jsx-a11y/anchor-is-valid": [ + "@typescript-eslint/no-unused-vars": [ "error", - { - "components": ["Link"], - "specialLink": ["hrefLeft", "hrefRight"], - "aspects": ["invalidHref", "preferButton"] - } + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ], - "import/order": [ - "error", - { - "groups": ["builtin", "external", "internal", ["parent", "sibling"], "object", "index"], - "newlines-between": "always", - "alphabetize": { "order": "asc", "caseInsensitive": true } - } - ] + "consistent-return": "off", + "no-else-return": "off", + "no-nested-ternary": "off", + "import/prefer-default-export": "off", + "react/function-component-definition": "off", + "react/jsx-key": "off", + "react/jsx-props-no-spreading": "off", + "react/require-default-props": "off", + "tailwindcss/no-custom-classname": "off" + }, + "settings": { + "tailwindcss": { + "callees": ["cn"], + "config": "./tailwind.config.js" + }, + "next": { + "rootDir": ["./"] + } }, "overrides": [ { - "files": "src/pages/_app.tsx", - "rules": { - "react/jsx-props-no-spreading": "off" + "files": ["*.ts", "*.tsx"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" } } ] diff --git a/.gitignore b/.gitignore index d848f3d..5180021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,17 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp +node_modules +.pnp .pnp.js # testing -/coverage +coverage # next.js -/.next/ -/out/ - -# production -/build +.next/ +out/ +build # misc .DS_Store @@ -31,14 +29,14 @@ yarn-error.log* .env.test.local .env.production.local -# vercel -.vercel +# turbo +.turbo + +.contentlayer +.env # typescript -*.tsbuildinfo +tsconfig.tsbuildinfo # eslint .eslintcache - -# env -.env diff --git a/.prettierignore b/.prettierignore deleted file mode 120000 index 3e4e48b..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -.gitignore \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..951d0ad --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +cache +.cache +package.json +package-lock.json +public +CHANGELOG.md +.yarn +dist +node_modules +.next +build +.contentlayer diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 5a7a8da..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "printWidth": 100, - "arrowParens": "avoid" -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..359e733 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "bierner.github-markdown-preview", + "davidanson.vscode-markdownlint", + "irongeek.vscode-env", + "dbaeumer.vscode-eslint", + "ms-azuretools.vscode-docker", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5232518 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "files.associations": { + "*.css": "tailwindcss" + }, + "tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]], + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/README.md b/README.md index 6476010..359b104 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ A web clone of [DevToys](https://github.com/veler/DevToys) +## Known issues + +- [Editor may not resize to fit container size](https://github.com/suren-atoyan/monaco-react/issues/346) +- CSS outlines messed up + ## Todo - [x] Add site layout @@ -15,5 +20,5 @@ A web clone of [DevToys](https://github.com/veler/DevToys) - [ ] Graphic - [ ] Settings - [x] Settings menu item - - [ ] Support dark mode + - [x] Support dark mode - [ ] Support i18n diff --git a/app/apple-icon.png b/app/apple-icon.png new file mode 100644 index 0000000..4e7b546 Binary files /dev/null and b/app/apple-icon.png differ diff --git a/app/converters/json-yaml/layout.tsx b/app/converters/json-yaml/layout.tsx new file mode 100644 index 0000000..e4aa120 --- /dev/null +++ b/app/converters/json-yaml/layout.tsx @@ -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; +} diff --git a/app/converters/json-yaml/page.tsx b/app/converters/json-yaml/page.tsx new file mode 100644 index 0000000..41b3ecf --- /dev/null +++ b/app/converters/json-yaml/page.tsx @@ -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(() => , []); + + const indentationConfig = ( + + + + + + 2 spaces + 4 spaces + + + } + /> + ); + + const jsonPasteButton = useMemo( + () => , + [setJsonReactively] + ); + + const yamlPasteButton = useMemo( + () => , + [setYamlReactively] + ); + + const jsonFileButton = useMemo( + () => ( + + ), + [setJsonReactively] + ); + + const yamlFileButton = useMemo( + () => ( + + ), + [setYamlReactively] + ); + + const jsonCopyButton = ; + const yamlCopyButton = ; + + const clearButton = useMemo( + () => , + [clearBoth] + ); + + const jsonControl = ( + + ); + + const yamlControl = ( + + ); + + return ( + + + + +
+ + + + + + +
+
+ ); +} diff --git a/app/converters/layout.tsx b/app/converters/layout.tsx new file mode 100644 index 0000000..c13501d --- /dev/null +++ b/app/converters/layout.tsx @@ -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; +} diff --git a/app/converters/number-base/layout.tsx b/app/converters/number-base/layout.tsx new file mode 100644 index 0000000..67c26f8 --- /dev/null +++ b/app/converters/number-base/layout.tsx @@ -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; +} diff --git a/app/converters/number-base/page.tsx b/app/converters/number-base/page.tsx new file mode 100644 index 0000000..c99d82f --- /dev/null +++ b/app/converters/number-base/page.tsx @@ -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(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(() => , []); + + const formatNumberConfig = useMemo( + () => ( + + } + /> + ), + [format, formatNumberIcon] + ); + + const decPasteButton = useMemo(() => , [trySetDec]); + const hexPasteButton = useMemo(() => , [trySetHex]); + const octPasteButton = useMemo(() => , [trySetOct]); + const binPasteButton = useMemo(() => , [trySetBin]); + + const decControl = ; + const hexControl = ; + const octControl = ; + const binControl = ; + + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/converters/page.tsx b/app/converters/page.tsx new file mode 100644 index 0000000..1e70aba --- /dev/null +++ b/app/converters/page.tsx @@ -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 ( + + + + ); +} diff --git a/app/encoders-decoders/base64/layout.tsx b/app/encoders-decoders/base64/layout.tsx new file mode 100644 index 0000000..4b34446 --- /dev/null +++ b/app/encoders-decoders/base64/layout.tsx @@ -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; +} diff --git a/app/encoders-decoders/base64/page.tsx b/app/encoders-decoders/base64/page.tsx new file mode 100644 index 0000000..2613516 --- /dev/null +++ b/app/encoders-decoders/base64/page.tsx @@ -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("�")) { + 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( + () => , + [setDecodedReactively] + ); + + const encodedPasteButton = useMemo( + () => , + [setEncodedReactively] + ); + + const decodedFileButton = useMemo( + () => ( + + ), + [setDecodedReactively] + ); + + const encodedFileButton = useMemo( + () => ( + + ), + [setEncodedReactively] + ); + + const decodedCopyButton = useMemo(() => , [decoded]); + const encodedCopyButton = useMemo(() => , [encoded]); + + const clearButton = useMemo( + () => , + [clearBoth] + ); + + const decodedControl = ( + + ); + + const encodedControl = ( + + ); + + return ( + + +