[WEB-3681]feat: added user timezone dates for cycle (#6820)
* feat: added user timezone dates for cycle * *chore: added translations *chore: refactored user timezone functions
This commit is contained in:
parent
c125bc54ba
commit
ae6e5a48fa
18 changed files with 247 additions and 40 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "進捗",
|
||||
|
|
|
|||
|
|
@ -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": "진행",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Прогресс",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "Прогрес",
|
||||
|
|
|
|||
|
|
@ -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": "进度",
|
||||
|
|
|
|||
|
|
@ -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": "進度",
|
||||
|
|
|
|||
|
|
@ -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<Props> = 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<Props> = 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<Props> = observer((props) => {
|
|||
<Eye className="h-4 w-4 my-auto text-custom-primary-200" />
|
||||
<span>{t("project_cycles.more_details")}</span>
|
||||
</button>
|
||||
|
||||
{showIssueCount && (
|
||||
<div className="flex items-center gap-1">
|
||||
<LayersIcon className="h-4 w-4 text-custom-text-300" />
|
||||
<span className="text-xs text-custom-text-300">{cycleDetails.total_issues}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CycleAdditionalActions cycleId={cycleId} projectId={projectId} />
|
||||
{showTransferIssues && (
|
||||
<div
|
||||
|
|
@ -206,37 +210,77 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
}}
|
||||
>
|
||||
<TransferIcon className="fill-custom-primary-200 w-4" />
|
||||
<span>Transfer {transferableIssuesCount} work items</span>
|
||||
<span>{t("project_cycles.transfer_work_items", { count: transferableIssuesCount })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isActive && cycleDetails.start_date && (
|
||||
<DateRangeDropdown
|
||||
buttonVariant={"transparent-with-text"}
|
||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>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 ? (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
{/* Duration */}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<span className="flex gap-1">
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
disabled={!isProjectTimeZoneDifferent()}
|
||||
tooltipHeading={t("project_cycles.date_range")}
|
||||
>
|
||||
<div className="flex gap-1 text-xs text-custom-text-300 font-medium items-center">
|
||||
<CalendarDays className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.start_date && <span>{format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}</span>}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{cycleDetails.end_date && <span>{format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{projectUTCOffset && (
|
||||
<span className="rounded-md text-xs px-2 cursor-default py-1 bg-custom-background-80 text-custom-text-300">
|
||||
{projectUTCOffset}
|
||||
</span>
|
||||
)}
|
||||
{/* created by */}
|
||||
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
cycleDetails.start_date && (
|
||||
<>
|
||||
<DateRangeDropdown
|
||||
buttonVariant={"transparent-with-text"}
|
||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
|
||||
buttonClassName="p-0"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(cycleDetails.start_date),
|
||||
to: getDate(cycleDetails.end_date),
|
||||
}}
|
||||
placeholder={{
|
||||
from: t("project_cycles.start_date"),
|
||||
to: t("project_cycles.end_date"),
|
||||
}}
|
||||
showTooltip={isProjectTimeZoneDifferent()}
|
||||
customTooltipHeading={t("project_cycles.in_your_timezone")}
|
||||
customTooltipContent={
|
||||
<span className="flex gap-1">
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.start_date ?? "")}
|
||||
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
|
||||
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
|
||||
</span>
|
||||
}
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled
|
||||
hideIcon={{
|
||||
from: false,
|
||||
to: false,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* created by */}
|
||||
{createdByDetails && !isActive && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||
|
||||
{!isActive && (
|
||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||
<div className="flex w-10 cursor-default items-center justify-center">
|
||||
|
|
@ -255,7 +299,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isEditingAllowed && !cycleDetails.archived_at && (
|
||||
<FavoriteStar
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export const CyclesListItem: FC<TCyclesListItem> = 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<TCyclesListItem> = observer((props) => {
|
|||
cycleId={cycleId}
|
||||
cycleDetails={cycleDetails}
|
||||
parentRef={parentRef}
|
||||
isActive={isActive}
|
||||
/>
|
||||
}
|
||||
quickActionElement={
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -50,9 +52,12 @@ type Props = {
|
|||
};
|
||||
renderByDefault?: boolean;
|
||||
renderPlaceholder?: boolean;
|
||||
customTooltipContent?: React.ReactNode;
|
||||
customTooltipHeading?: string;
|
||||
};
|
||||
|
||||
export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
buttonClassName,
|
||||
buttonContainerClassName,
|
||||
|
|
@ -69,8 +74,8 @@ export const DateRangeDropdown: React.FC<Props> = (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,
|
||||
|
|
@ -78,6 +83,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
|||
value,
|
||||
renderByDefault = true,
|
||||
renderPlaceholder = true,
|
||||
customTooltipContent,
|
||||
customTooltipHeading,
|
||||
} = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
|
@ -147,13 +154,15 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
|||
<DropdownButton
|
||||
className={buttonClassName}
|
||||
isActive={isOpen}
|
||||
tooltipHeading="Date range"
|
||||
tooltipHeading={customTooltipHeading ?? t("project_cycles.date_range")}
|
||||
tooltipContent={
|
||||
<>
|
||||
{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}
|
||||
|
|
|
|||
69
web/core/hooks/use-timezone-converter.tsx
Normal file
69
web/core/hooks/use-timezone-converter.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { useCallback } from "react";
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
const convertedDate = new Date(date).toLocaleString("en-US", { timeZone: userTimezone });
|
||||
// return the formatted date
|
||||
return format(convertedDate, formatToken);
|
||||
},
|
||||
[userTimezone]
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the project's UTC offset
|
||||
* @returns The project's UTC offset
|
||||
*/
|
||||
const getProjectUTCOffset = useCallback(() => {
|
||||
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")}`;
|
||||
}, [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,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue