[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
|
|
@ -17,6 +17,7 @@ from .user import urlpatterns as user_urls
|
||||||
from .views import urlpatterns as view_urls
|
from .views import urlpatterns as view_urls
|
||||||
from .webhook import urlpatterns as webhook_urls
|
from .webhook import urlpatterns as webhook_urls
|
||||||
from .workspace import urlpatterns as workspace_urls
|
from .workspace import urlpatterns as workspace_urls
|
||||||
|
from .timezone import urlpatterns as timezone_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
*analytic_urls,
|
*analytic_urls,
|
||||||
|
|
@ -38,4 +39,5 @@ urlpatterns = [
|
||||||
*workspace_urls,
|
*workspace_urls,
|
||||||
*api_urls,
|
*api_urls,
|
||||||
*webhook_urls,
|
*webhook_urls,
|
||||||
|
*timezone_urls,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
8
apiserver/plane/app/urls/timezone.py
Normal file
8
apiserver/plane/app/urls/timezone.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.app.views import TimezoneEndpoint
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# timezone endpoint
|
||||||
|
path("timezones/", TimezoneEndpoint.as_view(), name="timezone-list")
|
||||||
|
]
|
||||||
|
|
@ -204,3 +204,5 @@ from .error_404 import custom_404_view
|
||||||
|
|
||||||
from .notification.base import MarkAllReadNotificationViewSet
|
from .notification.base import MarkAllReadNotificationViewSet
|
||||||
from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint
|
from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint
|
||||||
|
|
||||||
|
from .timezone.base import TimezoneEndpoint
|
||||||
|
|
|
||||||
247
apiserver/plane/app/views/timezone/base.py
Normal file
247
apiserver/plane/app/views/timezone/base.py
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
# Python imports
|
||||||
|
import pytz
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||||
|
|
||||||
|
|
||||||
|
class TimezoneEndpoint(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
throttle_classes = [AuthenticationThrottle]
|
||||||
|
|
||||||
|
@method_decorator(cache_page(60 * 60 * 24))
|
||||||
|
def get(self, request):
|
||||||
|
timezone_mapping = {
|
||||||
|
"-1100": [
|
||||||
|
("Midway Island", "Pacific/Midway"),
|
||||||
|
("American Samoa", "Pacific/Pago_Pago"),
|
||||||
|
],
|
||||||
|
"-1000": [
|
||||||
|
("Hawaii", "Pacific/Honolulu"),
|
||||||
|
("Aleutian Islands", "America/Adak"),
|
||||||
|
],
|
||||||
|
"-0930": [("Marquesas Islands", "Pacific/Marquesas")],
|
||||||
|
"-0900": [
|
||||||
|
("Alaska", "America/Anchorage"),
|
||||||
|
("Gambier Islands", "Pacific/Gambier"),
|
||||||
|
],
|
||||||
|
"-0800": [
|
||||||
|
("Pacific Time (US and Canada)", "America/Los_Angeles"),
|
||||||
|
("Baja California", "America/Tijuana"),
|
||||||
|
],
|
||||||
|
"-0700": [
|
||||||
|
("Mountain Time (US and Canada)", "America/Denver"),
|
||||||
|
("Arizona", "America/Phoenix"),
|
||||||
|
("Chihuahua, Mazatlan", "America/Chihuahua"),
|
||||||
|
],
|
||||||
|
"-0600": [
|
||||||
|
("Central Time (US and Canada)", "America/Chicago"),
|
||||||
|
("Saskatchewan", "America/Regina"),
|
||||||
|
("Guadalajara, Mexico City, Monterrey", "America/Mexico_City"),
|
||||||
|
("Tegucigalpa, Honduras", "America/Tegucigalpa"),
|
||||||
|
("Costa Rica", "America/Costa_Rica"),
|
||||||
|
],
|
||||||
|
"-0500": [
|
||||||
|
("Eastern Time (US and Canada)", "America/New_York"),
|
||||||
|
("Lima", "America/Lima"),
|
||||||
|
("Bogota", "America/Bogota"),
|
||||||
|
("Quito", "America/Guayaquil"),
|
||||||
|
("Chetumal", "America/Cancun"),
|
||||||
|
],
|
||||||
|
"-0430": [("Caracas (Old Venezuela Time)", "America/Caracas")],
|
||||||
|
"-0400": [
|
||||||
|
("Atlantic Time (Canada)", "America/Halifax"),
|
||||||
|
("Caracas", "America/Caracas"),
|
||||||
|
("Santiago", "America/Santiago"),
|
||||||
|
("La Paz", "America/La_Paz"),
|
||||||
|
("Manaus", "America/Manaus"),
|
||||||
|
("Georgetown", "America/Guyana"),
|
||||||
|
("Bermuda", "Atlantic/Bermuda"),
|
||||||
|
],
|
||||||
|
"-0330": [("Newfoundland Time (Canada)", "America/St_Johns")],
|
||||||
|
"-0300": [
|
||||||
|
("Buenos Aires", "America/Argentina/Buenos_Aires"),
|
||||||
|
("Brasilia", "America/Sao_Paulo"),
|
||||||
|
("Greenland", "America/Godthab"),
|
||||||
|
("Montevideo", "America/Montevideo"),
|
||||||
|
("Falkland Islands", "Atlantic/Stanley"),
|
||||||
|
],
|
||||||
|
"-0200": [
|
||||||
|
(
|
||||||
|
"South Georgia and the South Sandwich Islands",
|
||||||
|
"Atlantic/South_Georgia",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"-0100": [
|
||||||
|
("Azores", "Atlantic/Azores"),
|
||||||
|
("Cape Verde Islands", "Atlantic/Cape_Verde"),
|
||||||
|
],
|
||||||
|
"+0000": [
|
||||||
|
("Dublin", "Europe/Dublin"),
|
||||||
|
("Reykjavik", "Atlantic/Reykjavik"),
|
||||||
|
("Lisbon", "Europe/Lisbon"),
|
||||||
|
("Monrovia", "Africa/Monrovia"),
|
||||||
|
("Casablanca", "Africa/Casablanca"),
|
||||||
|
],
|
||||||
|
"+0100": [
|
||||||
|
("Central European Time (Berlin, Rome, Paris)", "Europe/Paris"),
|
||||||
|
("West Central Africa", "Africa/Lagos"),
|
||||||
|
("Algiers", "Africa/Algiers"),
|
||||||
|
("Lagos", "Africa/Lagos"),
|
||||||
|
("Tunis", "Africa/Tunis"),
|
||||||
|
],
|
||||||
|
"+0200": [
|
||||||
|
("Eastern European Time (Cairo, Helsinki, Kyiv)", "Europe/Kiev"),
|
||||||
|
("Athens", "Europe/Athens"),
|
||||||
|
("Jerusalem", "Asia/Jerusalem"),
|
||||||
|
("Johannesburg", "Africa/Johannesburg"),
|
||||||
|
("Harare, Pretoria", "Africa/Harare"),
|
||||||
|
],
|
||||||
|
"+0300": [
|
||||||
|
("Moscow Time", "Europe/Moscow"),
|
||||||
|
("Baghdad", "Asia/Baghdad"),
|
||||||
|
("Nairobi", "Africa/Nairobi"),
|
||||||
|
("Kuwait, Riyadh", "Asia/Riyadh"),
|
||||||
|
],
|
||||||
|
"+0330": [("Tehran", "Asia/Tehran")],
|
||||||
|
"+0400": [
|
||||||
|
("Abu Dhabi", "Asia/Dubai"),
|
||||||
|
("Baku", "Asia/Baku"),
|
||||||
|
("Yerevan", "Asia/Yerevan"),
|
||||||
|
("Astrakhan", "Europe/Astrakhan"),
|
||||||
|
("Tbilisi", "Asia/Tbilisi"),
|
||||||
|
("Mauritius", "Indian/Mauritius"),
|
||||||
|
],
|
||||||
|
"+0500": [
|
||||||
|
("Islamabad", "Asia/Karachi"),
|
||||||
|
("Karachi", "Asia/Karachi"),
|
||||||
|
("Tashkent", "Asia/Tashkent"),
|
||||||
|
("Yekaterinburg", "Asia/Yekaterinburg"),
|
||||||
|
("Maldives", "Indian/Maldives"),
|
||||||
|
("Chagos", "Indian/Chagos"),
|
||||||
|
],
|
||||||
|
"+0530": [
|
||||||
|
("Chennai", "Asia/Kolkata"),
|
||||||
|
("Kolkata", "Asia/Kolkata"),
|
||||||
|
("Mumbai", "Asia/Kolkata"),
|
||||||
|
("New Delhi", "Asia/Kolkata"),
|
||||||
|
("Sri Jayawardenepura", "Asia/Colombo"),
|
||||||
|
],
|
||||||
|
"+0545": [("Kathmandu", "Asia/Kathmandu")],
|
||||||
|
"+0600": [
|
||||||
|
("Dhaka", "Asia/Dhaka"),
|
||||||
|
("Almaty", "Asia/Almaty"),
|
||||||
|
("Bishkek", "Asia/Bishkek"),
|
||||||
|
("Thimphu", "Asia/Thimphu"),
|
||||||
|
],
|
||||||
|
"+0630": [
|
||||||
|
("Yangon (Rangoon)", "Asia/Yangon"),
|
||||||
|
("Cocos Islands", "Indian/Cocos"),
|
||||||
|
],
|
||||||
|
"+0700": [
|
||||||
|
("Bangkok", "Asia/Bangkok"),
|
||||||
|
("Hanoi", "Asia/Ho_Chi_Minh"),
|
||||||
|
("Jakarta", "Asia/Jakarta"),
|
||||||
|
("Novosibirsk", "Asia/Novosibirsk"),
|
||||||
|
("Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||||
|
],
|
||||||
|
"+0800": [
|
||||||
|
("Beijing", "Asia/Shanghai"),
|
||||||
|
("Singapore", "Asia/Singapore"),
|
||||||
|
("Perth", "Australia/Perth"),
|
||||||
|
("Hong Kong", "Asia/Hong_Kong"),
|
||||||
|
("Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||||
|
("Palau", "Pacific/Palau"),
|
||||||
|
],
|
||||||
|
"+0845": [("Eucla", "Australia/Eucla")],
|
||||||
|
"+0900": [
|
||||||
|
("Tokyo", "Asia/Tokyo"),
|
||||||
|
("Seoul", "Asia/Seoul"),
|
||||||
|
("Yakutsk", "Asia/Yakutsk"),
|
||||||
|
],
|
||||||
|
"+0930": [
|
||||||
|
("Adelaide", "Australia/Adelaide"),
|
||||||
|
("Darwin", "Australia/Darwin"),
|
||||||
|
],
|
||||||
|
"+1000": [
|
||||||
|
("Sydney", "Australia/Sydney"),
|
||||||
|
("Brisbane", "Australia/Brisbane"),
|
||||||
|
("Guam", "Pacific/Guam"),
|
||||||
|
("Vladivostok", "Asia/Vladivostok"),
|
||||||
|
("Tahiti", "Pacific/Tahiti"),
|
||||||
|
],
|
||||||
|
"+1030": [("Lord Howe Island", "Australia/Lord_Howe")],
|
||||||
|
"+1100": [
|
||||||
|
("Solomon Islands", "Pacific/Guadalcanal"),
|
||||||
|
("Magadan", "Asia/Magadan"),
|
||||||
|
("Norfolk Island", "Pacific/Norfolk"),
|
||||||
|
("Bougainville Island", "Pacific/Bougainville"),
|
||||||
|
("Chokurdakh", "Asia/Srednekolymsk"),
|
||||||
|
],
|
||||||
|
"+1200": [
|
||||||
|
("Auckland", "Pacific/Auckland"),
|
||||||
|
("Wellington", "Pacific/Auckland"),
|
||||||
|
("Fiji Islands", "Pacific/Fiji"),
|
||||||
|
("Anadyr", "Asia/Anadyr"),
|
||||||
|
],
|
||||||
|
"+1245": [("Chatham Islands", "Pacific/Chatham")],
|
||||||
|
"+1300": [("Nuku'alofa", "Pacific/Tongatapu"), ("Samoa", "Pacific/Apia")],
|
||||||
|
"+1400": [("Kiritimati Island", "Pacific/Kiritimati")],
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone_list = []
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Process timezone mapping
|
||||||
|
for offset, locations in timezone_mapping.items():
|
||||||
|
sign = "-" if offset.startswith("-") else "+"
|
||||||
|
hours = offset[1:3]
|
||||||
|
minutes = offset[3:] if len(offset) > 3 else "00"
|
||||||
|
|
||||||
|
for friendly_name, tz_identifier in locations:
|
||||||
|
try:
|
||||||
|
tz = pytz.timezone(tz_identifier)
|
||||||
|
current_offset = now.astimezone(tz).strftime("%z")
|
||||||
|
|
||||||
|
# converting and formatting UTC offset to GMT offset
|
||||||
|
current_utc_offset = now.astimezone(tz).utcoffset()
|
||||||
|
total_seconds = int(current_utc_offset.total_seconds())
|
||||||
|
hours_offset = total_seconds // 3600
|
||||||
|
minutes_offset = abs(total_seconds % 3600) // 60
|
||||||
|
gmt_offset = (
|
||||||
|
f"GMT{'+' if hours_offset >= 0 else '-'}"
|
||||||
|
f"{abs(hours_offset):02}:{minutes_offset:02}"
|
||||||
|
)
|
||||||
|
|
||||||
|
timezone_value = {
|
||||||
|
"offset": int(current_offset),
|
||||||
|
"utc_offset": f"UTC{sign}{hours}:{minutes}",
|
||||||
|
"gmt_offset": gmt_offset,
|
||||||
|
"value": tz_identifier,
|
||||||
|
"label": f"{friendly_name}",
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone_list.append(timezone_value)
|
||||||
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by offset and then by label
|
||||||
|
timezone_list.sort(key=lambda x: (x["offset"], x["label"]))
|
||||||
|
|
||||||
|
# Remove offset from final output
|
||||||
|
for tz in timezone_list:
|
||||||
|
del tz["offset"]
|
||||||
|
|
||||||
|
return Response({"timezones": timezone_list}, status=status.HTTP_200_OK)
|
||||||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -33,3 +33,4 @@ export * from "./favorite";
|
||||||
export * from "./file";
|
export * from "./file";
|
||||||
export * from "./workspace-draft-issues/base";
|
export * from "./workspace-draft-issues/base";
|
||||||
export * from "./command-palette";
|
export * from "./command-palette";
|
||||||
|
export * from "./timezone";
|
||||||
|
|
|
||||||
8
packages/types/src/timezone.d.ts
vendored
Normal file
8
packages/types/src/timezone.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export type TTimezoneObject = {
|
||||||
|
utc_offset: string;
|
||||||
|
gmt_offset: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTimezones = { timezones: TTimezoneObject[] };
|
||||||
|
|
@ -20,9 +20,9 @@ import {
|
||||||
import { DeactivateAccountModal } from "@/components/account";
|
import { DeactivateAccountModal } from "@/components/account";
|
||||||
import { LogoSpinner } from "@/components/common";
|
import { LogoSpinner } from "@/components/common";
|
||||||
import { ImagePickerPopover, UserImageUploadModal, PageHead } from "@/components/core";
|
import { ImagePickerPopover, UserImageUploadModal, PageHead } from "@/components/core";
|
||||||
|
import { TimezoneSelect } from "@/components/global";
|
||||||
import { ProfileSettingContentWrapper } from "@/components/profile";
|
import { ProfileSettingContentWrapper } from "@/components/profile";
|
||||||
// constants
|
// constants
|
||||||
import { TIME_ZONES, TTimezone } from "@/constants/timezones";
|
|
||||||
import { USER_ROLES } from "@/constants/workspace";
|
import { USER_ROLES } from "@/constants/workspace";
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
|
|
@ -120,22 +120,6 @@ const ProfileSettingsPage = observer(() => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!currentUser)
|
if (!currentUser)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||||
|
|
@ -379,19 +363,12 @@ const ProfileSettingsPage = observer(() => {
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: "Please select a timezone" }}
|
rules={{ required: "Please select a timezone" }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSearchSelect
|
<TimezoneSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={
|
onChange={(value: string) => {
|
||||||
value
|
onChange(value);
|
||||||
? (getTimeZoneLabel(TIME_ZONES.find((t) => t.value === value)) ?? value)
|
}}
|
||||||
: "Select a timezone"
|
error={Boolean(errors.user_timezone)}
|
||||||
}
|
|
||||||
options={timeZoneOptions}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonClassName={errors.user_timezone ? "border-red-500" : ""}
|
|
||||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
|
||||||
optionsClassName="w-72"
|
|
||||||
input
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export * from "./product-updates";
|
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,
|
CustomEmojiIconPicker,
|
||||||
EmojiIconPickerTypes,
|
EmojiIconPickerTypes,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
CustomSearchSelect,
|
|
||||||
} from "@plane/ui";
|
} from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { Logo } from "@/components/common";
|
import { Logo } from "@/components/common";
|
||||||
import { ImagePickerPopover } from "@/components/core";
|
import { ImagePickerPopover } from "@/components/core";
|
||||||
|
import { TimezoneSelect } from "@/components/global";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_UPDATED } from "@/constants/event-tracker";
|
import { PROJECT_UPDATED } from "@/constants/event-tracker";
|
||||||
import { NETWORK_CHOICES } from "@/constants/project";
|
import { NETWORK_CHOICES } from "@/constants/project";
|
||||||
// helpers
|
// helpers
|
||||||
import { TTimezone, TIME_ZONES } from "@/constants/timezones";
|
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
|
|
@ -34,6 +33,7 @@ import { useEventTracker, useProject } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// services
|
// services
|
||||||
import { ProjectService } from "@/services/project";
|
import { ProjectService } from "@/services/project";
|
||||||
|
|
||||||
export interface IProjectDetailsForm {
|
export interface IProjectDetailsForm {
|
||||||
project: IProject;
|
project: IProject;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -68,20 +68,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
});
|
});
|
||||||
// derived values
|
// derived values
|
||||||
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network);
|
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");
|
const coverImage = watch("cover_image_url");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -393,20 +379,16 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: "Please select a timezone" }}
|
rules={{ required: "Please select a timezone" }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomSearchSelect
|
<>
|
||||||
value={value}
|
<TimezoneSelect
|
||||||
label={
|
value={value}
|
||||||
value
|
onChange={(value: string) => {
|
||||||
? (getTimeZoneLabel(TIME_ZONES.find((t) => t.value === value)) ?? value)
|
onChange(value);
|
||||||
: "Select a timezone"
|
}}
|
||||||
}
|
error={Boolean(errors.timezone)}
|
||||||
options={timeZoneOptions}
|
buttonClassName="border-none"
|
||||||
onChange={onChange}
|
/>
|
||||||
buttonClassName={errors.timezone ? "border-red-500" : "border-none"}
|
</>
|
||||||
className="rounded-md border-[0.5px] !border-custom-border-200"
|
|
||||||
optionsClassName="w-72"
|
|
||||||
input
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.timezone && <span className="text-xs text-red-500">{errors.timezone.message}</span>}
|
{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