- 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.
import type {
KeyboardEvent as RKeyboardEvent,
MouseEvent as RMouseEvent,
PointerEvent as RPointerEvent,
} from "react";
import {
PRIORITY_META,
checklistCounts,
formatDue,
isOverdue,
labelById,
memberById,
type Card,
} from "../model";
import { LABEL_STYLES } from "../model";
import { Avatar, IconCalendar, IconCheckSquare, IconFlag, IconMove } from "./Icons";
export type GhostInfo = { left: number; top: number; width: number } | null;
export type CardHandlers = {
onPointerDown: (e: RPointerEvent<HTMLDivElement>) => void;
onPointerMove: (e: RPointerEvent<HTMLDivElement>) => void;
onPointerUp: (e: RPointerEvent<HTMLDivElement>) => void;
onPointerCancel: (e: RPointerEvent<HTMLDivElement>) => void;
onClickCard: (e: RMouseEvent<HTMLDivElement>) => void;
onKeyDownCard: (e: RKeyboardEvent<HTMLDivElement>) => void;
onOpenMoveMenu: (e: RMouseEvent<HTMLButtonElement>) => void;
};
type Props = {
card: Card;
colId: string;
ghost: boolean;
ghostStyle: GhostInfo;
lifted: boolean;
handlers: CardHandlers;
};
export function CardView({ card, colId, ghost, ghostStyle, lifted, handlers }: Props) {
const member = memberById(card.assignee);
const overdue = isOverdue(card, colId);
const cl = checklistCounts(card);
const prio = PRIORITY_META[card.priority] ?? { label: card.priority, color: "#8A8F98" };
const parts: string[] = [`Card: ${card.title}`];
const labelNames = card.labels.map(l => labelById(l)?.name).filter(Boolean).join(", ");
if (labelNames) parts.push(`labels: ${labelNames}`);
parts.push(member ? `assigned to ${member.name}` : "unassigned");
if (card.due) parts.push(`due ${formatDue(card.due)}${overdue ? ", overdue" : ""}`);
if (cl.total > 0) parts.push(`checklist ${cl.done} of ${cl.total} done`);
parts.push(`${prio.label.toLowerCase()} priority`);
return (
<div
id={`card-${card.id}`}
data-card-id={card.id}
className={`card${ghost ? " ghost" : ""}${lifted ? " lifted" : ""}${overdue ? " overdue" : ""}`}
role="button"
tabIndex={0}
aria-label={parts.join(", ")}
style={ghost && ghostStyle ? { position: "fixed", left: ghostStyle.left, top: ghostStyle.top, width: ghostStyle.width } : undefined}
onPointerDown={handlers.onPointerDown}
onPointerMove={handlers.onPointerMove}
onPointerUp={handlers.onPointerUp}
onPointerCancel={handlers.onPointerCancel}
onClick={handlers.onClickCard}
onKeyDown={handlers.onKeyDownCard}
>
{card.labels.length > 0 && (
<div className="card-labels">
{card.labels.map(lid => {
const def = labelById(lid);
const st = LABEL_STYLES[lid];
if (!def || !st) return null;
return (
<span key={lid} className="label-chip" style={{ background: st.bg, color: st.fg, borderColor: st.bd }}>
{def.name}
</span>
);
})}
</div>
)}
<p className="card-title">{card.title}</p>
<div className="card-meta">
<span className="prio" style={{ color: prio.color }} title={`${prio.label} priority`}>
<IconFlag size={13} />
</span>
{card.due && (
<span className={`meta-chip due${overdue ? " overdue" : ""}`}>
<IconCalendar size={12} />
{formatDue(card.due)}
</span>
)}
{overdue && <span className="overdue-chip">Overdue</span>}
{cl.total > 0 && (
<span className="meta-chip" title={`Checklist: ${cl.done} of ${cl.total} done`}>
<IconCheckSquare size={12} />
{cl.done}/{cl.total}
</span>
)}
<span className="meta-spacer" />
<button
type="button"
className="move-btn"
data-nodrag
id={`movebtn-${card.id}`}
aria-haspopup="menu"
aria-label={`Move card “${card.title}” to another column`}
onClick={handlers.onOpenMoveMenu}
>
<IconMove size={12} />
Move
</button>
{member ? (
<Avatar member={member} />
) : (
<span className="unassigned">
<Avatar member={undefined} />
<span className="unassigned-text">Unassigned</span>
</span>
)}
</div>
</div>
);
}Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.