feat(roadmap): Now/Next/Later view
This commit is contained in:
parent
6e231374e2
commit
bc8cc9f776
2 changed files with 121 additions and 0 deletions
66
src/components/roadmap/band-view.tsx
Normal file
66
src/components/roadmap/band-view.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { RoadmapCard } from "@/components/roadmap/roadmap-card";
|
||||
import type { Band } from "@/lib/roadmap/band";
|
||||
import type { RoadmapCard as Card } from "@/lib/roadmap/load";
|
||||
|
||||
const BANDS: Array<{ key: Band; title: string; blurb: string }> = [
|
||||
{ key: "now", title: "Now", blurb: "In flight, critical, or due within a week" },
|
||||
{ key: "next", title: "Next", blurb: "Queued up" },
|
||||
{ key: "later", title: "Later", blurb: "Not started" },
|
||||
];
|
||||
|
||||
export function BandView({
|
||||
cards,
|
||||
inboxCount,
|
||||
}: {
|
||||
cards: Card[];
|
||||
inboxCount: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{inboxCount > 0 && (
|
||||
// Counted, not placed: an unaccepted request is not committed work,
|
||||
// but it should not be invisible either.
|
||||
<p className="text-xs text-text-muted">
|
||||
<span className="tabular">{inboxCount}</span>{" "}
|
||||
{inboxCount === 1 ? "request is" : "requests are"} waiting in an Inbox.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid gap-5 lg:grid-cols-3">
|
||||
{BANDS.map((band) => {
|
||||
const inBand = cards.filter((card) => card.band === band.key);
|
||||
|
||||
return (
|
||||
<section key={band.key} className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-text-secondary">
|
||||
{band.title}
|
||||
</h2>
|
||||
<span className="tabular rounded-full bg-surface-raised px-1.5 py-0.5 text-[11px] text-text-muted">
|
||||
{inBand.length}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 text-[11px] text-text-muted">{band.blurb}</p>
|
||||
</div>
|
||||
|
||||
{inBand.length === 0 ? (
|
||||
<p className="rounded-[var(--radius-tile)] border border-dashed border-border-subtle p-6 text-center text-xs text-text-muted">
|
||||
Nothing here
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{inBand.map((card) => (
|
||||
<li key={card.id}>
|
||||
<RoadmapCard card={card} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/components/roadmap/roadmap-card.tsx
Normal file
55
src/components/roadmap/roadmap-card.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { Pill } from "@/components/ui/pill";
|
||||
import { classifyDueDate, formatDueDate } from "@/lib/board/dates";
|
||||
import type { RoadmapCard as Card } from "@/lib/roadmap/load";
|
||||
|
||||
const PRIORITY_TONE = {
|
||||
critical: "critical",
|
||||
high: "high",
|
||||
medium: "medium",
|
||||
low: "low",
|
||||
} as const;
|
||||
|
||||
export function RoadmapCard({ card }: { card: Card }) {
|
||||
const due = classifyDueDate(card.targetDate);
|
||||
const dueLabel = formatDueDate(card.targetDate);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={card.href}
|
||||
className="block rounded-[var(--radius-tile)] border border-border-subtle bg-surface p-3 transition-colors hover:border-border-strong"
|
||||
>
|
||||
<div className="flex items-start gap-2.5">
|
||||
{/* The client's own colour, so a cross-client page is scannable. */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="mt-1 h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
style={{ background: card.clientColour }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13px] font-medium leading-[1.45] text-text-primary">
|
||||
{card.title}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[11px] text-text-muted">
|
||||
{card.clientName} · {card.projectName}
|
||||
</p>
|
||||
</div>
|
||||
{card.percent !== null && (
|
||||
<span className="tabular shrink-0 text-[11px] text-text-muted">
|
||||
{card.percent}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
||||
<Pill tone={PRIORITY_TONE[card.priority]}>{card.priority}</Pill>
|
||||
{dueLabel && (
|
||||
<Pill tone={due === "overdue" ? "accent" : "neutral"}>
|
||||
<span className="tabular">{dueLabel}</span>
|
||||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue