feat(reminders): validate frequency and time range
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { CustomReminder } from '../../types';
|
import { CustomReminder } from '../../types';
|
||||||
import { reminderIcons } from '../icons/Icons';
|
import { reminderIcons } from '../icons/Icons';
|
||||||
|
import {
|
||||||
|
MIN_REMINDER_FREQUENCY_MINUTES,
|
||||||
|
MAX_REMINDER_FREQUENCY_MINUTES,
|
||||||
|
validateReminderInputs,
|
||||||
|
} from './reminderValidation';
|
||||||
|
|
||||||
interface AddReminderModalProps {
|
interface AddReminderModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -19,9 +24,21 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
const [startTime, setStartTime] = useState('09:00');
|
const [startTime, setStartTime] = useState('09:00');
|
||||||
const [endTime, setEndTime] = useState('17:00');
|
const [endTime, setEndTime] = useState('17:00');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<{
|
||||||
|
frequency?: string;
|
||||||
|
timeRange?: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
return validateReminderInputs({
|
||||||
|
frequencyMinutes,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setTitle('');
|
setTitle('');
|
||||||
@@ -30,14 +47,26 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
setStartTime('09:00');
|
setStartTime('09:00');
|
||||||
setEndTime('17:00');
|
setEndTime('17:00');
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
setErrors({});
|
||||||
setTimeout(() => titleInputRef.current?.focus(), 100);
|
setTimeout(() => titleInputRef.current?.focus(), 100);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
setErrors(validate());
|
||||||
|
}, [isOpen, frequencyMinutes, startTime, endTime]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!title || isSaving) return;
|
if (!title || isSaving) return;
|
||||||
|
|
||||||
|
const validation = validate();
|
||||||
|
setErrors(validation);
|
||||||
|
if (Object.keys(validation).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await onAdd({
|
await onAdd({
|
||||||
@@ -125,11 +154,30 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
id='rem-frequency'
|
id='rem-frequency'
|
||||||
value={frequencyMinutes}
|
value={frequencyMinutes}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
setFrequencyMinutes(parseInt(e.target.value, 10))
|
setFrequencyMinutes(
|
||||||
|
Number.isNaN(parseInt(e.target.value, 10))
|
||||||
|
? 0
|
||||||
|
: parseInt(e.target.value, 10)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
min='1'
|
min={MIN_REMINDER_FREQUENCY_MINUTES}
|
||||||
|
max={MAX_REMINDER_FREQUENCY_MINUTES}
|
||||||
|
aria-invalid={Boolean(errors.frequency)}
|
||||||
|
aria-describedby='rem-frequency-help'
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
|
<p
|
||||||
|
id='rem-frequency-help'
|
||||||
|
className={`mt-1 text-sm ${
|
||||||
|
errors.frequency
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: 'text-slate-500 dark:text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{errors.frequency
|
||||||
|
? errors.frequency
|
||||||
|
: `Minimum ${MIN_REMINDER_FREQUENCY_MINUTES} minutes, maximum ${MAX_REMINDER_FREQUENCY_MINUTES} minutes.`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4'>
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
@@ -146,6 +194,10 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={e => setStartTime(e.target.value)}
|
onChange={e => setStartTime(e.target.value)}
|
||||||
required
|
required
|
||||||
|
aria-invalid={Boolean(errors.timeRange)}
|
||||||
|
aria-describedby={
|
||||||
|
errors.timeRange ? 'rem-time-help' : undefined
|
||||||
|
}
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,10 +214,22 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={e => setEndTime(e.target.value)}
|
onChange={e => setEndTime(e.target.value)}
|
||||||
required
|
required
|
||||||
|
aria-invalid={Boolean(errors.timeRange)}
|
||||||
|
aria-describedby={
|
||||||
|
errors.timeRange ? 'rem-time-help' : undefined
|
||||||
|
}
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.timeRange && (
|
||||||
|
<p
|
||||||
|
id='rem-time-help'
|
||||||
|
className='text-sm text-red-600 dark:text-red-400'
|
||||||
|
>
|
||||||
|
{errors.timeRange}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
|
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
|
||||||
<button
|
<button
|
||||||
@@ -178,7 +242,9 @@ const AddReminderModal: React.FC<AddReminderModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSaving}
|
disabled={
|
||||||
|
isSaving || Boolean(errors.frequency || errors.timeRange)
|
||||||
|
}
|
||||||
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
|
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
|
||||||
>
|
>
|
||||||
{isSaving ? 'Adding...' : 'Add Reminder'}
|
{isSaving ? 'Adding...' : 'Add Reminder'}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { CustomReminder } from '../../types';
|
import { CustomReminder } from '../../types';
|
||||||
import { reminderIcons } from '../icons/Icons';
|
import { reminderIcons } from '../icons/Icons';
|
||||||
|
import {
|
||||||
|
MIN_REMINDER_FREQUENCY_MINUTES,
|
||||||
|
MAX_REMINDER_FREQUENCY_MINUTES,
|
||||||
|
validateReminderInputs,
|
||||||
|
} from './reminderValidation';
|
||||||
|
|
||||||
interface EditReminderModalProps {
|
interface EditReminderModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -21,9 +26,20 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
const [startTime, setStartTime] = useState('09:00');
|
const [startTime, setStartTime] = useState('09:00');
|
||||||
const [endTime, setEndTime] = useState('17:00');
|
const [endTime, setEndTime] = useState('17:00');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<{
|
||||||
|
frequency?: string;
|
||||||
|
timeRange?: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const validate = () =>
|
||||||
|
validateReminderInputs({
|
||||||
|
frequencyMinutes,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && reminder) {
|
if (isOpen && reminder) {
|
||||||
setTitle(reminder.title);
|
setTitle(reminder.title);
|
||||||
@@ -32,14 +48,26 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
setStartTime(reminder.startTime);
|
setStartTime(reminder.startTime);
|
||||||
setEndTime(reminder.endTime);
|
setEndTime(reminder.endTime);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
setErrors({});
|
||||||
setTimeout(() => titleInputRef.current?.focus(), 100);
|
setTimeout(() => titleInputRef.current?.focus(), 100);
|
||||||
}
|
}
|
||||||
}, [isOpen, reminder]);
|
}, [isOpen, reminder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
setErrors(validate());
|
||||||
|
}, [isOpen, frequencyMinutes, startTime, endTime]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!title || !reminder || isSaving) return;
|
if (!title || !reminder || isSaving) return;
|
||||||
|
|
||||||
|
const validation = validate();
|
||||||
|
setErrors(validation);
|
||||||
|
if (Object.keys(validation).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await onUpdate({
|
await onUpdate({
|
||||||
@@ -126,12 +154,28 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
type='number'
|
type='number'
|
||||||
id='rem-edit-frequency'
|
id='rem-edit-frequency'
|
||||||
value={frequencyMinutes}
|
value={frequencyMinutes}
|
||||||
onChange={e =>
|
onChange={e => {
|
||||||
setFrequencyMinutes(parseInt(e.target.value, 10))
|
const value = parseInt(e.target.value, 10);
|
||||||
}
|
setFrequencyMinutes(Number.isNaN(value) ? 0 : value);
|
||||||
min='1'
|
}}
|
||||||
|
min={MIN_REMINDER_FREQUENCY_MINUTES}
|
||||||
|
max={MAX_REMINDER_FREQUENCY_MINUTES}
|
||||||
|
aria-invalid={Boolean(errors.frequency)}
|
||||||
|
aria-describedby='rem-edit-frequency-help'
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
|
<p
|
||||||
|
id='rem-edit-frequency-help'
|
||||||
|
className={`mt-1 text-sm ${
|
||||||
|
errors.frequency
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: 'text-slate-500 dark:text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{errors.frequency
|
||||||
|
? errors.frequency
|
||||||
|
: `Minimum ${MIN_REMINDER_FREQUENCY_MINUTES} minutes, maximum ${MAX_REMINDER_FREQUENCY_MINUTES} minutes.`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4'>
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
@@ -148,6 +192,10 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={e => setStartTime(e.target.value)}
|
onChange={e => setStartTime(e.target.value)}
|
||||||
required
|
required
|
||||||
|
aria-invalid={Boolean(errors.timeRange)}
|
||||||
|
aria-describedby={
|
||||||
|
errors.timeRange ? 'rem-edit-time-help' : undefined
|
||||||
|
}
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,10 +212,22 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={e => setEndTime(e.target.value)}
|
onChange={e => setEndTime(e.target.value)}
|
||||||
required
|
required
|
||||||
|
aria-invalid={Boolean(errors.timeRange)}
|
||||||
|
aria-describedby={
|
||||||
|
errors.timeRange ? 'rem-edit-time-help' : undefined
|
||||||
|
}
|
||||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.timeRange && (
|
||||||
|
<p
|
||||||
|
id='rem-edit-time-help'
|
||||||
|
className='text-sm text-red-600 dark:text-red-400'
|
||||||
|
>
|
||||||
|
{errors.timeRange}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
|
<div className='px-6 py-4 bg-slate-50 dark:bg-slate-700/50 flex justify-end space-x-3 rounded-b-lg border-t border-slate-200 dark:border-slate-700'>
|
||||||
<button
|
<button
|
||||||
@@ -180,7 +240,9 @@ const EditReminderModal: React.FC<EditReminderModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isSaving}
|
disabled={
|
||||||
|
isSaving || Boolean(errors.frequency || errors.timeRange)
|
||||||
|
}
|
||||||
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
|
className='px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center dark:focus:ring-offset-slate-800'
|
||||||
>
|
>
|
||||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
|
|||||||
72
components/modals/__tests__/AddReminderModal.test.tsx
Normal file
72
components/modals/__tests__/AddReminderModal.test.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import AddReminderModal from '../AddReminderModal';
|
||||||
|
|
||||||
|
describe('AddReminderModal validation', () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
const onAdd = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays an error when frequency is below the minimum', () => {
|
||||||
|
render(<AddReminderModal isOpen onClose={onClose} onAdd={onAdd} />);
|
||||||
|
|
||||||
|
const frequencyInput = screen.getByLabelText('Remind me every (minutes)');
|
||||||
|
fireEvent.change(frequencyInput, { target: { value: '1' } });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Choose a value between 5 and 720 minutes\./i)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
const submitButton = screen.getByRole('button', { name: /add reminder/i });
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables submit when end time is earlier than start time', () => {
|
||||||
|
render(<AddReminderModal isOpen onClose={onClose} onAdd={onAdd} />);
|
||||||
|
|
||||||
|
const startInput = screen.getByLabelText('From');
|
||||||
|
const endInput = screen.getByLabelText('Until');
|
||||||
|
|
||||||
|
fireEvent.change(startInput, { target: { value: '10:00' } });
|
||||||
|
fireEvent.change(endInput, { target: { value: '09:00' } });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(/End time must be later than start time\./i)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: /add reminder/i })
|
||||||
|
).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onAdd with valid data', async () => {
|
||||||
|
const resolveAdd = jest.fn().mockResolvedValue(undefined);
|
||||||
|
render(<AddReminderModal isOpen onClose={onClose} onAdd={resolveAdd} />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Title'), {
|
||||||
|
target: { value: 'Hydrate' },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('Remind me every (minutes)'), {
|
||||||
|
target: { value: '30' },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('From'), {
|
||||||
|
target: { value: '08:00' },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('Until'), {
|
||||||
|
target: { value: '12:00' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /add reminder/i }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(resolveAdd).toHaveBeenCalledTimes(1));
|
||||||
|
expect(resolveAdd).toHaveBeenCalledWith({
|
||||||
|
title: 'Hydrate',
|
||||||
|
icon: 'bell',
|
||||||
|
frequencyMinutes: 30,
|
||||||
|
startTime: '08:00',
|
||||||
|
endTime: '12:00',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
54
components/modals/reminderValidation.ts
Normal file
54
components/modals/reminderValidation.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export const MIN_REMINDER_FREQUENCY_MINUTES = 5;
|
||||||
|
export const MAX_REMINDER_FREQUENCY_MINUTES = 720;
|
||||||
|
|
||||||
|
export const parseTimeToMinutes = (time: string): number | null => {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
if (
|
||||||
|
Number.isNaN(hours) ||
|
||||||
|
Number.isNaN(minutes) ||
|
||||||
|
hours < 0 ||
|
||||||
|
hours > 23 ||
|
||||||
|
minutes < 0 ||
|
||||||
|
minutes > 59
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReminderValidationParams {
|
||||||
|
frequencyMinutes: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateReminderInputs = ({
|
||||||
|
frequencyMinutes,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
}: ReminderValidationParams): { frequency?: string; timeRange?: string } => {
|
||||||
|
const errors: { frequency?: string; timeRange?: string } = {};
|
||||||
|
const frequency = Number(frequencyMinutes);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(frequency) ||
|
||||||
|
frequency < MIN_REMINDER_FREQUENCY_MINUTES ||
|
||||||
|
frequency > MAX_REMINDER_FREQUENCY_MINUTES
|
||||||
|
) {
|
||||||
|
errors.frequency = `Choose a value between ${MIN_REMINDER_FREQUENCY_MINUTES} and ${MAX_REMINDER_FREQUENCY_MINUTES} minutes.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMinutes = parseTimeToMinutes(startTime);
|
||||||
|
const endMinutes = parseTimeToMinutes(endTime);
|
||||||
|
|
||||||
|
if (
|
||||||
|
startMinutes === null ||
|
||||||
|
endMinutes === null ||
|
||||||
|
endMinutes <= startMinutes
|
||||||
|
) {
|
||||||
|
errors.timeRange = 'End time must be later than start time.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user