made the sprint date selection beautiful

shows existing sprint dates
This commit is contained in:
Oliver Bryan
2026-01-17 02:47:09 +00:00
parent 0dcfe1b66b
commit af9df0376f
3 changed files with 53 additions and 3 deletions

View File

@@ -49,10 +49,12 @@ const getDefaultDates = () => {
export function CreateSprint({ export function CreateSprint({
projectId, projectId,
sprints,
trigger, trigger,
completeAction, completeAction,
}: { }: {
projectId?: number; projectId?: number;
sprints: SprintRecord[];
trigger?: React.ReactNode; trigger?: React.ReactNode;
completeAction?: (sprint: SprintRecord) => void | Promise<void>; completeAction?: (sprint: SprintRecord) => void | Promise<void>;
}) { }) {
@@ -203,6 +205,7 @@ export function CreateSprint({
setStartDate(getStartOfDay(value)); setStartDate(getStartOfDay(value));
}} }}
autoFocus autoFocus
sprints={sprints}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@@ -225,6 +228,8 @@ export function CreateSprint({
setEndDate(getEndOfDay(value)); setEndDate(getEndOfDay(value));
}} }}
autoFocus autoFocus
sprints={sprints}
isEnd
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -682,6 +682,7 @@ function OrganisationsDialog({
<Plus className="size-4" /> <Plus className="size-4" />
</Button> </Button>
} }
sprints={sprints}
/> />
)} )}
</div> </div>

View File

@@ -1,3 +1,4 @@
import type { SprintRecord } from "@sprint/shared";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
@@ -12,9 +13,13 @@ function Calendar({
buttonVariant = "ghost", buttonVariant = "ghost",
formatters, formatters,
components, components,
sprints,
isEnd,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]; buttonVariant?: React.ComponentProps<typeof Button>["variant"];
sprints?: SprintRecord[];
isEnd?: boolean;
}) { }) {
const defaultClassNames = getDefaultClassNames(); const defaultClassNames = getDefaultClassNames();
@@ -113,7 +118,7 @@ function Calendar({
return <ChevronDownIcon className={cn("size-4", className)} {...props} />; return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
}, },
DayButton: CalendarDayButton, DayButton: (props) => <CalendarDayButton {...props} sprints={sprints} isEnd={isEnd} />,
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...props }) => {
return ( return (
<td {...props}> <td {...props}>
@@ -130,7 +135,16 @@ function Calendar({
); );
} }
function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps<typeof DayButton>) { function CalendarDayButton({
className,
day,
modifiers,
sprints,
style,
disabled,
isEnd,
...props
}: React.ComponentProps<typeof DayButton> & { sprints?: SprintRecord[]; isEnd?: boolean }) {
const defaultClassNames = getDefaultClassNames(); const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null); const ref = React.useRef<HTMLButtonElement>(null);
@@ -138,6 +152,16 @@ function CalendarDayButton({ className, day, modifiers, ...props }: React.Compon
if (modifiers.focused) ref.current?.focus(); if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]); }, [modifiers.focused]);
let isDisabled = false;
let sprint: SprintRecord | null = null;
for (const entry of sprints || []) {
if (day.date >= new Date(entry.startDate) && day.date <= new Date(entry.endDate)) {
isDisabled = true;
sprint = entry;
}
}
return ( return (
<Button <Button
ref={ref} ref={ref}
@@ -156,14 +180,34 @@ function CalendarDayButton({ className, day, modifiers, ...props }: React.Compon
className={cn( className={cn(
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal", "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal",
"[&>span]:text-xs [&>span]:opacity-70", "[&>span]:text-xs [&>span]:opacity-70",
"hover:bg-primary/90 hover:text-foreground", !sprint?.color && "hover:bg-primary/90 hover:text-foreground",
"data-[selected-single=true]:!bg-foreground data-[selected-single=true]:!text-background data-[selected-single=true]:hover:!bg-foreground/90", "data-[selected-single=true]:!bg-foreground data-[selected-single=true]:!text-background data-[selected-single=true]:hover:!bg-foreground/90",
"data-[range-start=true]:!bg-foreground data-[range-start=true]:!text-background", "data-[range-start=true]:!bg-foreground data-[range-start=true]:!text-background",
"data-[range-middle=true]:!bg-foreground data-[range-middle=true]:!text-background", "data-[range-middle=true]:!bg-foreground data-[range-middle=true]:!text-background",
"data-[range-end=true]:!bg-foreground data-[range-end=true]:!text-background", "data-[range-end=true]:!bg-foreground data-[range-end=true]:!text-background",
sprint?.color && "border-t border-b !border-(--sprint-color) !bg-(--sprint-color)/5",
defaultClassNames.day, defaultClassNames.day,
className, className,
)} )}
style={
{
...style,
"--sprint-color": sprint?.color ? sprint.color : null,
"border-left":
sprint && day.date.getUTCDate() === new Date(sprint.startDate).getUTCDate()
? `1px solid ${sprint?.color}`
: day.date.getDay() === 0 // sunday (left side)
? `1px dashed ${sprint?.color}`
: `0px`,
"border-right":
sprint && day.date.getUTCDate() === new Date(sprint.endDate).getUTCDate()
? `1px solid ${sprint?.color}`
: day.date.getDay() === 6 // saturday (right side)
? `1px dashed ${sprint?.color}`
: `0px`,
} as React.CSSProperties
}
disabled={isDisabled || disabled}
{...props} {...props}
/> />
); );