From 750364833bd06eca037b4d92a4d0a1a79272c7b4 Mon Sep 17 00:00:00 2001 From: Vamsi krishna Date: Wed, 26 Mar 2025 12:39:42 +0530 Subject: [PATCH 1/2] feat: added user timezone dates for cycle --- .../cycles/list/cycle-list-item-action.tsx | 101 +++++++++++++----- .../cycles/list/cycles-list-item.tsx | 2 + web/core/components/dropdowns/date-range.tsx | 18 ++-- web/core/hooks/use-timezone-converter.tsx | 45 ++++++++ 4 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 web/core/hooks/use-timezone-converter.tsx diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index bc627fdf28c..6409012a5dd 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -1,10 +1,11 @@ "use client"; import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react"; +import { format, parseISO } from "date-fns"; import { observer } from "mobx-react"; import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; -import { Eye, Users } from "lucide-react"; +import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react"; // types import { CYCLE_FAVORITED, @@ -29,6 +30,7 @@ import { generateQueryParams } from "@/helpers/router.helper"; import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { useTimeZoneConverter } from "@/hooks/use-timezone-converter"; // plane web components import { CycleAdditionalActions } from "@/plane-web/components/cycles"; @@ -55,6 +57,8 @@ export const CycleListItemAction: FC = observer((props) => { // hooks const { isMobile } = usePlatformOS(); const { t } = useTranslation(); + const { isProjectTimeZoneDifferent, getProjectUTCOffset, renderFormattedDateInUserTimezone } = + useTimeZoneConverter(projectId); // router const router = useAppRouter(); const searchParams = useSearchParams(); @@ -88,6 +92,8 @@ export const CycleListItemAction: FC = observer((props) => { const showTransferIssues = routerProjectId && transferableIssuesCount > 0 && cycleStatus === "completed"; + const projectUTCOffset = getProjectUTCOffset(); + const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT, @@ -189,14 +195,12 @@ export const CycleListItemAction: FC = observer((props) => { {t("project_cycles.more_details")} - {showIssueCount && (
{cycleDetails.total_issues}
)} - {showTransferIssues && (
= observer((props) => { Transfer {transferableIssuesCount} work items
)} - - {!isActive && cycleDetails.start_date && ( - div]:hover:bg-transparent`} - buttonClassName="p-0" - minDate={new Date()} - value={{ - from: getDate(cycleDetails.start_date), - to: getDate(cycleDetails.end_date), - }} - placeholder={{ - from: "Start date", - to: "End date", - }} - showTooltip - required={cycleDetails.status !== "draft"} - disabled - hideIcon={{ - from: false, - to: false, - }} - /> + {isActive ? ( + <> +
+ {/* Duration */} + + {renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")} + + {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} + + } + disabled={!isProjectTimeZoneDifferent()} + tooltipHeading="In your timezone" + > +
+ + {cycleDetails.start_date && {format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}} + + {cycleDetails.end_date && {format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}} +
+
+ {projectUTCOffset && ( + + {projectUTCOffset} + + )} + {/* created by */} + {createdByDetails && } +
+ + ) : ( + cycleDetails.start_date && ( + <> + div]:hover:bg-transparent`} + buttonClassName="p-0" + minDate={new Date()} + value={{ + from: getDate(cycleDetails.start_date), + to: getDate(cycleDetails.end_date), + }} + placeholder={{ + from: "Start date", + to: "End date", + }} + showTooltip={isProjectTimeZoneDifferent()} + customTooltipHeading="In your timezone" + customTooltipContent={ + + {renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")} + + {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} + + } + required={cycleDetails.status !== "draft"} + disabled + hideIcon={{ + from: false, + to: false, + }} + /> + + ) )} - {/* created by */} {createdByDetails && !isActive && } - {!isActive && (
@@ -255,7 +299,6 @@ export const CycleListItemAction: FC = observer((props) => {
)} - {isEditingAllowed && !cycleDetails.archived_at && ( { diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx index aad2f692292..5fca9a9e7ca 100644 --- a/web/core/components/cycles/list/cycles-list-item.tsx +++ b/web/core/components/cycles/list/cycles-list-item.tsx @@ -53,6 +53,7 @@ export const CyclesListItem: FC = observer((props) => { // TODO: change this logic once backend fix the response const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; const isCompleted = cycleStatus === "completed"; + const isActive = cycleStatus === "current"; const cycleTotalIssues = cycleDetails.backlog_issues + @@ -113,6 +114,7 @@ export const CyclesListItem: FC = observer((props) => { cycleId={cycleId} cycleDetails={cycleDetails} parentRef={parentRef} + isActive={isActive} /> } quickActionElement={ diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index f33a8f8b7ea..08ab0bb0de1 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -50,6 +50,8 @@ type Props = { }; renderByDefault?: boolean; renderPlaceholder?: boolean; + customTooltipContent?: React.ReactNode; + customTooltipHeading?: string; }; export const DateRangeDropdown: React.FC = (props) => { @@ -78,6 +80,8 @@ export const DateRangeDropdown: React.FC = (props) => { value, renderByDefault = true, renderPlaceholder = true, + customTooltipContent, + customTooltipHeading, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -147,13 +151,15 @@ export const DateRangeDropdown: React.FC = (props) => { - {dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"} - {" - "} - {dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"} - + customTooltipContent ?? ( + <> + {dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"} + {" - "} + {dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"} + + ) } showTooltip={showTooltip} variant={buttonVariant} diff --git a/web/core/hooks/use-timezone-converter.tsx b/web/core/hooks/use-timezone-converter.tsx new file mode 100644 index 00000000000..bc0a7ca517a --- /dev/null +++ b/web/core/hooks/use-timezone-converter.tsx @@ -0,0 +1,45 @@ +import { format } from "date-fns"; +import { useProject, useUser } from "@/hooks/store"; + +export const useTimeZoneConverter = (projectId: string) => { + const { data: user } = useUser(); + const { getProjectById } = useProject(); + const userTimezone = user?.user_timezone; + const projectTimezone = getProjectById(projectId)?.timezone; + + return { + renderFormattedDateInUserTimezone: (date: string, formatToken: string = "MMM dd, yyyy") => { + // return if undefined + if (!date || !userTimezone) return; + // convert the date to the user's timezone + const convertedDate = new Date(date).toLocaleString("en-US", { timeZone: userTimezone }); + // return the formatted date + return format(convertedDate, formatToken); + }, + getProjectUTCOffset: () => { + if (!projectTimezone) return; + + // Get date in user's timezone + const projectDate = new Date(new Date().toLocaleString("en-US", { timeZone: projectTimezone })); + const utcDate = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); + + // Calculate offset in minutes + const offsetInMinutes = (projectDate.getTime() - utcDate.getTime()) / 60000; + + // return if undefined + if (!offsetInMinutes) return; + + // Convert to hours and minutes + const hours = Math.floor(Math.abs(offsetInMinutes) / 60); + const minutes = Math.abs(offsetInMinutes) % 60; + + // Format as +/-HH:mm + const sign = offsetInMinutes >= 0 ? "+" : "-"; + return `UTC ${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + }, + isProjectTimeZoneDifferent: () => { + if (!projectTimezone || !userTimezone) return false; + return projectTimezone !== userTimezone; + }, + }; +}; From eaa2868ab3e3e20f98e7f3efa811eab6431f04e8 Mon Sep 17 00:00:00 2001 From: Vamsi krishna Date: Wed, 26 Mar 2025 19:31:17 +0530 Subject: [PATCH 2/2] *chore: added translations *chore: refactored user timezone functions --- .../i18n/src/locales/cs/translations.json | 6 ++ .../i18n/src/locales/de/translations.json | 8 ++- .../i18n/src/locales/en/translations.json | 6 ++ .../i18n/src/locales/es/translations.json | 6 ++ .../i18n/src/locales/fr/translations.json | 6 ++ .../i18n/src/locales/it/translations.json | 6 ++ .../i18n/src/locales/ja/translations.json | 6 ++ .../i18n/src/locales/ko/translations.json | 6 ++ .../i18n/src/locales/pl/translations.json | 8 ++- .../i18n/src/locales/ru/translations.json | 6 ++ .../i18n/src/locales/sk/translations.json | 6 ++ .../i18n/src/locales/ua/translations.json | 6 ++ .../i18n/src/locales/zh-CN/translations.json | 6 ++ .../i18n/src/locales/zh-TW/translations.json | 6 ++ .../cycles/list/cycle-list-item-action.tsx | 10 +-- web/core/components/dropdowns/date-range.tsx | 9 ++- web/core/hooks/use-timezone-converter.tsx | 68 +++++++++++++------ 17 files changed, 143 insertions(+), 32 deletions(-) diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 1315ba75c9e..0e2764ec482 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Odeberte filtry pro zobrazení všech cyklů", "remove_search_criteria_to_see_all_cycles": "Odeberte kritéria pro zobrazení všech cyklů", "only_completed_cycles_can_be_archived": "Lze archivovat pouze dokončené cykly", + "start_date": "Začátek data", + "end_date": "Konec data", + "in_your_timezone": "V časovém pásmu", + "transfer_work_items": "Převést {count} pracovních položek", + "date_range": "Období data", + "add_date": "Přidat datum", "active_cycle": { "label": "Aktivní cyklus", "progress": "Pokrok", diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 4a3a5c06dad..872fc17462a 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Entfernen Sie Filter, um alle Zyklen anzuzeigen", "remove_search_criteria_to_see_all_cycles": "Entfernen Sie Suchkriterien, um alle Zyklen anzuzeigen", "only_completed_cycles_can_be_archived": "Nur abgeschlossene Zyklen können archiviert werden", + "start_date": "Startdatum", + "end_date": "Enddatum", + "in_your_timezone": "In Ihrer Zeitzone", + "transfer_work_items": "Übertragen von {count} Arbeitselementen", + "date_range": "Datumsbereich", + "add_date": "Datum hinzufügen", "active_cycle": { "label": "Aktiver Zyklus", "progress": "Fortschritt", @@ -2321,4 +2327,4 @@ "label": "{count, plural, one {Modul} few {Module} other {Module}}", "no_module": "Kein Modul" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 6beb2917b5f..3566ae98057 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1606,6 +1606,12 @@ "remove_filters_to_see_all_cycles": "Remove the filters to see all cycles", "remove_search_criteria_to_see_all_cycles": "Remove the search criteria to see all cycles", "only_completed_cycles_can_be_archived": "Only completed cycles can be archived", + "start_date": "Start date", + "end_date": "End date", + "in_your_timezone": "In your timezone", + "transfer_work_items": "Transfer {count} work items", + "date_range": "Date range", + "add_date": "Add date", "active_cycle": { "label": "Active cycle", "progress": "Progress", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index c349364d4bf..3c30865de29 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "Elimina los filtros para ver todos los ciclos", "remove_search_criteria_to_see_all_cycles": "Elimina los criterios de búsqueda para ver todos los ciclos", "only_completed_cycles_can_be_archived": "Solo los ciclos completados pueden ser archivados", + "start_date": "Fecha de inicio", + "end_date": "Fecha de finalización", + "in_your_timezone": "En tu zona horaria", + "transfer_work_items": "Transferir {count} elementos de trabajo", + "date_range": "Rango de fechas", + "add_date": "Agregar fecha", "active_cycle": { "label": "Ciclo activo", "progress": "Progreso", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 651622b9714..1b1332d5f24 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Supprimez les filtres pour voir tous les cycles", "remove_search_criteria_to_see_all_cycles": "Supprimez les critères de recherche pour voir tous les cycles", "only_completed_cycles_can_be_archived": "Seuls les cycles terminés peuvent être archivés", + "start_date": "Date de début", + "end_date": "Date de fin", + "in_your_timezone": "Dans votre fuseau horaire", + "transfer_work_items": "Transférer {count} éléments de travail", + "date_range": "Plage de dates", + "add_date": "Ajouter une date", "active_cycle": { "label": "Cycle actif", "progress": "Progression", diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index ba40f5b9bd1..c83af70a417 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -1772,6 +1772,12 @@ "remove_filters_to_see_all_cycles": "Rimuovi i filtri per vedere tutti i cicli", "remove_search_criteria_to_see_all_cycles": "Rimuovi i criteri di ricerca per vedere tutti i cicli", "only_completed_cycles_can_be_archived": "Solo i cicli completati possono essere archiviati", + "start_date": "Data di inizio", + "end_date": "Data di fine", + "in_your_timezone": "Nel tuo fuso orario", + "transfer_work_items": "Trasferisci {count} elementi di lavoro", + "date_range": "Intervallo di date", + "add_date": "Aggiungi data", "active_cycle": { "label": "Ciclo attivo", "progress": "Avanzamento", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 8f3c82e0596..cb4a2ac78fd 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "すべてのサイクルを表示するにはフィルターを解除してください", "remove_search_criteria_to_see_all_cycles": "すべてのサイクルを表示するには検索条件を解除してください", "only_completed_cycles_can_be_archived": "完了したサイクルのみアーカイブできます", + "start_date": "開始日", + "end_date": "終了日", + "in_your_timezone": "あなたのタイムゾーン", + "transfer_work_items": "作業項目を転送 {count}", + "date_range": "日付範囲", + "add_date": "日付を追加", "active_cycle": { "label": "アクティブなサイクル", "progress": "進捗", diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index b3b0a29d405..794f97e8411 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "모든 주기를 보려면 필터를 제거하세요", "remove_search_criteria_to_see_all_cycles": "모든 주기를 보려면 검색 기준을 제거하세요", "only_completed_cycles_can_be_archived": "완료된 주기만 아카이브할 수 있습니다", + "start_date": "시작일", + "end_date": "종료일", + "in_your_timezone": "내 시간대", + "transfer_work_items": "{count}개의 작업 항목 이전", + "date_range": "날짜 범위", + "add_date": "날짜 추가", "active_cycle": { "label": "활성 주기", "progress": "진행", diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 3cee6585357..98c8713b1ca 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Usuń filtry, aby wyświetlić wszystkie cykle", "remove_search_criteria_to_see_all_cycles": "Usuń kryteria wyszukiwania, aby wyświetlić wszystkie cykle", "only_completed_cycles_can_be_archived": "Można archiwizować tylko ukończone cykle", + "start_date": "Data początku", + "end_date": "Data końca", + "in_your_timezone": "W Twojej strefie czasowej", + "transfer_work_items": "Przenieś {count} elementów pracy", + "date_range": "Zakres dat", + "add_date": "Dodaj datę", "active_cycle": { "label": "Aktywny cykl", "progress": "Postęp", @@ -2321,4 +2327,4 @@ "label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}", "no_module": "Brak modułu" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index d753b30bac1..52c403d1882 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "Снимите фильтры для просмотра всех циклов", "remove_search_criteria_to_see_all_cycles": "Очистите поиск для просмотра всех циклов", "only_completed_cycles_can_be_archived": "Только завершённые циклы можно архивировать", + "start_date": "Дата начала", + "end_date": "Дата окончания", + "in_your_timezone": "В вашем часовом поясе", + "transfer_work_items": "Перенести {count} рабочих элементов", + "date_range": "Диапазон дат", + "add_date": "Добавить дату", "active_cycle": { "label": "Активный цикл", "progress": "Прогресс", diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 30a1a4db835..8a4645fd46a 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -1773,6 +1773,12 @@ "remove_filters_to_see_all_cycles": "Odstráňte filtre pre zobrazenie všetkých cyklov", "remove_search_criteria_to_see_all_cycles": "Odstráňte kritériá pre zobrazenie všetkých cyklov", "only_completed_cycles_can_be_archived": "Archivovať je možné iba dokončené cykly", + "start_date": "Dátum začiatku", + "end_date": "Dátum konca", + "in_your_timezone": "Váš časový pásmo", + "transfer_work_items": "Presunúť {count} pracovných položiek", + "date_range": "Dátumový rozsah", + "add_date": "Pridať dátum", "active_cycle": { "label": "Aktívny cyklus", "progress": "Pokrok", diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index ace8349fff3..044cfbadc6a 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -1747,6 +1747,12 @@ "remove_filters_to_see_all_cycles": "Приберіть фільтри, щоб побачити всі цикли", "remove_search_criteria_to_see_all_cycles": "Приберіть критерії пошуку, щоб побачити всі цикли", "only_completed_cycles_can_be_archived": "Архівувати можна лише завершені цикли", + "start_date": "Дата початку", + "end_date": "Дата завершення", + "in_your_timezone": "У вашому часовому поясі", + "transfer_work_items": "Перенести {count} робочих одиниць", + "date_range": "Діапазон дат", + "add_date": "Додати дату", "active_cycle": { "label": "Активний цикл", "progress": "Прогрес", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 5e2725127b5..4620d06177c 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -1774,6 +1774,12 @@ "remove_filters_to_see_all_cycles": "移除筛选器以查看所有周期", "remove_search_criteria_to_see_all_cycles": "移除搜索条件以查看所有周期", "only_completed_cycles_can_be_archived": "只能归档已完成的周期", + "start_date": "开始日期", + "end_date": "结束日期", + "in_your_timezone": "在您的时区", + "transfer_work_items": "转移 {count} 工作项", + "date_range": "日期范围", + "add_date": "添加日期", "active_cycle": { "label": "活动周期", "progress": "进度", diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index a59d78e4dd1..c371f6fc6a2 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -1776,6 +1776,12 @@ "remove_filters_to_see_all_cycles": "移除篩選器以檢視所有週期", "remove_search_criteria_to_see_all_cycles": "移除搜尋條件以檢視所有週期", "only_completed_cycles_can_be_archived": "只有已完成的週期可以封存", + "start_date": "開始日期", + "end_date": "結束日期", + "in_your_timezone": "在您的時區", + "transfer_work_items": "轉移 {count} 工作事項", + "date_range": "日期範圍", + "add_date": "新增日期", "active_cycle": { "label": "使用中的週期", "progress": "進度", diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index 6409012a5dd..8b346b3cd79 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -210,7 +210,7 @@ export const CycleListItemAction: FC = observer((props) => { }} > - Transfer {transferableIssuesCount} work items + {t("project_cycles.transfer_work_items", { count: transferableIssuesCount })} )} {isActive ? ( @@ -226,7 +226,7 @@ export const CycleListItemAction: FC = observer((props) => { } disabled={!isProjectTimeZoneDifferent()} - tooltipHeading="In your timezone" + tooltipHeading={t("project_cycles.date_range")} >
@@ -257,11 +257,11 @@ export const CycleListItemAction: FC = observer((props) => { to: getDate(cycleDetails.end_date), }} placeholder={{ - from: "Start date", - to: "End date", + from: t("project_cycles.start_date"), + to: t("project_cycles.end_date"), }} showTooltip={isProjectTimeZoneDifferent()} - customTooltipHeading="In your timezone" + customTooltipHeading={t("project_cycles.in_your_timezone")} customTooltipContent={ {renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")} diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index 08ab0bb0de1..0ab9fd44be3 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -6,6 +6,8 @@ import { DateRange, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react"; import { Combobox } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, Calendar } from "@plane/ui"; // helpers @@ -55,6 +57,7 @@ type Props = { }; export const DateRangeDropdown: React.FC = (props) => { + const { t } = useTranslation(); const { buttonClassName, buttonContainerClassName, @@ -71,8 +74,8 @@ export const DateRangeDropdown: React.FC = (props) => { maxDate, onSelect, placeholder = { - from: "Add date", - to: "Add date", + from: t("project_cycles.add_date"), + to: t("project_cycles.add_date"), }, placement, showTooltip = false, @@ -151,7 +154,7 @@ export const DateRangeDropdown: React.FC = (props) => { diff --git a/web/core/hooks/use-timezone-converter.tsx b/web/core/hooks/use-timezone-converter.tsx index bc0a7ca517a..97646a33977 100644 --- a/web/core/hooks/use-timezone-converter.tsx +++ b/web/core/hooks/use-timezone-converter.tsx @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { format } from "date-fns"; import { useProject, useUser } from "@/hooks/store"; @@ -7,8 +8,14 @@ export const useTimeZoneConverter = (projectId: string) => { const userTimezone = user?.user_timezone; const projectTimezone = getProjectById(projectId)?.timezone; - return { - renderFormattedDateInUserTimezone: (date: string, formatToken: string = "MMM dd, yyyy") => { + /** + * Render a date in the user's timezone + * @param date - The date to render + * @param formatToken - The format token to use + * @returns The formatted date + */ + const renderFormattedDateInUserTimezone = useCallback( + (date: string, formatToken: string = "MMM dd, yyyy") => { // return if undefined if (!date || !userTimezone) return; // convert the date to the user's timezone @@ -16,30 +23,47 @@ export const useTimeZoneConverter = (projectId: string) => { // return the formatted date return format(convertedDate, formatToken); }, - getProjectUTCOffset: () => { - if (!projectTimezone) return; + [userTimezone] + ); - // Get date in user's timezone - const projectDate = new Date(new Date().toLocaleString("en-US", { timeZone: projectTimezone })); - const utcDate = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); + /** + * Get the project's UTC offset + * @returns The project's UTC offset + */ + const getProjectUTCOffset = useCallback(() => { + if (!projectTimezone) return; - // Calculate offset in minutes - const offsetInMinutes = (projectDate.getTime() - utcDate.getTime()) / 60000; + // Get date in user's timezone + const projectDate = new Date(new Date().toLocaleString("en-US", { timeZone: projectTimezone })); + const utcDate = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" })); - // return if undefined - if (!offsetInMinutes) return; + // Calculate offset in minutes + const offsetInMinutes = (projectDate.getTime() - utcDate.getTime()) / 60000; - // Convert to hours and minutes - const hours = Math.floor(Math.abs(offsetInMinutes) / 60); - const minutes = Math.abs(offsetInMinutes) % 60; + // return if undefined + if (!offsetInMinutes) return; - // Format as +/-HH:mm - const sign = offsetInMinutes >= 0 ? "+" : "-"; - return `UTC ${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; - }, - isProjectTimeZoneDifferent: () => { - if (!projectTimezone || !userTimezone) return false; - return projectTimezone !== userTimezone; - }, + // Convert to hours and minutes + const hours = Math.floor(Math.abs(offsetInMinutes) / 60); + const minutes = Math.abs(offsetInMinutes) % 60; + + // Format as +/-HH:mm + const sign = offsetInMinutes >= 0 ? "+" : "-"; + return `UTC ${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + }, [projectTimezone]); + + /** + * Check if the project's timezone is different from the user's timezone + * @returns True if the project's timezone is different from the user's timezone, false otherwise + */ + const isProjectTimeZoneDifferent = useCallback(() => { + if (!projectTimezone || !userTimezone) return false; + return projectTimezone !== userTimezone; + }, [projectTimezone, userTimezone]); + + return { + renderFormattedDateInUserTimezone, + getProjectUTCOffset, + isProjectTimeZoneDifferent, }; };