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:
lenhanphung
2025-10-16 15:40:48 +07:00
committed by Benoit TELLIER
parent 2ab7c448bf
commit 6421f53a8d
4 changed files with 137 additions and 143 deletions
+13 -63
View File
@@ -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]) {
+8 -40
View File
@@ -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",
}}
/>
)}
/>
);
}
+8 -40
View File
@@ -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>