From 75e19b6b7b4dff1972d8e33f3a11cee91f36d6ca Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:45:24 +0700 Subject: [PATCH 1/2] feat: add react router for better hot reloading --- frontend/bun.lock | 7 + frontend/package.json | 1 + frontend/src/App.css | 3 + frontend/src/App.jsx | 62 +-- frontend/src/ToolRouter.jsx | 49 +++ .../src/components/DateTimeOutputField.jsx | 50 +++ frontend/src/components/Sidebar.jsx | 19 +- frontend/src/main.jsx | 5 +- .../src/pages/DateTimeConverter/index.jsx | 372 +++++++++++------- frontend/src/utils/datetimeHelpers.js | 114 ++++++ frontend/src/utils/storage.js | 77 ++++ vite.config.js | 4 + 12 files changed, 566 insertions(+), 197 deletions(-) create mode 100644 frontend/src/ToolRouter.jsx create mode 100644 frontend/src/components/DateTimeOutputField.jsx create mode 100644 frontend/src/utils/datetimeHelpers.js create mode 100644 frontend/src/utils/storage.js diff --git a/frontend/bun.lock b/frontend/bun.lock index 9e6ddad..fa39023 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -19,6 +19,7 @@ "qrcode": "^1.5.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.29.0", "sass": "^1.96.0", "sql-formatter": "^15.6.11", "ulid": "^3.0.2", @@ -220,6 +221,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], @@ -494,6 +497,10 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], + + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], diff --git a/frontend/package.json b/frontend/package.json index d701f84..80d0706 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "php-serialize": "^5.1.3", "qrcode": "^1.5.4", "react": "^18.2.0", + "react-router-dom": "^6.29.0", "react-dom": "^18.2.0", "sass": "^1.96.0", "sql-formatter": "^15.6.11", diff --git a/frontend/src/App.css b/frontend/src/App.css index 074e77f..be74ba5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -83,6 +83,9 @@ transition: all 0.2s; border: 1px solid transparent; position: relative; + /* Reset anchor tag defaults for NavLink */ + text-decoration: none; + cursor: pointer; } .nav-button:hover { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7eafa1..17e4ea0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,22 +1,10 @@ import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; import './App.css'; import { Sidebar } from './components/Sidebar'; import { TitleBar } from './components/TitleBar'; import { Theme } from '@carbon/react'; - -// Tools Imports -import DateTimeConverter from './pages/DateTimeConverter'; -import JwtDebugger from './pages/JwtDebugger'; -import RegExpTester from './pages/RegExpTester'; -import CronJobParser from './pages/CronJobParser'; -import TextDiffChecker from './pages/TextDiffChecker'; -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'; -import ColorConverter from './pages/ColorConverter'; +import ToolRouter from './ToolRouter'; // Error boundary for catching React rendering errors class ErrorBoundary extends React.Component { @@ -65,7 +53,6 @@ class ErrorBoundary extends React.Component { function App() { console.log('App mounting'); - 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' @@ -103,37 +90,6 @@ function App() { return () => window.removeEventListener('keydown', handleKeyDown); }, [isSidebarOpen]); - const renderTool = () => { - switch (activeTool) { - case 'text-converter': - return ; - case 'string-utilities': - return ; - case 'datetime-converter': - return ; - case 'jwt': - return ; - case 'barcode': - return ; - case 'data-generator': - return ; - case 'code-formatter': - return ; - case 'color-converter': - return ; - case 'regexp': - return ; - case 'cron': - return ; - case 'diff': - return ; - case 'number-converter': - return ; - default: - return
Select a tool
; - } - }; - return ( @@ -146,14 +102,16 @@ function App() { />
- +
-
{renderTool()}
+
+ + } /> + } /> + } /> + +
diff --git a/frontend/src/ToolRouter.jsx b/frontend/src/ToolRouter.jsx new file mode 100644 index 0000000..88676ba --- /dev/null +++ b/frontend/src/ToolRouter.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +// Tool Imports +import DateTimeConverter from './pages/DateTimeConverter'; +import JwtDebugger from './pages/JwtDebugger'; +import RegExpTester from './pages/RegExpTester'; +import CronJobParser from './pages/CronJobParser'; +import TextDiffChecker from './pages/TextDiffChecker'; +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'; +import ColorConverter from './pages/ColorConverter'; + +const toolComponents = { + 'text-converter': TextConverter, + 'string-utilities': StringUtilities, + 'datetime-converter': DateTimeConverter, + 'jwt': JwtDebugger, + 'barcode': BarcodeGenerator, + 'data-generator': DataGenerator, + 'code-formatter': CodeFormatter, + 'color-converter': ColorConverter, + 'regexp': RegExpTester, + 'cron': CronJobParser, + 'diff': TextDiffChecker, + 'number-converter': NumberConverter, +}; + +function ToolRouter() { + const { toolId } = useParams(); + const ToolComponent = toolComponents[toolId]; + + if (!ToolComponent) { + return ( +
+

Tool Not Found

+

The tool "{toolId}" doesn't exist.

+
+ ); + } + + return ; +} + +export default ToolRouter; \ No newline at end of file diff --git a/frontend/src/components/DateTimeOutputField.jsx b/frontend/src/components/DateTimeOutputField.jsx new file mode 100644 index 0000000..7f21afb --- /dev/null +++ b/frontend/src/components/DateTimeOutputField.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Tile, CopyButton } from '@carbon/react'; +import { getMonospaceFontFamily, getDataFontSize } from '../utils/inputUtils'; + +/** + * Display-only output field with label and copy button + * + * @param {Object} props + * @param {string} props.label - Label text displayed above the value + * @param {string} props.value - Value to display + * @param {string} [props.className] - Additional CSS classes for the container + * @param {Object} [props.style] - Additional inline styles for the container + */ +export default function DateTimeOutputField({ label, value, className, style }) { + return ( + +
+ {label} +
+
+ + {value} + + navigator.clipboard.writeText(value)} size="sm" /> +
+
+ ); +} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 2b843cd..578739c 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; +import { NavLink } from 'react-router-dom'; -export function Sidebar({ activeTool, setActiveTool, isVisible }) { +export function Sidebar({ isVisible }) { const [searchTerm, setSearchTerm] = useState(''); const [pinned, setPinned] = useState(() => { try { @@ -80,9 +81,9 @@ export function Sidebar({ activeTool, setActiveTool, isVisible }) {
    {pinnedTools.map((tool) => (
  • - +
  • ))}
@@ -107,9 +108,9 @@ export function Sidebar({ activeTool, setActiveTool, isVisible }) {
    {regularTools.map((tool) => (
  • - +
  • ))}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index da3e04e..f57881d 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; import './index.scss'; // Use our new SCSS import App from './App'; @@ -9,6 +10,8 @@ const root = createRoot(container); root.render( - + + + ); diff --git a/frontend/src/pages/DateTimeConverter/index.jsx b/frontend/src/pages/DateTimeConverter/index.jsx index d614375..aa886cd 100644 --- a/frontend/src/pages/DateTimeConverter/index.jsx +++ b/frontend/src/pages/DateTimeConverter/index.jsx @@ -1,9 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { Button, TextInput, Tag, Tile, CopyButton, ComboBox } from '@carbon/react'; -import { Time, Sun, Moon, Sunrise, SendToBack, Calendar, Clean } from '@carbon/icons-react'; +import { Button, TextInput, Tag, ComboBox } from '@carbon/react'; +import { Time, Sun, Moon, Sunrise, SendToBack, Calendar, Clean, Close, Add } from '@carbon/icons-react'; import { ToolHeader, ToolControls } from '../../components/ToolUI'; +import DateTimeOutputField from '../../components/DateTimeOutputField'; +import { getDayOfYear, getWeekOfYear, isLeapYear, parseMathExpression } from '../../utils/datetimeHelpers'; +import storage from '../../utils/storage'; -// Output formats +// Output formats for "Other formats" section const OUTPUT_FORMATS = [ { id: 'iso', label: 'ISO 8601' }, { id: 'rfc2822', label: 'RFC 2822' }, @@ -13,7 +16,7 @@ const OUTPUT_FORMATS = [ { id: 'compact', label: 'Compact' }, ]; -// Timezones +// Common timezones const TIMEZONES = [ { id: 'local', label: 'Local Time' }, { id: 'UTC', label: 'UTC' }, @@ -28,6 +31,22 @@ const TIMEZONES = [ { id: 'Australia/Sydney', label: 'Sydney' }, ]; +// Extended timezone list for "Add timezone" dropdown +const ALL_TIMEZONES = [ + ...TIMEZONES, + { id: 'Asia/Shanghai', label: 'Shanghai' }, + { id: 'Asia/Singapore', label: 'Singapore' }, + { id: 'Asia/Dubai', label: 'Dubai' }, + { id: 'Europe/Berlin', label: 'Berlin' }, + { id: 'Europe/Moscow', label: 'Moscow' }, + { id: 'Pacific/Auckland', label: 'Auckland' }, + { id: 'Pacific/Honolulu', label: 'Honolulu' }, + { id: 'America/Sao_Paulo', label: 'São Paulo' }, +]; + +// Storage key +const STORAGE_KEY = 'datetime-converter.timezones'; + // Helper for presets const toSQLFormat = (d) => { const pad = (n) => n.toString().padStart(2, '0'); @@ -77,12 +96,19 @@ const PRESETS = [ }, ]; -// Parse input to Date object +// Parse input to Date object (handles math expressions) function parseInput(input) { if (!input || !input.trim()) return null; const trimmed = input.trim(); + // Try math expression first + const mathResult = parseMathExpression(trimmed); + if (mathResult !== null) { + // Treat as timestamp (seconds) and convert to date + return new Date(mathResult * 1000); + } + // Try as timestamp (numeric) if (/^\d+$/.test(trimmed)) { const ts = parseInt(trimmed, 10); @@ -110,7 +136,7 @@ function parseInput(input) { return null; } -// Helper to get date object shifted to target timezone (so UTC getters return target time) +// Helper to get date object shifted to target timezone function getShiftedDate(date, timezone) { let year, month, day, hour, minute, second; @@ -206,14 +232,22 @@ function getRelativeTime(date) { export default function DateTimeConverter() { // Main input const [input, setInput] = useState(''); - const [outputFormat, setOutputFormat] = useState('iso'); - const [outputTimezone, setOutputTimezone] = useState('local'); const [timezone, setTimezone] = useState('local'); // Parsed result const [parsedDate, setParsedDate] = useState(null); const [error, setError] = useState(null); + // Custom timezones + const [customTimezones, setCustomTimezones] = useState([]); + const [selectedNewTimezone, setSelectedNewTimezone] = useState(null); + + // Load custom timezones from storage on mount + useEffect(() => { + const saved = storage.getArray(STORAGE_KEY); + setCustomTimezones(saved); + }, []); + // Parse input useEffect(() => { const date = parseInput(input); @@ -229,67 +263,82 @@ export default function DateTimeConverter() { } }, [input]); - const copyToClipboard = (text) => { - if (text) navigator.clipboard.writeText(text); - }; - const handlePreset = (preset) => { setInput(preset.getValue().toString()); }; + const addTimezone = () => { + if (selectedNewTimezone && !customTimezones.includes(selectedNewTimezone.id)) { + const newTimezones = [...customTimezones, selectedNewTimezone.id]; + setCustomTimezones(newTimezones); + storage.setArray(STORAGE_KEY, newTimezones); + setSelectedNewTimezone(null); + } + }; + + const removeTimezone = (tzId) => { + const newTimezones = customTimezones.filter((id) => id !== tzId); + setCustomTimezones(newTimezones); + storage.setArray(STORAGE_KEY, newTimezones); + }; + + // Get available timezones for dropdown (exclude already added) + const availableTimezones = ALL_TIMEZONES.filter( + (tz) => !customTimezones.includes(tz.id) + ); + return (
- {/* Main Input Section */} + {/* Control Section */} + +
+ {PRESETS.map((preset) => ( + + ))} +
+
+ - {/* Quick Presets */}
-
- {PRESETS.map((preset) => ( - - ))} -
-
+
setInput(e.target.value)} - placeholder="e.g., 1738412345, 2026-02-01T12:24:05Z, 02/01/2026..." - style={{ fontFamily: "'IBM Plex Mono', monospace", minWidth: '350px' }} + placeholder="e.g., 1738412345, 2026-02-01T12:24:05Z, 1738412345 + 3600..." + style={{ fontFamily: "'IBM Plex Mono', monospace" }} /> +
+ Math operators + - * / are supported +
+
+
(item ? item.label : '')} selectedItem={TIMEZONES.find((t) => t.id === timezone)} onChange={({ selectedItem }) => selectedItem && setTimezone(selectedItem.id)} - style={{ minWidth: '250px' }} + style={{ minWidth: '200px' }} /> - - +
+ placeholder +
- {/* Settings */} - - (item ? item.label : '')} - selectedItem={OUTPUT_FORMATS.find((f) => f.id === outputFormat)} - onChange={({ selectedItem }) => selectedItem && setOutputFormat(selectedItem.id)} - style={{ minWidth: '250px' }} - /> - (item ? item.label : '')} - selectedItem={TIMEZONES.find((t) => t.id === timezone)} - onChange={({ selectedItem }) => selectedItem && setOutputTimezone(selectedItem.id)} - style={{ minWidth: '250px' }} - defaultValue={'local'} - /> - - {/* Error */} {error && {error}} - {/* Main Result */} + {/* Results Section */} {parsedDate && ( - -
-
- - {formatDate(parsedDate, outputFormat, outputTimezone)} - - - copyToClipboard(formatDate(parsedDate, outputFormat, outputTimezone)) - } - size="sm" + <> +
+ {/* Left Column - Primary Outputs */} +
+ + + +
-
- {getRelativeTime(parsedDate)} - Unix: {Math.floor(parsedDate.getTime() / 1000)} - - Unix (ms): {parsedDate.getTime()} + {/* Right Column - Metadata */} +
+ {/* Three fields in a row */} +
+ + + +
+ + {/* Other formats section - 2 per row */} +
+
+ Other formats +
+
+ {OUTPUT_FORMATS.map((fmt) => ( + + ))} +
+
- - )} - {/* All Formats */} - {parsedDate && ( -
- {OUTPUT_FORMATS.map((fmt) => ( - -
- {fmt.label} -
-
+
+ + Other timezones: + + (item ? item.label : '')} + selectedItem={selectedNewTimezone} + onChange={({ selectedItem }) => setSelectedNewTimezone(selectedItem)} + style={{ minWidth: '160px', maxWidth: '180px' }} + size="sm" + /> +
- - ))} -
+ Add + + + {customTimezones.length > 0 && ( +
+ {customTimezones.map((tzId) => { + const tz = ALL_TIMEZONES.find((t) => t.id === tzId) || { id: tzId, label: tzId }; + return ( +
+
+
+ {tz.label} +
+
+ {formatDate(parsedDate, 'sql', tzId)} +
+
+
+ ); + })} +
+ )} +
+
+ )}
); diff --git a/frontend/src/utils/datetimeHelpers.js b/frontend/src/utils/datetimeHelpers.js new file mode 100644 index 0000000..3116701 --- /dev/null +++ b/frontend/src/utils/datetimeHelpers.js @@ -0,0 +1,114 @@ +/** + * Datetime utilities for calculations and parsing + */ + +/** + * Get the day of year for a given date (1-366) + * @param {Date} date - Date to get day of year for + * @returns {number} Day of year (1-366), or null if invalid + */ +export const getDayOfYear = (date) => { + if (!(date instanceof Date) || isNaN(date.getTime())) { + return null; + } + + const startOfYear = new Date(date.getFullYear(), 0, 1); + const diff = date - startOfYear; + const oneDay = 24 * 60 * 60 * 1000; + const dayOfYear = Math.floor(diff / oneDay) + 1; + + return dayOfYear; +}; + +/** + * Get the ISO week number for a given date (1-53) + * @param {Date} date - Date to get week number for + * @returns {number} ISO week number (1-53), or null if invalid + */ +export const getWeekOfYear = (date) => { + if (!(date instanceof Date) || isNaN(date.getTime())) { + return null; + } + + const d = new Date(date); + d.setHours(0, 0, 0, 0); + + const dayOfWeek = (d.getDay() + 6) % 7; + d.setDate(d.getDate() - dayOfWeek + 3); + + const firstThursday = new Date(d.getFullYear(), 0, 4); + firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7)); + + const diff = d - firstThursday; + const oneWeek = 7 * 24 * 60 * 60 * 1000; + const weekNumber = Math.floor(diff / oneWeek) + 1; + + return weekNumber; +}; + +/** + * Check if a year is a leap year + * @param {number} year - Year to check + * @returns {boolean} True if leap year, false otherwise + */ +export const isLeapYear = (year) => { + if (typeof year !== 'number' || !Number.isFinite(year)) { + return false; + } + + if (year % 4 !== 0) { + return false; + } + + if (year % 100 !== 0) { + return true; + } + + return year % 400 === 0; +}; + +/** + * Parse a math expression and return the calculated result + * Supports: +, -, *, / operators with format: number operator number + * @param {string} input - Math expression string + * @returns {number|null} Calculated result or null if not a valid expression + */ +export const parseMathExpression = (input) => { + if (typeof input !== 'string') { + return null; + } + + const trimmed = input.trim(); + if (!trimmed) { + return null; + } + + const match = trimmed.match(/^\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)\s*$/); + if (!match) { + return null; + } + + const [, num1Str, operator, num2Str] = match; + const num1 = parseFloat(num1Str); + const num2 = parseFloat(num2Str); + + if (!Number.isFinite(num1) || !Number.isFinite(num2)) { + return null; + } + + switch (operator) { + case '+': + return num1 + num2; + case '-': + return num1 - num2; + case '*': + return num1 * num2; + case '/': + if (num2 === 0) { + return null; + } + return num1 / num2; + default: + return null; + } +}; diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js new file mode 100644 index 0000000..80b6f34 --- /dev/null +++ b/frontend/src/utils/storage.js @@ -0,0 +1,77 @@ +/** + * Storage utility for persisting data in localStorage + * Works in both browser and Wails environments + */ + +const storage = { + /** + * Gets a value from localStorage + * @param {string} key - The key to retrieve + * @returns {any|null} - The parsed value or null if not found or on error + */ + get(key) { + try { + const item = window.localStorage.getItem(key); + if (item === null) return null; + return JSON.parse(item); + } catch (error) { + console.error(`Error getting item from localStorage: ${key}`, error); + return null; + } + }, + + /** + * Sets a value in localStorage + * @param {string} key - The key to set + * @param {any} value - The value to store (will be JSON stringified) + * @returns {boolean} - True if successful, false on error + */ + set(key, value) { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error(`Error setting item in localStorage: ${key}`, error); + return false; + } + }, + + /** + * Gets a value and parses it as a JSON array + * @param {string} key - The key to retrieve + * @returns {Array} - The parsed array or empty array if not found/invalid + */ + getArray(key) { + try { + const item = window.localStorage.getItem(key); + if (item === null) return []; + const parsed = JSON.parse(item); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + console.error(`Error getting array from localStorage: ${key}`, error); + return []; + } + }, + + /** + * Stringifies and saves an array + * @param {string} key - The key to set + * @param {Array} array - The array to store + * @returns {boolean} - True if successful, false on error + */ + setArray(key, array) { + try { + if (!Array.isArray(array)) { + console.error(`Value is not an array: ${key}`); + return false; + } + window.localStorage.setItem(key, JSON.stringify(array)); + return true; + } catch (error) { + console.error(`Error setting array in localStorage: ${key}`, error); + return false; + } + } +}; + +export default storage; diff --git a/vite.config.js b/vite.config.js index 6ce8f76..96bd637 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,6 +6,10 @@ import path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), wails("./bindings")], + server: { + port: 3000, + historyApiFallback: true + }, css: { preprocessorOptions: { scss: { From 5a23f5ec4c5a962c2e8690225fcaa6224c1e2c2d Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:34:10 +0700 Subject: [PATCH 2/2] fix: datetime converter and overall layout for all pages with grid/column --- ...5-01-27-number-converter-implementation.md | 485 +++++++++++++++ .../2025-01-27-number-converter-redesign.md | 306 ++++++++++ ...-datetime-converter-improvements-design.md | 82 +++ frontend/src/pages/BarcodeGenerator.jsx | 44 +- frontend/src/pages/CodeFormatter/index.jsx | 48 +- frontend/src/pages/CronJobParser.jsx | 22 +- frontend/src/pages/DataGenerator/index.jsx | 78 +-- .../DateTimeConverter/api/dateTimeAPI.js | 45 ++ .../components/DateTimeOutputField.jsx | 2 +- .../components/InputSection.jsx | 54 ++ .../components/PresetsSection.jsx | 24 + .../components/ResultsGrid.jsx | 122 ++++ .../src/pages/DateTimeConverter/constants.js | 60 ++ .../DateTimeConverter/datetimeHelpers.js | 217 +++++++ .../DateTimeConverter/hooks/useDateTime.js | 155 +++++ .../src/pages/DateTimeConverter/index.jsx | 570 +++--------------- frontend/src/pages/JwtDebugger/index.jsx | 77 ++- .../NumberConverter/components/BitCell.jsx | 83 +++ .../NumberConverter/components/BitGrid.jsx | 132 ++++ .../components/BitwiseToolbar.jsx | 66 ++ .../components/ConversionCard.jsx | 132 ++++ .../src/pages/NumberConverter/constants.js | 174 ++++++ frontend/src/pages/NumberConverter/index.jsx | 445 ++++++++------ .../NumberConverter/numberConverterReducer.js | 232 +++++++ frontend/src/pages/NumberConverter/utils.js | 382 ++++++++++++ frontend/src/pages/RegExpTester.jsx | 48 +- frontend/src/pages/StringUtilities/index.jsx | 59 +- frontend/src/pages/TextConverter/index.jsx | 77 ++- frontend/src/pages/TextDiffChecker.jsx | 65 +- frontend/src/utils/datetimeHelpers.js | 114 ---- internal/datetimeconverter/timezone.go | 7 +- 31 files changed, 3360 insertions(+), 1047 deletions(-) create mode 100644 docs/plans/2025-01-27-number-converter-implementation.md create mode 100644 docs/plans/2025-01-27-number-converter-redesign.md create mode 100644 docs/plans/2026-03-01-datetime-converter-improvements-design.md create mode 100644 frontend/src/pages/DateTimeConverter/api/dateTimeAPI.js rename frontend/src/{ => pages/DateTimeConverter}/components/DateTimeOutputField.jsx (94%) create mode 100644 frontend/src/pages/DateTimeConverter/components/InputSection.jsx create mode 100644 frontend/src/pages/DateTimeConverter/components/PresetsSection.jsx create mode 100644 frontend/src/pages/DateTimeConverter/components/ResultsGrid.jsx create mode 100644 frontend/src/pages/DateTimeConverter/constants.js create mode 100644 frontend/src/pages/DateTimeConverter/datetimeHelpers.js create mode 100644 frontend/src/pages/DateTimeConverter/hooks/useDateTime.js create mode 100644 frontend/src/pages/NumberConverter/components/BitCell.jsx create mode 100644 frontend/src/pages/NumberConverter/components/BitGrid.jsx create mode 100644 frontend/src/pages/NumberConverter/components/BitwiseToolbar.jsx create mode 100644 frontend/src/pages/NumberConverter/components/ConversionCard.jsx create mode 100644 frontend/src/pages/NumberConverter/constants.js create mode 100644 frontend/src/pages/NumberConverter/numberConverterReducer.js create mode 100644 frontend/src/pages/NumberConverter/utils.js delete mode 100644 frontend/src/utils/datetimeHelpers.js diff --git a/docs/plans/2025-01-27-number-converter-implementation.md b/docs/plans/2025-01-27-number-converter-implementation.md new file mode 100644 index 0000000..4f99bab --- /dev/null +++ b/docs/plans/2025-01-27-number-converter-implementation.md @@ -0,0 +1,485 @@ +# Number Converter Implementation Plan + +**Design Document:** `docs/plans/2025-01-27-number-converter-redesign.md` +**Goal:** Implement visual bit editor with parallel workstreams + +--- + +## Workstream Overview + +This implementation is divided into **7 parallel workstreams**. Each can be developed independently and tested in isolation before integration. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ WORKSTREAM 1 WORKSTREAM 2 WORKSTREAM 3 │ +│ State Management Utility Functions BitGrid │ +│ (Reducer) (Conversions) Component │ +│ │ +│ WORKSTREAM 4 WORKSTREAM 5 WORKSTREAM 6 │ +│ ConversionCard BitwiseToolbar Constants │ +│ Component Component & Types │ +│ │ +│ WORKSTREAM 7 │ +│ Main Integration │ +│ & Final Assembly │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Workstream 1: State Management (Reducer) +**File:** `frontend/src/pages/NumberConverter/numberConverterReducer.js` + +**Deliverables:** +```javascript +// Initial state +const initialState = { + value: 0, + inputMode: 'decimal', + customBase: 36, + errors: {} +}; + +// Action types +const actions = { + SET_VALUE, + TOGGLE_BIT, + SET_INPUT_MODE, + SET_CUSTOM_BASE, + SET_ERROR, + CLEAR_ERROR, + APPLY_BITWISE_OP +}; + +// Reducer function +function numberConverterReducer(state, action) { ... } + +// Action creators +const actionCreators = { + setValue, + toggleBit, + setInputMode, + setCustomBase, + setError, + clearError, + applyBitwiseOp +}; +``` + +**Test in isolation:** +```javascript +// Test cases needed: +- Toggle bit 0 on 0 → should be 1 +- Toggle bit 0 on 1 → should be 0 +- Set value to 255 → all conversions should update +- Set error on hex input → error object populated +- Clear error → error object empty +``` + +**Dependencies:** None (pure logic) + +--- + +## Workstream 2: Utility Functions +**File:** `frontend/src/pages/NumberConverter/utils.js` + +**Deliverables:** +```javascript +// Parsing (string → number) +export function parseInput(input, base); +export function parseHex(input); +export function parseBinary(input); +export function parseOctal(input); +export function parseDecimal(input); +export function parseCustomBase(input, base); + +// Formatting (number → string) +export function formatNumber(value, base); +export function formatHex(value); +export function formatBinary(value); +export function formatOctal(value); +export function formatDecimal(value); +export function formatCustomBase(value, base); + +// Bit manipulation +export function toggleBit(value, position); +export function shiftLeft(value, n); +export function shiftRight(value, n); +export function bitwiseNot(value); +export function bitwiseAnd(value, mask); +export function bitwiseOr(value, mask); + +// Validation +export function validateInput(input, base); +export function sanitizeInput(input); +``` + +**Edge Cases to Handle:** +- Empty string returns null +- Whitespace trimmed +- Case insensitivity for hex +- Overflow clamping to 32-bit +- Invalid characters throw with message + +**Test in isolation:** +```javascript +// Test matrix: +Input | Base | Expected Value +"FF" | 16 | 255 +"1010" | 2 | 10 +"377" | 8 | 255 +"255" | 10 | 255 +" 42 " | 10 | 42 (trimmed) +"GG" | 16 | throw Error +"99999999999" | 10 | 4294967295 (clamped) +"-5" | 10 | throw Error +``` + +**Dependencies:** None (pure logic) + +--- + +## Workstream 3: BitGrid Component +**File:** `frontend/src/pages/NumberConverter/components/BitGrid.jsx` + +**Deliverables:** +```jsx +// BitGrid.jsx +export function BitGrid({ value, onToggleBit, layout }) { + // Display 4 rows × 8 bits + // Row 0: bits 31-24 (MSB) + // Row 1: bits 23-16 + // Row 2: bits 15-8 + // Row 3: bits 7-0 (LSB) +} + +// BitCell.jsx (sub-component) +export function BitCell({ + bitValue, // 0 or 1 + position, // 0-31 + onToggle, // callback(position) + isHovered // hover state from parent +}) { + // Clickable 32×32px cell + // Hover: scale(1.1) + // Active: filled with primary color + // Inactive: outlined +} +``` + +**Props Interface:** +```typescript +interface BitGridProps { + value: number; // 32-bit value + onToggleBit: (position: number) => void; + layout: 'horizontal' | 'vertical'; +} +``` + +**Visual Specs:** +- Cell size: 32×32px +- Gap: 4px +- Active color: `var(--cds-interactive-01)` +- Inactive border: `var(--cds-border-strong)` +- Row label font: monospace, `var(--cds-text-secondary)` + +**Test in isolation:** +```javascript +- Renders 32 cells +- Clicking cell calls onToggleBit with correct position +- Value 0xFF shows bits 0-7 as active +- Value 0xFF000000 shows bits 24-31 as active +- Keyboard navigation works (Tab, Space, Enter) +``` + +**Dependencies:** None (uses only Carbon CSS variables) + +--- + +## Workstream 4: ConversionCard Component +**File:** `frontend/src/pages/NumberConverter/components/ConversionCard.jsx` + +**Deliverables:** +```jsx +// ConversionCard.jsx +export function ConversionCard({ + label, // "Decimal", "Hexadecimal", etc. + base, // 10, 16, 2, 8, or custom + value, // Current numeric value + error, // Error message or null + onChange, // callback(newValue) - parse and update + onCopy, // callback() - copy to clipboard + onSync // callback() - sync from this field +}) { + // TextInput with label + // Copy button + // Sync button (sets this as source) + // Error display +} +``` + +**Props Interface:** +```typescript +interface ConversionCardProps { + label: string; + base: number; + value: number; + error?: string; + onChange: (input: string) => void; + onCopy: () => void; + onSync: () => void; +} +``` + +**Features:** +- Monospace font for binary display +- Copy button uses Carbon `Button` with Copy icon +- Sync button to reverse-sync (input becomes source) +- Inline error display below input +- Placeholder shows example: "Enter decimal number..." + +**Test in isolation:** +```javascript +- Typing valid input calls onChange +- Typing invalid input shows error +- Copy button copies formatted value +- Sync button calls onSync +- Error state shows red border +``` + +**Dependencies:** Carbon `TextInput`, `Button`, `InlineNotification` + +--- + +## Workstream 5: BitwiseToolbar Component +**File:** `frontend/src/pages/NumberConverter/components/BitwiseToolbar.jsx` + +**Deliverables:** +```jsx +// BitwiseToolbar.jsx +export function BitwiseToolbar({ onOperation }) { + // Button group: + // [<< 1] [>> 1] [NOT] [& 0xFF] [| 1] +} + +// Operations supported: +// 'shiftLeft': value << 1 +// 'shiftRight': value >>> 1 (logical) +// 'not': ~value +// 'maskByte': value & 0xFF +// 'setLSB': value | 1 +``` + +**Props Interface:** +```typescript +interface BitwiseToolbarProps { + onOperation: (operation: string) => void; +} +``` + +**Visual Specs:** +- Button group with `kind="secondary"` +- Size: `sm` (small) +- Gap: 8px between buttons +- Tooltip on hover showing operation description + +**Test in isolation:** +```javascript +- Clicking [<< 1] calls onOperation('shiftLeft') +- Clicking [NOT] calls onOperation('not') +- All 5 buttons rendered +- Keyboard accessible (Tab, Enter) +``` + +**Dependencies:** Carbon `Button`, `ButtonSet` + +--- + +## Workstream 6: Constants & Types +**File:** `frontend/src/pages/NumberConverter/constants.js` + +**Deliverables:** +```javascript +// Base configurations +export const BASES = { + BINARY: { id: 'bin', label: 'Binary', base: 2 }, + OCTAL: { id: 'oct', label: 'Octal', base: 8 }, + DECIMAL: { id: 'dec', label: 'Decimal', base: 10 }, + HEXADECIMAL: { id: 'hex', label: 'Hexadecimal', base: 16 }, +}; + +// Custom base options (2-36) +export const CUSTOM_BASE_OPTIONS = Array.from({ length: 35 }, (_, i) => ({ + id: `${i + 2}`, + label: `Base ${i + 2}`, + value: i + 2, +})); + +// Bitwise operations +export const BITWISE_OPERATIONS = { + SHIFT_LEFT: { id: 'shiftLeft', label: '<< 1', description: 'Shift left by 1' }, + SHIFT_RIGHT: { id: 'shiftRight', label: '>> 1', description: 'Shift right by 1' }, + NOT: { id: 'not', label: 'NOT', description: 'Flip all bits' }, + MASK_BYTE: { id: 'maskByte', label: '& 0xFF', description: 'Keep lowest byte' }, + SET_LSB: { id: 'setLSB', label: '| 1', description: 'Set least significant bit' }, +}; + +// Validation messages +export const ERROR_MESSAGES = { + INVALID_CHAR: (char, base) => `Invalid character '${char}' for base ${base}`, + NEGATIVE: 'Negative numbers are not supported', + OVERFLOW: 'Value clamped to 32-bit maximum', + EMPTY: 'Input cannot be empty', +}; + +// Limits +export const MAX_32BIT = 0xFFFFFFFF; // 4,294,967,295 +export const MIN_32BIT = 0; +``` + +**Dependencies:** None + +--- + +## Workstream 7: Main Integration +**File:** `frontend/src/pages/NumberConverter/index.jsx` + +**Deliverables:** +Complete page component integrating all workstreams: + +```jsx +export default function NumberConverter() { + // State + const [state, dispatch] = useReducer(numberConverterReducer, initialState); + const layout = useLayoutToggle({ toolKey: 'number-converter', ... }); + + // Handlers + const handleToggleBit = (position) => dispatch(toggleBit(position)); + const handleConversionInput = (base, input) => { ... }; + const handleBitwiseOp = (operation) => { ... }; + const handleCopy = (value) => navigator.clipboard.writeText(value); + + // Render + return ( +
+ + + + +
+ + +
+ + +
+ {Object.values(BASES).map(baseConfig => ( + handleConversionInput(baseConfig.base, input)} + onCopy={() => handleCopy(formatNumber(state.value, baseConfig.base))} + onSync={() => dispatch(setInputMode(baseConfig.id))} + /> + ))} + +
+
+
+ ); +} +``` + +**Integration Checklist:** +- [ ] Import all workstream components +- [ ] Wire up reducer and actions +- [ ] Connect layout toggle +- [ ] Handle all user interactions +- [ ] Display errors from state +- [ ] Format values for display +- [ ] Add copy functionality + +**Dependencies:** ALL other workstreams + +--- + +## Implementation Order + +### Phase 1: Foundation (Parallel) +Workstreams 1, 2, and 6 can start immediately and run in parallel: +- **WS1** (Reducer) - No dependencies +- **WS2** (Utils) - No dependencies +- **WS6** (Constants) - No dependencies + +### Phase 2: Components (Parallel) +Once Phase 1 is done, workstreams 3, 4, and 5 can run in parallel: +- **WS3** (BitGrid) - Uses WS2 for bit manipulation +- **WS4** (ConversionCard) - Uses WS2 for formatting +- **WS5** (BitwiseToolbar) - No dependencies + +### Phase 3: Integration (Sequential) +Workstream 7 depends on ALL others: +- **WS7** (Main) - Uses WS1, WS2, WS3, WS4, WS5, WS6 + +--- + +## Testing Strategy + +### Unit Tests (Per Workstream) +Each workstream should include unit tests before integration: + +**WS1 (Reducer):** Test all state transitions +**WS2 (Utils):** Test conversion functions with edge cases +**WS3 (BitGrid):** Test rendering and interactions +**WS4 (ConversionCard):** Test input handling and validation +**WS5 (BitwiseToolbar):** Test button callbacks +**WS6 (Constants):** N/A - just definitions + +### Integration Tests +After WS7, test the complete flow: +- End-to-end conversion scenarios +- Bit manipulation workflows +- Error handling paths +- Accessibility compliance + +### Visual Regression Tests +- Screenshot tests for different values +- Layout toggle states +- Error states +- Dark/light mode + +--- + +## Success Criteria + +✅ All 7 workstreams complete +✅ Unit tests pass for each workstream +✅ Integration tests pass +✅ No console errors +✅ Accessibility audit passes +✅ Visual design matches spec +✅ Performance: <100ms for bit toggle feedback + +--- + +## Notes for Agents + +1. **Work independently** - Each workstream can be developed without blocking others +2. **Use mocks** - If a dependency isn't ready, mock it with the expected interface +3. **Write tests first** - Each workstream should be testable in isolation +4. **Document exports** - Make sure the interface is clear for integration +5. **Follow Carbon** - Use existing Carbon components and CSS variables +6. **Check AGENTS.md** - Follow project conventions for UI patterns + +**Ready to start?** Pick any Phase 1 workstream and begin implementation. diff --git a/docs/plans/2025-01-27-number-converter-redesign.md b/docs/plans/2025-01-27-number-converter-redesign.md new file mode 100644 index 0000000..d8b0bd1 --- /dev/null +++ b/docs/plans/2025-01-27-number-converter-redesign.md @@ -0,0 +1,306 @@ +# Number Converter Redesign - Visual Bit Editor + +**Date:** 2025-01-27 +**Status:** Design Complete +**Approach:** Visual Bit Editor with Interactive Bitwise Operations + +--- + +## 1. Overview & Architecture + +### Concept +Transform the Number Converter from a form-filling task into an interactive exploration tool. Users interact with a visual 32-bit representation where they can toggle individual bits, perform bitwise operations, and instantly see conversions across all bases. + +### Core Architecture +- **32-bit representation as source of truth** - All conversions derive from a single 32-bit unsigned integer +- **Bidirectional input** - Click bits in the grid OR type into conversion fields; changes sync both ways +- **Visual hierarchy** - Bit grid dominates (4 rows × 8 bits), color-coded by byte significance +- **Simple bitwise operations** - One-click toolbar for common operations + +### Technology Stack +- React with Carbon Design System (existing) +- No new dependencies +- Layout toggle with localStorage persistence + +--- + +## 2. Components & UI Structure + +### Layout +Split-pane with layout toggle (horizontal/vertical), following existing `useLayoutToggle` pattern. + +### Left Pane - Visual Bit Grid (60% width) +**Header:** +- "Bit Pattern" label +- Bit position indicators (31-0) +- Byte hex summaries + +**Grid:** +``` +Row 0 (31-24): ■ □ □ ■ □ □ ■ □ [0x4A] +Row 1 (23-16): □ ■ □ □ ■ □ □ ■ [0x25] +Row 2 (15-8): ■ ■ □ □ □ ■ □ □ [0xC3] +Row 3 (7-0): □ □ ■ ■ □ □ ■ □ [0x32] +``` + +**Bit Cells (32×32px):** +- `1` = filled with `--cds-interactive-01` (primary accent) +- `0` = outlined with `--cds-border-strong` +- Hover: scale(1.1) with shadow +- Click: toggle bit + +**Toolbar (above grid):** +- `<< 1` - Shift left +- `>> 1` - Shift right +- `NOT` - Flip all bits +- `& 0xFF` - Mask to byte +- `| 1` - Set LSB + +### Right Pane - Conversion Cards (40% width) +Stacked cards showing derived values: +1. **Decimal** (largest, most prominent) +2. **Hexadecimal** +3. **Binary** (monospace, wrapped) +4. **Octal** +5. **Custom Base** (dropdown 2-36) + +Each card: label, input field, copy button, "sync" button to reverse-sync + +--- + +## 3. Data Flow & State Management + +### State Shape +```typescript +interface NumberConverterState { + value: number; // 32-bit unsigned integer (0 to 2^32-1) + inputMode: 'decimal' | 'hex' | 'binary' | 'octal' | 'custom'; + customBase: number; // 2-36 + errors: { + decimal?: string; + hex?: string; + binary?: string; + octal?: string; + custom?: string; + }; + layout: 'horizontal' | 'vertical'; +} +``` + +### Data Flow Patterns + +**1. Bit Grid Click:** +``` +User clicks bit at position N +↓ +Calculate: newValue = currentValue ^ (1 << N) +↓ +Update state.value +↓ +Re-render all conversion displays +``` + +**2. Conversion Input:** +``` +User types in Hex input field +↓ +Parse with base 16 +↓ +Valid: Update state.value, clear error +Invalid: Show error inline, keep last valid value +↓ +Bit grid re-renders from new value +``` + +**3. Bitwise Operation:** +``` +User clicks "<< 1" button +↓ +Calculate: newValue = (currentValue << 1) & 0xFFFFFFFF +↓ +Update state.value +↓ +All displays update +``` + +### Performance Optimizations +- `useMemo` for expensive base conversions +- Bit grid uses CSS transforms (GPU-accelerated) +- No unnecessary re-renders + +--- + +## 4. Bitwise Operations + +### Operation Toolbar +Simple buttons above the bit grid: + +| Button | Operation | Example | +|--------|-----------|---------| +| `<< 1` | Shift left by 1 | `0x0F` → `0x1E` | +| `>> 1` | Logical shift right by 1 | `0xF0` → `0x78` | +| `NOT` | Bitwise NOT | `0x0F` → `0xFFFFFFF0` | +| `& 0xFF` | AND with 0xFF | `0xABCD` → `0xCD` | +| `\| 1` | OR with 1 | `0xFE` → `0xFF` | + +### Interaction +- Click button → operation applies immediately +- No preview mode, no operand input +- Simple undo: click opposite operation +- Visual feedback: button press animation + +--- + +## 5. Error Handling & Edge Cases + +### Input Validation Errors + +**Invalid Characters:** +- Hex input with non-hex chars (G-Z) +- Binary input with digits other than 0-1 +- Octal input with digits 8-9 +- Show: "Invalid character 'X' for base Y" + +**Format Errors:** +- Empty string → valid (no change) +- Whitespace-only → trim or error +- Leading/trailing whitespace → auto-trim + +**Range Errors:** +- Value > 2^32-1 (4,294,967,295) → clamp to max +- Value < 0 → error: "Negative numbers not supported" +- Scientific notation → parse or error + +### Bit Manipulation Edge Cases + +**Shift Operations:** +- Shift by 0 → no change +- Shift by 32+ → wraps (JS behavior: `x << 32 === x`) +- Shift negative → error + +**Identity Operations:** +- `x & x` = x +- `x | x` = x +- `x ^ 0` = x +- Handle gracefully + +**Boundary Values:** +- Bit 31 toggle (0x80000000) +- All zeros (0x00000000) +- All ones (0xFFFFFFFF) +- Single bit set (powers of 2) + +### Display Edge Cases + +**Binary Display:** +- Always show 32 bits for consistency +- Group by 4 bits: `0001 0010 0011 0100` +- Wrap in monospace textarea + +**Decimal Display:** +- Large values: show full precision +- Add thousand separators: `4,294,967,295` + +**Custom Base:** +- Base 36: 0-9, A-Z +- Handle uppercase/lowercase consistently + +### Accessibility + +**Keyboard Navigation:** +- Tab through bit cells +- Space/Enter to toggle bit +- Shift+Tab for reverse navigation + +**Screen Readers:** +- Bit cells: `aria-pressed` for state +- Error messages: `aria-live="polite"` +- Labels on all interactive elements + +### Race Conditions & Performance + +**Rapid Interactions:** +- Debounce rapid bit clicks (50ms) +- Prevent double-submission of operations + +**Copy Operations:** +- Handle copy failure gracefully +- Show toast on success/error + +**Layout Changes:** +- Maintain scroll position on layout toggle +- Responsive bit grid sizing + +--- + +## 6. Testing Checklist + +### Functional Tests +- [ ] All base conversions work bidirectionally +- [ ] Bit toggles update all displays instantly +- [ ] Each bitwise operation produces correct result +- [ ] Invalid input shows appropriate error +- [ ] Copy button copies correct value + +### Edge Case Tests +- [ ] Maximum value (0xFFFFFFFF) +- [ ] Zero value +- [ ] Negative input rejected +- [ ] Overflow clamped +- [ ] Whitespace trimmed +- [ ] Case insensitivity (hex) + +### UX Tests +- [ ] Layout toggle persists +- [ ] Keyboard navigation works +- [ ] Touch targets adequate size +- [ ] Hover states visible +- [ ] Error messages clear + +### Accessibility Tests +- [ ] Screen reader announces bit state +- [ ] Focus visible on all interactive elements +- [ ] Color not sole error indicator + +--- + +## 7. Implementation Notes + +### File Structure +``` +frontend/src/pages/NumberConverter/ +├── index.jsx # Main component +├── numberConverterReducer.js # State management +├── utils.js # Conversion utilities +├── constants.js # Base configs +└── components/ + ├── BitGrid.jsx # Visual bit grid + ├── BitCell.jsx # Individual bit toggle + ├── ConversionCard.jsx # Input with copy/sync + ├── BitwiseToolbar.jsx # Operation buttons + └── ByteLabel.jsx # Byte hex display +``` + +### Key Utilities Needed +```javascript +// utils.js +export const parseInput = (input, base) => { ... } +export const formatNumber = (value, base) => { ... } +export const toggleBit = (value, position) => { ... } +export const shiftLeft = (value, n) => { ... } +export const shiftRight = (value, n) => { ... } +export const bitwiseNot = (value) => { ... } +``` + +### Carbon Components Used +- `TextInput` - Conversion inputs +- `Button` - Operations, copy +- `Dropdown` - Custom base selector +- `Grid/Column` - Layout +- `InlineNotification` - Errors + +--- + +## Summary + +This redesign transforms the Number Converter from a passive form into an active exploration tool. The visual bit grid makes binary tangible, bitwise operations are one-click away, and all conversions update in real-time. The design maintains consistency with existing Carbon Design System usage while adding engaging interactivity. diff --git a/docs/plans/2026-03-01-datetime-converter-improvements-design.md b/docs/plans/2026-03-01-datetime-converter-improvements-design.md new file mode 100644 index 0000000..e7a4968 --- /dev/null +++ b/docs/plans/2026-03-01-datetime-converter-improvements-design.md @@ -0,0 +1,82 @@ +# DateTime Converter Improvements Design + +## Overview +Redesign the DateTime Converter tool to match reference design with labeled output fields, math operators support, and persistent custom timezones. + +## Goals +- Replace grid layout with explicit labeled output fields +- Add math operators (+, -, *, /) for timestamp calculations +- Support persistent custom timezones across browser and Wails environments + +## Phase 1: Core Layout (Priority: High) + +### Layout Structure +Three distinct zones following AGENTS.md guidelines: + +**Header Zone** +- Tool title "DateTime Converter" +- Description text + +**Control Zone** +- Preset buttons: Now, Start of Day, End of Day, Tomorrow, Yesterday, Next Week +- Input field with placeholder text +- Input timezone selector +- Clear button +- Math operators helper text + +**Workspace Zone (Two-column layout)** +- Left column: Primary outputs (Local, UTC ISO 8601, Relative, Unix time) +- Right column: Metadata (Day of year, Week of year, Is leap year, Other formats) + +Each field gets labeled box with copy button. + +### Component Changes +- New `OutputField` component: Label + value + copy button +- Modify `DateTimeConverter/index.jsx`: Reorganize layout, add math parser +- Update styling to match reference design proportions + +## Phase 2: Timezone Storage (Priority: High) + +### Storage Interface +```javascript +const storage = { + get: (key) => localStorage.getItem(key) || wailsGet(key), + set: (key, value) => { localStorage.setItem(key, value); wailsSet(key, value); } +}; +``` + +### Data Structure +```javascript +{ + "datetime-converter.timezones": ["Asia/Tokyo", "Europe/London"] +} +``` + +### Behavior +- "Add timezone" dropdown below main outputs +- Selected timezones render as additional output fields +- Remove button (×) on each field +- Persist to both localStorage and Wails backend + +## Phase 3: Math Operators (Priority: Medium) + +### Supported Operations +- `+` addition (e.g., `1738412345 + 3600`) +- `-` subtraction (e.g., `now - 86400`) +- `*` multiplication +- `/` division + +### Implementation +- Regex parser for `number operator number` pattern +- Real-time calculation on input change +- Error display for invalid expressions + +## Error Handling +- Invalid date: Red tag "Invalid date or timestamp" +- Math error: Inline red text below input +- Timezone error: Fallback to UTC with warning + +## Testing Plan +- Unit tests for math parser +- Integration tests for storage interface +- Visual regression for layout changes diff --git a/frontend/src/pages/BarcodeGenerator.jsx b/frontend/src/pages/BarcodeGenerator.jsx index 84f49c6..8bdf472 100644 --- a/frontend/src/pages/BarcodeGenerator.jsx +++ b/frontend/src/pages/BarcodeGenerator.jsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { Button, Dropdown, InlineLoading } from '@carbon/react'; +import { Grid, Column, Button, Dropdown, InlineLoading } from '@carbon/react'; import { Renew, Download } from '@carbon/icons-react'; import { ToolHeader, ToolPane, ToolSplitPane, ToolLayoutToggle } from '../components/ToolUI'; import useLayoutToggle from '../hooks/useLayoutToggle'; @@ -233,27 +233,30 @@ export default function BarcodeGenerator() { const isQR = standard === 'QR'; return ( -
- + + + {/* Controls */} -
+ +
+
+ {/* Input Pane */}
-
+ + ); } diff --git a/frontend/src/pages/CodeFormatter/index.jsx b/frontend/src/pages/CodeFormatter/index.jsx index f29f576..b6ab448 100644 --- a/frontend/src/pages/CodeFormatter/index.jsx +++ b/frontend/src/pages/CodeFormatter/index.jsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useEffect } from 'react'; -import { Button, Select, SelectItem, TextInput, IconButton } from '@carbon/react'; +import { Grid, Column, Button, Select, SelectItem, TextInput, IconButton } from '@carbon/react'; import { Code, TrashCan, Close } from '@carbon/icons-react'; import { ToolHeader, @@ -202,16 +202,19 @@ export default function CodeFormatter() { }, [filter, formattedOutput]); return ( -
- + + + - + +