- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.1.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import type { BoardState, CardData, ChecklistItem, ColumnData, Priority } from "./types";
export interface Filters {
query: string;
assignee: string;
label: string;
}
export const EMPTY_FILTERS: Filters = { query: "", assignee: "all", label: "all" };
export const UNASSIGNED = "unassigned";
export function filtersActive(filters: Filters): boolean {
return filters.query.trim() !== "" || filters.assignee !== "all" || filters.label !== "all";
}
export function cardMatchesFilters(card: CardData, filters: Filters): boolean {
if (filters.assignee === UNASSIGNED) {
if (card.assignee !== null) return false;
} else if (filters.assignee !== "all" && card.assignee !== filters.assignee) {
return false;
}
if (filters.label !== "all" && !card.labels.includes(filters.label)) return false;
const query = filters.query.trim().toLowerCase();
if (query) {
const haystack = `${card.title}\n${card.description}`.toLowerCase();
if (!haystack.includes(query)) return false;
}
return true;
}
export function visibleCardIds(state: BoardState, filters: Filters, column: ColumnData): string[] {
if (!filtersActive(filters)) return column.card_ids;
return column.card_ids.filter((id) => {
const card = state.cards[id];
return card ? cardMatchesFilters(card, filters) : false;
});
}
export function findColumnOf(
state: BoardState,
cardId: string,
): { column: ColumnData; index: number } | null {
for (const column of state.columns) {
const index = column.card_ids.indexOf(cardId);
if (index !== -1) return { column, index };
}
return null;
}
export function getColumn(state: BoardState, columnId: string): ColumnData | null {
return state.columns.find((column) => column.id === columnId) ?? null;
}
export function isColumnFull(state: BoardState, columnId: string): boolean {
const column = getColumn(state, columnId);
if (!column) return false;
return column.wip_limit !== null && column.card_ids.length >= column.wip_limit;
}
export function isDoneColumn(column: ColumnData): boolean {
return column.id === "done" || column.title.trim().toLowerCase() === "done";
}
export function isCardOverdue(state: BoardState, card: CardData, columnId: string, today: string): boolean {
const column = getColumn(state, columnId);
if (!card.due || !column || isDoneColumn(column)) return false;
return card.due < today;
}
export function applyMove(
state: BoardState,
cardId: string,
toColumnId: string,
toIndex: number,
): BoardState {
const location = findColumnOf(state, cardId);
if (!location) return state;
const columns = state.columns.map((column) => {
if (column.id === location.column.id) {
return { ...column, card_ids: column.card_ids.filter((id) => id !== cardId) };
}
return column;
});
const target = columns.find((column) => column.id === toColumnId);
if (!target) return state;
const insertAt = Math.max(0, Math.min(toIndex, target.card_ids.length));
const nextIds = [...target.card_ids.slice(0, insertAt), cardId, ...target.card_ids.slice(insertAt)];
return {
...state,
columns: columns.map((column) =>
column.id === toColumnId ? { ...column, card_ids: nextIds } : column,
),
};
}
export function applyPatch(state: BoardState, cardId: string, patch: Partial<CardData>): BoardState {
const card = state.cards[cardId];
if (!card) return state;
return { ...state, cards: { ...state.cards, [cardId]: { ...card, ...patch } } };
}
export function insertIndexForVisibleDrop(
state: BoardState,
filters: Filters,
columnId: string,
visibleIndex: number,
draggedId: string,
): number {
const column = getColumn(state, columnId);
if (!column) return 0;
const others = column.card_ids.filter((id) => id !== draggedId);
if (!filtersActive(filters)) return Math.max(0, Math.min(visibleIndex, others.length));
const visible = others.filter((id) => {
const card = state.cards[id];
return card ? cardMatchesFilters(card, filters) : false;
});
if (visibleIndex >= visible.length) return others.length;
const anchor = visible[visibleIndex];
const index = others.indexOf(anchor);
return index === -1 ? others.length : index;
}
export function finalIndexAfterRemoval(state: BoardState, cardId: string, toColumnId: string, toIndex: number): number {
const location = findColumnOf(state, cardId);
if (!location) return 0;
const targetLength =
location.column.id === toColumnId
? location.column.card_ids.length - 1
: (getColumn(state, toColumnId)?.card_ids.length ?? 0);
return Math.max(0, Math.min(toIndex, targetLength));
}
export function formatDueShort(iso: string): string {
const parsed = parseIsoDate(iso);
if (!parsed) return iso;
return `${MONTHS_SHORT[parsed.getMonth()]} ${parsed.getDate()}`;
}
export function formatDueLong(iso: string): string {
const parsed = parseIsoDate(iso);
if (!parsed) return iso;
const weekday = WEEKDAYS[parsed.getDay()];
return `${weekday}, ${MONTHS_LONG[parsed.getMonth()]} ${parsed.getDate()}, ${parsed.getFullYear()}`;
}
const MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MONTHS_LONG = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
function parseIsoDate(iso: string): Date | null {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
if (!match) return null;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
return Number.isNaN(date.getTime()) ? null : date;
}
export function priorityLabel(priority: Priority): string {
return priority.charAt(0).toUpperCase() + priority.slice(1);
}
let idCounter = 0;
export function newCardId(): string {
idCounter += 1;
return `card-${Date.now().toString(36)}-${idCounter.toString(36)}`;
}
export function newChecklistId(): string {
idCounter += 1;
return `item-${Date.now().toString(36)}-${idCounter.toString(36)}`;
}
export function newChecklistItem(text: string): ChecklistItem {
return { id: newChecklistId(), text, done: false };
}
export function pluralize(count: number, singular: string, plural = `${singular}s`): string {
return count === 1 ? singular : plural;
}