update normal mode event createa update modal
This commit is contained in:
Generated
+50
-4
@@ -17,7 +17,7 @@
|
|||||||
"@fullcalendar/moment-timezone": "^6.1.19",
|
"@fullcalendar/moment-timezone": "^6.1.19",
|
||||||
"@fullcalendar/react": "^6.1.17",
|
"@fullcalendar/react": "^6.1.17",
|
||||||
"@fullcalendar/timegrid": "^6.1.17",
|
"@fullcalendar/timegrid": "^6.1.17",
|
||||||
"@linagora/twake-mui": "1.1.4",
|
"@linagora/twake-mui": "^1.1.7",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||||
"@mui/icons-material": "^7.1.2",
|
"@mui/icons-material": "^7.1.2",
|
||||||
"@mui/material": "^7.1.2",
|
"@mui/material": "^7.1.2",
|
||||||
@@ -83,6 +83,52 @@
|
|||||||
"node": "24.x"
|
"node": "24.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../twake-ui/packages/twake-mui": {
|
||||||
|
"name": "@linagora/twake-mui",
|
||||||
|
"version": "1.1.4",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "7.19.1",
|
||||||
|
"@types/react": "18.3.26",
|
||||||
|
"@types/react-dom": "18.3.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
|
"cozy-tsconfig": "1.8.1",
|
||||||
|
"eslint": "8.35.0",
|
||||||
|
"eslint-config-cozy-app": "6.7.1",
|
||||||
|
"eslint-config-prettier": "8.6.0",
|
||||||
|
"eslint-plugin-import": "2.27.5",
|
||||||
|
"eslint-plugin-jest": "27.9.0",
|
||||||
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
|
"eslint-plugin-promise": "6.1.1",
|
||||||
|
"eslint-plugin-react": "7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
|
"prettier": "2.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "24.x"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/material": "^7.1.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": false
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
"version": "4.4.4",
|
"version": "4.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
||||||
@@ -3136,9 +3182,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@linagora/twake-mui": {
|
"node_modules/@linagora/twake-mui": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.7",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.4/56321d9e10cd99550e751f15c34aaac6842442c1",
|
"resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.7/dd45fa10aeb5c7e6b6e89f59ff9d88c7d2f7e507",
|
||||||
"integrity": "sha512-L6e+6KgqFUxFY6avmk4UG4/bsdfzo6TxBkahUZlKxvCNEEikVe8z2Cxd0JtUUAWrPGZonTII9n2KhdFSNmNxvg==",
|
"integrity": "sha512-9ZRCeWUWObbIk9h2jg3N0fZlJ+QRBouXzhMNMC4rjQBgFvnCZxZHiMt6kGXQmOov8JsHFKSRhunwfNBYy1cv8Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"@fullcalendar/moment-timezone": "^6.1.19",
|
"@fullcalendar/moment-timezone": "^6.1.19",
|
||||||
"@fullcalendar/react": "^6.1.17",
|
"@fullcalendar/react": "^6.1.17",
|
||||||
"@fullcalendar/timegrid": "^6.1.17",
|
"@fullcalendar/timegrid": "^6.1.17",
|
||||||
"@linagora/twake-mui": "1.1.4",
|
"@linagora/twake-mui": "^1.1.7",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||||
"@mui/icons-material": "^7.1.2",
|
"@mui/icons-material": "^7.1.2",
|
||||||
"@mui/material": "^7.1.2",
|
"@mui/material": "^7.1.2",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default function UserSearch({
|
|||||||
setAttendees,
|
setAttendees,
|
||||||
disabled,
|
disabled,
|
||||||
inputSlot,
|
inputSlot,
|
||||||
|
placeholder,
|
||||||
}: {
|
}: {
|
||||||
attendees: userAttendee[];
|
attendees: userAttendee[];
|
||||||
setAttendees: Function;
|
setAttendees: Function;
|
||||||
@@ -19,8 +20,9 @@ export default function UserSearch({
|
|||||||
inputSlot?: (
|
inputSlot?: (
|
||||||
params: ExtendedAutocompleteRenderInputParams
|
params: ExtendedAutocompleteRenderInputParams
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
const [selectedUsers, setSelectedUsers] = useState(
|
const [selectedUsers, setSelectedUsers] = useState<User[]>(
|
||||||
attendees.map((attendee) => ({
|
attendees.map((attendee) => ({
|
||||||
email: attendee.cal_address,
|
email: attendee.cal_address,
|
||||||
displayName: attendee.cn ?? "",
|
displayName: attendee.cn ?? "",
|
||||||
@@ -44,6 +46,7 @@ export default function UserSearch({
|
|||||||
objectTypes={["user", "contact"]}
|
objectTypes={["user", "contact"]}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputSlot={inputSlot}
|
inputSlot={inputSlot}
|
||||||
|
placeholder={placeholder}
|
||||||
onChange={(event: any, value: User[]) => {
|
onChange={(event: any, value: User[]) => {
|
||||||
setAttendees(
|
setAttendees(
|
||||||
value.map((attendee: User) =>
|
value.map((attendee: User) =>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ A highly reusable dialog component that supports both normal and expanded (fulls
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ✅ **Two Modes**: Normal popup (685px) and expanded fullscreen mode
|
- ✅ **Two Modes**: Normal popup (570px) and expanded fullscreen mode
|
||||||
- ✅ **Preserves Header**: Expanded mode doesn't cover app header (90px default)
|
- ✅ **Preserves Header**: Expanded mode doesn't cover app header (90px default)
|
||||||
- ✅ **Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
|
- ✅ **Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
|
||||||
- ✅ **Instant Transition**: No animation when expanding for immediate feedback
|
- ✅ **Instant Transition**: No animation when expanding for immediate feedback
|
||||||
@@ -62,7 +62,7 @@ function MyComponent() {
|
|||||||
| `actions` | `ReactNode` | - | Action buttons |
|
| `actions` | `ReactNode` | - | Action buttons |
|
||||||
| `isExpanded` | `boolean` | `false` | Toggle fullscreen mode |
|
| `isExpanded` | `boolean` | `false` | Toggle fullscreen mode |
|
||||||
| `onExpandToggle` | `() => void` | - | Handler for back button click when expanded |
|
| `onExpandToggle` | `() => void` | - | Handler for back button click when expanded |
|
||||||
| `normalMaxWidth` | `string` | `"685px"` | Max width in normal mode |
|
| `normalMaxWidth` | `string` | `"570px"` | Max width in normal mode |
|
||||||
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
|
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
|
||||||
| `headerHeight` | `string` | `"90px"` | App header height to preserve |
|
| `headerHeight` | `string` | `"90px"` | App header height to preserve |
|
||||||
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
|
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
|
||||||
@@ -160,7 +160,7 @@ For apps with different header heights:
|
|||||||
|
|
||||||
### Normal Mode (`isExpanded={false}`)
|
### Normal Mode (`isExpanded={false}`)
|
||||||
|
|
||||||
- Dialog: max-width = `normalMaxWidth` (default 685px)
|
- Dialog: max-width = `normalMaxWidth` (default 570px)
|
||||||
- Content: 100% width
|
- Content: 100% width
|
||||||
- Height: auto (fits content)
|
- Height: auto (fits content)
|
||||||
- Position: centered with 32px margin
|
- Position: centered with 32px margin
|
||||||
@@ -215,7 +215,7 @@ import { ResponsiveDialog } from "@/components/Dialog";
|
|||||||
1. **Use `isExpanded` for Show More/Less**: Toggle between modes for better UX
|
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
|
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
|
3. **Hide expand button in actions when expanded**: Back arrow in header provides collapse action
|
||||||
4. **Keep `normalMaxWidth` reasonable**: 685px works well for forms
|
4. **Keep `normalMaxWidth` reasonable**: 570px works well for forms
|
||||||
5. **Center content in expanded mode**: Default 990px provides good reading width
|
5. **Center content in expanded mode**: Default 990px provides good reading width
|
||||||
6. **Preserve header**: Default 90px - adjust via `headerHeight` prop if needed
|
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
|
7. **MUI Stack handles spacing**: Children wrapped in Stack with configurable spacing prop - override with `normalSpacing`/`expandedSpacing` if needed
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import React, { ReactNode } from "react";
|
|||||||
* ResponsiveDialog - A reusable dialog component that can switch between normal and expanded modes
|
* ResponsiveDialog - A reusable dialog component that can switch between normal and expanded modes
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Normal mode: Dialog with customizable max-width (default 685px)
|
* - Normal mode: Dialog with customizable max-width (default 570px)
|
||||||
* - Expanded mode: Full height dialog (excluding app header) with centered content container
|
* - Expanded mode: Full height dialog (excluding app header) with centered content container
|
||||||
* - Fully customizable with sx props for Dialog, DialogTitle, and DialogContent
|
* - Fully customizable with sx props for Dialog, DialogTitle, and DialogContent
|
||||||
* - Preserves app header visibility in expanded mode
|
* - Preserves app header visibility in expanded mode
|
||||||
@@ -58,7 +58,7 @@ interface ResponsiveDialogProps extends Omit<
|
|||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
/** Callback when expand/collapse button is clicked (required if using isExpanded) */
|
/** Callback when expand/collapse button is clicked (required if using isExpanded) */
|
||||||
onExpandToggle?: () => void;
|
onExpandToggle?: () => void;
|
||||||
/** Max width in normal mode (default: "685px") */
|
/** Max width in normal mode (default: "570px") */
|
||||||
normalMaxWidth?: string;
|
normalMaxWidth?: string;
|
||||||
/** Max width of content container in expanded mode (default: "990px") */
|
/** Max width of content container in expanded mode (default: "990px") */
|
||||||
expandedContentMaxWidth?: string;
|
expandedContentMaxWidth?: string;
|
||||||
@@ -98,7 +98,7 @@ function ResponsiveDialog({
|
|||||||
actions,
|
actions,
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
onExpandToggle,
|
onExpandToggle,
|
||||||
normalMaxWidth = "685px",
|
normalMaxWidth = "570px",
|
||||||
expandedContentMaxWidth = "990px",
|
expandedContentMaxWidth = "990px",
|
||||||
headerHeight = "90px",
|
headerHeight = "90px",
|
||||||
normalSpacing = 2,
|
normalSpacing = 2,
|
||||||
@@ -202,8 +202,9 @@ function ResponsiveDialog({
|
|||||||
onClick={onExpandToggle}
|
onClick={onExpandToggle}
|
||||||
aria-label="expand"
|
aria-label="expand"
|
||||||
size="small"
|
size="small"
|
||||||
|
sx={{ marginRight: 1 }}
|
||||||
>
|
>
|
||||||
<OpenInFullIcon />
|
<OpenInFullIcon sx={{ padding: "2px" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<IconButton onClick={onClose} aria-label="close" size="small">
|
<IconButton onClick={onClose} aria-label="close" size="small">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Box, Button, TextField } from "@linagora/twake-mui";
|
import { Box, Button, TextField, useTheme } from "@linagora/twake-mui";
|
||||||
import { Description as DescriptionIcon } from "@mui/icons-material";
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import { Notes as NotesIcon } from "@mui/icons-material";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||||
|
|
||||||
@@ -28,21 +29,40 @@ export function AddDescButton({
|
|||||||
| "warning";
|
| "warning";
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!showDescription && (
|
{!showDescription && (
|
||||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
<FieldWithLabel
|
||||||
<Box display="flex" gap={1}>
|
label={showMore || showDescription ? t("event.form.description") : ""}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
|
{!showMore ? (
|
||||||
<Button
|
<Button
|
||||||
startIcon={<DescriptionIcon />}
|
startIcon={<NotesIcon />}
|
||||||
onClick={() => setShowDescription(true)}
|
onClick={() => setShowDescription(true)}
|
||||||
size="medium"
|
fullWidth
|
||||||
variant={buttonVariant}
|
sx={{
|
||||||
color={buttonColor}
|
justifyContent: "flex-start",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("event.form.addDescription")}
|
{t("event.form.addDescription")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
) : (
|
||||||
|
<Box display="flex" gap={1}>
|
||||||
|
<Button
|
||||||
|
startIcon={<NotesIcon />}
|
||||||
|
onClick={() => setShowDescription(true)}
|
||||||
|
size="medium"
|
||||||
|
variant={buttonVariant}
|
||||||
|
color={buttonColor}
|
||||||
|
>
|
||||||
|
{t("event.form.addDescription")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
)}
|
)}
|
||||||
{showDescription && (
|
{showDescription && (
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ import {
|
|||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
Typography,
|
Typography,
|
||||||
|
useTheme,
|
||||||
} from "@linagora/twake-mui";
|
} from "@linagora/twake-mui";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
import {
|
import {
|
||||||
Close as DeleteIcon,
|
Close as DeleteIcon,
|
||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
Public as PublicIcon,
|
Public as PublicIcon,
|
||||||
|
LocationOn as LocationIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
|
||||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
@@ -35,6 +39,7 @@ import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
|||||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||||
import { AddDescButton } from "./AddDescButton";
|
import { AddDescButton } from "./AddDescButton";
|
||||||
import { DateTimeFields } from "./components/DateTimeFields";
|
import { DateTimeFields } from "./components/DateTimeFields";
|
||||||
|
import { DateTimeSummary } from "./components/DateTimeSummary";
|
||||||
import { FieldWithLabel } from "./components/FieldWithLabel";
|
import { FieldWithLabel } from "./components/FieldWithLabel";
|
||||||
import RepeatEvent from "./EventRepeat";
|
import RepeatEvent from "./EventRepeat";
|
||||||
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
||||||
@@ -156,6 +161,7 @@ export default function EventFormFields({
|
|||||||
onHasEndDateChangedChange,
|
onHasEndDateChangedChange,
|
||||||
}: EventFormFieldsProps) {
|
}: EventFormFieldsProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
// Internal state for 4 separate fields
|
// Internal state for 4 separate fields
|
||||||
const [startDate, setStartDate] = React.useState("");
|
const [startDate, setStartDate] = React.useState("");
|
||||||
@@ -166,10 +172,27 @@ export default function EventFormFields({
|
|||||||
// Track if user has manually changed end date in extended mode
|
// Track if user has manually changed end date in extended mode
|
||||||
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
|
const [hasEndDateChanged, setHasEndDateChanged] = React.useState(false);
|
||||||
|
|
||||||
// Reset hasEndDateChanged when modal closes
|
// Track if user has clicked on datetime clickable section in normal mode
|
||||||
|
// Once clicked, normal mode will always show full fields until modal closes
|
||||||
|
const [hasClickedDateTimeSection, setHasClickedDateTimeSection] = React.useState(false);
|
||||||
|
|
||||||
|
// Track if user has clicked on location section in normal mode
|
||||||
|
const [hasClickedLocationSection, setHasClickedLocationSection] = React.useState(false);
|
||||||
|
|
||||||
|
// Track if user has clicked on calendar section in normal mode
|
||||||
|
const [hasClickedCalendarSection, setHasClickedCalendarSection] = React.useState(false);
|
||||||
|
|
||||||
|
// Track if user has clicked on video meeting section in normal mode
|
||||||
|
const [hasClickedVideoMeetingSection, setHasClickedVideoMeetingSection] = React.useState(false);
|
||||||
|
|
||||||
|
// Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setHasEndDateChanged(false);
|
setHasEndDateChanged(false);
|
||||||
|
setHasClickedDateTimeSection(false);
|
||||||
|
setHasClickedLocationSection(false);
|
||||||
|
setHasClickedCalendarSection(false);
|
||||||
|
setHasClickedVideoMeetingSection(false);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -255,6 +278,7 @@ export default function EventFormFields({
|
|||||||
prevShowMoreRef.current = showMore;
|
prevShowMoreRef.current = showMore;
|
||||||
}, [showMore, isOpen]);
|
}, [showMore, isOpen]);
|
||||||
|
|
||||||
|
|
||||||
// Sync start prop to startDate/startTime
|
// Sync start prop to startDate/startTime
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (start) {
|
if (start) {
|
||||||
@@ -389,6 +413,12 @@ export default function EventFormFields({
|
|||||||
setDescription(updatedDescription);
|
setDescription(updatedDescription);
|
||||||
setHasVideoConference(true);
|
setHasVideoConference(true);
|
||||||
setMeetingLink(newMeetingLink);
|
setMeetingLink(newMeetingLink);
|
||||||
|
// Open description field when adding video conference
|
||||||
|
setShowDescription(true);
|
||||||
|
// Track click on video meeting section in normal mode
|
||||||
|
if (!showMore) {
|
||||||
|
setHasClickedVideoMeetingSection(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [openToast, setOpenToast] = React.useState(false);
|
const [openToast, setOpenToast] = React.useState(false);
|
||||||
@@ -421,7 +451,10 @@ export default function EventFormFields({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldWithLabel label={t("event.form.title")} isExpanded={showMore}>
|
<FieldWithLabel
|
||||||
|
label={showMore ? t("event.form.title") : ""}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label=""
|
label=""
|
||||||
@@ -437,6 +470,224 @@ export default function EventFormFields({
|
|||||||
/>
|
/>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|
||||||
|
<FieldWithLabel
|
||||||
|
label={
|
||||||
|
!showMore && !hasClickedDateTimeSection ? "" : t("event.form.dateTime")
|
||||||
|
}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
|
{!showMore && !hasClickedDateTimeSection ? (
|
||||||
|
<DateTimeSummary
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
timezone={timezone}
|
||||||
|
repetition={repetition}
|
||||||
|
showEndDate={
|
||||||
|
allday ||
|
||||||
|
(hasEndDateChanged && startDate !== endDate) ||
|
||||||
|
(!allday && startDate !== endDate)
|
||||||
|
}
|
||||||
|
onClick={() => setHasClickedDateTimeSection(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DateTimeFields
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
showMore={showMore}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
|
validation={validation}
|
||||||
|
onStartDateChange={handleStartDateChange}
|
||||||
|
onStartTimeChange={handleStartTimeChange}
|
||||||
|
onEndDateChange={handleEndDateChange}
|
||||||
|
onEndTimeChange={handleEndTimeChange}
|
||||||
|
showEndDate={
|
||||||
|
showMore ||
|
||||||
|
allday ||
|
||||||
|
(hasEndDateChanged && startDate !== endDate) ||
|
||||||
|
(!showMore && !allday && startDate !== endDate)
|
||||||
|
}
|
||||||
|
onToggleEndDate={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
|
||||||
|
{!(!showMore && !hasClickedDateTimeSection) && (
|
||||||
|
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||||
|
<Box display="flex" gap={2} alignItems="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox checked={allday} onChange={handleAllDayToggle} />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="h6">{t("event.form.allDay")}</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
||||||
|
}
|
||||||
|
disabled={typeOfAction === "solo"}
|
||||||
|
onChange={() => {
|
||||||
|
const newShowRepeat = !showRepeat;
|
||||||
|
setShowRepeat(newShowRepeat);
|
||||||
|
if (newShowRepeat) {
|
||||||
|
setRepetition({
|
||||||
|
freq: "daily",
|
||||||
|
interval: 1,
|
||||||
|
occurrences: 0,
|
||||||
|
endDate: "",
|
||||||
|
byday: null,
|
||||||
|
} as RepetitionObject);
|
||||||
|
} else {
|
||||||
|
setRepetition({
|
||||||
|
freq: "",
|
||||||
|
interval: 1,
|
||||||
|
occurrences: 0,
|
||||||
|
endDate: "",
|
||||||
|
byday: null,
|
||||||
|
} as RepetitionObject);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="h6">{t("event.form.repeat")}</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TimezoneAutocomplete
|
||||||
|
value={timezone}
|
||||||
|
onChange={setTimezone}
|
||||||
|
zones={timezoneList.zones}
|
||||||
|
getTimezoneOffset={(tzName: string) =>
|
||||||
|
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
||||||
|
}
|
||||||
|
showIcon={false}
|
||||||
|
width={240}
|
||||||
|
size="small"
|
||||||
|
placeholder={t("event.form.timezonePlaceholder")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!(!showMore && !hasClickedDateTimeSection) &&
|
||||||
|
(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
|
||||||
|
<FieldWithLabel label=" " isExpanded={showMore}>
|
||||||
|
<RepeatEvent
|
||||||
|
repetition={repetition}
|
||||||
|
eventStart={new Date(start)}
|
||||||
|
setRepetition={setRepetition}
|
||||||
|
isOwn={typeOfAction !== "solo"}
|
||||||
|
/>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FieldWithLabel label="" isExpanded={showMore}>
|
||||||
|
<AttendeeSelector
|
||||||
|
attendees={attendees}
|
||||||
|
setAttendees={setAttendees}
|
||||||
|
placeholder="Add guests"
|
||||||
|
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||||
|
/>
|
||||||
|
</FieldWithLabel>
|
||||||
|
|
||||||
|
<FieldWithLabel
|
||||||
|
label={showMore ? t("event.form.videoMeeting") : ""}
|
||||||
|
isExpanded={showMore}
|
||||||
|
sx={
|
||||||
|
hasClickedVideoMeetingSection && !showMore
|
||||||
|
? {
|
||||||
|
marginTop: "0 !important",
|
||||||
|
"& > .MuiBox-root": {
|
||||||
|
marginTop: "0 !important",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!showMore ? (
|
||||||
|
<Button
|
||||||
|
startIcon={
|
||||||
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
|
}
|
||||||
|
onClick={handleAddVideoConference}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
display: hasVideoConference ? "none" : "flex",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("event.form.addVisioConference")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Box display="flex" gap={1} alignItems="center">
|
||||||
|
<Button
|
||||||
|
startIcon={
|
||||||
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
|
}
|
||||||
|
onClick={handleAddVideoConference}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: hasVideoConference ? "none" : "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("event.form.addVisioConference")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{hasVideoConference && meetingLink && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
startIcon={
|
||||||
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
|
}
|
||||||
|
onClick={() => window.open(meetingLink, "_blank")}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "4px",
|
||||||
|
mr: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("event.form.joinVisioConference")}
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCopyMeetingLink}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: "primary.main" }}
|
||||||
|
aria-label={t("event.form.copyMeetingLink")}
|
||||||
|
title={t("event.form.copyMeetingLink")}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteVideoConference}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: "error.main" }}
|
||||||
|
aria-label={t("event.form.removeVideoConference")}
|
||||||
|
title={t("event.form.removeVideoConference")}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
|
||||||
<AddDescButton
|
<AddDescButton
|
||||||
showDescription={showDescription}
|
showDescription={showDescription}
|
||||||
setShowDescription={setShowDescription}
|
setShowDescription={setShowDescription}
|
||||||
@@ -447,199 +698,88 @@ export default function EventFormFields({
|
|||||||
buttonColor="secondary"
|
buttonColor="secondary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldWithLabel label={t("event.form.dateTime")} isExpanded={showMore}>
|
<FieldWithLabel
|
||||||
<DateTimeFields
|
label={
|
||||||
startDate={startDate}
|
showMore || hasClickedLocationSection
|
||||||
startTime={startTime}
|
? t("event.form.location")
|
||||||
endDate={endDate}
|
: ""
|
||||||
endTime={endTime}
|
}
|
||||||
allday={allday}
|
isExpanded={showMore}
|
||||||
showMore={showMore}
|
>
|
||||||
hasEndDateChanged={hasEndDateChanged}
|
{!showMore && !hasClickedLocationSection ? (
|
||||||
validation={validation}
|
<Button
|
||||||
onStartDateChange={handleStartDateChange}
|
startIcon={<LocationIcon />}
|
||||||
onStartTimeChange={handleStartTimeChange}
|
onClick={() => setHasClickedLocationSection(true)}
|
||||||
onEndDateChange={handleEndDateChange}
|
fullWidth
|
||||||
onEndTimeChange={handleEndTimeChange}
|
sx={{
|
||||||
showEndDate={
|
justifyContent: "flex-start",
|
||||||
showMore ||
|
borderRadius: "4px",
|
||||||
allday ||
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
(hasEndDateChanged && startDate !== endDate) ||
|
}}
|
||||||
(!showMore && !allday && startDate !== endDate)
|
>
|
||||||
}
|
{location || t("event.form.locationPlaceholder")}
|
||||||
onToggleEndDate={() => {}}
|
</Button>
|
||||||
/>
|
) : (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label=""
|
||||||
|
inputProps={{ "aria-label": t("event.form.location") }}
|
||||||
|
placeholder={t("event.form.locationPlaceholder")}
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|
||||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
<FieldWithLabel
|
||||||
<Box display="flex" gap={2} alignItems="center">
|
label={
|
||||||
<FormControlLabel
|
showMore || hasClickedCalendarSection
|
||||||
control={
|
? t("event.form.calendar")
|
||||||
<Checkbox checked={allday} onChange={handleAllDayToggle} />
|
: ""
|
||||||
}
|
}
|
||||||
label={
|
isExpanded={showMore}
|
||||||
<Typography variant="h6">{t("event.form.allDay")}</Typography>
|
>
|
||||||
}
|
{!showMore && !hasClickedCalendarSection ? (
|
||||||
/>
|
<Button
|
||||||
<FormControlLabel
|
startIcon={
|
||||||
control={
|
<SquareRoundedIcon
|
||||||
<Checkbox
|
sx={{
|
||||||
checked={
|
color:
|
||||||
showRepeat || (typeOfAction === "solo" && !!repetition?.freq)
|
userPersonalCalendars.find((cal) => cal.id === calendarid)
|
||||||
}
|
?.color?.light ?? "#3788D8",
|
||||||
disabled={typeOfAction === "solo"}
|
width: 24,
|
||||||
onChange={() => {
|
height: 24,
|
||||||
const newShowRepeat = !showRepeat;
|
|
||||||
setShowRepeat(newShowRepeat);
|
|
||||||
if (newShowRepeat) {
|
|
||||||
setRepetition({
|
|
||||||
freq: "daily",
|
|
||||||
interval: 1,
|
|
||||||
occurrences: 0,
|
|
||||||
endDate: "",
|
|
||||||
byday: null,
|
|
||||||
} as RepetitionObject);
|
|
||||||
} else {
|
|
||||||
setRepetition({
|
|
||||||
freq: "",
|
|
||||||
interval: 1,
|
|
||||||
occurrences: 0,
|
|
||||||
endDate: "",
|
|
||||||
byday: null,
|
|
||||||
} as RepetitionObject);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
onClick={() => setHasClickedCalendarSection(true)}
|
||||||
<Typography variant="h6">{t("event.form.repeat")}</Typography>
|
fullWidth
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TimezoneAutocomplete
|
|
||||||
value={timezone}
|
|
||||||
onChange={setTimezone}
|
|
||||||
zones={timezoneList.zones}
|
|
||||||
getTimezoneOffset={(tzName: string) =>
|
|
||||||
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
|
||||||
}
|
|
||||||
showIcon={false}
|
|
||||||
width={240}
|
|
||||||
size="small"
|
|
||||||
placeholder={t("event.form.timezonePlaceholder")}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</FieldWithLabel>
|
|
||||||
|
|
||||||
{(showRepeat || (typeOfAction === "solo" && repetition?.freq)) && (
|
|
||||||
<FieldWithLabel label=" " isExpanded={showMore}>
|
|
||||||
<RepeatEvent
|
|
||||||
repetition={repetition}
|
|
||||||
eventStart={new Date(start)}
|
|
||||||
setRepetition={setRepetition}
|
|
||||||
isOwn={typeOfAction !== "solo"}
|
|
||||||
/>
|
|
||||||
</FieldWithLabel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FieldWithLabel
|
|
||||||
label={t("event.form.participants")}
|
|
||||||
isExpanded={showMore}
|
|
||||||
>
|
|
||||||
<AttendeeSelector
|
|
||||||
attendees={attendees}
|
|
||||||
setAttendees={setAttendees}
|
|
||||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
|
||||||
/>
|
|
||||||
</FieldWithLabel>
|
|
||||||
|
|
||||||
<FieldWithLabel
|
|
||||||
label={t("event.form.videoMeeting")}
|
|
||||||
isExpanded={showMore}
|
|
||||||
>
|
|
||||||
<Box display="flex" gap={1} alignItems="center">
|
|
||||||
<Button
|
|
||||||
startIcon={
|
|
||||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
|
||||||
}
|
|
||||||
onClick={handleAddVideoConference}
|
|
||||||
size="medium"
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
sx={{
|
sx={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
display: hasVideoConference ? "none" : "flex",
|
color: alpha(theme.palette.grey[900], 0.9),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("event.form.addVisioConference")}
|
{userPersonalCalendars.find((cal) => cal.id === calendarid)?.name ||
|
||||||
|
t("event.form.calendar")}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
{hasVideoConference && meetingLink && (
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<>
|
<Select
|
||||||
<Button
|
value={calendarid ?? ""}
|
||||||
startIcon={
|
label=""
|
||||||
<img src={iconCamera} alt="camera" width={24} height={24} />
|
SelectDisplayProps={{ "aria-label": t("event.form.calendar") }}
|
||||||
}
|
displayEmpty
|
||||||
onClick={() => window.open(meetingLink, "_blank")}
|
onChange={(e: SelectChangeEvent) =>
|
||||||
size="medium"
|
handleCalendarChange(e.target.value)
|
||||||
variant="contained"
|
}
|
||||||
color="primary"
|
>
|
||||||
sx={{
|
{CalendarItemList(userPersonalCalendars)}
|
||||||
borderRadius: "4px",
|
</Select>
|
||||||
mr: 1,
|
</FormControl>
|
||||||
}}
|
)}
|
||||||
>
|
|
||||||
{t("event.form.joinVisioConference")}
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleCopyMeetingLink}
|
|
||||||
size="small"
|
|
||||||
sx={{ color: "primary.main" }}
|
|
||||||
aria-label={t("event.form.copyMeetingLink")}
|
|
||||||
title={t("event.form.copyMeetingLink")}
|
|
||||||
>
|
|
||||||
<CopyIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleDeleteVideoConference}
|
|
||||||
size="small"
|
|
||||||
sx={{ color: "error.main" }}
|
|
||||||
aria-label={t("event.form.removeVideoConference")}
|
|
||||||
title={t("event.form.removeVideoConference")}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</FieldWithLabel>
|
|
||||||
|
|
||||||
<FieldWithLabel label={t("event.form.location")} isExpanded={showMore}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label=""
|
|
||||||
inputProps={{ "aria-label": t("event.form.location") }}
|
|
||||||
placeholder={t("event.form.locationPlaceholder")}
|
|
||||||
value={location}
|
|
||||||
onChange={(e) => setLocation(e.target.value)}
|
|
||||||
size="small"
|
|
||||||
margin="dense"
|
|
||||||
/>
|
|
||||||
</FieldWithLabel>
|
|
||||||
|
|
||||||
<FieldWithLabel label={t("event.form.calendar")} isExpanded={showMore}>
|
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
|
||||||
<Select
|
|
||||||
value={calendarid ?? ""}
|
|
||||||
label=""
|
|
||||||
SelectDisplayProps={{ "aria-label": t("event.form.calendar") }}
|
|
||||||
displayEmpty
|
|
||||||
onChange={(e: SelectChangeEvent) =>
|
|
||||||
handleCalendarChange(e.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{CalendarItemList(userPersonalCalendars)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|
||||||
{showMore && (
|
{showMore && (
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Box, Typography, useTheme } from "@linagora/twake-mui";
|
||||||
|
import { alpha } from "@mui/material/styles";
|
||||||
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import React from "react";
|
||||||
|
import { useI18n } from "twake-i18n";
|
||||||
|
import { getTimezoneOffset } from "../../Calendar/TimezoneSelector";
|
||||||
|
import { RepetitionObject } from "@/features/Events/EventsTypes";
|
||||||
|
import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters";
|
||||||
|
import { toDateTime } from "../utils/dateTimeHelpers";
|
||||||
|
|
||||||
|
interface DateTimeSummaryProps {
|
||||||
|
startDate: string;
|
||||||
|
startTime: string;
|
||||||
|
endDate: string;
|
||||||
|
endTime: string;
|
||||||
|
allday: boolean;
|
||||||
|
timezone: string;
|
||||||
|
repetition: RepetitionObject;
|
||||||
|
showEndDate: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateTimeSummary: React.FC<DateTimeSummaryProps> = ({
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
timezone,
|
||||||
|
repetition,
|
||||||
|
showEndDate,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const { t, lang } = useI18n();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Format date: "Wednesday, January 28, 2026"
|
||||||
|
const formatDate = (dateStr: string): string => {
|
||||||
|
if (!dateStr) return "";
|
||||||
|
const date = dayjs(dateStr);
|
||||||
|
return date.format(LONG_DATE_FORMAT);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format time: "1:00pm - 2:00pm"
|
||||||
|
const formatTime = (startTimeStr: string, endTimeStr: string): string => {
|
||||||
|
if (allday || !startTimeStr || !endTimeStr) return "";
|
||||||
|
|
||||||
|
const format12Hour = (timeStr: string): string => {
|
||||||
|
const [hours, minutes] = timeStr.split(":").map(Number);
|
||||||
|
const hour12 = hours % 12 || 12;
|
||||||
|
const ampm = hours >= 12 ? "pm" : "am";
|
||||||
|
const mins = minutes === 0 ? "" : `:${minutes.toString().padStart(2, "0")}`;
|
||||||
|
return `${hour12}${mins}${ampm}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = format12Hour(startTimeStr);
|
||||||
|
const end = format12Hour(endTimeStr);
|
||||||
|
return `${start} - ${end}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format timezone: "(UTC+2) Paris"
|
||||||
|
const formatTimezone = (tz: string): string => {
|
||||||
|
if (!tz) return "";
|
||||||
|
try {
|
||||||
|
const offset = getTimezoneOffset(tz, new Date());
|
||||||
|
const tzName = tz.replace(/_/g, " ");
|
||||||
|
return `(${offset}) ${tzName}`;
|
||||||
|
} catch (error) {
|
||||||
|
return tz.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format repeat: "Doesn't repeat" or repeat info
|
||||||
|
const formatRepeat = (rep: RepetitionObject): string => {
|
||||||
|
if (!rep || !rep.freq) {
|
||||||
|
return "Doesn't repeat";
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = rep.interval || 1;
|
||||||
|
const freqMap: { [key: string]: string } = {
|
||||||
|
daily: t("event.repeat.frequency.days"),
|
||||||
|
weekly: t("event.repeat.frequency.weeks"),
|
||||||
|
monthly: t("event.repeat.frequency.months"),
|
||||||
|
yearly: t("event.repeat.frequency.years"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const freqText = freqMap[rep.freq] || rep.freq;
|
||||||
|
|
||||||
|
if (interval === 1) {
|
||||||
|
return `Every ${freqText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Every ${interval} ${freqText}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format date text: show both start and end date if showEndDate is true
|
||||||
|
const formatDateText = (): string => {
|
||||||
|
if (showEndDate && endDate && endDate !== startDate) {
|
||||||
|
const startDateText = formatDate(startDate);
|
||||||
|
const endDateText = formatDate(endDate);
|
||||||
|
return `${startDateText} - ${endDateText}`;
|
||||||
|
}
|
||||||
|
return formatDate(startDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateText = formatDateText();
|
||||||
|
const timeText = formatTime(startTime, endTime);
|
||||||
|
const timezoneText = formatTimezone(timezone);
|
||||||
|
const repeatText = formatRepeat(repetition);
|
||||||
|
|
||||||
|
// Don't render if no date
|
||||||
|
if (!startDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={onClick}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 1.5,
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccessTimeIcon sx={{ color: "text.secondary", mt: 0.5 }} />
|
||||||
|
<Box flex={1}>
|
||||||
|
<Box display="flex" gap={2} alignItems="center" mb={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{dateText}
|
||||||
|
</Typography>
|
||||||
|
{timeText && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: alpha(theme.palette.grey[900], 0.9), fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{timeText}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" gap={2} alignItems="center">
|
||||||
|
<Typography variant="caption" sx={{ color: "#444746" }}>
|
||||||
|
{timezoneText}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: "#444746" }}>
|
||||||
|
{repeatText}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user