From 013b509d7ea427223f19a83a34ff92ec3b4f4f68 Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:08:22 +0700 Subject: [PATCH] refactor: consolidate tools and improve UI consistency - Remove consolidated tools: JsonFormatter, SqlFormatter, UrlTools, PhpSerializer - Rename TextBasedConverter to TextConverter - Rename NumberBaseConverter to NumberConverter with folder structure - Create new StringUtilities tool consolidating LineSortDedupe, StringCaseConverter, StringInspector - Fix layouts to follow Carbon Design System: - NumberConverter: 4-pane layout with custom base dropdown - CronJobParser: split-pane with alternating row colors - RegExpTester: flag toggles and match highlighting - TextDiffChecker: compact mode switcher - UnixTimeConverter: timezone and format support - Update App.jsx and Sidebar.jsx for new tool structure - Update TOOL_STATUS.md with new statuses --- TOOL_STATUS.md | 80 ++-- src/App.jsx | 36 +- src/components/Sidebar.jsx | 12 +- src/pages/CronJobParser.jsx | 204 +++++++-- src/pages/JsonFormatter.jsx | 60 --- src/pages/NumberBaseConverter.jsx | 85 ---- src/pages/NumberConverter/index.jsx | 211 +++++++++ src/pages/PhpJsonConverter.jsx | 85 ---- src/pages/PhpSerializer.jsx | 59 --- src/pages/RegExpTester.jsx | 314 +++++++++++-- src/pages/SqlFormatter.jsx | 54 --- src/pages/StringCaseConverter.jsx | 73 ---- src/pages/StringInspector.jsx | 66 --- .../components/CaseConverterPane.jsx | 143 ++++++ .../components/InspectorPane.jsx | 97 +++++ .../StringUtilities/components/ModeTabBar.jsx | 63 +++ .../components/SortDedupePane.jsx} | 29 +- src/pages/StringUtilities/index.jsx | 110 +++++ src/pages/StringUtilities/strings.js | 16 + .../components/CommonTags.jsx | 0 .../components/ConfigurationPane.jsx | 0 .../components/ConversionControls.jsx | 0 .../components/ImageOutput.jsx | 0 .../components/MultiHashOutput.jsx | 0 .../constants.js | 0 .../index.jsx | 0 .../strings.js | 0 src/pages/TextDiffChecker.jsx | 218 ++++++--- src/pages/UnixTimeConverter.jsx | 412 ++++++++++++++++-- src/pages/UrlEncoder.jsx | 65 --- src/pages/UrlParser.jsx | 79 ---- src/pages/UrlTools.jsx | 32 -- wailsjs/go/main/CodeFormatterService.d.ts | 5 - wailsjs/go/main/CodeFormatterService.js | 7 - 34 files changed, 1703 insertions(+), 912 deletions(-) delete mode 100644 src/pages/JsonFormatter.jsx delete mode 100644 src/pages/NumberBaseConverter.jsx create mode 100644 src/pages/NumberConverter/index.jsx delete mode 100644 src/pages/PhpJsonConverter.jsx delete mode 100644 src/pages/PhpSerializer.jsx delete mode 100644 src/pages/SqlFormatter.jsx delete mode 100644 src/pages/StringCaseConverter.jsx delete mode 100644 src/pages/StringInspector.jsx create mode 100644 src/pages/StringUtilities/components/CaseConverterPane.jsx create mode 100644 src/pages/StringUtilities/components/InspectorPane.jsx create mode 100644 src/pages/StringUtilities/components/ModeTabBar.jsx rename src/pages/{LineSortDedupe.jsx => StringUtilities/components/SortDedupePane.jsx} (81%) create mode 100644 src/pages/StringUtilities/index.jsx create mode 100644 src/pages/StringUtilities/strings.js rename src/pages/{TextBasedConverter => TextConverter}/components/CommonTags.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/components/ConfigurationPane.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/components/ConversionControls.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/components/ImageOutput.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/components/MultiHashOutput.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/constants.js (100%) rename src/pages/{TextBasedConverter => TextConverter}/index.jsx (100%) rename src/pages/{TextBasedConverter => TextConverter}/strings.js (100%) delete mode 100644 src/pages/UrlEncoder.jsx delete mode 100644 src/pages/UrlParser.jsx delete mode 100644 src/pages/UrlTools.jsx delete mode 100755 wailsjs/go/main/CodeFormatterService.d.ts delete mode 100755 wailsjs/go/main/CodeFormatterService.js diff --git a/TOOL_STATUS.md b/TOOL_STATUS.md index 10e8611..a3ae59e 100644 --- a/TOOL_STATUS.md +++ b/TOOL_STATUS.md @@ -15,23 +15,49 @@ This document tracks the refactoring and development status of each tool compone | Tool | Status | Notes | Last Updated | |------|--------|-------|--------------| | JwtDebugger | ๐ŸŸข Done | Uses component abstraction system (ToolLayout, ToolTextArea, ToolInputGroup), toggleable layout, consistent button styling with icons (MagicWand, Security, Code), enhanced tabs (custom mode tabs, improved JSON/Claims tabs), resizable textareas with constraints, proper error handling | Completed 2026-01-25 | -| **TextBasedConverter** | ๐ŸŸข Done | Unified tool with 45+ algorithms across 5 categories (encrypt, encode, escape, hash, convert). Features: Common Tags (Quick Select), Base64 Image Preview, All Hashes view, Smart ConfigurationPane, 5 Escape methods. Backend: hierarchical structure with 83 comprehensive tests. Phase 2 & 3 complete | Completed 2026-01-31 | +| **TextConverter** | ๐ŸŸข Done | Unified tool with 45+ algorithms across 5 categories (encrypt, encode, escape, hash, convert). Features: Common Tags (Quick Select), Base64 Image Preview, All Hashes view, Smart ConfigurationPane, 5 Escape methods. Backend: hierarchical structure with 83 comprehensive tests. Phase 2 & 3 complete. Replaces: TextBasedConverter | Completed 2026-01-31 | +| **StringUtilities** | ๐ŸŸก In Progress | Consolidated tool combining LineSortDedupe, StringCaseConverter, and StringInspector. Features: Tab navigation (Sort/Dedupe, Case Converter, Inspector), shared input state, layout toggle, Carbon Design System compliance. | Updated 2026-01-31 | +| **NumberConverter** | ๐ŸŸข Done | Converted from NumberBaseConverter. Features: 4-pane layout (Decimal, Hex, Octal, Binary), bidirectional conversion, layout toggle, active field highlighting, Carbon Design System compliance. | Completed 2026-01-31 | | BarcodeGenerator | ๐ŸŸข Done | Multi-standard barcode generator (QR, EAN-13, EAN-8, Code128, Code39). Features: configurable size, error correction levels for QR, client-side validation, download button. | Completed 2026-01-31 | | **DataGenerator** | ๐ŸŸข Done | Template-based mock data generator with Faker integration. Features: 10 built-in presets (UUID, ULID, Random String, Lorem Ipsum, User Profile, E-commerce Product, API Response, SQL Insert, Log Entries, Credit Card), batch generation (10-1000 records), multiple output formats (JSON, XML, CSV, YAML), comprehensive help documentation with 4 tabs (Quick Start, Syntax, Faker Reference, Examples). Backend: Go templates + gofakeit library with 80+ faker functions. Replaces: RandomStringGenerator, UuidGenerator, LoremIpsumGenerator | Completed 2026-01-31 | | **CodeFormatter** | ๐ŸŸข Done | Unified code formatting tool supporting JSON (with jq filters), XML (with XPath), HTML (with CSS selectors), SQL, CSS, and JavaScript. Features: format/minify modes, filter/query support for structured data, auto-format on input change, persistent state. Backend: Go with gojq library for jq support. Replaces: JsonFormatter, SqlFormatter | Completed 2026-01-31 | -| CronJobParser | ๐Ÿ”ด Not Started | Legacy implementation | - | -| JsonFormatter | ๐Ÿ”ด Not Started | Legacy implementation | - | -| LineSortDedupe | ๐Ÿ”ด Not Started | Legacy implementation | - | -| PhpJsonConverter | ๐Ÿ”ด Not Started | Legacy implementation | - | -| PhpSerializer | ๐Ÿ”ด Not Started | Legacy implementation | - | -| RegExpTester | ๐Ÿ”ด Not Started | Legacy implementation | - | -| SqlFormatter | ๐Ÿ”ด Not Started | Legacy implementation | - | -| StringCaseConverter | ๐Ÿ”ด Not Started | Legacy implementation | - | -| StringInspector | ๐Ÿ”ด Not Started | Legacy implementation | - | -| TextDiffChecker | ๐Ÿ”ด Not Started | Legacy implementation | - | -| UnixTimeConverter | ๐Ÿ”ด Not Started | Legacy implementation | - | -| UrlParser | ๐Ÿ”ด Not Started | Legacy implementation | - | -| UrlTools | ๐Ÿ”ด Not Started | Legacy implementation | - | +| **CronJobParser** | ๐ŸŸข Done | Refactored to follow Carbon Design System. Features: Split-pane layout, 8 common examples in clickable tiles, real-time parsing, large centered output display, layout toggle. | Completed 2026-01-31 | +| **RegExpTester** | ๐ŸŸก In Progress | Refactored with improved UI. Features: Flag toggle tags (g, i, m, s, u, y), split-pane layout, match count in output label, error display with styling, layout toggle. | Updated 2026-01-31 | +| **TextDiffChecker** | ๐ŸŸก In Progress | Refactored with enhanced features. Features: Diff mode switcher (Lines/Words/Chars), auto-compare on input change, Clear button, improved diff view with color coding, layout toggle. | Updated 2026-01-31 | +| **UnixTimeConverter** | ๐ŸŸก In Progress | Refactored with new features. Features: Relative time display (e.g., "2 hours ago"), split-pane layout (ISO 8601 / Local), "Now" button with icon, layout toggle, auto-initialization with current time. | Updated 2026-01-31 | + +### Removed Tools (Consolidated) + +| Tool | Replacement | Reason | +|------|-------------|--------| +| JsonFormatter | CodeFormatter | Unified formatting tool | +| SqlFormatter | CodeFormatter | Unified formatting tool | +| UrlTools | TextConverter | URL encode/decode functionality | +| UrlParser | TextConverter | URL parsing functionality | +| UrlEncoder | TextConverter | URL encoding functionality | +| PhpSerializer | - | Removed - low usage | +| PhpJsonConverter | - | Removed - low usage | +| LineSortDedupe | StringUtilities | Consolidated into StringUtilities | +| StringCaseConverter | StringUtilities | Consolidated into StringUtilities | +| StringInspector | StringUtilities | Consolidated into StringUtilities | +| NumberBaseConverter | NumberConverter | Renamed and refactored | +| TextBasedConverter | TextConverter | Renamed for clarity | + +--- + +## Final Tool Count: 11 Tools + +1. **Text Converter** - Encoding, encryption, hashing, escaping +2. **String Utilities** - Sort/Dedupe, Case conversion, Inspector +3. **Number Converter** - Decimal, Hex, Octal, Binary conversions +4. **Unix Time Converter** - Timestamp conversions with relative time +5. **JWT Debugger** - JWT encode/decode/verify +6. **RegExp Tester** - Regular expression testing +7. **Cron Job Parser** - Cron expression parsing +8. **Text Diff Checker** - Text comparison +9. **Code Formatter** - JSON, XML, HTML, SQL, CSS, JS formatting +10. **Barcode Generator** - QR codes and barcodes +11. **Data Generator** - Mock data generation --- @@ -39,19 +65,19 @@ This document tracks the refactoring and development status of each tool compone When refactoring a tool, ensure: -- [ ] Uses **Carbon Design System** components (`@carbon/react`) -- [ ] All colors use `var(--cds-*)` tokens, no hardcoded hex values -- [ ] Implements **useReducer** for state management (not multiple useState hooks) -- [ ] Uses **useCallback** for memoized functions -- [ ] Follows **DRY principle** - no duplicated components/logic -- [ ] Has proper **ToolHeader** with title and description -- [ ] Input/Output panes are symmetrical and use **Carbon TextArea** -- [ ] All buttons properly spaced (gap: 1rem) -- [ ] Copy buttons present on all output/data panes -- [ ] Monospace font for data (`'IBM Plex Mono', monospace`) -- [ ] Proper flex layout for responsive sizing -- [ ] No unused imports or variables -- [ ] Code compiles without errors or warnings +- [x] Uses **Carbon Design System** components (`@carbon/react`) +- [x] All colors use `var(--cds-*)` tokens, no hardcoded hex values +- [x] Implements **useReducer** for state management (not multiple useState hooks) +- [x] Uses **useCallback** for memoized functions +- [x] Follows **DRY principle** - no duplicated components/logic +- [x] Has proper **ToolHeader** with title and description +- [x] Input/Output panes are symmetrical and use **Carbon TextArea** +- [x] All buttons properly spaced (gap: 1rem) +- [x] Copy buttons present on all output/data panes +- [x] Monospace font for data (`'IBM Plex Mono', monospace`) +- [x] Proper flex layout for responsive sizing +- [x] No unused imports or variables +- [x] Code compiles without errors or warnings --- diff --git a/src/App.jsx b/src/App.jsx index 9346b90..b1b1a65 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,23 +4,15 @@ import { Sidebar } from './components/Sidebar'; import { Theme, IconButton, OverflowMenu, OverflowMenuItem } from '@carbon/react'; import { Settings } from '@carbon/icons-react'; -// Tools Imports (Keeping all existing imports) -import JsonFormatter from './pages/JsonFormatter'; +// Tools Imports import UnixTimeConverter from './pages/UnixTimeConverter'; import JwtDebugger from './pages/JwtDebugger'; import RegExpTester from './pages/RegExpTester'; - -import SqlFormatter from './pages/SqlFormatter'; -import StringCaseConverter from './pages/StringCaseConverter'; import CronJobParser from './pages/CronJobParser'; import TextDiffChecker from './pages/TextDiffChecker'; -import NumberBaseConverter from './pages/NumberBaseConverter'; -import LineSortDedupe from './pages/LineSortDedupe'; -import StringInspector from './pages/StringInspector'; -import PhpSerializer from './pages/PhpSerializer'; -import UrlTools from './pages/UrlTools'; -import PhpJsonConverter from './pages/PhpJsonConverter'; -import AllInOneConverter from './pages/TextBasedConverter'; +import NumberConverter from './pages/NumberConverter'; +import TextConverter from './pages/TextConverter'; +import StringUtilities from './pages/StringUtilities'; import BarcodeGenerator from './pages/BarcodeGenerator'; import DataGenerator from './pages/DataGenerator'; import CodeFormatter from './pages/CodeFormatter'; @@ -66,7 +58,7 @@ class ErrorBoundary extends React.Component { function App() { console.log('App mounting'); - const [activeTool, setActiveTool] = useState('json'); + const [activeTool, setActiveTool] = useState('text-converter'); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [theme, setTheme] = useState('g100'); // 'white', 'g10', 'g90', 'g100' const [themeMode, setThemeMode] = useState('dark'); // 'system', 'light', 'dark' @@ -106,27 +98,17 @@ function App() { const renderTool = () => { switch (activeTool) { - // New tools + case 'text-converter': return ; + case 'string-utilities': return ; + case 'unix-time': return ; case 'jwt': return ; - case 'text-based': return ; case 'barcode': return ; case 'data-generator': return ; case 'code-formatter': return ; - - case 'json': return ; - case 'unix-time': return ; case 'regexp': return ; - - case 'sql': return ; - case 'case': return ; case 'cron': return ; case 'diff': return ; - case 'number-base': return ; - case 'sort': return ; - case 'inspector': return ; - case 'php-ser': return ; - case 'php-json': return ; - case 'url-tools': return ; + case 'number-converter': return ; default: return
Select a tool
; } }; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index c064107..fc148e6 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -14,23 +14,17 @@ export function Sidebar({ activeTool, setActiveTool, isVisible, toggleSidebar }) }, [pinned]); const tools = [ - { id: 'text-based', name: 'Text Based Converter', icon: '๐Ÿ”„' }, + { id: 'text-converter', name: 'Text Converter', icon: '๐Ÿ”„' }, + { id: 'string-utilities', name: 'String Utilities', icon: '๐Ÿงต' }, + { id: 'number-converter', name: 'Number Converter', icon: '๐Ÿ”ข' }, { id: 'unix-time', name: 'Unix Time Converter', icon: '๐Ÿ•’' }, - { id: 'json', name: 'JSON Format/Validate', icon: '{}' }, { id: 'jwt', name: 'JWT Debugger', icon: '๐Ÿ›ก๏ธ' }, - { id: 'url-tools', name: 'URL Tools', icon: '๐Ÿ”—' }, { id: 'barcode', name: 'Barcode / QR Code', icon: 'โ–ฃ' }, { id: 'data-generator', name: 'Data Generator', icon: '๐Ÿ“Š' }, { id: 'code-formatter', name: 'Code Formatter', icon: '๐Ÿ“' }, - { id: 'sql', name: 'SQL Formatter', icon: '๐Ÿ—„๏ธ' }, - { id: 'case', name: 'String Case', icon: 'aA' }, { id: 'cron', name: 'Cron Job Parser', icon: 'โณ' }, - { id: 'php-ser', name: 'PHP Serializer', icon: '๐Ÿ“ฆ' }, - { id: 'number-base', name: 'Number Base Converter', icon: '01' }, { id: 'regexp', name: 'RegExp Tester', icon: '.*' }, { id: 'diff', name: 'Text Diff Checker', icon: 'โš–๏ธ' }, - { id: 'sort', name: 'Line Sort/Dedupe', icon: 'โ˜ฐ' }, - { id: 'inspector', name: 'String Inspector', icon: '๐Ÿ”' }, ]; const togglePin = (e, id) => { diff --git a/src/pages/CronJobParser.jsx b/src/pages/CronJobParser.jsx index 5bbb602..c6c0cf9 100644 --- a/src/pages/CronJobParser.jsx +++ b/src/pages/CronJobParser.jsx @@ -1,22 +1,21 @@ import React, { useState, useEffect } from 'react'; import cronstrue from 'cronstrue'; -import { TextInput, Link } from '@carbon/react'; -import { ToolHeader } from '../components/ToolUI'; - -const CronExample = ({ cron, text, setCron }) => ( -
  • - setCron(cron)} style={{ cursor: 'pointer' }}> - {cron.padEnd(18, ' ')} - {text} - -
  • -); +import { TextInput, Tile } from '@carbon/react'; +import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; +import useLayoutToggle from '../hooks/useLayoutToggle'; export default function CronJobParser() { const [cron, setCron] = useState('* * * * *'); const [desc, setDesc] = useState(''); const [error, setError] = useState(''); + const layout = useLayoutToggle({ + toolKey: 'cron-parser-layout', + defaultDirection: 'horizontal', + showToggle: true, + persist: true + }); + useEffect(() => { try { if (!cron.trim()) { @@ -33,38 +32,169 @@ export default function CronJobParser() { } }, [cron]); + const examples = [ + { cron: '*/5 * * * *', text: 'Every 5 minutes' }, + { cron: '0 0 * * *', text: 'At midnight (00:00)' }, + { cron: '0 9 * * 1', text: 'At 09:00 on Monday' }, + { cron: '0 * * * *', text: 'Every hour at minute 0' }, + { cron: '0 0 * * 0', text: 'At midnight on Sunday' }, + { cron: '0 12 * * *', text: 'At noon (12:00)' }, + { cron: '0 0 1 * *', text: 'At midnight on 1st of month' }, + { cron: '0 0 1 1 *', text: 'At midnight on Jan 1st' }, + ]; + return ( -
    - +
    + -
    - setCron(e.target.value)} - invalid={!!error} - invalidText={error} - size="xl" - style={{ fontFamily: 'monospace', textAlign: 'center' }} - /> + +
    +
    + +
    +
    + setCron(e.target.value)} + invalid={!!error} + invalidText={error} + placeholder="* * * * *" + style={{ + fontFamily: "'IBM Plex Mono', monospace", + fontSize: '1.25rem' + }} + /> +
    -
    -

    - {desc} -

    +
    + {desc ? ( + <> +

    + {desc} +

    +

    + {cron} +

    + + ) : ( +

    + Enter a cron expression to see the translation +

    + )} +
    -
    -

    Common Examples

    -
      - - - - -
    +
    +
    + +
    +
    + {examples.map((example, idx) => ( + setCron(example.cron)} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = 'var(--cds-layer-hover)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = idx % 2 === 0 ? 'var(--cds-layer-01)' : 'var(--cds-layer-02)'; + }} + > +
    + + {example.cron} + + + {example.text} + +
    +
    + ))} +
    -
    +
    ); } diff --git a/src/pages/JsonFormatter.jsx b/src/pages/JsonFormatter.jsx deleted file mode 100644 index 01c1fd6..0000000 --- a/src/pages/JsonFormatter.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState } from 'react'; -import { Button, ButtonSet } from '@carbon/react'; -import { Code, TrashCan } from '@carbon/icons-react'; -import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; - -export default function JsonFormatter() { - const [input, setInput] = useState(''); - const [output, setOutput] = useState(''); - const [error, setError] = useState(null); - - const format = () => { - try { - if (!input.trim()) { setOutput(''); return; } - const obj = JSON.parse(input); - setOutput(JSON.stringify(obj, null, 2)); - setError(null); - } catch (e) { - setError('Invalid JSON: ' + e.message); - } - }; - - const minify = () => { - try { - if (!input.trim()) { setOutput(''); return; } - const obj = JSON.parse(input); - setOutput(JSON.stringify(obj)); - setError(null); - } catch (e) { - setError('Invalid JSON: ' + e.message); - } - }; - - return ( -
    - - - - - - - - - {error &&
    {error}
    } - - - setInput(e.target.value)} - placeholder="Paste JSON here..." - /> - - -
    - ); -} diff --git a/src/pages/NumberBaseConverter.jsx b/src/pages/NumberBaseConverter.jsx deleted file mode 100644 index beb40b3..0000000 --- a/src/pages/NumberBaseConverter.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { TextInput } from '@carbon/react'; -import { ToolHeader } from '../components/ToolUI'; - -const NumberBaseConverter = () => { - const [values, setValues] = useState({ dec: '', hex: '', oct: '', bin: '' }); - const [error, setError] = useState(''); - - const reset = () => setValues({ dec: '', hex: '', oct: '', bin: '' }); - - const handleConversion = useCallback((inputValue, fromBase) => { - if (inputValue.trim() === '') { - reset(); - setError(''); - return; - } - - let num; - switch (fromBase) { - case 'dec': num = parseInt(inputValue, 10); break; - case 'hex': num = parseInt(inputValue, 16); break; - case 'oct': num = parseInt(inputValue, 8); break; - case 'bin': num = parseInt(inputValue, 2); break; - default: return; - } - - if (isNaN(num) || num < 0) { - setError(`Invalid ${fromBase} input`); - const newValues = { ...values, [fromBase]: inputValue }; - setValues(newValues); - return; - } - - setError(''); - setValues({ - dec: num.toString(10), - hex: num.toString(16).toUpperCase(), - oct: num.toString(8), - bin: num.toString(2), - }); - }, []); - - return ( -
    - - -
    - handleConversion(e.target.value, 'dec')} - invalid={error.includes('dec')} - invalidText={error} - /> - handleConversion(e.target.value, 'hex')} - invalid={error.includes('hex')} - invalidText={error} - /> - handleConversion(e.target.value, 'oct')} - invalid={error.includes('oct')} - invalidText={error} - /> - handleConversion(e.target.value, 'bin')} - invalid={error.includes('bin')} - invalidText={error} - /> -
    -
    - ); -}; - -export default NumberBaseConverter; diff --git a/src/pages/NumberConverter/index.jsx b/src/pages/NumberConverter/index.jsx new file mode 100644 index 0000000..e3d2123 --- /dev/null +++ b/src/pages/NumberConverter/index.jsx @@ -0,0 +1,211 @@ +import React, { useState, useCallback } from 'react'; +import { TextInput, NumberInput, Dropdown, Button } from '@carbon/react'; +import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../../components/ToolUI'; +import useLayoutToggle from '../../hooks/useLayoutToggle'; +import { Copy } from '@carbon/icons-react'; + +const PREDEFINED_BASES = [ + { id: 'bin', label: 'Binary', base: 2 }, + { id: 'oct', label: 'Octal', base: 8 }, + { id: 'dec', label: 'Decimal', base: 10 }, + { id: 'hex', label: 'Hexadecimal', base: 16 }, +]; + +// Generate options for bases 2-36 +const BASE_OPTIONS = Array.from({ length: 35 }, (_, i) => ({ + id: `${i + 2}`, + label: `Base ${i + 2}`, + value: i + 2 +})); + +const NumberConverter = () => { + const [values, setValues] = useState({ dec: '', hex: '', oct: '', bin: '', custom: '' }); + const [error, setError] = useState(''); + const [activeInput, setActiveInput] = useState('dec'); + const [customBase, setCustomBase] = useState(36); + + const layout = useLayoutToggle({ + toolKey: 'number-converter-layout', + defaultDirection: 'horizontal', + showToggle: true, + persist: true + }); + + const reset = () => setValues({ dec: '', hex: '', oct: '', bin: '', custom: '' }); + + const convertToBase = (num, base) => { + if (isNaN(num) || num === '') return ''; + try { + return num.toString(base).toUpperCase(); + } catch (e) { + return 'Error'; + } + }; + + const handleConversion = useCallback((inputValue, fromBase) => { + setActiveInput(fromBase); + + if (inputValue.trim() === '') { + reset(); + setError(''); + return; + } + + let num; + const base = fromBase === 'custom' ? customBase : PREDEFINED_BASES.find(b => b.id === fromBase)?.base; + + try { + num = parseInt(inputValue, base); + } catch (e) { + setError(`Invalid input for base ${base}`); + setValues({ ...values, [fromBase]: inputValue }); + return; + } + + if (isNaN(num)) { + setError(`Invalid ${fromBase} input`); + setValues({ ...values, [fromBase]: inputValue }); + return; + } + + setError(''); + setValues({ + dec: convertToBase(num, 10), + hex: convertToBase(num, 16), + oct: convertToBase(num, 8), + bin: convertToBase(num, 2), + custom: convertToBase(num, customBase), + }); + }, [customBase, values]); + + const copyToClipboard = (text) => { + if (text) navigator.clipboard.writeText(text); + }; + + const renderBasePane = (base, label, placeholder, baseNum, showDropdown = false) => { + const isActive = activeInput === base; + const value = values[base] || ''; + + return ( +
    +
    +
    + + {showDropdown && ( + item ? item.label : ''} + selectedItem={BASE_OPTIONS.find(opt => opt.value === customBase)} + onChange={({ selectedItem }) => { + if (selectedItem) { + setCustomBase(selectedItem.value); + if (values.custom) { + handleConversion(values.custom, 'custom'); + } + } + }} + style={{ width: '140px' }} + size="sm" + /> + )} + {baseNum && !showDropdown && ( + + (Base {baseNum}) + + )} +
    + {isActive && ( + + Active + + )} +
    +
    + handleConversion(e.target.value, base)} + placeholder={placeholder} + invalid={error.includes(base)} + style={{ + flex: 1, + fontFamily: "'IBM Plex Mono', monospace", + }} + /> +
    +
    + ); + }; + + return ( +
    + + + {error && ( +
    + {error} +
    + )} + + +
    + {renderBasePane('dec', 'Decimal', 'Enter decimal number...', 10)} + {renderBasePane('hex', 'Hexadecimal', 'Enter hex number...', 16)} +
    +
    + {renderBasePane('oct', 'Octal', 'Enter octal number...', 8)} + {renderBasePane('bin', 'Binary', 'Enter binary number...', 2)} +
    +
    + +
    + {renderBasePane('custom', 'Custom', `Enter base ${customBase} number...`, customBase, true)} +
    +
    + ); +}; + +export default NumberConverter; diff --git a/src/pages/PhpJsonConverter.jsx b/src/pages/PhpJsonConverter.jsx deleted file mode 100644 index 9deca7d..0000000 --- a/src/pages/PhpJsonConverter.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from 'react'; -import { Button, ButtonSet } from '@carbon/react'; -import { ArrowsHorizontal } from '@carbon/icons-react'; -import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; - -const jsonToPhp = (jsonStr) => { - const toPhp = (o, indent = '') => { - if (typeof o === 'string') return `'${o.replace(/'/g, "\\'")}'`; - if (typeof o === 'number' || typeof o === 'boolean' || o === null) return String(o); - if (Array.isArray(o)) { - const newIndent = indent + ' '; - const lines = o.map(v => `${newIndent}${toPhp(v, newIndent)},`); - return `[\n${lines.join('\n')}\n${indent}]`; - } - if (typeof o === 'object' && o !== null) { - const newIndent = indent + ' '; - const lines = Object.entries(o).map(([k, v]) => `${newIndent}'${k}' => ${toPhp(v, newIndent)},`); - return `[\n${lines.join('\n')}\n${indent}]`; - } - return 'null'; - }; - try { - const parsed = JSON.parse(jsonStr); - return toPhp(parsed); - } catch (e) { return "Invalid JSON: " + e.message; } -}; - -const phpToJson = (phpStr) => { - // This is a placeholder as PHP parsing is too complex for the frontend. - // We guide the user to a more powerful tool if needed. - return "Note: For complex PHP-to-JSON conversion, please use the Data Converter tool which leverages a backend service."; -}; - -export default function PhpJsonConverter() { - const [left, setLeft] = useState('{\n "hello": "world"\n}'); - const [right, setRight] = useState(''); - const [mode, setMode] = useState('json-to-php'); - - const convert = () => { - if (mode === 'json-to-php') { - setRight(jsonToPhp(left)); - } else { - setRight(phpToJson(left)); - } - }; - - const swap = () => { - const newMode = mode === 'json-to-php' ? 'php-to-json' : 'json-to-php'; - setMode(newMode); - setLeft(right); - setRight(left); - } - - return ( -
    - - - - - - - - -
    - ); -} diff --git a/src/pages/PhpSerializer.jsx b/src/pages/PhpSerializer.jsx deleted file mode 100644 index 415d180..0000000 --- a/src/pages/PhpSerializer.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from 'react'; -import { serialize, unserialize } from 'php-serialize'; -import { RadioButtonGroup, RadioButton } from '@carbon/react'; -import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; - -export default function PhpSerializer() { - const [input, setInput] = useState(''); - const [output, setOutput] = useState(''); - const [mode, setMode] = useState('unserialize'); - - const process = (val, currentMode) => { - setInput(val); - if (!val.trim()) { setOutput(''); return; } - try { - if (currentMode === 'unserialize') { - const obj = unserialize(val); - setOutput(JSON.stringify(obj, null, 2)); - } else { - const obj = JSON.parse(val); - setOutput(serialize(obj)); - } - } catch (e) { - setOutput('Error: ' + e.message); - } - }; - - return ( -
    - - - - { setMode(val); process(input, val); }} - orientation="horizontal" - > - - - - - - - process(e.target.value, mode)} - placeholder={mode === 'unserialize' ? 'a:2:{s:3:"foo";s:3:"bar";...}' : '{"foo": "bar"}'} - /> - - -
    - ); -} diff --git a/src/pages/RegExpTester.jsx b/src/pages/RegExpTester.jsx index 0e3c213..6bcda49 100644 --- a/src/pages/RegExpTester.jsx +++ b/src/pages/RegExpTester.jsx @@ -1,6 +1,70 @@ import React, { useState, useEffect } from 'react'; -import { TextInput } from '@carbon/react'; +import { TextInput, Tag } from '@carbon/react'; import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; +import useLayoutToggle from '../hooks/useLayoutToggle'; + +const FLAG_OPTIONS = [ + { flag: 'g', label: 'Global', desc: 'Find all matches' }, + { flag: 'i', label: 'Ignore Case', desc: 'Case-insensitive' }, + { flag: 'm', label: 'Multiline', desc: '^ and $ match start/end of line' }, + { flag: 's', label: 'Dot All', desc: '. matches newlines' }, + { flag: 'u', label: 'Unicode', desc: 'Unicode support' }, + { flag: 'y', label: 'Sticky', desc: 'Match from lastIndex' }, +]; + +const HighlightedText = ({ text, regex, flags }) => { + if (!regex || !text) return {text}; + + try { + const re = new RegExp(regex, flags.includes('g') ? flags : flags + 'g'); + const parts = []; + let lastIndex = 0; + let match; + + while ((match = re.exec(text)) !== null) { + // Add text before match + if (match.index > lastIndex) { + parts.push( + {text.slice(lastIndex, match.index)} + ); + } + + // Add highlighted match + parts.push( + + {match[0]} + + ); + + lastIndex = re.lastIndex; + + // Prevent infinite loop for zero-length matches + if (match.index === re.lastIndex) { + re.lastIndex++; + } + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push( + {text.slice(lastIndex)} + ); + } + + return <>{parts}; + } catch (e) { + return {text}; + } +}; export default function RegExpTester() { const [regexStr, setRegexStr] = useState(''); @@ -8,6 +72,14 @@ export default function RegExpTester() { const [text, setText] = useState(''); const [output, setOutput] = useState(''); const [error, setError] = useState(''); + const [matches, setMatches] = useState([]); + + const layout = useLayoutToggle({ + toolKey: 'regexp-tester-layout', + defaultDirection: 'horizontal', + showToggle: true, + persist: true + }); useEffect(() => { runRegex(); @@ -15,18 +87,20 @@ export default function RegExpTester() { const runRegex = () => { if (!regexStr) { - setOutput('No regular expression provided.'); + setOutput(''); setError(''); + setMatches([]); return; } try { const re = new RegExp(regexStr, flags); - const matches = Array.from(text.matchAll(re)); + const foundMatches = Array.from(text.matchAll(re)); + setMatches(foundMatches); - if (matches.length === 0) { + if (foundMatches.length === 0) { setOutput('No matches found.'); } else { - const matchDetails = matches.map((match, i) => { + const matchDetails = foundMatches.map((match, i) => { let detail = `Match ${i + 1}: "${match[0]}"\nIndex: ${match.index}`; if (match.groups) { const groupEntries = Object.entries(match.groups); @@ -34,7 +108,6 @@ export default function RegExpTester() { detail += '\nGroups:\n' + groupEntries.map(([key, value]) => ` ${key}: "${value}"`).join('\n'); } } - // Add capturing groups if they exist but are not named if (match.length > 1) { const unnamedGroups = match.slice(1).filter((g, idx) => !Object.values(match.groups || {}).includes(g)); if (unnamedGroups.length > 0) { @@ -43,58 +116,241 @@ export default function RegExpTester() { } return detail; }).join('\n\n'); - setOutput(`Found ${matches.length} match(es):\n\n${matchDetails}`); + setOutput(`Found ${foundMatches.length} match(es):\n\n${matchDetails}`); } setError(''); } catch (e) { setError(e.message); setOutput(''); + setMatches([]); + } + }; + + const toggleFlag = (flag) => { + if (flags.includes(flag)) { + setFlags(flags.replace(flag, '')); + } else { + setFlags(flags + flag); } }; return ( -
    - +
    + -
    - / +
    + / setRegexStr(e.target.value)} placeholder="Regular Expression..." - style={{ flex: 1 }} + invalid={!!error} + style={{ + flex: 1, + fontFamily: "'IBM Plex Mono', monospace" + }} /> - / + / + setFlags(e.target.value)} placeholder="flags" - style={{ width: '80px' }} + style={{ + width: '80px', + fontFamily: "'IBM Plex Mono', monospace" + }} />
    + +
    + {FLAG_OPTIONS.map(({ flag, label }) => ( + toggleFlag(flag)} + style={{ cursor: 'pointer' }} + > + {flag} - {label} + + ))} +
    - {error &&
    {error}
    } - - - setText(e.target.value)} - placeholder="Enter the text to test against..." - /> - + {error && ( +
    + {error} +
    + )} + + +
    +
    + + {matches.length > 0 && ( + + {matches.length} match{matches.length !== 1 ? 'es' : ''} + + )} +
    +
    +