From fe41c092f631c108d2078e53900499738c638f9a Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Mon, 7 Jul 2025 21:27:32 -0700 Subject: [PATCH] feat: add Crontab Guru tool for parsing and validating crontab expressions --- package-lock.json | 2 + package.json | 2 + src/components/input/ToolTextInput.tsx | 5 +- .../crontab-guru/crontab-guru.service.test.ts | 26 +++++ src/pages/tools/time/crontab-guru/index.tsx | 108 ++++++++++++++++++ src/pages/tools/time/crontab-guru/meta.ts | 24 ++++ src/pages/tools/time/crontab-guru/service.ts | 23 ++++ src/pages/tools/time/crontab-guru/types.ts | 4 + src/pages/tools/time/index.ts | 4 +- 9 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/pages/tools/time/crontab-guru/crontab-guru.service.test.ts create mode 100644 src/pages/tools/time/crontab-guru/index.tsx create mode 100644 src/pages/tools/time/crontab-guru/meta.ts create mode 100644 src/pages/tools/time/crontab-guru/service.ts create mode 100644 src/pages/tools/time/crontab-guru/types.ts diff --git a/package-lock.json b/package-lock.json index 173cc4e..4600001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,8 @@ "browser-image-compression": "^2.0.2", "buffer": "^6.0.3", "color": "^4.2.3", + "cron-validator": "^1.3.1", + "cronstrue": "^3.0.0", "dayjs": "^1.11.13", "formik": "^2.4.6", "jimp": "^0.22.12", diff --git a/package.json b/package.json index ed7ebcf..86276e5 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "browser-image-compression": "^2.0.2", "buffer": "^6.0.3", "color": "^4.2.3", + "cron-validator": "^1.3.1", + "cronstrue": "^3.0.0", "dayjs": "^1.11.13", "formik": "^2.4.6", "jimp": "^0.22.12", diff --git a/src/components/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx index e5741f6..793e0d7 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); @@ -50,6 +52,7 @@ export default function ToolTextInput({ fullWidth multiline rows={10} + placeholder={placeholder} sx={{ '&.MuiTextField-root': { backgroundColor: 'background.paper' diff --git a/src/pages/tools/time/crontab-guru/crontab-guru.service.test.ts b/src/pages/tools/time/crontab-guru/crontab-guru.service.test.ts new file mode 100644 index 0000000..bce7fd9 --- /dev/null +++ b/src/pages/tools/time/crontab-guru/crontab-guru.service.test.ts @@ -0,0 +1,26 @@ +import { expect, describe, it } from 'vitest'; +import { validateCrontab, explainCrontab } from './service'; + +describe('crontab-guru service', () => { + it('validates correct crontab expressions', () => { + expect(validateCrontab('35 16 * * 0-5')).toBe(true); + expect(validateCrontab('* * * * *')).toBe(true); + expect(validateCrontab('0 12 1 * *')).toBe(true); + }); + + it('invalidates incorrect crontab expressions', () => { + expect(validateCrontab('invalid expression')).toBe(false); + expect(validateCrontab('61 24 * * *')).toBe(false); + }); + + it('explains valid crontab expressions', () => { + expect(explainCrontab('35 16 * * 0-5')).toMatch(/At 04:35 PM/); + expect(explainCrontab('* * * * *')).toMatch(/Every minute/); + }); + + it('returns error for invalid crontab explanation', () => { + expect(explainCrontab('invalid expression')).toMatch( + /Invalid crontab expression/ + ); + }); +}); diff --git a/src/pages/tools/time/crontab-guru/index.tsx b/src/pages/tools/time/crontab-guru/index.tsx new file mode 100644 index 0000000..4d39ed2 --- /dev/null +++ b/src/pages/tools/time/crontab-guru/index.tsx @@ -0,0 +1,108 @@ +import { Box, Typography, 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, validateCrontab, explainCrontab } from './service'; +import { InitialValuesType } from './types'; + +const initialValues: InitialValuesType = {}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Every day at 16:35, Sunday to Friday', + description: 'At 16:35 on every day-of-week from Sunday through Friday.', + sampleText: '35 16 * * 0-5', + sampleResult: 'At 04:35 PM, Sunday through Friday', + sampleOptions: {} + }, + { + title: 'Every minute', + description: 'Runs every minute.', + sampleText: '* * * * *', + sampleResult: 'Every minute', + sampleOptions: {} + }, + { + title: 'Every 5 minutes', + description: 'Runs every 5 minutes.', + sampleText: '*/5 * * * *', + sampleResult: 'Every 5 minutes', + sampleOptions: {} + }, + { + title: 'At 12:00 PM on the 1st of every month', + description: 'Runs at noon on the first day of each month.', + sampleText: '0 12 1 * *', + sampleResult: 'At 12:00 PM, on day 1 of the month', + sampleOptions: {} + } +]; + +export default function CrontabGuru({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + const [isValid, setIsValid] = useState(null); + + const compute = (values: InitialValuesType, input: string) => { + setIsValid(validateCrontab(input)); + setResult(main(input, values)); + }; + + const handleExample = (expr: string) => { + setInput(expr); + setIsValid(validateCrontab(expr)); + setResult(main(expr, initialValues)); + }; + + const getGroups: GetGroupsType | null = () => []; + + return ( + + + + {exampleCards.map((ex, i) => ( + + ))} + + + } + resultComponent={ + <> + {isValid === false && ( + Invalid crontab expression. + )} + + + } + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/time/crontab-guru/meta.ts b/src/pages/tools/time/crontab-guru/meta.ts new file mode 100644 index 0000000..7ae5994 --- /dev/null +++ b/src/pages/tools/time/crontab-guru/meta.ts @@ -0,0 +1,24 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('time', { + name: 'Crontab Guru', + path: 'crontab-guru', + icon: 'mdi:calendar-clock', + description: + 'Parse, validate, and explain crontab expressions in plain English.', + shortDescription: 'Crontab expression parser and explainer', + keywords: [ + 'crontab', + 'cron', + 'schedule', + 'guru', + 'time', + 'expression', + 'parser', + 'explain' + ], + longDescription: + 'Enter a crontab expression (like "35 16 * * 0-5") to get a human-readable explanation and validation. Useful for understanding and debugging cron schedules. Inspired by crontab.guru.', + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/time/crontab-guru/service.ts b/src/pages/tools/time/crontab-guru/service.ts new file mode 100644 index 0000000..1c81ba8 --- /dev/null +++ b/src/pages/tools/time/crontab-guru/service.ts @@ -0,0 +1,23 @@ +import { InitialValuesType } from './types'; +import cronstrue from 'cronstrue'; +import { isValidCron } from 'cron-validator'; + +export function explainCrontab(expr: string): string { + try { + return cronstrue.toString(expr); + } catch (e: any) { + return `Invalid crontab expression: ${e.message}`; + } +} + +export function validateCrontab(expr: string): boolean { + return isValidCron(expr, { seconds: false, allowBlankDay: true }); +} + +export function main(input: string, options: InitialValuesType): string { + if (!input.trim()) return ''; + if (!validateCrontab(input)) { + return 'Invalid crontab expression.'; + } + return explainCrontab(input); +} diff --git a/src/pages/tools/time/crontab-guru/types.ts b/src/pages/tools/time/crontab-guru/types.ts new file mode 100644 index 0000000..54b3d7e --- /dev/null +++ b/src/pages/tools/time/crontab-guru/types.ts @@ -0,0 +1,4 @@ +// Options for crontab-guru tool. Currently empty, but can be extended for advanced features. +export type InitialValuesType = { + // Add future options here +}; diff --git a/src/pages/tools/time/index.ts b/src/pages/tools/time/index.ts index 9b80e65..1e9145d 100644 --- a/src/pages/tools/time/index.ts +++ b/src/pages/tools/time/index.ts @@ -1,3 +1,4 @@ +import { tool as timeCrontabGuru } from './crontab-guru/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, + timeCrontabGuru ];