From 6421f53a8dadbdece8cbc359c49299a6421c9914 Mon Sep 17 00:00:00 2001 From: lenhanphung Date: Thu, 16 Oct 2025 15:40:48 +0700 Subject: [PATCH] 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 --- src/components/Calendar/TimezoneSelector.tsx | 76 +++--------- src/components/Event/EventFormFields.tsx | 48 ++------ .../Timezone/TimezoneAutocomplete.tsx | 108 ++++++++++++++++++ src/features/Events/EventModal.tsx | 48 ++------ 4 files changed, 137 insertions(+), 143 deletions(-) create mode 100644 src/components/Timezone/TimezoneAutocomplete.tsx diff --git a/src/components/Calendar/TimezoneSelector.tsx b/src/components/Calendar/TimezoneSelector.tsx index d17c0c2..1c3d346 100644 --- a/src/components/Calendar/TimezoneSelector.tsx +++ b/src/components/Calendar/TimezoneSelector.tsx @@ -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) => { setAnchorEl(event.currentTarget); @@ -59,7 +46,7 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) { lineHeight: 1.2, }} > - {selectedOption ? selectedOption.offset : "Select Timezone"} + {selectedOffset || "Select Timezone"} - ); } -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 ( - { - if (newValue) { - onChange(newValue.value); - handleClose(); // close after selection - } - }} - options={options} - getOptionLabel={(option) => `${option.offset} ${option.label}`} - renderInput={(params) => ( - - )} - disableClearable - renderValue={(value) =>
{value.offset}
} - /> - ); -} export function resolveTimezone(tzName: string): string { if (TIMEZONES.zones[tzName]) { diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx index b945d43..1999d31 100644 --- a/src/components/Event/EventFormFields.tsx +++ b/src/components/Event/EventFormFields.tsx @@ -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" /> - - `(${timezoneList.getTimezoneOffset(option)}) ${option.replace(/_/g, " ")}` - } - onChange={(event, newValue) => { - if (newValue) { - setTimezone(newValue); - } - }} - renderInput={(params) => ( - - - {params.InputProps.startAdornment} - - ), - }, - }} - inputProps={{ - ...params.inputProps, - autoComplete: "new-password", - }} - /> - )} + placeholder="Select timezone" /> diff --git a/src/components/Timezone/TimezoneAutocomplete.tsx b/src/components/Timezone/TimezoneAutocomplete.tsx new file mode 100644 index 0000000..e9f817a --- /dev/null +++ b/src/components/Timezone/TimezoneAutocomplete.tsx @@ -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(() => { + 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 ( + { + if (newValue) { + onChange(newValue.value); + onClose?.(); + } + }} + options={options} + getOptionLabel={(option) => `(${option.offset}) ${option.label}`} + size={size} + sx={width ? { width } : undefined} + disableClearable={disableClearable} + renderInput={(params) => ( + + + {params.InputProps.startAdornment} + + ) : ( + params.InputProps.startAdornment + ), + ...(inputFontSize || inputPadding + ? { + style: { + ...(inputFontSize ? { fontSize: inputFontSize } : {}), + ...(inputPadding ? { padding: inputPadding } : {}), + }, + } + : {}), + }, + }} + inputProps={{ + ...params.inputProps, + autoComplete: "new-password", + }} + /> + )} + /> + ); +} + diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 7fa1065..5b5b973 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -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" /> - - `(${getTimezoneOffset(option)}) ${option.replace(/_/g, " ")}` - } - onChange={(event, newValue) => { - if (newValue) { - setTimezone(newValue); - } - }} - renderInput={(params) => ( - - - {params.InputProps.startAdornment} - - ), - }, - }} - inputProps={{ - ...params.inputProps, - autoComplete: "new-password", - }} - /> - )} + placeholder="Select timezone" />