[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:
parent
0a320a8540
commit
9b71a702c7
12 changed files with 444 additions and 59 deletions
|
|
@ -1 +1,3 @@
|
|||
export * from "./product-updates";
|
||||
|
||||
export * from "./timezone-select";
|
||||
|
|
|
|||
53
web/core/components/global/timezone-select.tsx
Normal file
53
web/core/components/global/timezone-select.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>}
|
||||
|
|
|
|||
80
web/core/hooks/use-timezone.tsx
Normal file
80
web/core/hooks/use-timezone.tsx
Normal 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;
|
||||
23
web/core/services/timezone.service.ts
Normal file
23
web/core/services/timezone.service.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue