From 91fe3262610c871f8ce861099574c3d051c336e4 Mon Sep 17 00:00:00 2001 From: andres Date: Tue, 14 May 2024 22:26:00 +0200 Subject: [PATCH] feat: implement text diff tool --- app/text/diff/layout.tsx | 17 +++ app/text/diff/page.tsx | 140 ++++++++++++++++++++++++ components/buttons/index.ts | 1 + components/buttons/toggle-full-size.tsx | 22 ++++ components/icons.tsx | 6 + components/panel-resize-handler.tsx | 36 ++++++ config/persistence-keys.ts | 8 ++ config/tools.ts | 12 +- package.json | 1 + pnpm-lock.yaml | 52 +++------ 10 files changed, 259 insertions(+), 36 deletions(-) create mode 100644 app/text/diff/layout.tsx create mode 100644 app/text/diff/page.tsx create mode 100644 components/buttons/toggle-full-size.tsx create mode 100644 components/panel-resize-handler.tsx create mode 100644 config/persistence-keys.ts diff --git a/app/text/diff/layout.tsx b/app/text/diff/layout.tsx new file mode 100644 index 0000000..fdd5631 --- /dev/null +++ b/app/text/diff/layout.tsx @@ -0,0 +1,17 @@ +import { Metadata } from "next"; + +import { toolGroups } from "@/config/tools"; + +export const metadata: Metadata = { + title: toolGroups.text.tools.inspector_and_case_converter.longTitle, + description: toolGroups.text.tools.inspector_and_case_converter.description, + robots: { + googleBot: { + index: true, + }, + }, +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/text/diff/page.tsx b/app/text/diff/page.tsx new file mode 100644 index 0000000..d1c59cc --- /dev/null +++ b/app/text/diff/page.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { Panel, PanelGroup } from "react-resizable-panels"; + +import { PERSISTENCE_KEY } from "@/config/persistence-keys"; +import { toolGroups } from "@/config/tools"; +import { cn } from "@/lib/style"; +import { DiffEditor } from "@/components/ui/diff-editor"; +import { Editor } from "@/components/ui/editor"; +import * as Button from "@/components/buttons"; +import { Configuration } from "@/components/configuration"; +import { Configurations } from "@/components/configurations"; +import { ControlMenu } from "@/components/control-menu"; +import * as Icon from "@/components/icons"; +import * as icons from "@/components/icons"; +import { Rows } from "@/components/icons"; +import { LabeledSwitch } from "@/components/labeled-switch"; +import { PageRootSection } from "@/components/page-root-section"; +import { PageSection } from "@/components/page-section"; +import { PanelResizeHandle } from "@/components/panel-resize-handler"; + +/** No particular reason for these sizes, just feels like a good balance */ +const VERTICAL_PANEL_MAX_SIZE = 80; +const HORIZONTAL_PANEL_MAX_SIZE = 90; +const PANEL_FULL_SIZE = 100; + +export default function Page() { + const [input1, setInput1] = useState("Hello world"); + const [input2, setInput2] = useState("Hello, World!"); + const [diffFullHeight, setDiffFullHeight] = useState(false); + const [inlineMode, setInlineMode] = useState(false); + const diffPanelMaxSize = useMemo( + () => (diffFullHeight ? PANEL_FULL_SIZE : VERTICAL_PANEL_MAX_SIZE), + [diffFullHeight] + ); + + const clearInput1 = useCallback(() => setInput1(""), [setInput1]); + const clearInput2 = useCallback(() => setInput2(""), [setInput2]); + const toggleFullHeight = useCallback(() => setDiffFullHeight(prev => !prev), [setDiffFullHeight]); + + const inlineModeConfig = useMemo( + () => ( + } + title="Inline mode" + control={ + + } + /> + ), + [inlineMode, setInlineMode] + ); + const input1Control = useMemo( + () => ( + , + , + , + ]} + /> + ), + [setInput1, clearInput1] + ); + const input2Control = useMemo( + () => ( + , + , + , + ]} + /> + ), + [setInput2, clearInput2] + ); + + const diffControl = useMemo( + () => ( + , + ]} + /> + ), + [diffFullHeight, toggleFullHeight] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/components/buttons/index.ts b/components/buttons/index.ts index 0ebf6b1..dfec7ed 100644 --- a/components/buttons/index.ts +++ b/components/buttons/index.ts @@ -2,3 +2,4 @@ export * from "./clear"; export * from "./copy"; export * from "./file"; export * from "./paste"; +export * from "./toggle-full-size"; diff --git a/components/buttons/toggle-full-size.tsx b/components/buttons/toggle-full-size.tsx new file mode 100644 index 0000000..1ed2443 --- /dev/null +++ b/components/buttons/toggle-full-size.tsx @@ -0,0 +1,22 @@ +import { memo } from "react"; +import equal from "react-fast-compare"; + +import * as Icon from "@/components/icons"; + +import { Base, BaseProps } from "./base"; + +export type ToggleFullSizeProps = Omit & { + expanded: boolean; +}; + +function RawToggleFullSize({ expanded, ...props }: ToggleFullSizeProps) { + return ( + : } + labelText={expanded ? "Collapse" : "Expand"} + /> + ); +} + +export const ToggleFullSize = memo(RawToggleFullSize, equal); diff --git a/components/icons.tsx b/components/icons.tsx index 9677b01..6560170 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -14,15 +14,21 @@ export const ChevronDown = memo(icons.ChevronDown, equal); export const Clipboard = memo(icons.Clipboard, equal); export const Code = memo(icons.Code2, equal); export const Copy = memo(icons.Copy, equal); +export const Diff = memo(icons.Diff, equal); export const Equal = memo(icons.Equal, equal); export const File = memo(icons.FileIcon, equal); export const Fingerprint = memo(icons.Fingerprint, equal); +export const GripHorizontal = memo(icons.GripHorizontal, equal); +export const GripVertical = memo(icons.GripVertical, equal); export const Hash = memo(icons.Hash, equal); export const Home = memo(icons.Home, equal); export const Key = memo(icons.Key, equal); export const Link = memo(icons.Link2, equal); +export const Maximize = memo(icons.Maximize2, equal); +export const Minimize = memo(icons.Minimize2, equal); export const PackagePlus = memo(icons.PackagePlus, equal); export const Paintbrush = memo(icons.Paintbrush2, equal); +export const Rows = memo(icons.Rows, equal); export const Search = memo(icons.Search, equal); export const Settings = memo(icons.Settings, equal); export const Settings2 = memo(icons.Settings2, equal); diff --git a/components/panel-resize-handler.tsx b/components/panel-resize-handler.tsx new file mode 100644 index 0000000..1fe91a5 --- /dev/null +++ b/components/panel-resize-handler.tsx @@ -0,0 +1,36 @@ +import { ComponentPropsWithoutRef, useMemo } from "react"; +import { PanelResizeHandle as PanelResizeHandlePrimitive } from "react-resizable-panels"; + +import { cn } from "@/lib/style"; +import * as Icon from "@/components/icons"; + +type Props = { + direction?: "vertical" | "horizontal"; +} & ComponentPropsWithoutRef; + +export const PanelResizeHandle = ({ direction = "vertical", className, ...props }: Props) => { + const isVertical = direction === "vertical"; + const isHorizontal = direction === "horizontal"; + + const classNames = useMemo( + () => + cn( + isVertical && "w-4", + isHorizontal && "h-4", + "flex items-center justify-center", + "data-[resize-handle-state=drag]:bg-neutral-200", + "dark:data-[resize-handle-state=drag]:bg-neutral-600", + "data-[resize-handle-state=hover]:bg-neutral-300", + "dark:data-[resize-handle-state=hover]:bg-neutral-700", + className + ), + [isVertical, isHorizontal, className] + ); + + return ( + + {isVertical && } + {isHorizontal && } + + ); +}; diff --git a/config/persistence-keys.ts b/config/persistence-keys.ts new file mode 100644 index 0000000..c3d562a --- /dev/null +++ b/config/persistence-keys.ts @@ -0,0 +1,8 @@ +export const PERSISTENCE_KEY = { + panels: { + textDiff: { + vertical: "panels/text-diff/vertical", + horizontal: "panels/text-diff/horizontal", + }, + }, +} as const; diff --git a/config/tools.ts b/config/tools.ts index 92f0be8..fc46afc 100644 --- a/config/tools.ts +++ b/config/tools.ts @@ -55,7 +55,7 @@ export const toolGroups = { shortTitle: "HTML", longTitle: "HTML Encoder / Decoder", description: - "Encode or decode all the applicable characters to their corresponding HTML entities", + "Encode or decode all the applicable characters to their corresponding HTML entities", keywords: "html encoder escaper decocder unescaper", href: "/encoders-decoders/html", }, @@ -64,7 +64,7 @@ export const toolGroups = { shortTitle: "URL", longTitle: "URL Encoder / Decoder", description: - "Encode or decode all the applicable characters to their corresponding URL entities", + "Encode or decode all the applicable characters to their corresponding URL entities", keywords: "url encoder escaper decocder unescaper", href: "/encoders-decoders/url", }, @@ -137,6 +137,14 @@ export const toolGroups = { keywords: "case converter convert text inspector inspect", href: "/text/inspector", }, + diff: { + Icon: icons.Diff, + shortTitle: "Text diff", + longTitle: "Text Inspector & Case Converter", + description: "Analyze text and convert it to a different case", + keywords: "case converter convert text inspector inspect", + href: "/text/diff", + }, }, }, } as const satisfies ToolGroups; diff --git a/package.json b/package.json index 9e7d522..e7bb4ff 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "react-day-picker": "^8.7.1", "react-dom": "18.2.0", "react-fast-compare": "^3.2.2", + "react-resizable-panels": "^2.0.19", "sharp": "^0.32.1", "tailwindcss-animate": "^1.0.5", "url-slug": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91d3308..52f3952 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@monaco-editor/react': specifier: ^4.5.1 @@ -86,6 +82,9 @@ dependencies: react-fast-compare: specifier: ^3.2.2 version: 3.2.2 + react-resizable-panels: + specifier: ^2.0.19 + version: 2.0.19(react-dom@18.2.0)(react@18.2.0) sharp: specifier: ^0.32.1 version: 0.32.1 @@ -2831,35 +2830,6 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.9)(eslint@8.41.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 5.59.7(eslint@8.41.0)(typescript@5.0.4) - debug: 3.2.7 - eslint: 8.41.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -2912,7 +2882,7 @@ packages: doctrine: 2.1.0 eslint: 8.41.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.9)(eslint@8.41.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.7)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.41.0) has: 1.0.4 is-core-module: 2.13.0 is-glob: 4.0.3 @@ -4563,6 +4533,16 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.7)(react@18.2.0) dev: false + /react-resizable-panels@2.0.19(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-v3E41kfKSuCPIvJVb4nL4mIZjjKIn/gh6YqZF/gDfQDolv/8XnhJBek4EiV2gOr3hhc5A3kOGOayk3DhanpaQw==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.7)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -5506,3 +5486,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false