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 {
|
||||
Autocomplete,
|
||||
Button,
|
||||
ListItem,
|
||||
Popover,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Button, Popover } from "@mui/material";
|
||||
import { MouseEvent, useMemo, useState } from "react";
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||
|
||||
interface TimezoneSelectProps {
|
||||
value: string;
|
||||
@@ -24,16 +19,8 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
||||
|
||||
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 =
|
||||
options.find((opt) => opt.value === value) || options[0];
|
||||
const selectedOffset = getTimezoneOffset(value);
|
||||
|
||||
const handleOpen = (event: MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -59,7 +46,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{selectedOption ? selectedOption.offset : "Select Timezone"}
|
||||
{selectedOffset || "Select Timezone"}
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
@@ -80,58 +67,21 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TimeZoneSearch
|
||||
selectedOption={selectedOption}
|
||||
<TimezoneAutocomplete
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
handleClose={handleClose}
|
||||
options={options}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={getTimezoneOffset}
|
||||
autoFocus={true}
|
||||
inputFontSize="10px"
|
||||
inputPadding="2px 4px"
|
||||
onClose={handleClose}
|
||||
disableClearable={true}
|
||||
/>
|
||||
</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 {
|
||||
if (TIMEZONES.zones[tzName]) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Typography,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Autocomplete,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Description as DescriptionIcon,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
CameraAlt as VideocamIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Close as DeleteIcon,
|
||||
PublicOutlined as TimezoneIcon,
|
||||
} from "@mui/icons-material";
|
||||
import AttendeeSelector from "../Attendees/AttendeeSearch";
|
||||
import RepeatEvent from "./EventRepeat";
|
||||
@@ -34,6 +32,7 @@ import {
|
||||
generateMeetingLink,
|
||||
addVideoConferenceToDescription,
|
||||
} from "../../utils/videoConferenceUtils";
|
||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||
|
||||
// Helper component for field with label
|
||||
export const FieldWithLabel = React.memo(
|
||||
@@ -407,46 +406,15 @@ export default function EventFormFields({
|
||||
}
|
||||
label="Repeat"
|
||||
/>
|
||||
<Autocomplete
|
||||
<TimezoneAutocomplete
|
||||
value={timezone}
|
||||
options={timezoneList.zones}
|
||||
onChange={setTimezone}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={timezoneList.getTimezoneOffset}
|
||||
showIcon={true}
|
||||
width={240}
|
||||
size="small"
|
||||
sx={{ width: 240 }}
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
placeholder="Select timezone"
|
||||
/>
|
||||
</Box>
|
||||
</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,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Autocomplete,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Description as DescriptionIcon,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
CameraAlt as VideocamIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Close as DeleteIcon,
|
||||
PublicOutlined as TimezoneIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
@@ -52,6 +50,7 @@ import {
|
||||
} from "../../components/Calendar/TimezoneSelector";
|
||||
import { getCalendarRange } from "../../utils/dateUtils";
|
||||
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
|
||||
import { TimezoneAutocomplete } from "../../components/Timezone/TimezoneAutocomplete";
|
||||
|
||||
// Helper component for field with label
|
||||
const FieldWithLabel = React.memo(
|
||||
@@ -620,46 +619,15 @@ function EventPopover({
|
||||
}
|
||||
label="Repeat"
|
||||
/>
|
||||
<Autocomplete
|
||||
<TimezoneAutocomplete
|
||||
value={timezone}
|
||||
options={timezoneList.zones}
|
||||
onChange={setTimezone}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={getTimezoneOffset}
|
||||
showIcon={true}
|
||||
width={240}
|
||||
size="small"
|
||||
sx={{ width: 240 }}
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
placeholder="Select timezone"
|
||||
/>
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
|
||||
Reference in New Issue
Block a user