import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { cn } from '@/presentation/utils'; import { getFutureMatches, isTimeMatches } from '@datasert/cronjs-matcher'; import { parse } from '@datasert/cronjs-parser'; import { AlertCircle, CalendarDays, CheckCircle, Clock } from 'lucide-react'; import { type FC, useMemo } from 'react'; import type { CronDisplayProps, CronNextRun, CronValidationResult, } from './types.js'; const CronDisplay: FC = ({ expression, className, showNextRuns = true, nextRunsCount = 5, timezone = 'UTC', showDescription = true, withCard = true, }) => { const validationResult = useMemo((): CronValidationResult => { if (!expression) { return { isValid: false, error: 'No expression provided' }; } try { const _parsed = parse(`${expression} *`, { hasSeconds: true }); return { isValid: true, description: generateDescription(expression), }; } catch (error) { return { isValid: false, error: error instanceof Error ? error.message : 'Invalid expression', }; } }, [expression]); const nextRuns = useMemo((): CronNextRun[] => { if (!expression || !validationResult.isValid || !showNextRuns) { return []; } try { const matches = getFutureMatches(`${expression} *`, { matchCount: nextRunsCount, timezone, formatInTimezone: true, hasSeconds: true, }); return matches.map((match) => { const date = new Date(match); return { date, timestamp: date.getTime(), formatted: date.toLocaleString(), relative: getRelativeTime(date), }; }); } catch (error) { console.warn('Failed to get future matches:', error); return []; } }, [ expression, validationResult.isValid, showNextRuns, nextRunsCount, timezone, ]); const isCurrentTimeMatch = useMemo(() => { if (!expression || !validationResult.isValid) { return false; } try { return isTimeMatches( `${expression} *`, new Date().toISOString(), timezone ); } catch (_error: unknown) { return false; } }, [expression, validationResult.isValid, timezone]); if (!expression) { return (
No cron expression set
); } return (
Cron Expression {isCurrentTimeMatch && ( Active Now )} {expression}
{validationResult.isValid && showDescription && validationResult.description && ( {validationResult.description} )} {!validationResult.isValid && validationResult.error && ( {validationResult.error} )}
{validationResult.isValid && showNextRuns && nextRuns.length > 0 && (

Next Runs {timezone}

{nextRuns.map((run, index) => (
#{index + 1} {run.formatted}
{run.relative}
))}
)}
); }; function generateDescription(expression: string): string { // Enhanced description generator based on common patterns const parts = expression.split(' '); if (parts.length !== 6) { return expression; } const [sec, min, hour, day, month, weekday] = parts; // Common patterns const patterns: Record = { '* * * * * *': 'Every second', '0 * * * * *': 'Every minute', '0 0 * * * *': 'Every hour', '0 0 0 * * *': 'Daily at midnight', '0 0 0 * * 0': 'Every Sunday at midnight', '0 0 0 * * 1': 'Every Monday at midnight', '0 0 0 * * 2': 'Every Tuesday at midnight', '0 0 0 * * 3': 'Every Wednesday at midnight', '0 0 0 * * 4': 'Every Thursday at midnight', '0 0 0 * * 5': 'Every Friday at midnight', '0 0 0 * * 6': 'Every Saturday at midnight', '0 0 0 1 * *': 'Monthly on the 1st at midnight', '0 0 0 1 1 *': 'Yearly on January 1st at midnight', '0 30 9 * * 1-5': 'Weekdays at 9:30 AM', '0 0 */6 * * *': 'Every 6 hours', '0 */30 * * * *': 'Every 30 minutes', '0 */15 * * * *': 'Every 15 minutes', '0 */5 * * * *': 'Every 5 minutes', }; if (patterns[expression]) { return patterns[expression]; } // Generate dynamic description let description = 'At '; if (sec !== '*' && sec !== '0') { description += `second ${sec}, `; } if (min !== '*') { description += `minute ${min}, `; } if (hour !== '*') { description += `hour ${hour}, `; } if (day !== '*' && weekday !== '*') { description += `on day ${day} and weekday ${weekday} `; } else if (day !== '*') { description += `on day ${day} `; } else if (weekday !== '*') { description += `on weekday ${weekday} `; } if (month !== '*') { description += `in month ${month}`; } // biome-ignore lint/performance/useTopLevelRegex: return description.replace(/,\s*$/, '').replace(/At\s*$/, 'Every occurrence'); } function getRelativeTime(date: Date): string { const now = new Date(); const diffMs = date.getTime() - now.getTime(); if (diffMs < 0) { return 'Past'; } const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) { return `in ${diffSec}s`; } if (diffMin < 60) { return `in ${diffMin}m`; } if (diffHour < 24) { return `in ${diffHour}h`; } if (diffDay < 7) { return `in ${diffDay}d`; } return `in ${Math.floor(diffDay / 7)}w`; } export { CronDisplay };