- 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 ReactKeyboardEvent, PointerEvent as ReactPointerEvent } from "react";
import { LABEL_STYLE, dueInfo, getLabel, getMember } from "../data";
import type { Card } from "../types";
import { Avatar, AvatarEmpty } from "./Avatar";
import { IconCalendar, IconChecklist, IconDots } from "../icons";
export interface CardItemProps {
card: Card;
columnId: string;
hidden: boolean;
kbdLifted: boolean;
menuOpen: boolean;
onOpen: () => void;
onKeyDown: (e: ReactKeyboardEvent<HTMLElement>, card: Card, columnId: string) => void;
onPointerDown: (e: ReactPointerEvent<HTMLElement>, cardId: string, columnId: string) => void;
onMenuButton: (btn: HTMLButtonElement, cardId: string) => void;
}
export function CardItem(props: CardItemProps) {
const { card, columnId, hidden, kbdLifted, menuOpen, onOpen, onKeyDown, onPointerDown, onMenuButton } = props;
const member = getMember(card.assignee);
const due = dueInfo(card, columnId);
const doneCount = card.checklist.filter((i) => i.done).length;
const classes = [
"card",
hidden ? "card--hidden" : "",
kbdLifted ? "card--kbd" : "",
menuOpen ? "card--menu-open" : "",
]
.filter(Boolean)
.join(" ");
const ariaParts = [
card.title,
card.labels.length > 0
? `Labels: ${card.labels.map((l) => getLabel(l)?.name ?? l).join(", ")}`
: null,
member ? `Assigned to ${member.name}` : "Unassigned",
due ? due.text : null,
card.checklist.length > 0 ? `${doneCount} of ${card.checklist.length} checklist items done` : null,
].filter(Boolean);
return (
<li className="cards__item">
<article
className={classes}
data-card-id={card.id}
data-priority={card.priority}
tabIndex={0}
aria-label={ariaParts.join(". ") + "."}
aria-describedby="card-kbd-hint"
draggable={false}
onDragStart={(e) => e.preventDefault()}
onClick={() => onOpen()}
onKeyDown={(e) => onKeyDown(e, card, columnId)}
onPointerDown={(e) => onPointerDown(e, card.id, columnId)}
>
{card.labels.length > 0 && (
<div className="card__labels">
{card.labels.map((lid) => {
const label = getLabel(lid);
if (!label) return null;
const style = LABEL_STYLE[lid];
return (
<span key={lid} className="tag" style={{ background: style?.bg, color: style?.fg }}>
{label.name}
</span>
);
})}
</div>
)}
<h3 className="card__title">{card.title}</h3>
<div className="card__meta">
{member ? (
<span className="meta-chip meta-chip--person">
<Avatar member={member} size={17} />
<span>{member.name.split(" ")[0]}</span>
</span>
) : (
<span className="meta-chip meta-chip--person meta-chip--unassigned">
<AvatarEmpty size={15} />
<span>Unassigned</span>
</span>
)}
{due && (
<span className={`meta-chip meta-chip--due meta-chip--${due.tone}`}>
{due.tone === "overdue" ? (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 4.2 3 19.3h18z" />
<path d="M12 9.8v4.2" />
<path d="M12 16.6v.4" />
</svg>
) : (
<IconCalendar width={12} height={12} />
)}
<span>{due.text}</span>
</span>
)}
{card.checklist.length > 0 && (
<span className="meta-chip meta-chip--list">
<IconChecklist width={12} height={12} />
<span>
{doneCount}/{card.checklist.length}
</span>
</span>
)}
</div>
<button
type="button"
className="card__menu icon-btn"
aria-label={`Card actions for “${card.title}”`}
aria-haspopup="menu"
aria-expanded={menuOpen || undefined}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onMenuButton(e.currentTarget, card.id);
}}
>
<IconDots width={15} height={15} />
</button>
</article>
</li>
);
}Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.