feat(board): whole-day due date classification and formatting
This commit is contained in:
parent
2949fdabb7
commit
193fe5bd86
2 changed files with 115 additions and 0 deletions
54
src/lib/board/dates.test.ts
Normal file
54
src/lib/board/dates.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { classifyDueDate, formatDueDate } from "@/lib/board/dates";
|
||||
|
||||
const today = new Date("2026-09-16T10:00:00Z");
|
||||
|
||||
describe("classifyDueDate", () => {
|
||||
it("returns none for an unset date", () => {
|
||||
expect(classifyDueDate("", today)).toBe("none");
|
||||
expect(classifyDueDate("2026-09-16 00:00:00.000Z", today)).toBe("today");
|
||||
});
|
||||
|
||||
it("returns overdue for a past date", () => {
|
||||
expect(classifyDueDate("2026-09-15", today)).toBe("overdue");
|
||||
});
|
||||
|
||||
it("returns today for the current date", () => {
|
||||
expect(classifyDueDate("2026-09-16", today)).toBe("today");
|
||||
});
|
||||
|
||||
it("returns soon for within three days", () => {
|
||||
expect(classifyDueDate("2026-09-17", today)).toBe("soon");
|
||||
expect(classifyDueDate("2026-09-19", today)).toBe("soon");
|
||||
});
|
||||
|
||||
it("returns later beyond three days", () => {
|
||||
expect(classifyDueDate("2026-09-20", today)).toBe("later");
|
||||
});
|
||||
|
||||
it("compares whole days, ignoring the time of day", () => {
|
||||
// A date due "today" must not flip to overdue at 00:01 simply because the
|
||||
// stored timestamp is midnight and the clock has moved past it.
|
||||
const lateInDay = new Date("2026-09-16T23:59:00Z");
|
||||
expect(classifyDueDate("2026-09-16 00:00:00.000Z", lateInDay)).toBe("today");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDueDate", () => {
|
||||
it("returns an empty string for an unset date", () => {
|
||||
expect(formatDueDate("", today)).toBe("");
|
||||
});
|
||||
|
||||
it("writes today and tomorrow in words", () => {
|
||||
expect(formatDueDate("2026-09-16", today)).toBe("Today");
|
||||
expect(formatDueDate("2026-09-17", today)).toBe("Tomorrow");
|
||||
});
|
||||
|
||||
it("writes a nearby date as a short day and month", () => {
|
||||
expect(formatDueDate("2026-09-24", today)).toBe("24 Sep");
|
||||
});
|
||||
|
||||
it("includes the year when the date is in another year", () => {
|
||||
expect(formatDueDate("2027-02-03", today)).toBe("3 Feb 2027");
|
||||
});
|
||||
});
|
||||
61
src/lib/board/dates.ts
Normal file
61
src/lib/board/dates.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* Target date classification and formatting.
|
||||
*
|
||||
* Comparisons are whole-day, in UTC. PocketBase stores a date field as
|
||||
* "2026-09-16 00:00:00.000Z", so a naive timestamp comparison marks anything
|
||||
* due today as overdue the moment the clock passes midnight — the board would
|
||||
* turn red every morning for cards that are perfectly on time.
|
||||
*/
|
||||
|
||||
export type DueClass = "none" | "overdue" | "today" | "soon" | "later";
|
||||
|
||||
/** Whole days between two dates, positive when `date` is in the future. */
|
||||
function dayDelta(date: string, now: Date): number {
|
||||
const target = Date.UTC(
|
||||
Number(date.slice(0, 4)),
|
||||
Number(date.slice(5, 7)) - 1,
|
||||
Number(date.slice(8, 10))
|
||||
);
|
||||
const reference = Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate()
|
||||
);
|
||||
return Math.round((target - reference) / 86_400_000);
|
||||
}
|
||||
|
||||
export function classifyDueDate(
|
||||
targetDate: string,
|
||||
now: Date = new Date()
|
||||
): DueClass {
|
||||
if (!targetDate) return "none";
|
||||
|
||||
const delta = dayDelta(targetDate, now);
|
||||
if (delta < 0) return "overdue";
|
||||
if (delta === 0) return "today";
|
||||
if (delta <= 3) return "soon";
|
||||
return "later";
|
||||
}
|
||||
|
||||
const MONTHS = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
];
|
||||
|
||||
export function formatDueDate(
|
||||
targetDate: string,
|
||||
now: Date = new Date()
|
||||
): string {
|
||||
if (!targetDate) return "";
|
||||
|
||||
const delta = dayDelta(targetDate, now);
|
||||
if (delta === 0) return "Today";
|
||||
if (delta === 1) return "Tomorrow";
|
||||
|
||||
const year = Number(targetDate.slice(0, 4));
|
||||
const month = MONTHS[Number(targetDate.slice(5, 7)) - 1];
|
||||
const day = Number(targetDate.slice(8, 10));
|
||||
|
||||
if (year !== now.getUTCFullYear()) return `${day} ${month} ${year}`;
|
||||
return `${day} ${month}`;
|
||||
}
|
||||
Loading…
Reference in a new issue