-
-
- {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 (
-
-
+
+
+
-
+
+
+
{error && (
-
- {error}
-
+
+
+ {error}
+
+
)}
+
-
+
+
);
}
diff --git a/frontend/src/pages/CronJobParser.jsx b/frontend/src/pages/CronJobParser.jsx
index 9c71b42..995f3b9 100644
--- a/frontend/src/pages/CronJobParser.jsx
+++ b/frontend/src/pages/CronJobParser.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import cronstrue from 'cronstrue';
-import { TextInput, Tile } from '@carbon/react';
+import { Grid, Column, TextInput, Tile } from '@carbon/react';
import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI';
import useLayoutToggle from '../hooks/useLayoutToggle';
@@ -44,16 +44,19 @@ export default function CronJobParser() {
];
return (
-
+
+
);
}
diff --git a/frontend/src/pages/DataGenerator/index.jsx b/frontend/src/pages/DataGenerator/index.jsx
index 9eeef5c..2746682 100644
--- a/frontend/src/pages/DataGenerator/index.jsx
+++ b/frontend/src/pages/DataGenerator/index.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react';
-import { InlineNotification } from '@carbon/react';
+import { Grid, Column, InlineNotification } from '@carbon/react';
import {
ToolHeader,
ToolControls,
@@ -132,25 +132,30 @@ export default function DataGenerator() {
const presetLabels = state.presets.reduce((acc, p) => ({ ...acc, [p.id]: p.name }), {});
return (
-
-
+
+
+
{state.error && (
- dispatch({ type: 'SET_ERROR', payload: null })}
- />
+
+ dispatch({ type: 'SET_ERROR', payload: null })}
+ />
+
)}
-
+
+
+
-
-
-
- dispatch({ type: 'SET_TEMPLATE', payload: e.target.value })}
- placeholder="Enter your template here..."
+
+
+
+
+
+
+ dispatch({ type: 'SET_TEMPLATE', payload: e.target.value })}
+ placeholder="Enter your template here..."
+ />
- 0 ? `Output (${state.duration}ms)` : 'Output'}
- value={state.output}
- readOnly
- placeholder="Generated data will appear here..."
- />
-
+ 0 ? `Output (${state.duration}ms)` : 'Output'}
+ value={state.output}
+ readOnly
+ placeholder="Generated data will appear here..."
+ />
+
+
dispatch({ type: 'TOGGLE_HELP' })} />
-
+
);
}
diff --git a/frontend/src/pages/DateTimeConverter/api/dateTimeAPI.js b/frontend/src/pages/DateTimeConverter/api/dateTimeAPI.js
new file mode 100644
index 0000000..e4c2358
--- /dev/null
+++ b/frontend/src/pages/DateTimeConverter/api/dateTimeAPI.js
@@ -0,0 +1,45 @@
+import * as wailsDateTime from '../../../generated/wails/dateTimeService';
+import * as httpDateTime from '../../../generated/http/dateTimeService';
+
+/**
+ * Universal API client that works in both Wails (desktop) and Web (browser) environments
+ */
+export const dateTimeAPI = {
+ async GetAvailableTimezones() {
+ // Check if we're in Wails environment (desktop app)
+ const isWails = typeof window !== 'undefined' &&
+ window.go &&
+ typeof window.go.main !== 'undefined';
+
+ console.log('Environment check - isWails:', isWails);
+ console.log('Window.go:', typeof window !== 'undefined' ? window.go : 'undefined');
+
+ if (isWails) {
+ try {
+ console.log('Trying Wails API...');
+ const result = await wailsDateTime.GetAvailableTimezones();
+ console.log('Wails API success:', result);
+ return result;
+ } catch (e) {
+ console.error('Wails API failed:', e);
+ console.log('Falling back to HTTP API...');
+ }
+ }
+
+ // Fallback to HTTP API (web app)
+ try {
+ console.log('Trying HTTP API...');
+ const result = await httpDateTime.GetAvailableTimezones();
+ console.log('HTTP API success:', result);
+ return result;
+ } catch (e) {
+ console.error('HTTP API failed:', e);
+ throw e;
+ }
+ }
+};
+
+// Expose for debugging
+if (typeof window !== 'undefined') {
+ window.debugDateTimeAPI = dateTimeAPI;
+}
diff --git a/frontend/src/components/DateTimeOutputField.jsx b/frontend/src/pages/DateTimeConverter/components/DateTimeOutputField.jsx
similarity index 94%
rename from frontend/src/components/DateTimeOutputField.jsx
rename to frontend/src/pages/DateTimeConverter/components/DateTimeOutputField.jsx
index 7f21afb..6b127c9 100644
--- a/frontend/src/components/DateTimeOutputField.jsx
+++ b/frontend/src/pages/DateTimeConverter/components/DateTimeOutputField.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Tile, CopyButton } from '@carbon/react';
-import { getMonospaceFontFamily, getDataFontSize } from '../utils/inputUtils';
+import { getMonospaceFontFamily, getDataFontSize } from '../../../utils/inputUtils';
/**
* Display-only output field with label and copy button
diff --git a/frontend/src/pages/DateTimeConverter/components/InputSection.jsx b/frontend/src/pages/DateTimeConverter/components/InputSection.jsx
new file mode 100644
index 0000000..562b924
--- /dev/null
+++ b/frontend/src/pages/DateTimeConverter/components/InputSection.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Button, TextInput, ComboBox } from '@carbon/react';
+import { Add } from '@carbon/icons-react';
+import { ToolControls } from '../../../components/ToolUI';
+
+export function InputSection({ availableTimezones, input, setInput, timezone, setTimezone, selectedNewTimezone, setSelectedNewTimezone, onAddTimezone, disabled }) {
+ return (
+
-
+
+
+
+
+ {/* Main Input Section */}
+
+
+
+
- );
- };
- return (
-
-
-
- {error && (
-
- {error}
-
- )}
+ }}>
+
+ Decimal value: {currentValue.toLocaleString()}
+
+
+
+ )}
+
+
-
-
- {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)}
-
-
+ {/* Results Grid */}
+
+
+ {bases.map((base) => {
+ if (base.id === inputBase) return null; // Skip current input base
+
+ const value = formatNumber(currentValue, base.base);
+ const displayValue = value || '-';
-
- {renderBasePane('custom', 'Custom', `Enter base ${customBase} number...`, customBase, true)}
+ return (
+
+
+
+
+ {base.label}
+
+
+ Base {base.base}
+
+
+
+
+
+
+ {base.prefix}{displayValue}
+
+
+ {!inputValue && (
+
+ e.g., {base.prefix}{base.example}
+
+ )}
+
+ );
+ })}
-
+
+
+ {/* Quick Tips */}
+
+
+
+ Common Values
+
+
+
+ {[
+ { dec: '255', hex: 'FF', bin: '11111111' },
+ { dec: '256', hex: '100', bin: '100000000' },
+ { dec: '1024', hex: '400', bin: '10000000000' },
+ { dec: '4096', hex: '1000', bin: '1000000000000' },
+ ].map((row, idx) => (
+
+ ))}
+
+
+
+
);
};
diff --git a/frontend/src/pages/NumberConverter/numberConverterReducer.js b/frontend/src/pages/NumberConverter/numberConverterReducer.js
new file mode 100644
index 0000000..242d799
--- /dev/null
+++ b/frontend/src/pages/NumberConverter/numberConverterReducer.js
@@ -0,0 +1,232 @@
+// Number Converter state management
+
+import { INPUT_MODES, INITIAL_STATE } from './constants';
+
+/**
+ * Action types
+ */
+export const ACTION_TYPES = {
+ SET_VALUE: 'SET_VALUE',
+ TOGGLE_BIT: 'TOGGLE_BIT',
+ SET_INPUT_MODE: 'SET_INPUT_MODE',
+ SET_CUSTOM_BASE: 'SET_CUSTOM_BASE',
+ SET_ERROR: 'SET_ERROR',
+ CLEAR_ERROR: 'CLEAR_ERROR',
+ APPLY_BITWISE_OP: 'APPLY_BITWISE_OP',
+ CLEAR_ALL: 'CLEAR_ALL',
+};
+
+/**
+ * Reducer function for Number Converter state
+ * @param {object} state - Current state
+ * @param {object} action - Action to apply
+ * @returns {object} New state
+ */
+export function numberConverterReducer(state = INITIAL_STATE, action) {
+ switch (action.type) {
+ case ACTION_TYPES.SET_VALUE:
+ return {
+ ...state,
+ value: action.payload.value,
+ errors: {}, // Clear all errors on successful value set
+ };
+
+ case ACTION_TYPES.TOGGLE_BIT: {
+ const { position } = action.payload;
+ // XOR with bit mask to toggle
+ const newValue = (state.value ^ (1 << position)) >>> 0;
+ return {
+ ...state,
+ value: newValue,
+ };
+ }
+
+ case ACTION_TYPES.SET_INPUT_MODE:
+ return {
+ ...state,
+ inputMode: action.payload.mode,
+ };
+
+ case ACTION_TYPES.SET_CUSTOM_BASE:
+ return {
+ ...state,
+ customBase: action.payload.base,
+ };
+
+ case ACTION_TYPES.SET_ERROR:
+ return {
+ ...state,
+ errors: {
+ ...state.errors,
+ [action.payload.field]: action.payload.error,
+ },
+ };
+
+ case ACTION_TYPES.CLEAR_ERROR: {
+ const newErrors = { ...state.errors };
+ delete newErrors[action.payload.field];
+ return {
+ ...state,
+ errors: newErrors,
+ };
+ }
+
+ case ACTION_TYPES.APPLY_BITWISE_OP: {
+ const { operation } = action.payload;
+ let newValue = state.value;
+
+ switch (operation) {
+ case 'shiftLeft':
+ newValue = ((state.value << 1) >>> 0);
+ break;
+ case 'shiftRight':
+ newValue = (state.value >>> 1);
+ break;
+ case 'not':
+ newValue = (~state.value >>> 0);
+ break;
+ case 'maskByte':
+ newValue = (state.value & 0xFF) >>> 0;
+ break;
+ case 'setLSB':
+ newValue = (state.value | 1) >>> 0;
+ break;
+ default:
+ break;
+ }
+
+ return {
+ ...state,
+ value: newValue,
+ };
+ }
+
+ case ACTION_TYPES.CLEAR_ALL:
+ return {
+ ...INITIAL_STATE,
+ };
+
+ default:
+ return state;
+ }
+}
+
+/**
+ * Action creator: Set the current value
+ * @param {number} value - New value
+ * @returns {object} Action object
+ */
+export function setValue(value) {
+ return {
+ type: ACTION_TYPES.SET_VALUE,
+ payload: { value },
+ };
+}
+
+/**
+ * Action creator: Toggle a bit at specific position
+ * @param {number} position - Bit position (0-31)
+ * @returns {object} Action object
+ */
+export function toggleBit(position) {
+ return {
+ type: ACTION_TYPES.TOGGLE_BIT,
+ payload: { position },
+ };
+}
+
+/**
+ * Action creator: Set the current input mode
+ * @param {string} mode - Input mode ('bin', 'oct', 'dec', 'hex', 'custom')
+ * @returns {object} Action object
+ */
+export function setInputMode(mode) {
+ return {
+ type: ACTION_TYPES.SET_INPUT_MODE,
+ payload: { mode },
+ };
+}
+
+/**
+ * Action creator: Set custom base
+ * @param {number} base - Custom base (2-36)
+ * @returns {object} Action object
+ */
+export function setCustomBase(base) {
+ return {
+ type: ACTION_TYPES.SET_CUSTOM_BASE,
+ payload: { base },
+ };
+}
+
+/**
+ * Action creator: Set an error for a specific field
+ * @param {string} field - Field name (e.g., 'hex', 'binary')
+ * @param {string} error - Error message
+ * @returns {object} Action object
+ */
+export function setError(field, error) {
+ return {
+ type: ACTION_TYPES.SET_ERROR,
+ payload: { field, error },
+ };
+}
+
+/**
+ * Action creator: Clear error for a specific field
+ * @param {string} field - Field name
+ * @returns {object} Action object
+ */
+export function clearError(field) {
+ return {
+ type: ACTION_TYPES.CLEAR_ERROR,
+ payload: { field },
+ };
+}
+
+/**
+ * Action creator: Apply a bitwise operation
+ * @param {string} operation - Operation name ('shiftLeft', 'shiftRight', 'not', 'maskByte', 'setLSB')
+ * @returns {object} Action object
+ */
+export function applyBitwiseOp(operation) {
+ return {
+ type: ACTION_TYPES.APPLY_BITWISE_OP,
+ payload: { operation },
+ };
+}
+
+/**
+ * Action creator: Clear all state (reset)
+ * @returns {object} Action object
+ */
+export function clearAll() {
+ return {
+ type: ACTION_TYPES.CLEAR_ALL,
+ };
+}
+
+/**
+ * Hook to handle input from conversion fields
+ * Parses input and dispatches appropriate actions
+ * @param {function} dispatch - Dispatch function
+ * @param {string} input - Raw input string
+ * @param {number} base - Base of the input
+ * @param {string} field - Field identifier
+ * @param {function} parseFn - Parse function for this base
+ */
+export function handleConversionInput(dispatch, input, base, field, parseFn) {
+ const result = parseFn(input);
+
+ if (result.error) {
+ // Set error but keep current value
+ dispatch(setError(field, result.error));
+ } else if (result.value !== null) {
+ // Valid value - update and clear error
+ dispatch(setValue(result.value));
+ dispatch(clearError(field));
+ } else {
+ // Empty input - just clear error
+ dispatch(clearError(field));
+ }
+}
diff --git a/frontend/src/pages/NumberConverter/utils.js b/frontend/src/pages/NumberConverter/utils.js
new file mode 100644
index 0000000..016fde0
--- /dev/null
+++ b/frontend/src/pages/NumberConverter/utils.js
@@ -0,0 +1,382 @@
+// Utility functions for number conversion and bit manipulation
+
+import { LIMITS, ERROR_MESSAGES, getValidCharsForBase } from './constants';
+
+/**
+ * Sanitize input string - trim whitespace and remove non-printable chars
+ * @param {string} input - Raw input
+ * @returns {string} Sanitized input
+ */
+export function sanitizeInput(input) {
+ if (!input || typeof input !== 'string') {
+ return '';
+ }
+ return input.trim().replace(/[\s\u0000-\u001F\u007F-\u009F]/g, '');
+}
+
+/**
+ * Check if input contains only valid characters for a base
+ * @param {string} input - Input to validate
+ * @param {number} base - Base (2-36)
+ * @returns {object} { valid: boolean, invalidChar: string|null }
+ */
+export function validateInputChars(input, base) {
+ const validChars = getValidCharsForBase(base);
+
+ for (const char of input) {
+ if (!validChars.includes(char)) {
+ return { valid: false, invalidChar: char };
+ }
+ }
+
+ return { valid: true, invalidChar: null };
+}
+
+/**
+ * Parse input string to 32-bit unsigned integer
+ * @param {string} input - Input string
+ * @param {number} base - Base (2-36)
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseInput(input, base) {
+ const sanitized = sanitizeInput(input);
+
+ if (sanitized === '') {
+ return { value: null, error: null }; // Empty is valid (no change)
+ }
+
+ // Check for negative sign
+ if (sanitized.startsWith('-')) {
+ return { value: null, error: ERROR_MESSAGES.NEGATIVE };
+ }
+
+ // Check for scientific notation
+ if (/[eE]/.test(sanitized)) {
+ return { value: null, error: ERROR_MESSAGES.PARSE_ERROR(base) };
+ }
+
+ // Validate characters
+ const { valid, invalidChar } = validateInputChars(sanitized, base);
+ if (!valid) {
+ return { value: null, error: ERROR_MESSAGES.INVALID_CHAR(invalidChar, base) };
+ }
+
+ // Parse the number
+ const parsed = parseInt(sanitized, base);
+
+ if (isNaN(parsed)) {
+ return { value: null, error: ERROR_MESSAGES.PARSE_ERROR(base) };
+ }
+
+ // Check for overflow and clamp
+ let value = parsed;
+ let error = null;
+
+ if (value < 0) {
+ return { value: null, error: ERROR_MESSAGES.NEGATIVE };
+ }
+
+ if (value > LIMITS.MAX_32BIT_DECIMAL) {
+ value = LIMITS.MAX_32BIT;
+ error = ERROR_MESSAGES.OVERFLOW;
+ }
+
+ // Ensure unsigned 32-bit
+ value = value >>> 0;
+
+ return { value, error };
+}
+
+/**
+ * Parse decimal input
+ * @param {string} input - Decimal string
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseDecimal(input) {
+ return parseInput(input, 10);
+}
+
+/**
+ * Parse hexadecimal input
+ * @param {string} input - Hex string (with or without 0x prefix)
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseHex(input) {
+ let sanitized = sanitizeInput(input);
+
+ // Remove 0x or 0X prefix if present
+ if (sanitized.toLowerCase().startsWith('0x')) {
+ sanitized = sanitized.slice(2);
+ }
+
+ return parseInput(sanitized, 16);
+}
+
+/**
+ * Parse binary input
+ * @param {string} input - Binary string (with or without 0b prefix)
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseBinary(input) {
+ let sanitized = sanitizeInput(input);
+
+ // Remove 0b or 0B prefix if present
+ if (sanitized.toLowerCase().startsWith('0b')) {
+ sanitized = sanitized.slice(2);
+ }
+
+ return parseInput(sanitized, 2);
+}
+
+/**
+ * Parse octal input
+ * @param {string} input - Octal string (with or without 0o prefix)
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseOctal(input) {
+ let sanitized = sanitizeInput(input);
+
+ // Remove 0o or 0O prefix if present
+ if (sanitized.toLowerCase().startsWith('0o')) {
+ sanitized = sanitized.slice(2);
+ }
+
+ return parseInput(sanitized, 8);
+}
+
+/**
+ * Parse input for custom base
+ * @param {string} input - Input string
+ * @param {number} base - Custom base (2-36)
+ * @returns {object} { value: number|null, error: string|null }
+ */
+export function parseCustomBase(input, base) {
+ if (base < 2 || base > 36) {
+ return { value: null, error: 'Base must be between 2 and 36' };
+ }
+ return parseInput(input, base);
+}
+
+/**
+ * Format number as decimal string
+ * @param {number} value - 32-bit unsigned integer
+ * @returns {string} Decimal representation
+ */
+export function formatDecimal(value) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ return '';
+ }
+ // Ensure unsigned and format with thousand separators
+ const unsigned = value >>> 0;
+ return unsigned.toLocaleString('en-US');
+}
+
+/**
+ * Format number as hexadecimal string
+ * @param {number} value - 32-bit unsigned integer
+ * @returns {string} Hex representation (uppercase, no prefix)
+ */
+export function formatHex(value) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ return '';
+ }
+ const unsigned = value >>> 0;
+ return unsigned.toString(16).toUpperCase();
+}
+
+/**
+ * Format number as binary string
+ * @param {number} value - 32-bit unsigned integer
+ * @returns {string} Binary representation (32 bits, grouped by 4)
+ */
+export function formatBinary(value) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ return '';
+ }
+ const unsigned = value >>> 0;
+ const binary = unsigned.toString(2).padStart(32, '0');
+ // Group by 4 bits
+ return binary.match(/.{4}/g).join(' ');
+}
+
+/**
+ * Format number as octal string
+ * @param {number} value - 32-bit unsigned integer
+ * @returns {string} Octal representation
+ */
+export function formatOctal(value) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ return '';
+ }
+ const unsigned = value >>> 0;
+ return unsigned.toString(8);
+}
+
+/**
+ * Format number as custom base string
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} base - Base (2-36)
+ * @returns {string} Formatted representation
+ */
+export function formatCustomBase(value, base) {
+ if (typeof value !== 'number' || isNaN(value)) {
+ return '';
+ }
+ if (base < 2 || base > 36) {
+ return '';
+ }
+ const unsigned = value >>> 0;
+ return unsigned.toString(base).toUpperCase();
+}
+
+/**
+ * Format number for display in specified base
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} base - Base (2-36)
+ * @returns {string} Formatted representation
+ */
+export function formatNumber(value, base) {
+ switch (base) {
+ case 2:
+ return formatBinary(value);
+ case 8:
+ return formatOctal(value);
+ case 10:
+ return formatDecimal(value);
+ case 16:
+ return formatHex(value);
+ default:
+ return formatCustomBase(value, base);
+ }
+}
+
+/**
+ * Get bit value at specific position
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} position - Bit position (0-31)
+ * @returns {number} 0 or 1
+ */
+export function getBit(value, position) {
+ if (position < 0 || position > 31) {
+ return 0;
+ }
+ return ((value >>> position) & 1);
+}
+
+/**
+ * Toggle bit at specific position
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} position - Bit position (0-31)
+ * @returns {number} New value with bit toggled
+ */
+export function toggleBit(value, position) {
+ if (position < 0 || position > 31) {
+ return value >>> 0;
+ }
+ return ((value ^ (1 << position)) >>> 0);
+}
+
+/**
+ * Set bit at specific position
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} position - Bit position (0-31)
+ * @returns {number} New value with bit set
+ */
+export function setBit(value, position) {
+ if (position < 0 || position > 31) {
+ return value >>> 0;
+ }
+ return ((value | (1 << position)) >>> 0);
+}
+
+/**
+ * Clear bit at specific position
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} position - Bit position (0-31)
+ * @returns {number} New value with bit cleared
+ */
+export function clearBit(value, position) {
+ if (position < 0 || position > 31) {
+ return value >>> 0;
+ }
+ return ((value & ~(1 << position)) >>> 0);
+}
+
+/**
+ * Shift left by n bits
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} n - Number of bits to shift
+ * @returns {number} Shifted value (32-bit)
+ */
+export function shiftLeft(value, n = 1) {
+ const shiftAmount = Math.max(0, Math.floor(n));
+ if (shiftAmount >= 32) {
+ return 0;
+ }
+ return ((value << shiftAmount) >>> 0);
+}
+
+/**
+ * Logical shift right by n bits
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} n - Number of bits to shift
+ * @returns {number} Shifted value (32-bit)
+ */
+export function shiftRight(value, n = 1) {
+ const shiftAmount = Math.max(0, Math.floor(n));
+ if (shiftAmount >= 32) {
+ return 0;
+ }
+ return (value >>> shiftAmount);
+}
+
+/**
+ * Bitwise NOT operation
+ * @param {number} value - 32-bit unsigned integer
+ * @returns {number} Inverted value (32-bit)
+ */
+export function bitwiseNot(value) {
+ return (~value >>> 0);
+}
+
+/**
+ * Bitwise AND with mask
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} mask - Mask value
+ * @returns {number} Result (32-bit)
+ */
+export function bitwiseAnd(value, mask) {
+ return ((value & mask) >>> 0);
+}
+
+/**
+ * Bitwise OR with mask
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} mask - Mask value
+ * @returns {number} Result (32-bit)
+ */
+export function bitwiseOr(value, mask) {
+ return ((value | mask) >>> 0);
+}
+
+/**
+ * Get byte value (0-255) at specific byte position
+ * @param {number} value - 32-bit unsigned integer
+ * @param {number} bytePos - Byte position (0-3, 0 = LSB)
+ * @returns {number} Byte value (0-255)
+ */
+export function getByte(value, bytePos) {
+ if (bytePos < 0 || bytePos > 3) {
+ return 0;
+ }
+ return ((value >>> (bytePos * 8)) & 0xFF);
+}
+
+/**
+ * Format byte as hex string
+ * @param {number} byteValue - Byte value (0-255)
+ * @returns {string} Hex string (2 digits, uppercase)
+ */
+export function formatByte(byteValue) {
+ return byteValue.toString(16).toUpperCase().padStart(2, '0');
+}
diff --git a/frontend/src/pages/RegExpTester.jsx b/frontend/src/pages/RegExpTester.jsx
index 01138a7..46bb312 100644
--- a/frontend/src/pages/RegExpTester.jsx
+++ b/frontend/src/pages/RegExpTester.jsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { TextInput, CopyButton } from '@carbon/react';
+import { Grid, Column, TextInput, CopyButton } from '@carbon/react';
import { ChevronDown } from '@carbon/icons-react';
import { ToolHeader, ToolSplitPane } from '../components/ToolUI';
import useLayoutToggle from '../hooks/useLayoutToggle';
@@ -857,16 +857,19 @@ export default function RegExpTester() {
};
return (
-
-
+
+
+
{/* Regex Input Row - Unified Input Group */}
+
+
{/* Error Display */}
{error && (
-
- {error}
-
+
+
+ {error}
+
+
)}
+
{/* Left Pane: Live Highlighted Input */}
-
+
+
);
}
diff --git a/frontend/src/pages/StringUtilities/index.jsx b/frontend/src/pages/StringUtilities/index.jsx
index 93015f2..81b289d 100644
--- a/frontend/src/pages/StringUtilities/index.jsx
+++ b/frontend/src/pages/StringUtilities/index.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import { Grid, Column } from '@carbon/react';
import { ToolHeader } from '../../components/ToolUI';
import useLayoutToggle from '../../hooks/useLayoutToggle';
import ToolLayoutToggle from '../../components/layout/ToolLayoutToggle';
@@ -48,41 +49,37 @@ export default function StringUtilities() {
};
return (
-
-
+
-
+
-
-
- {layout.showToggle && (
-
- )}
-
+
+
+
+ {layout.showToggle && (
+
+ )}
+
+
- {renderPane()}
-
+
+ {renderPane()}
+
+
);
}
diff --git a/frontend/src/pages/TextConverter/index.jsx b/frontend/src/pages/TextConverter/index.jsx
index dea7fe4..d6ff595 100644
--- a/frontend/src/pages/TextConverter/index.jsx
+++ b/frontend/src/pages/TextConverter/index.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
+import { Grid, Column } from '@carbon/react';
import { ToolHeader, ToolPane, ToolSplitPane } from '../../components/ToolUI';
import useLayoutToggle from '../../hooks/useLayoutToggle';
import ConversionControls from './components/ConversionControls';
@@ -164,46 +165,55 @@ export default function TextBasedConverter() {
const isImageOutput = !isAllHashes && isBase64Image(output);
return (
-
-
+
+
+
- {
- setCategory(cat);
- setMethod(meth);
- }}
- customTags={customTags}
- />
+
+ {
+ setCategory(cat);
+ setMethod(meth);
+ }}
+ customTags={customTags}
+ />
+
- {
- setCategory(c);
- setMethod(CONVERTER_MAP[c][0]);
- }}
- method={method}
- setMethod={setMethod}
- subMode={subMode}
- setSubMode={setSubMode}
- layout={layout}
- autoRun={config.autoRun}
- setAutoRun={(val) => updateConfig({ autoRun: val })}
- onConvert={handleConvert}
- isAllHashes={isAllHashes}
- onAddQuickAction={addCurrentToQuickActions}
- isCurrentInQuickActions={isCurrentInQuickActions()}
- />
+
+ {
+ setCategory(c);
+ setMethod(CONVERTER_MAP[c][0]);
+ }}
+ method={method}
+ setMethod={setMethod}
+ subMode={subMode}
+ setSubMode={setSubMode}
+ layout={layout}
+ autoRun={config.autoRun}
+ setAutoRun={(val) => updateConfig({ autoRun: val })}
+ onConvert={handleConvert}
+ isAllHashes={isAllHashes}
+ onAddQuickAction={addCurrentToQuickActions}
+ isCurrentInQuickActions={isCurrentInQuickActions()}
+ />
+
{showConfig && (
-
+
+
+
)}
-
+
+
)}
-
+
+
);
}
diff --git a/frontend/src/pages/TextDiffChecker.jsx b/frontend/src/pages/TextDiffChecker.jsx
index c04342d..288bb02 100644
--- a/frontend/src/pages/TextDiffChecker.jsx
+++ b/frontend/src/pages/TextDiffChecker.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import * as Diff from 'diff';
-import { Button, ContentSwitcher, Switch } from '@carbon/react';
+import { Grid, Column, Button, ContentSwitcher, Switch } from '@carbon/react';
import { Compare, Renew } from '@carbon/icons-react';
import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI';
import useLayoutToggle from '../hooks/useLayoutToggle';
@@ -147,16 +147,19 @@ export default function TextDiffChecker() {
}, [oldText, newText, diffMode]);
return (
-
-
+
+
+
-
+
+
-
-
- setOldText(e.target.value)}
- placeholder="Paste original text..."
- />
- setNewText(e.target.value)}
- placeholder="Paste new text..."
- />
-
-
-
-
-
-
+
+
+
+
+
+
+ setOldText(e.target.value)}
+ placeholder="Paste original text..."
+ />
+ setNewText(e.target.value)}
+ placeholder="Paste new text..."
+ />
+
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/utils/datetimeHelpers.js b/frontend/src/utils/datetimeHelpers.js
deleted file mode 100644
index 3116701..0000000
--- a/frontend/src/utils/datetimeHelpers.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * 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/internal/datetimeconverter/timezone.go b/internal/datetimeconverter/timezone.go
index f6e9001..c8dd871 100644
--- a/internal/datetimeconverter/timezone.go
+++ b/internal/datetimeconverter/timezone.go
@@ -1,7 +1,6 @@
package datetimeconverter
import (
- "fmt"
"os"
"strings"
)
@@ -35,9 +34,9 @@ func readTimezonesFromFile(path string, timezones *[]TimezoneInfo) {
tzAbbr := (path + "/" + f.Name())[1:]
label := tzAbbr
- if strings.Contains(tzAbbr, "/") {
- label = fmt.Sprintf("%s (%s)", f.Name(), tzAbbr)
- }
+ // if strings.Contains(tzAbbr, "/") {
+ // label = fmt.Sprintf("%s (%s)", f.Name(), tzAbbr)
+ // }
*timezones = append(*timezones, TimezoneInfo{
Label: label,