feat: redesign event modal with responsive dialog and improved layout

Implemented a new ResponsiveDialog component and redesigned EventModal
with better UX for both normal and extended modes.

New Features:
- Created reusable ResponsiveDialog component (src/components/Dialog/)
  * Normal mode: 685px centered popup
  * Extended mode: fullscreen with 90px header preservation
  * Auto spacing via MUI Stack (16px normal, 24px extended)
  * Back arrow navigation in extended mode
  * No backdrop/shadow in extended mode for seamless integration
  * Configurable props for all dimensions and behaviors

EventModal Improvements:
- Replaced native checkbox with MUI Checkbox component
- Migrated from Popover to ResponsiveDialog
- Reorganized field layout

=> Comprehensive documentation in Dialog/README.md

Note: RepeatEvent integration tests need additional refactoring (tracked separately)
This commit is contained in:
lenhanphung
2025-09-30 16:53:29 +07:00
parent 65309bba18
commit 7c6240442c
7 changed files with 1045 additions and 244 deletions
+229
View File
@@ -0,0 +1,229 @@
# ResponsiveDialog Component
A highly reusable dialog component that supports both normal and expanded (fullscreen) modes while preserving app header visibility.
## Features
-**Two Modes**: Normal popup (685px) and expanded fullscreen mode
-**Preserves Header**: Expanded mode doesn't cover app header (90px default)
-**Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
-**Instant Transition**: No animation when expanding for immediate feedback
-**Back Navigation**: Expanded mode shows back arrow icon in header for easy collapse
-**MUI Stack Spacing**: Uses MUI Stack component with configurable spacing prop (2=16px normal, 3=24px expanded)
-**Fully Customizable**: Override styles with `sx`, `contentSx`, `titleSx` props
-**Container Support**: Content container with configurable max-width
-**Type Safe**: Full TypeScript support
-**No Custom CSS**: Uses MUI `sx` prop pattern
## Basic Usage
```tsx
import { ResponsiveDialog } from "../../components/Dialog";
import { useState } from "react";
function MyComponent() {
const [open, setOpen] = useState(false);
const [showMore, setShowMore] = useState(false);
const actions = (
<>
{!showMore && (
<Button onClick={() => setShowMore(true)}>Show More</Button>
)}
<Button onClick={() => setOpen(false)}>Close</Button>
</>
);
return (
<ResponsiveDialog
open={open}
onClose={() => setOpen(false)}
title="My Dialog"
isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)}
actions={actions}
>
<TextField label="Name" fullWidth />
<TextField label="Email" fullWidth />
{/* Wrapped in Stack with spacing={2} (normal) or spacing={3} (expanded) */}
</ResponsiveDialog>
);
}
```
## Props
| Prop | Type | Default | Description |
| ------------------------- | --------------------- | --------- | ----------------------------------------------------------- |
| `open` | `boolean` | required | Whether dialog is open |
| `onClose` | `() => void` | required | Close handler |
| `title` | `string \| ReactNode` | required | Dialog title (replaced by back icon when expanded) |
| `children` | `ReactNode` | required | Dialog content (wrapped in MUI Stack) |
| `actions` | `ReactNode` | - | Action buttons |
| `isExpanded` | `boolean` | `false` | Toggle fullscreen mode |
| `onExpandToggle` | `() => void` | - | Handler for back button click when expanded |
| `normalMaxWidth` | `string` | `"685px"` | Max width in normal mode |
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
| `headerHeight` | `string` | `"90px"` | App header height to preserve |
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
| `expandedSpacing` | `number` | `3` | Stack spacing in expanded mode (MUI spacing units: 1 = 8px) |
| `contentSx` | `SxProps<Theme>` | - | Custom styles for DialogContent |
| `titleSx` | `SxProps<Theme>` | - | Custom styles for DialogTitle |
| `dialogContentProps` | `DialogContentProps` | - | Additional DialogContent props |
| `dialogTitleProps` | `DialogTitleProps` | - | Additional DialogTitle props |
| `dividers` | `boolean` | `false` | Show dividers between sections |
## Advanced Examples
### Custom Container Styles
```tsx
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Custom Styled Dialog"
isExpanded={showMore}
contentSx={{
backgroundColor: "#f5f5f5",
padding: 4,
}}
titleSx={{
backgroundColor: "primary.main",
color: "white",
}}
>
<TextField label="Field" />
</ResponsiveDialog>
```
### With Dividers
```tsx
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Dialog with Dividers"
dividers={true}
actions={<Button>Save</Button>}
>
<TextField label="Content" />
</ResponsiveDialog>
```
### Different Sizes
```tsx
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Large Dialog"
normalMaxWidth="900px"
expandedContentMaxWidth="1200px"
>
<TextField label="Content" />
</ResponsiveDialog>
```
### Custom Spacing
```tsx
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Custom Spacing"
normalSpacing={1} // 8px spacing in normal mode
expandedSpacing={4} // 32px spacing in expanded mode
>
<TextField label="Field 1" />
<TextField label="Field 2" />
{/* MUI spacing units: 1=8px, 2=16px, 3=24px, 4=32px */}
</ResponsiveDialog>
```
### Custom Header Height
For apps with different header heights:
```tsx
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Custom Header Height"
headerHeight="80px"
isExpanded={true}
>
<TextField label="Content" />
</ResponsiveDialog>
```
## Layout Behavior
### Normal Mode (`isExpanded={false}`)
- Dialog: max-width = `normalMaxWidth` (default 685px)
- Content: 100% width
- Height: auto (fits content)
- Position: centered with 32px margin
- Children spacing: `normalSpacing={2}` (16px via MUI Stack component)
### Expanded Mode (`isExpanded={true}`)
- Dialog: full width, height = `calc(100vh - headerHeight)`
- Content: max-width = `expandedContentMaxWidth` (default 990px), centered
- Position: top = `headerHeight` (preserves header visibility - default 90px)
- Backdrop: opacity = 0 (seamless integration with page)
- Shadow: removed (no elevation in expanded mode)
- Transition: disabled (instant expand/collapse for better UX)
- Title: replaced by back arrow IconButton (calls `onExpandToggle`)
- Children spacing: `expandedSpacing={3}` (24px via MUI Stack component)
- Actions (MuiBox): max-width = `expandedContentMaxWidth` (990px), centered container with buttons right-aligned
- padding: 0 12px
- width: 100%
- justifyContent: flex-end
## Style Merging
All `sx` props are **merged** with base styles, not overridden:
```tsx
// Base styles are preserved, your styles are added
<ResponsiveDialog
contentSx={{
padding: 5, // Adds to base styles
}}
>
```
## TypeScript Support
Full type inference and validation:
```tsx
import { ResponsiveDialog } from "../../components/Dialog";
// All props are type-checked
<ResponsiveDialog
open={open}
onClose={handleClose}
title="Typed Dialog"
// TypeScript will validate all props
>
```
## Best Practices
1. **Use `isExpanded` for Show More/Less**: Toggle between modes for better UX
2. **Provide `onExpandToggle` handler**: Required for back button functionality in expanded mode
3. **Hide expand button in actions when expanded**: Back arrow in header provides collapse action
4. **Keep `normalMaxWidth` reasonable**: 685px works well for forms
5. **Center content in expanded mode**: Default 990px provides good reading width
6. **Preserve header**: Default 90px - adjust via `headerHeight` prop if needed
7. **MUI Stack handles spacing**: Children wrapped in Stack with configurable spacing prop - override with `normalSpacing`/`expandedSpacing` if needed
8. **Seamless expanded mode**: No backdrop/shadow/transition creates clean integration with page
9. **Instant expand**: No animation when expanding provides immediate feedback
10. **Customize with `sx` props**: Avoid creating custom CSS files
## See Also
- `EventModal.tsx` - Real-world example usage
- MUI Dialog documentation: https://mui.com/material-ui/react-dialog/
+170
View File
@@ -0,0 +1,170 @@
import {
Dialog,
DialogActions,
DialogContent,
DialogContentProps,
DialogTitle,
DialogTitleProps,
DialogProps,
IconButton,
Stack,
SxProps,
Theme,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import React, { ReactNode } from "react";
/**
* ResponsiveDialog - A reusable dialog component that can switch between normal and expanded modes
*
* Features:
* - Normal mode: Dialog with customizable max-width (default 685px)
* - Expanded mode: Full height dialog (excluding app header) with centered content container
* - Fully customizable with sx props for Dialog, DialogTitle, and DialogContent
* - Preserves app header visibility in expanded mode
*
* @example
* ```tsx
* <ResponsiveDialog
* open={open}
* onClose={handleClose}
* title="My Dialog"
* isExpanded={showMore}
* actions={<Button onClick={handleSave}>Save</Button>}
* contentSx={{ padding: 3 }}
* >
* <TextField label="Name" />
* </ResponsiveDialog>
* ```
*/
interface ResponsiveDialogProps
extends Omit<DialogProps, "maxWidth" | "title"> {
/** Whether the dialog is open */
open: boolean;
/** Callback fired when the dialog should be closed */
onClose: () => void;
/** Dialog title - can be string or custom ReactNode */
title: string | ReactNode;
/** Dialog content - form fields, text, etc. */
children: ReactNode;
/** Optional actions rendered in DialogActions (buttons, etc.) */
actions?: ReactNode;
/** Toggle between normal and expanded (fullscreen) mode */
isExpanded?: boolean;
/** Callback when expand/collapse button is clicked (required if using isExpanded) */
onExpandToggle?: () => void;
/** Max width in normal mode (default: "685px") */
normalMaxWidth?: string;
/** Max width of content container in expanded mode (default: "990px") */
expandedContentMaxWidth?: string;
/** Height of app header to preserve visibility (default: "90px") */
headerHeight?: string;
/** Spacing between children in normal mode (default: 2 = 16px) */
normalSpacing?: number;
/** Spacing between children in expanded mode (default: 3 = 24px) */
expandedSpacing?: number;
/** Custom styles for DialogContent - merged with base styles */
contentSx?: SxProps<Theme>;
/** Custom styles for DialogTitle */
titleSx?: SxProps<Theme>;
/** Additional props for DialogContent (excluding sx) */
dialogContentProps?: Omit<DialogContentProps, "sx">;
/** Additional props for DialogTitle (excluding sx) */
dialogTitleProps?: Omit<DialogTitleProps, "sx">;
/** Whether to display dividers between title/content/actions */
dividers?: boolean;
}
function ResponsiveDialog({
open,
onClose,
title,
children,
actions,
isExpanded = false,
onExpandToggle,
normalMaxWidth = "685px",
expandedContentMaxWidth = "990px",
headerHeight = "90px",
normalSpacing = 2,
expandedSpacing = 3,
contentSx,
titleSx,
dialogContentProps,
dialogTitleProps,
dividers = false,
sx,
...otherDialogProps
}: ResponsiveDialogProps) {
const baseSx: SxProps<Theme> = {
"& .MuiBackdrop-root": {
opacity: isExpanded ? "0 !important" : undefined,
transition: isExpanded ? "none !important" : undefined,
},
"& .MuiDialog-paper": {
maxWidth: isExpanded ? "100%" : normalMaxWidth,
width: "100%",
height: isExpanded ? `calc(100vh - ${headerHeight})` : "auto",
margin: isExpanded ? `${headerHeight} 0 0 0` : "32px",
maxHeight: isExpanded
? `calc(100vh - ${headerHeight})`
: `calc(100vh - 90px)`,
boxShadow: isExpanded ? "none !important" : undefined,
transition: isExpanded ? "none !important" : undefined,
},
"& .MuiDialogActions-root .MuiBox-root": {
maxWidth: isExpanded ? expandedContentMaxWidth : undefined,
margin: isExpanded ? "0 auto" : undefined,
padding: isExpanded ? "0 12px" : undefined,
width: isExpanded ? "100%" : undefined,
justifyContent: isExpanded ? "flex-end" : undefined,
},
};
const baseContentSx: SxProps<Theme> = {
maxWidth: isExpanded ? expandedContentMaxWidth : "100%",
margin: isExpanded ? "0 auto" : "0",
width: "100%",
};
const currentSpacing = isExpanded ? expandedSpacing : normalSpacing;
return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
fullWidth
transitionDuration={isExpanded ? 0 : 300}
sx={[baseSx, ...(Array.isArray(sx) ? sx : [sx])]}
{...otherDialogProps}
>
<DialogTitle sx={titleSx} {...dialogTitleProps}>
{isExpanded && onExpandToggle ? (
<IconButton
onClick={onExpandToggle}
aria-label="show less"
sx={{ marginLeft: "-8px" }}
>
<ArrowBackIcon />
</IconButton>
) : (
title
)}
</DialogTitle>
<DialogContent
dividers={dividers}
sx={[
baseContentSx,
...(Array.isArray(contentSx) ? contentSx : [contentSx]),
]}
{...dialogContentProps}
>
<Stack spacing={currentSpacing}>{children}</Stack>
</DialogContent>
{actions && <DialogActions>{actions}</DialogActions>}
</Dialog>
);
}
export default ResponsiveDialog;
+1
View File
@@ -0,0 +1 @@
export { default as ResponsiveDialog } from "./ResponsiveDialog";
+302 -221
View File
@@ -2,14 +2,11 @@ import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import {
Box,
Button,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
Popover,
Select,
SelectChangeEvent,
TextField,
@@ -18,7 +15,7 @@ import {
import React, { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import AttendeeSelector from "../../components/Attendees/AttendeeSearch";
import { TIMEZONES } from "../../utils/timezone-data";
import { ResponsiveDialog } from "../../components/Dialog";
import { putEventAsync } from "../Calendars/CalendarSlice";
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
@@ -26,6 +23,58 @@ import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { createSelector } from "@reduxjs/toolkit";
import RepeatEvent from "../../components/Event/EventRepeat";
// Helper component for field with label
const FieldWithLabel = React.memo(
({
label,
isExpanded,
children,
}: {
label: string;
isExpanded: boolean;
children: React.ReactNode;
}) => {
if (!isExpanded) {
// Normal mode: label on top
return (
<Box>
<Typography
component="label"
sx={{
display: "block",
marginBottom: "4px",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
{label}
</Typography>
{children}
</Box>
);
}
// Extended mode: label on left
return (
<Box display="flex" alignItems="center">
<Typography
component="label"
sx={{
minWidth: "115px",
marginRight: "12px",
flexShrink: 0,
}}
>
{label}
</Typography>
<Box flexGrow={1}>{children}</Box>
</Box>
);
}
);
FieldWithLabel.displayName = "FieldWithLabel";
function EventPopover({
anchorEl,
open,
@@ -63,7 +112,6 @@ function EventPopover({
const userPersonnalCalendars: Calendars[] = useAppSelector(
selectPersonnalCalendars
);
const timezones = TIMEZONES.aliases;
const [showMore, setShowMore] = useState(false);
const [title, setTitle] = useState(event?.title ?? "");
@@ -90,9 +138,7 @@ function EventPopover({
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
const [busy, setBusy] = useState(event?.transp ?? "OPAQUE");
const [timezone, setTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
useEffect(() => {
if (selectedRange) {
@@ -111,7 +157,9 @@ function EventPopover({
}, [event, organizer?.cal_address]);
const handleClose = () => {
onClose({}, "backdropClick"); // Reset
onClose({}, "backdropClick");
// Reset state
setShowMore(false);
setTitle("");
setDescription("");
setAttendees([]);
@@ -126,7 +174,7 @@ function EventPopover({
calId: userPersonnalCalendars[calendarid].id,
title,
URL: `/calendars/${userPersonnalCalendars[calendarid].id}/${newEventUID}.ics`,
start: new Date(start),
start: new Date(start).toISOString(),
allday,
uid: newEventUID,
description,
@@ -150,244 +198,277 @@ function EventPopover({
alarm: { trigger: alarm, action: "EMAIL" },
};
if (end) {
newEvent.end = new Date(end);
newEvent.end = new Date(end).toISOString();
}
if (attendees.length > 0) {
newEvent.attendee = newEvent.attendee.concat(attendees);
}
await dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
})
);
// Close popup immediately
onClose({}, "backdropClick");
// Reset
// Reset state
setShowMore(false);
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setCalendarid(0);
// Save to API in background
dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
})
);
};
const dialogActions = (
<Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && (
<Button onClick={() => setShowMore(!showMore)}>Show More</Button>
)}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>
<Button variant="contained" onClick={handleSave} disabled={!title}>
Save
</Button>
</Box>
</Box>
);
return (
<Popover
<ResponsiveDialog
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
}}
transformOrigin={{
vertical: "center",
horizontal: "center",
}}
title={event?.uid ? "Duplicate Event" : "Create Event"}
isExpanded={showMore}
onExpandToggle={() => setShowMore(!showMore)}
actions={dialogActions}
>
<Card>
<CardHeader title={event?.uid ? "Duplicate Event" : "Create Event"} />
<CardContent
style={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
>
<TextField
fullWidth
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
<FormControl fullWidth margin="dense" size="small">
<FieldWithLabel label="Title" isExpanded={showMore}>
<TextField
fullWidth
label={!showMore ? "Title" : ""}
value={title}
onChange={(e) => setTitle(e.target.value)}
size="small"
margin="dense"
/>
</FieldWithLabel>
<FieldWithLabel label="Description" isExpanded={showMore}>
<TextField
fullWidth
label={!showMore ? "Description" : ""}
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
</FieldWithLabel>
<FieldWithLabel label="Calendar" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
{!showMore && (
<InputLabel id="calendar-select-label">Calendar</InputLabel>
<Select
labelId="calendar-select-label"
value={calendarid.toString()}
label="Calendar"
onChange={(e: SelectChangeEvent) =>
setCalendarid(Number(e.target.value))
}
>
{Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
{userPersonnalCalendars[index].name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
fullWidth
label="Start"
type={allday ? "date" : "datetime-local"}
value={allday ? start.split("T")[0] : start}
onChange={(e) => {
const newStart = e.target.value;
setStart(newStart);
const newRange = {
...selectedRange,
start: new Date(newStart),
startStr: newStart,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<TextField
fullWidth
label="End"
type={allday ? "date" : "datetime-local"}
value={allday ? end.split("T")[0] : end}
onChange={(e) => {
const newEnd = e.target.value;
setEnd(newEnd);
const newRange = {
...selectedRange,
end: new Date(newEnd),
endStr: newEnd,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
<label>
<input
type="checkbox"
checked={allday}
onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
)}
<Select
labelId="calendar-select-label"
value={calendarid.toString()}
label={!showMore ? "Calendar" : ""}
onChange={(e: SelectChangeEvent) =>
setCalendarid(Number(e.target.value))
}
>
{Object.keys(userPersonnalCalendars).map((calendar, index) => (
<MenuItem key={index} value={index}>
{userPersonnalCalendars[index].name}
</MenuItem>
))}
</Select>
</FormControl>
</FieldWithLabel>
<FieldWithLabel label="Date & Time" isExpanded={showMore}>
<Box display="flex" gap={2}>
<Box flexGrow={1}>
{showMore && (
<Typography variant="caption" display="block" mb={0.5}>
Start
</Typography>
)}
<TextField
fullWidth
label={!showMore ? "Start" : ""}
type={allday ? "date" : "datetime-local"}
value={allday ? start.split("T")[0] : start}
onChange={(e) => {
const newStart = e.target.value;
setStart(newStart);
const newRange = {
...selectedRange,
startStr: allday ? start.split("T")[0] : start,
endStr: allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString(),
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(
allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString()
),
start: new Date(newStart),
startStr: newStart,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
All day
</label>
<TextField
fullWidth
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
size="small"
margin="dense"
multiline
rows={2}
/>
<TextField
fullWidth
label="Location"
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
{/* Extended options */}
{showMore && (
<>
<RepeatEvent
repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition}
/>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="alarm">Alarm</InputLabel>
<Select
labelId="alarm"
value={alarm}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
>
<MenuItem value={""}>No Alarm</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="Visibility">Visibility</InputLabel>
<Select
labelId="Visibility"
label="Visibility"
value={eventClass}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense" size="small">
<InputLabel id="busy">is Busy</InputLabel>
<Select
labelId="busy"
value={busy}
label="is busy"
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
</>
)}
</CardContent>
<CardActions>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>
<Button size="small" onClick={() => setShowMore(!showMore)}>
{showMore ? "Show Less" : "Show More"}
</Button>
<Button variant="contained" onClick={handleSave} disabled={!title}>
Save
</Button>
</Box>
</CardActions>
</Card>
</Popover>
<Box flexGrow={1}>
{showMore && (
<Typography variant="caption" display="block" mb={0.5}>
End
</Typography>
)}
<TextField
fullWidth
label={!showMore ? "End" : ""}
type={allday ? "date" : "datetime-local"}
value={allday ? end.split("T")[0] : end}
onChange={(e) => {
const newEnd = e.target.value;
setEnd(newEnd);
const newRange = {
...selectedRange,
end: new Date(newEnd),
endStr: newEnd,
allDay: allday,
};
setSelectedRange(newRange);
calendarRef.current?.select(newRange);
}}
size="small"
margin="dense"
InputLabelProps={{ shrink: true }}
/>
</Box>
</Box>
</FieldWithLabel>
<FieldWithLabel label=" " isExpanded={showMore}>
<Box>
<FormControlLabel
control={
<Checkbox
checked={allday}
onChange={() => {
const endDate = new Date(end);
const startDate = new Date(start);
setAllDay(!allday);
if (endDate.getDate() === startDate.getDate()) {
endDate.setDate(startDate.getDate() + 1);
setEnd(formatLocalDateTime(endDate));
}
const newRange = {
...selectedRange,
startStr: allday ? start.split("T")[0] : start,
endStr: allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString(),
start: new Date(allday ? start.split("T")[0] : start),
end: new Date(
allday
? endDate.toISOString().split("T")[0]
: endDate.toISOString()
),
allDay: allday,
};
setSelectedRange(newRange);
}}
/>
}
label="All day"
sx={{ padding: "0 8px 0 0" }}
/>
</Box>
</FieldWithLabel>
<FieldWithLabel label="Attendees" isExpanded={showMore}>
<AttendeeSelector attendees={attendees} setAttendees={setAttendees} />
</FieldWithLabel>
<FieldWithLabel label="Location" isExpanded={showMore}>
<TextField
fullWidth
label={!showMore ? "Location" : ""}
value={location}
onChange={(e) => setLocation(e.target.value)}
size="small"
margin="dense"
/>
</FieldWithLabel>
{/* Extended options */}
{showMore && (
<>
<FieldWithLabel label="Repeat" isExpanded={showMore}>
<RepeatEvent
repetition={repetition}
eventStart={selectedRange?.start ?? new Date()}
setRepetition={setRepetition}
/>
</FieldWithLabel>
<FieldWithLabel label="Alarm" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
<Select
labelId="alarm"
value={alarm}
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
>
<MenuItem value={""}>No Alarm</MenuItem>
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
<MenuItem value={"-PT1D"}>1 day</MenuItem>
<MenuItem value={"-PT2D"}>2 days</MenuItem>
<MenuItem value={"-PT1W"}>1 week</MenuItem>
</Select>
</FormControl>
</FieldWithLabel>
<FieldWithLabel label="Visibility" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
<Select
labelId="Visibility"
value={eventClass}
onChange={(e: SelectChangeEvent) =>
setEventClass(e.target.value)
}
>
<MenuItem value={"PUBLIC"}>Public</MenuItem>
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
<MenuItem value={"PRIVATE"}>Private</MenuItem>
</Select>
</FormControl>
</FieldWithLabel>
<FieldWithLabel label="Show as" isExpanded={showMore}>
<FormControl fullWidth margin="dense" size="small">
<Select
labelId="busy"
value={busy}
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
>
<MenuItem value={"TRANSPARENT"}>Free</MenuItem>
<MenuItem value={"OPAQUE"}>Busy </MenuItem>
</Select>
</FormControl>
</FieldWithLabel>
</>
)}
</ResponsiveDialog>
);
}