refactor: extract reusable TimezoneAutocomplete component
- Create new TimezoneAutocomplete component for shared timezone selection logic - Update TimezoneSelector to use the new component - Replace inline Autocomplete in EventFormFields with TimezoneAutocomplete - Replace inline Autocomplete in EventModal with TimezoneAutocomplete - Reduce code duplication and improve maintainability
This commit is contained in:
committed by
Benoit TELLIER
parent
2ab7c448bf
commit
6421f53a8d
@@ -1,12 +1,7 @@
|
|||||||
import {
|
import { Button, Popover } from "@mui/material";
|
||||||
Autocomplete,
|
|
||||||
Button,
|
|
||||||
ListItem,
|
|
||||||
Popover,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { MouseEvent, useMemo, useState } from "react";
|
import { MouseEvent, useMemo, useState } from "react";
|
||||||
import { TIMEZONES } from "../../utils/timezone-data";
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
|
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||||
|
|
||||||
interface TimezoneSelectProps {
|
interface TimezoneSelectProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -24,16 +19,8 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
|||||||
|
|
||||||
return { zones, browserTz, getTimezoneOffset };
|
return { zones, browserTz, getTimezoneOffset };
|
||||||
}, []);
|
}, []);
|
||||||
const options = useMemo(() => {
|
|
||||||
return timezoneList.zones.map((tz) => ({
|
|
||||||
value: tz,
|
|
||||||
label: tz.replace(/_/g, " "),
|
|
||||||
offset: timezoneList.getTimezoneOffset(tz),
|
|
||||||
}));
|
|
||||||
}, [timezoneList]);
|
|
||||||
|
|
||||||
const selectedOption =
|
const selectedOffset = getTimezoneOffset(value);
|
||||||
options.find((opt) => opt.value === value) || options[0];
|
|
||||||
|
|
||||||
const handleOpen = (event: MouseEvent<HTMLElement>) => {
|
const handleOpen = (event: MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -59,7 +46,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
|||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedOption ? selectedOption.offset : "Select Timezone"}
|
{selectedOffset || "Select Timezone"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
@@ -80,58 +67,21 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TimeZoneSearch
|
<TimezoneAutocomplete
|
||||||
selectedOption={selectedOption}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
handleClose={handleClose}
|
zones={timezoneList.zones}
|
||||||
options={options}
|
getTimezoneOffset={getTimezoneOffset}
|
||||||
|
autoFocus={true}
|
||||||
|
inputFontSize="10px"
|
||||||
|
inputPadding="2px 4px"
|
||||||
|
onClose={handleClose}
|
||||||
|
disableClearable={true}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function TimeZoneSearch({
|
|
||||||
selectedOption,
|
|
||||||
onChange,
|
|
||||||
handleClose,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
selectedOption: { value: string; label: string; offset: string };
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
handleClose: () => void;
|
|
||||||
options: { value: string; label: string; offset: string }[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Autocomplete
|
|
||||||
autoFocus
|
|
||||||
value={selectedOption}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
onChange(newValue.value);
|
|
||||||
handleClose(); // close after selection
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={options}
|
|
||||||
getOptionLabel={(option) => `${option.offset} ${option.label}`}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
style: {
|
|
||||||
fontSize: "10px",
|
|
||||||
padding: "2px 4px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
disableClearable
|
|
||||||
renderValue={(value) => <div>{value.offset}</div>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveTimezone(tzName: string): string {
|
export function resolveTimezone(tzName: string): string {
|
||||||
if (TIMEZONES.zones[tzName]) {
|
if (TIMEZONES.zones[tzName]) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
Autocomplete,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
Description as DescriptionIcon,
|
Description as DescriptionIcon,
|
||||||
@@ -23,7 +22,6 @@ import {
|
|||||||
CameraAlt as VideocamIcon,
|
CameraAlt as VideocamIcon,
|
||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
Close as DeleteIcon,
|
Close as DeleteIcon,
|
||||||
PublicOutlined as TimezoneIcon,
|
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import AttendeeSelector from "../Attendees/AttendeeSearch";
|
import AttendeeSelector from "../Attendees/AttendeeSearch";
|
||||||
import RepeatEvent from "./EventRepeat";
|
import RepeatEvent from "./EventRepeat";
|
||||||
@@ -34,6 +32,7 @@ import {
|
|||||||
generateMeetingLink,
|
generateMeetingLink,
|
||||||
addVideoConferenceToDescription,
|
addVideoConferenceToDescription,
|
||||||
} from "../../utils/videoConferenceUtils";
|
} from "../../utils/videoConferenceUtils";
|
||||||
|
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||||
|
|
||||||
// Helper component for field with label
|
// Helper component for field with label
|
||||||
export const FieldWithLabel = React.memo(
|
export const FieldWithLabel = React.memo(
|
||||||
@@ -407,46 +406,15 @@ export default function EventFormFields({
|
|||||||
}
|
}
|
||||||
label="Repeat"
|
label="Repeat"
|
||||||
/>
|
/>
|
||||||
<Autocomplete
|
<TimezoneAutocomplete
|
||||||
value={timezone}
|
value={timezone}
|
||||||
options={timezoneList.zones}
|
onChange={setTimezone}
|
||||||
|
zones={timezoneList.zones}
|
||||||
|
getTimezoneOffset={timezoneList.getTimezoneOffset}
|
||||||
|
showIcon={true}
|
||||||
|
width={240}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 240 }}
|
placeholder="Select timezone"
|
||||||
getOptionLabel={(option) =>
|
|
||||||
`(${timezoneList.getTimezoneOffset(option)}) ${option.replace(/_/g, " ")}`
|
|
||||||
}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
setTimezone(newValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder="Select timezone"
|
|
||||||
autoComplete="off"
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
...params.InputProps,
|
|
||||||
startAdornment: (
|
|
||||||
<>
|
|
||||||
<TimezoneIcon
|
|
||||||
style={{
|
|
||||||
marginRight: 8,
|
|
||||||
color: "rgba(0, 0, 0, 0.54)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{params.InputProps.startAdornment}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
...params.inputProps,
|
|
||||||
autoComplete: "new-password",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { Autocomplete, TextField } from "@mui/material";
|
||||||
|
import { PublicOutlined as TimezoneIcon } from "@mui/icons-material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface TimezoneOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
offset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimezoneAutocompleteProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (timezone: string) => void;
|
||||||
|
zones: string[];
|
||||||
|
getTimezoneOffset: (tzName: string) => string;
|
||||||
|
showIcon?: boolean;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
width?: number | string;
|
||||||
|
size?: "small" | "medium";
|
||||||
|
placeholder?: string;
|
||||||
|
inputFontSize?: string;
|
||||||
|
inputPadding?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
disableClearable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimezoneAutocomplete({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
zones,
|
||||||
|
getTimezoneOffset,
|
||||||
|
showIcon = false,
|
||||||
|
autoFocus = false,
|
||||||
|
width,
|
||||||
|
size = "small",
|
||||||
|
placeholder = "Select timezone",
|
||||||
|
inputFontSize,
|
||||||
|
inputPadding,
|
||||||
|
onClose,
|
||||||
|
disableClearable = false,
|
||||||
|
}: TimezoneAutocompleteProps) {
|
||||||
|
const options = useMemo<TimezoneOption[]>(() => {
|
||||||
|
return zones.map((tz) => ({
|
||||||
|
value: tz,
|
||||||
|
label: tz.replace(/_/g, " "),
|
||||||
|
offset: getTimezoneOffset(tz),
|
||||||
|
}));
|
||||||
|
}, [zones, getTimezoneOffset]);
|
||||||
|
|
||||||
|
const selectedOption = options.find((opt) => opt.value === value) || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue.value);
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={(option) => `(${option.offset}) ${option.label}`}
|
||||||
|
size={size}
|
||||||
|
sx={width ? { width } : undefined}
|
||||||
|
disableClearable={disableClearable}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={placeholder}
|
||||||
|
variant="outlined"
|
||||||
|
autoComplete="off"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
...params.InputProps,
|
||||||
|
startAdornment: showIcon ? (
|
||||||
|
<>
|
||||||
|
<TimezoneIcon
|
||||||
|
style={{
|
||||||
|
marginRight: 8,
|
||||||
|
color: "rgba(0, 0, 0, 0.54)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{params.InputProps.startAdornment}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
params.InputProps.startAdornment
|
||||||
|
),
|
||||||
|
...(inputFontSize || inputPadding
|
||||||
|
? {
|
||||||
|
style: {
|
||||||
|
...(inputFontSize ? { fontSize: inputFontSize } : {}),
|
||||||
|
...(inputPadding ? { padding: inputPadding } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
...params.inputProps,
|
||||||
|
autoComplete: "new-password",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
Autocomplete,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
Description as DescriptionIcon,
|
Description as DescriptionIcon,
|
||||||
@@ -21,7 +20,6 @@ import {
|
|||||||
CameraAlt as VideocamIcon,
|
CameraAlt as VideocamIcon,
|
||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
Close as DeleteIcon,
|
Close as DeleteIcon,
|
||||||
PublicOutlined as TimezoneIcon,
|
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { Box, Button } from "@mui/material";
|
import { Box, Button } from "@mui/material";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
@@ -52,6 +50,7 @@ import {
|
|||||||
} from "../../components/Calendar/TimezoneSelector";
|
} from "../../components/Calendar/TimezoneSelector";
|
||||||
import { getCalendarRange } from "../../utils/dateUtils";
|
import { getCalendarRange } from "../../utils/dateUtils";
|
||||||
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
|
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
|
||||||
|
import { TimezoneAutocomplete } from "../../components/Timezone/TimezoneAutocomplete";
|
||||||
|
|
||||||
// Helper component for field with label
|
// Helper component for field with label
|
||||||
const FieldWithLabel = React.memo(
|
const FieldWithLabel = React.memo(
|
||||||
@@ -620,46 +619,15 @@ function EventPopover({
|
|||||||
}
|
}
|
||||||
label="Repeat"
|
label="Repeat"
|
||||||
/>
|
/>
|
||||||
<Autocomplete
|
<TimezoneAutocomplete
|
||||||
value={timezone}
|
value={timezone}
|
||||||
options={timezoneList.zones}
|
onChange={setTimezone}
|
||||||
|
zones={timezoneList.zones}
|
||||||
|
getTimezoneOffset={getTimezoneOffset}
|
||||||
|
showIcon={true}
|
||||||
|
width={240}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 240 }}
|
placeholder="Select timezone"
|
||||||
getOptionLabel={(option) =>
|
|
||||||
`(${getTimezoneOffset(option)}) ${option.replace(/_/g, " ")}`
|
|
||||||
}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
setTimezone(newValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder="Select timezone"
|
|
||||||
autoComplete="off"
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
...params.InputProps,
|
|
||||||
startAdornment: (
|
|
||||||
<>
|
|
||||||
<TimezoneIcon
|
|
||||||
style={{
|
|
||||||
marginRight: 8,
|
|
||||||
color: "rgba(0, 0, 0, 0.54)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{params.InputProps.startAdornment}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
...params.inputProps,
|
|
||||||
autoComplete: "new-password",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|||||||
Reference in New Issue
Block a user