diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..1f6f415 --- /dev/null +++ b/apps/web/src/components/ui/dialog.tsx @@ -0,0 +1,75 @@ +import { X } from 'lucide-react'; +import type { ComponentProps } from 'react'; +import { Dialog as DialogPrimitive } from 'radix-ui'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogClose = DialogPrimitive.Close; + +function DialogContent({ + children, + className, + ...props +}: ComponentProps) { + return ( + + + + {children} + + + + + ); +} + +function DialogTitle({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +}; diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx new file mode 100644 index 0000000..ce8a481 --- /dev/null +++ b/apps/web/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +import type { ComponentProps } from 'react'; +import { Switch as SwitchPrimitive } from 'radix-ui'; + +import { cn } from '@/lib/utils'; + +export function Switch({ + className, + ...props +}: ComponentProps) { + return ( + + + + ); +} diff --git a/apps/web/src/features/routines/create/editor/routine-editor.test.tsx b/apps/web/src/features/routines/create/editor/routine-editor.test.tsx index bea9163..969f261 100644 --- a/apps/web/src/features/routines/create/editor/routine-editor.test.tsx +++ b/apps/web/src/features/routines/create/editor/routine-editor.test.tsx @@ -39,7 +39,7 @@ describe('routine editor', () => { screen.queryByRole('button', { name: 'Preview' }), ).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Add Exercise' })).toBeVisible(); - expect(screen.getByRole('button', { name: 'Add schedule' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Add schedule' })).toBeEnabled(); expect( screen.getByRole('link', { name: 'Back to routines' }), ).toHaveAttribute('href', '/routines'); diff --git a/apps/web/src/features/routines/create/editor/routine-schedule-field.test.tsx b/apps/web/src/features/routines/create/editor/routine-schedule-field.test.tsx new file mode 100644 index 0000000..f30e08d --- /dev/null +++ b/apps/web/src/features/routines/create/editor/routine-schedule-field.test.tsx @@ -0,0 +1,25 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, expect, it } from 'vitest'; + +import { RoutineScheduleField } from './routine-schedule-field'; + +afterEach(cleanup); + +it('opens a weekly schedule dialog and reveals time controls for selected days', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Add schedule' })); + + expect( + screen.getByRole('heading', { name: 'Schedule Routine' }), + ).toBeVisible(); + expect(screen.getAllByRole('switch')).toHaveLength(7); + expect(screen.queryByLabelText('Monday time')).toBeNull(); + + await user.click(screen.getByRole('switch', { name: 'Schedule Monday' })); + + expect(screen.getByLabelText('Monday time')).toHaveValue('18:00'); + expect(screen.getByRole('button', { name: 'Save Schedule' })).toBeDisabled(); +}); diff --git a/apps/web/src/features/routines/create/editor/routine-schedule-field.tsx b/apps/web/src/features/routines/create/editor/routine-schedule-field.tsx index 3546bb9..0a85c60 100644 --- a/apps/web/src/features/routines/create/editor/routine-schedule-field.tsx +++ b/apps/web/src/features/routines/create/editor/routine-schedule-field.tsx @@ -1,8 +1,29 @@ -/** Displays routine schedule selections and the entry point for adding one. */ +/** Displays routine schedules and opens the responsive weekly schedule editor. */ import { Plus } from 'lucide-react'; +import { useState } from 'react'; import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { cn } from '@/lib/utils'; + +const WEEKDAYS = [ + { label: 'Sunday', shortLabel: 'Sun' }, + { label: 'Monday', shortLabel: 'Mon' }, + { label: 'Tuesday', shortLabel: 'Tue' }, + { label: 'Wednesday', shortLabel: 'Wed' }, + { label: 'Thursday', shortLabel: 'Thu' }, + { label: 'Friday', shortLabel: 'Fri' }, + { label: 'Saturday', shortLabel: 'Sat' }, +] as const; export function RoutineScheduleField() { return ( @@ -10,17 +31,103 @@ export function RoutineScheduleField() {
+ +
+ + ); +} + +function ScheduleDialog() { + const [selectedDays, setSelectedDays] = useState>(new Set()); + + function setDaySelected(day: string, isSelected: boolean) { + setSelectedDays((currentDays) => { + const nextDays = new Set(currentDays); + + if (isSelected) { + nextDays.add(day); + } else { + nextDays.delete(day); + } + + return nextDays; + }); + } + + return ( + + - - + + + + Schedule Routine + + Choose the days and time for this routine. + + +
+ {WEEKDAYS.map((day) => { + const isSelected = selectedDays.has(day.label); + + return ( +
+ + setDaySelected(day.label, checked) + } + /> + {day.shortLabel} + + {isSelected ? ( + + ) : ( + + — + + )} +
+ ); + })} +
+ +
+ + + + +
+
+
); }