Files
kan/apps/web/src/views/card/components/DueDateSelector.tsx
Ridham Khandar 57186b6b4d feat: paginate and merge frequent activities (#274)
* feat: paginate and merge frequent activities

* feat: due date activities

* fix: requested changes fixes

* refactor: slight reshuffle and improve types

* chore: translations

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2025-12-16 09:07:08 +00:00

143 lines
4.2 KiB
TypeScript

import { t } from "@lingui/core/macro";
import { format } from "date-fns";
import { useEffect, useState } from "react";
import { HiMiniPlus } from "react-icons/hi2";
import DateSelector from "~/components/DateSelector";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation";
interface DueDateSelectorProps {
cardPublicId: string;
dueDate: Date | null | undefined;
isLoading?: boolean;
}
export function DueDateSelector({
cardPublicId,
dueDate,
isLoading = false,
}: DueDateSelectorProps) {
const { showPopup } = usePopup();
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const [pendingDate, setPendingDate] = useState<Date | null | undefined>(
dueDate,
);
// Sync pendingDate with dueDate when it changes externally
useEffect(() => {
if (!isOpen) {
setPendingDate(dueDate);
}
}, [dueDate, isOpen]);
const updateDueDate = api.card.update.useMutation({
onMutate: async (update) => {
await utils.card.byId.cancel();
const previousCard = utils.card.byId.getData({ cardPublicId });
utils.card.byId.setData({ cardPublicId }, (oldCard) => {
if (!oldCard) return oldCard;
return {
...oldCard,
dueDate:
update.dueDate !== undefined
? (update.dueDate as Date | null)
: oldCard.dueDate,
};
});
return { previousCard };
},
onError: (_error, _update, context) => {
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
showPopup({
header: t`Unable to update due date`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
onSettled: async () => {
await invalidateCard(utils, cardPublicId);
await utils.board.byId.invalidate();
},
});
const handleDateSelect = (date: Date | undefined) => {
// Only update local state, don't fire mutation
setPendingDate(date ?? null);
};
const handleBackdropClick = () => {
// Only fire mutation if date actually changed
const pendingIsNull = pendingDate === null || pendingDate === undefined;
const dueIsNull = dueDate === null || dueDate === undefined;
let dateChanged = false;
if (pendingIsNull && !dueIsNull) {
dateChanged = true;
} else if (!pendingIsNull && dueIsNull) {
dateChanged = true;
} else if (!pendingIsNull && !dueIsNull) {
// Both are non-null at this point
if (pendingDate instanceof Date && dueDate instanceof Date) {
dateChanged = pendingDate.getTime() !== dueDate.getTime();
}
}
// Close popover immediately
setIsOpen(false);
// Fire mutation if date changed (optimistic update will handle UI)
if (dateChanged) {
updateDueDate.mutate({
cardPublicId,
dueDate: pendingDate ?? null,
});
}
};
return (
<div className="relative flex w-full items-center text-left">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
disabled={isLoading}
className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100"
>
{dueDate ? (
<span>{format(dueDate, "MMM d, yyyy")}</span>
) : (
<>
<HiMiniPlus size={22} className="pr-2" />
{t`Set due date`}
</>
)}
</button>
{isOpen && (
<>
<div className="fixed inset-0 z-10" onClick={handleBackdropClick} />
<div
className="absolute -left-8 top-full z-20 mt-2 rounded-md border border-light-200 bg-light-50 shadow-lg dark:border-dark-200 dark:bg-dark-100"
onClick={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
>
<DateSelector
selectedDate={pendingDate ?? undefined}
onDateSelect={handleDateSelect}
/>
</div>
</>
)}
</div>
);
}