diff --git a/apps/web/src/components/DateSelector.tsx b/apps/web/src/components/DateSelector.tsx
new file mode 100644
index 00000000..a6bb3459
--- /dev/null
+++ b/apps/web/src/components/DateSelector.tsx
@@ -0,0 +1,135 @@
+import {
+ addMonths,
+ eachDayOfInterval,
+ endOfMonth,
+ endOfWeek,
+ format,
+ isSameDay,
+ isToday,
+ startOfMonth,
+ startOfWeek,
+ subMonths,
+} from "date-fns";
+import { useMemo, useState } from "react";
+import { HiChevronLeft, HiChevronRight } from "react-icons/hi2";
+import { twMerge } from "tailwind-merge";
+
+interface DateSelectorProps {
+ selectedDate?: Date | null;
+ onDateSelect?: (date: Date | undefined) => void;
+}
+
+const DateSelector = ({ selectedDate, onDateSelect }: DateSelectorProps) => {
+ const [currentMonth, setCurrentMonth] = useState(() => {
+ return selectedDate ? startOfMonth(selectedDate) : startOfMonth(new Date());
+ });
+
+ const monthName = format(currentMonth, "MMMM");
+ const year = format(currentMonth, "yyyy");
+
+ const dayHeaders = useMemo(() => {
+ const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); // Monday
+ return eachDayOfInterval({
+ start: weekStart,
+ end: new Date(weekStart.getTime() + 6 * 24 * 60 * 60 * 1000),
+ }).map((date) => format(date, "EEEEEE")); // Shortest localized day name
+ }, []);
+
+ const days = useMemo(() => {
+ const monthStart = startOfMonth(currentMonth);
+ const monthEnd = endOfMonth(currentMonth);
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 }); // Monday
+ const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 }); // Monday
+
+ return eachDayOfInterval({ start: calendarStart, end: calendarEnd }).map(
+ (date) => {
+ const dateString = format(date, "yyyy-MM-dd");
+ return {
+ date: dateString,
+ isToday: isToday(date),
+ isSelected: selectedDate ? isSameDay(date, selectedDate) : false,
+ isCurrentMonth: date >= monthStart && date <= monthEnd,
+ dateObj: date,
+ };
+ },
+ );
+ }, [currentMonth, selectedDate]);
+
+ const handlePreviousMonth = () => {
+ setCurrentMonth(subMonths(currentMonth, 1));
+ };
+
+ const handleNextMonth = () => {
+ setCurrentMonth(addMonths(currentMonth, 1));
+ };
+
+ const handleDateClick = (date: Date, e: React.MouseEvent) => {
+ e.stopPropagation();
+ // If clicking the same date that's already selected, unselect it
+ if (selectedDate && isSameDay(date, selectedDate)) {
+ onDateSelect?.(undefined);
+ } else {
+ onDateSelect?.(date);
+ }
+ };
+
+ return (
+
+
+
+
+ {monthName} {year}
+
+
+
+
+ {dayHeaders.map((day, index) => (
+
{day}
+ ))}
+
+
+ {days.map((day) => (
+
+ ))}
+
+
+ );
+};
+
+export default DateSelector;
diff --git a/apps/web/src/views/card/components/DueDateSelector.tsx b/apps/web/src/views/card/components/DueDateSelector.tsx
new file mode 100644
index 00000000..22133767
--- /dev/null
+++ b/apps/web/src/views/card/components/DueDateSelector.tsx
@@ -0,0 +1,141 @@
+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";
+
+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(
+ 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 utils.card.byId.invalidate({ 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 (
+
+
+ {isOpen && (
+ <>
+
+
{
+ e.stopPropagation();
+ }}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ }}
+ >
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx
index 10e9de09..7dba67b1 100644
--- a/apps/web/src/views/card/index.tsx
+++ b/apps/web/src/views/card/index.tsx
@@ -27,6 +27,7 @@ import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
import Dropdown from "./components/Dropdown";
+import { DueDateSelector } from "./components/DueDateSelector";
import LabelSelector from "./components/LabelSelector";
import ListSelector from "./components/ListSelector";
import MemberSelector from "./components/MemberSelector";
@@ -124,7 +125,7 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
/>
{!isTemplate && (
-
);
}