[WEB-2884] chore: Update timezone list, add new endpoint, and update timezone dropdowns (#6231)

* dev: updated timezones list

* chore: added rate limiting
This commit is contained in:
guru_sainath 2024-12-19 20:15:55 +05:30 committed by GitHub
parent 0a320a8540
commit 9b71a702c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 444 additions and 59 deletions

View file

@ -1 +1,3 @@
export * from "./product-updates";
export * from "./timezone-select";

View file

@ -0,0 +1,53 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { CustomSearchSelect } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import useTimezone from "@/hooks/use-timezone";
type TTimezoneSelect = {
value: string | undefined;
onChange: (value: string) => void;
error?: boolean;
label?: string;
buttonClassName?: string;
className?: string;
optionsClassName?: string;
disabled?: boolean;
};
export const TimezoneSelect: FC<TTimezoneSelect> = observer((props) => {
// props
const {
value,
onChange,
error = false,
label = "Select a timezone",
buttonClassName = "",
className = "",
optionsClassName = "",
disabled = false,
} = props;
// hooks
const { disabled: isDisabled, timezones, selectedValue } = useTimezone();
return (
<div>
<CustomSearchSelect
value={value}
label={selectedValue ? selectedValue(value) : label}
options={isDisabled || disabled ? [] : timezones}
onChange={onChange}
buttonClassName={cn(buttonClassName, {
"border-red-500": error,
})}
className={cn("rounded-md border-[0.5px] !border-custom-border-200", className)}
optionsClassName={cn("w-72", optionsClassName)}
input
disabled={isDisabled || disabled}
/>
</div>
);
});

View file

@ -16,16 +16,15 @@ import {
CustomEmojiIconPicker,
EmojiIconPickerTypes,
Tooltip,
CustomSearchSelect,
} from "@plane/ui";
// components
import { Logo } from "@/components/common";
import { ImagePickerPopover } from "@/components/core";
import { TimezoneSelect } from "@/components/global";
// constants
import { PROJECT_UPDATED } from "@/constants/event-tracker";
import { NETWORK_CHOICES } from "@/constants/project";
// helpers
import { TTimezone, TIME_ZONES } from "@/constants/timezones";
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getFileURL } from "@/helpers/file.helper";
@ -34,6 +33,7 @@ import { useEventTracker, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { ProjectService } from "@/services/project";
export interface IProjectDetailsForm {
project: IProject;
workspaceSlug: string;
@ -68,20 +68,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
});
// derived values
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
const getTimeZoneLabel = (timezone: TTimezone | undefined) => {
if (!timezone) return undefined;
return (
<div className="flex gap-1.5">
<span className="text-custom-text-400">{timezone.gmtOffset}</span>
<span className="text-custom-text-200">{timezone.name}</span>
</div>
);
};
const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
value: timeZone.value,
query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value,
content: getTimeZoneLabel(timeZone),
}));
const coverImage = watch("cover_image_url");
useEffect(() => {
@ -393,20 +379,16 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
control={control}
rules={{ required: "Please select a timezone" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? (getTimeZoneLabel(TIME_ZONES.find((t) => t.value === value)) ?? value)
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
buttonClassName={errors.timezone ? "border-red-500" : "border-none"}
className="rounded-md border-[0.5px] !border-custom-border-200"
optionsClassName="w-72"
input
/>
<>
<TimezoneSelect
value={value}
onChange={(value: string) => {
onChange(value);
}}
error={Boolean(errors.timezone)}
buttonClassName="border-none"
/>
</>
)}
/>
{errors.timezone && <span className="text-xs text-red-500">{errors.timezone.message}</span>}

View file

@ -0,0 +1,80 @@
import useSWR from "swr";
import { TTimezoneObject } from "@plane/types";
// services
import timezoneService from "@/services/timezone.service";
// group timezones by value
const groupTimezones = (timezones: TTimezoneObject[]): TTimezoneObject[] => {
const groupedMap = timezones.reduce((acc, timezone: TTimezoneObject) => {
const key = timezone.value;
if (!acc.has(key)) {
acc.set(key, {
utc_offset: timezone.utc_offset,
gmt_offset: timezone.gmt_offset,
value: timezone.value,
label: timezone.label,
});
} else {
const existing = acc.get(key);
existing.label = `${existing.label}, ${timezone.label}`;
}
return acc;
}, new Map());
return Array.from(groupedMap.values());
};
const useTimezone = () => {
// fetching the timezone from the server
const {
data: timezones,
isLoading: timezoneIsLoading,
error: timezonesError,
} = useSWR("TIMEZONES_LIST", () => timezoneService.fetch(), {
refreshInterval: 0,
});
// derived values
const isDisabled = timezoneIsLoading || timezonesError || !timezones;
const getTimeZoneLabel = (timezone: TTimezoneObject | undefined) => {
if (!timezone) return undefined;
return (
<div className="flex gap-1.5">
<span className="text-custom-text-400">{timezone.utc_offset}</span>
<span className="text-custom-text-200">{timezone.label}</span>
</div>
);
};
const options = [
...groupTimezones(timezones?.timezones || [])?.map((timezone) => ({
value: timezone.value,
query: `${timezone.value} ${timezone.label}, ${timezone.gmt_offset}, ${timezone.utc_offset}`,
content: getTimeZoneLabel(timezone),
})),
{
value: "UTC",
query: "utc, coordinated universal time",
content: "UTC",
},
{
value: "Universal",
query: "universal, coordinated universal time",
content: "Universal",
},
];
const selectedTimezone = (value: string | undefined) => options.find((option) => option.value === value)?.content;
return {
timezones: options,
isLoading: timezoneIsLoading,
error: timezonesError,
disabled: isDisabled,
selectedValue: selectedTimezone,
};
};
export default useTimezone;

View file

@ -0,0 +1,23 @@
import { TTimezones } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// api services
import { APIService } from "@/services/api.service";
export class TimezoneService extends APIService {
constructor() {
super(API_BASE_URL);
}
async fetch(): Promise<TTimezones> {
return this.get(`/api/timezones/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
const timezoneService = new TimezoneService();
export default timezoneService;