From 549f35e27d0a2098cf35e295b6119caa3a76a3fd Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Mon, 7 Jul 2025 23:34:07 -0700 Subject: [PATCH 1/4] feat: add Epoch Converter tool for converting between Unix timestamps and human-readable dates --- src/components/input/ToolTextInput.tsx | 5 +- .../epoch-converter.service.test.ts | 30 ++++ .../tools/time/epoch-converter/index.tsx | 143 ++++++++++++++++++ src/pages/tools/time/epoch-converter/meta.ts | 23 +++ .../tools/time/epoch-converter/service.ts | 26 ++++ src/pages/tools/time/epoch-converter/types.ts | 3 + src/pages/tools/time/index.ts | 4 +- 7 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 src/pages/tools/time/epoch-converter/epoch-converter.service.test.ts create mode 100644 src/pages/tools/time/epoch-converter/index.tsx create mode 100644 src/pages/tools/time/epoch-converter/meta.ts create mode 100644 src/pages/tools/time/epoch-converter/service.ts create mode 100644 src/pages/tools/time/epoch-converter/types.ts diff --git a/src/components/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx index e5741f6..1d6f3e1 100644 --- a/src/components/input/ToolTextInput.tsx +++ b/src/components/input/ToolTextInput.tsx @@ -7,11 +7,13 @@ import InputFooter from './InputFooter'; export default function ToolTextInput({ value, onChange, - title = 'Input text' + title = 'Input text', + placeholder }: { title?: string; value: string; onChange: (value: string) => void; + placeholder?: string; }) { const { showSnackBar } = useContext(CustomSnackBarContext); const fileInputRef = useRef(null); @@ -48,6 +50,7 @@ export default function ToolTextInput({ value={value} onChange={(event) => onChange(event.target.value)} fullWidth + placeholder={placeholder} multiline rows={10} sx={{ diff --git a/src/pages/tools/time/epoch-converter/epoch-converter.service.test.ts b/src/pages/tools/time/epoch-converter/epoch-converter.service.test.ts new file mode 100644 index 0000000..efb0754 --- /dev/null +++ b/src/pages/tools/time/epoch-converter/epoch-converter.service.test.ts @@ -0,0 +1,30 @@ +import { expect, describe, it } from 'vitest'; +import { epochToDate, dateToEpoch, main } from './service'; + +describe('epoch-converter service', () => { + it('converts epoch (seconds) to date', () => { + expect(epochToDate('1609459200')).toBe('Fri, 01 Jan 2021 00:00:00 GMT'); + }); + it('converts epoch (milliseconds) to date', () => { + expect(epochToDate('1609459200000')).toBe('Fri, 01 Jan 2021 00:00:00 GMT'); + }); + it('returns error for invalid epoch', () => { + expect(epochToDate('notanumber')).toMatch(/Invalid epoch/); + }); + it('converts date string to epoch', () => { + expect(dateToEpoch('2021-01-01T00:00:00Z')).toBe('1609459200'); + }); + it('returns error for invalid date string', () => { + expect(dateToEpoch('notadate')).toMatch(/Invalid date/); + }); + it('main: detects and converts epoch', () => { + expect(main('1609459200', {})).toBe('Fri, 01 Jan 2021 00:00:00 GMT'); + }); + it('main: detects and converts date', () => { + expect(main('2021-01-01T00:00:00Z', {})).toBe('1609459200'); + }); + it('main: returns error for invalid input', () => { + expect(main('notadate', {})).toMatch(/Invalid date/); + expect(main('notanumber', {})).toMatch(/Invalid date/); + }); +}); diff --git a/src/pages/tools/time/epoch-converter/index.tsx b/src/pages/tools/time/epoch-converter/index.tsx new file mode 100644 index 0000000..ff151d8 --- /dev/null +++ b/src/pages/tools/time/epoch-converter/index.tsx @@ -0,0 +1,143 @@ +import { Box, Stack, Button, Alert } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolTextInput from '@components/input/ToolTextInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { main, epochToDate, dateToEpoch } from './service'; +import { InitialValuesType } from './types'; + +const initialValues: InitialValuesType = {}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Epoch to Date (seconds)', + description: 'Convert Unix timestamp (seconds) to date', + sampleText: '1609459200', + sampleResult: 'Fri, 01 Jan 2021 00:00:00 GMT', + sampleOptions: {} + }, + { + title: 'Epoch to Date (milliseconds)', + description: 'Convert Unix timestamp (milliseconds) to date', + sampleText: '1609459200000', + sampleResult: 'Fri, 01 Jan 2021 00:00:00 GMT', + sampleOptions: {} + }, + { + title: 'Date to Epoch', + description: 'Convert date string to Unix timestamp (seconds)', + sampleText: '2021-01-01T00:00:00Z', + sampleResult: '1609459200', + sampleOptions: {} + } +]; + +export default function EpochConverter({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + const [hasInteracted, setHasInteracted] = useState(false); + const [isValid, setIsValid] = useState(null); + + const compute = (_values: InitialValuesType, input: string) => { + let output = main(input, {}); + const invalid = output.startsWith('Invalid'); + setIsValid(!invalid); + setResult(output); + }; + + const handleExample = (expr: string) => { + setInput(expr); + setHasInteracted(true); + compute({}, expr); + }; + + const handleInputChange = (val: string) => { + if (!hasInteracted) setHasInteracted(true); + setInput(val); + }; + + return ( + + + + {exampleCards.map((ex, i) => ( + + ))} + + + } + resultComponent={ +
+ {hasInteracted && isValid === false && ( +
+ + Invalid input. Please enter a valid epoch timestamp or date + string. + +
+ )} +
+ +
+
+ } + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={null} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/time/epoch-converter/meta.ts b/src/pages/tools/time/epoch-converter/meta.ts new file mode 100644 index 0000000..c163dce --- /dev/null +++ b/src/pages/tools/time/epoch-converter/meta.ts @@ -0,0 +1,23 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('time', { + name: 'Epoch Converter', + path: 'epoch-converter', + icon: 'mdi:clock-time-four-outline', + description: + 'Convert Unix epoch timestamps to human-readable dates and vice versa.', + shortDescription: 'Convert between Unix timestamps and dates.', + keywords: [ + 'epoch', + 'converter', + 'timestamp', + 'date', + 'unix', + 'time', + 'convert' + ], + longDescription: + 'Enter a Unix timestamp (in seconds or milliseconds) to get a human-readable date, or enter a date string (e.g., 2021-01-01T00:00:00Z) to get the corresponding Unix timestamp. Useful for developers, sysadmins, and anyone working with time data.', + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/time/epoch-converter/service.ts b/src/pages/tools/time/epoch-converter/service.ts new file mode 100644 index 0000000..477b87c --- /dev/null +++ b/src/pages/tools/time/epoch-converter/service.ts @@ -0,0 +1,26 @@ +import { InitialValuesType } from './types'; + +export function epochToDate(input: string): string { + const num = Number(input); + if (isNaN(num)) return 'Invalid epoch timestamp.'; + // Support both seconds and milliseconds + const date = new Date(num > 1e12 ? num : num * 1000); + if (isNaN(date.getTime())) return 'Invalid epoch timestamp.'; + return date.toUTCString(); +} + +export function dateToEpoch(input: string): string { + const date = new Date(input); + if (isNaN(date.getTime())) return 'Invalid date string.'; + return Math.floor(date.getTime() / 1000).toString(); +} + +export function main(input: string, _options: any): string { + if (!input.trim()) return ''; + // If input is a number, treat as epoch + if (/^-?\d+(\.\d+)?$/.test(input.trim())) { + return epochToDate(input.trim()); + } + // Otherwise, treat as date string + return dateToEpoch(input.trim()); +} diff --git a/src/pages/tools/time/epoch-converter/types.ts b/src/pages/tools/time/epoch-converter/types.ts new file mode 100644 index 0000000..d4135c9 --- /dev/null +++ b/src/pages/tools/time/epoch-converter/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + // splitSeparator: string; +}; diff --git a/src/pages/tools/time/index.ts b/src/pages/tools/time/index.ts index 9b80e65..79c2cd4 100644 --- a/src/pages/tools/time/index.ts +++ b/src/pages/tools/time/index.ts @@ -1,3 +1,4 @@ +import { tool as timeEpochConverter } from './epoch-converter/meta'; import { tool as timeBetweenDates } from './time-between-dates/meta'; import { tool as daysDoHours } from './convert-days-to-hours/meta'; import { tool as hoursToDays } from './convert-hours-to-days/meta'; @@ -11,5 +12,6 @@ export const timeTools = [ convertSecondsToTime, convertTimetoSeconds, truncateClockTime, - timeBetweenDates + timeBetweenDates, + timeEpochConverter ]; From 8a0d757718895b7e1dd29599cc705672300e8a87 Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Tue, 8 Jul 2025 10:36:30 -0700 Subject: [PATCH 2/4] feat: implement ValidatedToolResult component for improved validation feedback in Epoch Converter tool --- src/components/result/ValidatedToolResult.tsx | 60 +++++++++++++++++++ .../tools/time/epoch-converter/index.tsx | 50 +++------------- 2 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 src/components/result/ValidatedToolResult.tsx diff --git a/src/components/result/ValidatedToolResult.tsx b/src/components/result/ValidatedToolResult.tsx new file mode 100644 index 0000000..c1a0b86 --- /dev/null +++ b/src/components/result/ValidatedToolResult.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Alert } from '@mui/material'; + +interface ValidatedToolResultProps { + isValid: boolean | null; + hasInteracted: boolean; + errorMessage?: string; + children: React.ReactNode; +} + +const ValidatedToolResult: React.FC = ({ + isValid, + hasInteracted, + errorMessage = 'Invalid input.', + children +}) => ( +
+ {hasInteracted && isValid === false && ( +
+ + {errorMessage} + +
+ )} +
+ {hasInteracted && isValid === false + ? React.cloneElement(children as React.ReactElement, { value: '' }) + : children} +
+
+); + +export default ValidatedToolResult; diff --git a/src/pages/tools/time/epoch-converter/index.tsx b/src/pages/tools/time/epoch-converter/index.tsx index ff151d8..210a087 100644 --- a/src/pages/tools/time/epoch-converter/index.tsx +++ b/src/pages/tools/time/epoch-converter/index.tsx @@ -8,6 +8,7 @@ import { GetGroupsType } from '@components/options/ToolOptions'; import { CardExampleType } from '@components/examples/ToolExamples'; import { main, epochToDate, dateToEpoch } from './service'; import { InitialValuesType } from './types'; +import ValidatedToolResult from '@components/result/ValidatedToolResult'; const initialValues: InitialValuesType = {}; @@ -89,48 +90,13 @@ export default function EpochConverter({ } resultComponent={ -
- {hasInteracted && isValid === false && ( -
- - Invalid input. Please enter a valid epoch timestamp or date - string. - -
- )} -
- -
-
+ + + } initialValues={initialValues} exampleCards={exampleCards} From 7e848d3d3044611d52dd90d4c1bcd1a16f5878a9 Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Tue, 8 Jul 2025 10:39:25 -0700 Subject: [PATCH 3/4] removed duplicate placeholder attribute in ToolTextInput.tsx --- src/components/input/ToolTextInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx index 18a4027..1d6f3e1 100644 --- a/src/components/input/ToolTextInput.tsx +++ b/src/components/input/ToolTextInput.tsx @@ -53,7 +53,6 @@ export default function ToolTextInput({ placeholder={placeholder} multiline rows={10} - placeholder={placeholder} sx={{ '&.MuiTextField-root': { backgroundColor: 'background.paper' From 47d4aa0c8c7dd395a7708299176e847d99c9e861 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 8 Jul 2025 21:55:23 +0100 Subject: [PATCH 4/4] fix: misc --- .../tools/time/epoch-converter/index.tsx | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/src/pages/tools/time/epoch-converter/index.tsx b/src/pages/tools/time/epoch-converter/index.tsx index ff151d8..e9a5b34 100644 --- a/src/pages/tools/time/epoch-converter/index.tsx +++ b/src/pages/tools/time/epoch-converter/index.tsx @@ -1,12 +1,11 @@ -import { Box, Stack, Button, Alert } from '@mui/material'; +import { Alert, Button, Stack } from '@mui/material'; import React, { useState } from 'react'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import ToolTextInput from '@components/input/ToolTextInput'; import ToolTextResult from '@components/result/ToolTextResult'; -import { GetGroupsType } from '@components/options/ToolOptions'; import { CardExampleType } from '@components/examples/ToolExamples'; -import { main, epochToDate, dateToEpoch } from './service'; +import { main } from './service'; import { InitialValuesType } from './types'; const initialValues: InitialValuesType = {}; @@ -45,18 +44,12 @@ export default function EpochConverter({ const [isValid, setIsValid] = useState(null); const compute = (_values: InitialValuesType, input: string) => { - let output = main(input, {}); + const output = main(input, {}); const invalid = output.startsWith('Invalid'); setIsValid(!invalid); setResult(output); }; - const handleExample = (expr: string) => { - setInput(expr); - setHasInteracted(true); - compute({}, expr); - }; - const handleInputChange = (val: string) => { if (!hasInteracted) setHasInteracted(true); setInput(val); @@ -67,26 +60,11 @@ export default function EpochConverter({ title={title} input={input} inputComponent={ - <> - - - {exampleCards.map((ex, i) => ( - - ))} - - + } resultComponent={