Compare commits

...

1 Commits

Author SHA1 Message Date
Vamsi krishna
750364833b feat: added user timezone dates for cycle 2025-03-26 12:39:42 +05:30
4 changed files with 131 additions and 35 deletions

View File

@@ -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
@@ -209,34 +213,74 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
<span>Transfer {transferableIssuesCount} work items</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="In your timezone"
>
<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: "Start date",
to: "End date",
}}
showTooltip={isProjectTimeZoneDifferent()}
customTooltipHeading="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) => {

View File

@@ -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={

View File

@@ -50,6 +50,8 @@ type Props = {
};
renderByDefault?: boolean;
renderPlaceholder?: boolean;
customTooltipContent?: React.ReactNode;
customTooltipHeading?: string;
};
export const DateRangeDropdown: React.FC<Props> = (props) => {
@@ -78,6 +80,8 @@ export const DateRangeDropdown: React.FC<Props> = (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> = (props) => {
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Date range"
tooltipHeading={customTooltipHeading ?? "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}

View File

@@ -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;
},
};
};