Skip to content

Commit 40744a8

Browse files
committed
feat(roadmap): update timeline to group projects by teams, and color style update
1 parent 3c84220 commit 40744a8

2 files changed

Lines changed: 114 additions & 30 deletions

File tree

src/components/RoadmapBoard.astro

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
---
2-
import type { ApiProject } from '../types/roadmap'
2+
import type { ApiProject, BoardTeam } from '../types/roadmap'
33
44
interface Props {
55
projects?: ApiProject[]
6+
teams?: BoardTeam[]
67
}
78
8-
const { projects = [] }: Props = Astro.props
9+
const { projects = [], teams = [] }: Props = Astro.props
910
1011
// ─── Icon resolution ─────────────────────────────────────────────────────────
1112
@@ -200,8 +201,66 @@ const nowYear = now.getUTCFullYear()
200201
const nowMonth = now.getUTCMonth()
201202
const nowLeft = pctLeft(now).toFixed(2)
202203
203-
// Project rows start at grid row 3 (rows 1–2 are the header)
204-
const PROJECT_ROW_OFFSET = 3
204+
// ─── Team grouping ───────────────────────────────────────────────────────────
205+
206+
const teamMap = new Map(teams.map((t) => [t.id, t]))
207+
208+
// Root teams: not referenced as a child by any other team
209+
const allChildIds = new Set(teams.flatMap((t) => t.childrenIds))
210+
const rootTeams = teams.filter((t) => !allChildIds.has(t.id))
211+
212+
// Index projects by their direct team ID
213+
const projectsByTeamId = new Map<string, ApiProject[]>()
214+
for (const proj of projects) {
215+
if (!proj.team) continue
216+
const tid = proj.team.id
217+
if (!projectsByTeamId.has(tid)) projectsByTeamId.set(tid, [])
218+
projectsByTeamId.get(tid)!.push(proj)
219+
}
220+
221+
// Collect all projects for a root team (own + children), sorted by sortOrder
222+
function collectTeamProjects(teamId: string): ApiProject[] {
223+
const team = teamMap.get(teamId)
224+
if (!team) return []
225+
const own = projectsByTeamId.get(teamId) ?? []
226+
const fromChildren = team.childrenIds.flatMap(
227+
(cid) => projectsByTeamId.get(cid) ?? []
228+
)
229+
return [...own, ...fromChildren].sort((a, b) => a.sortOrder - b.sortOrder)
230+
}
231+
232+
// Build flat grid item list with pre-assigned row numbers
233+
type GridItem =
234+
| { type: 'team-header'; team: BoardTeam; row: number; teamColor: string }
235+
| { type: 'project'; project: ApiProject; row: number; teamColor: string }
236+
237+
const gridItems: GridItem[] = []
238+
let currentRow = 3 // rows 1–2 are quarter/month headers
239+
240+
for (const rootTeam of rootTeams) {
241+
const teamProjects = collectTeamProjects(rootTeam.id)
242+
if (teamProjects.length === 0) continue
243+
const teamColor = rootTeam.color ?? '#888888'
244+
gridItems.push({ type: 'team-header', team: rootTeam, row: currentRow, teamColor })
245+
currentRow++
246+
for (const proj of teamProjects) {
247+
gridItems.push({ type: 'project', project: proj, row: currentRow, teamColor })
248+
currentRow++
249+
}
250+
}
251+
252+
// Append uncategorised projects (no team, or team not in the hierarchy)
253+
const assignedIds = new Set(
254+
gridItems.flatMap((i) => (i.type === 'project' ? [i.project.id] : []))
255+
)
256+
const uncategorised = projects
257+
.filter((p) => !assignedIds.has(p.id))
258+
.sort((a, b) => a.sortOrder - b.sortOrder)
259+
for (const proj of uncategorised) {
260+
const teamColor = proj.team?.color ?? '#888888'
261+
gridItems.push({ type: 'project', project: proj, row: currentRow, teamColor })
262+
currentRow++
263+
}
205264
---
206265

207266
<div class="board-outer">
@@ -261,12 +320,25 @@ const PROJECT_ROW_OFFSET = 3
261320
))
262321
}
263322

264-
<!-- ── rows 3+: One row per project ── -->
323+
<!-- ── rows 3+: Team headers and project rows ── -->
265324
{
266-
projects.map((proj, pi) => {
267-
const row = pi + PROJECT_ROW_OFFSET
325+
gridItems.map((item) => {
326+
if (item.type === 'team-header') {
327+
const { team, row, teamColor } = item
328+
return (
329+
<div
330+
class="team-header"
331+
style={`grid-column: 1 / -1; grid-row: ${row}; background: ${teamColor}cc; --team-color: ${teamColor}`}
332+
>
333+
<span class="team-color-dot" />
334+
{team.name}
335+
</div>
336+
)
337+
}
338+
339+
const { project: proj, row, teamColor } = item
268340
const projColor = proj.color ?? '#888'
269-
const tintRgb = hexToRgb(proj.color)
341+
const tintRgb = hexToRgb(teamColor)
270342
const iconEmoji = resolveIcon(proj.icon)
271343
const hasDates = !!(proj.startDate && proj.targetDate)
272344
const barLeft = hasDates
@@ -280,7 +352,7 @@ const PROJECT_ROW_OFFSET = 3
280352
: null
281353
const datedMilestones = proj.milestones.filter((ms) => ms.targetDate)
282354

283-
// Improvement 1: completed project with no targetDate but a completedAt
355+
// Completed project with no targetDate but a completedAt
284356
const hasCompletedBar = !!(
285357
proj.startDate &&
286358
!proj.targetDate &&
@@ -296,7 +368,7 @@ const PROJECT_ROW_OFFSET = 3
296368
).toFixed(2)
297369
: null
298370

299-
// Improvement 2: milestones that extend past the project's targetDate
371+
// Milestones that extend past the project's targetDate
300372
const lastMilestoneDate =
301373
datedMilestones.length > 0
302374
? new Date(
@@ -326,7 +398,7 @@ const PROJECT_ROW_OFFSET = 3
326398
{/* Project label — col 1, explicit row */}
327399
<div
328400
class="proj-label"
329-
style={`grid-column: 1; grid-row: ${row}; --tint-rgb: ${tintRgb}`}
401+
style={`grid-column: 1; grid-row: ${row}; background-color: ${teamColor}cc; --team-color: ${teamColor}`}
330402
>
331403
{proj.url ? (
332404
<a
@@ -358,7 +430,7 @@ const PROJECT_ROW_OFFSET = 3
358430
{/* Project timeline track — cols 2 to last, explicit row */}
359431
<div
360432
class="proj-track"
361-
style={`grid-column: 2 / -1; grid-row: ${row}; --tint-rgb: ${tintRgb}`}
433+
style={`grid-column: 2 / -1; grid-row: ${row}; background-color: ${teamColor}cc; --team-color: ${teamColor}`}
362434
>
363435
{/* "Today" marker */}
364436
<div class="now-line" />
@@ -376,7 +448,7 @@ const PROJECT_ROW_OFFSET = 3
376448
/>
377449
)}
378450

379-
{/* Improvement 2: milestone overflow extension beyond targetDate */}
451+
{/* Milestone overflow extension beyond targetDate */}
380452
{hasExtension && (
381453
<div
382454
class="proj-bar proj-bar--extension"
@@ -389,7 +461,7 @@ const PROJECT_ROW_OFFSET = 3
389461
/>
390462
)}
391463

392-
{/* Improvement 1: completed bar (no targetDate, resolved via completedAt) */}
464+
{/* Completed bar (no targetDate, resolved via completedAt) */}
393465
{hasCompletedBar && (
394466
<div
395467
class="proj-bar proj-bar--completed"
@@ -544,6 +616,29 @@ const PROJECT_ROW_OFFSET = 3
544616
letter-spacing: 0.05em;
545617
}
546618

619+
/* ── Team header row ── */
620+
.team-header {
621+
border-top: 2px solid var(--team-color);
622+
border-bottom: 1px solid var(--team-color);
623+
padding: 0.35rem 0.75rem;
624+
display: flex;
625+
align-items: center;
626+
gap: 0.5rem;
627+
font-size: var(--step--2);
628+
font-weight: 700;
629+
text-transform: uppercase;
630+
letter-spacing: 0.06em;
631+
color: #fff;
632+
}
633+
634+
.team-color-dot {
635+
width: 8px;
636+
height: 8px;
637+
border-radius: 50%;
638+
background: rgba(255, 255, 255, 0.6);
639+
flex-shrink: 0;
640+
}
641+
547642
/* ── Project label cell ── */
548643
.proj-label {
549644
position: sticky;
@@ -554,8 +649,7 @@ const PROJECT_ROW_OFFSET = 3
554649
rgb(var(--tint-rgb, 255 255 255)) 5%,
555650
white
556651
);
557-
border-right: 1px solid #e5e7eb;
558-
border-bottom: 1px solid #f0f0f0;
652+
border-right: 1px solid var(--team-color);
559653
padding: 0 0.75rem;
560654
display: flex;
561655
align-items: center;
@@ -598,20 +692,8 @@ const PROJECT_ROW_OFFSET = 3
598692
rgb(var(--tint-rgb, 255 255 255)) 4%,
599693
white
600694
);
601-
border-bottom: 1px solid #f0f0f0;
602695
min-height: 3.25rem;
603696
overflow: visible;
604-
/*
605-
Repeating vertical lines align with month column boundaries.
606-
One segment per month = 100% / n-months wide.
607-
*/
608-
background-image: repeating-linear-gradient(
609-
90deg,
610-
transparent 0,
611-
transparent calc(100% / var(--n-months) - 1px),
612-
#e5e7eb calc(100% / var(--n-months) - 1px),
613-
#e5e7eb calc(100% / var(--n-months))
614-
);
615697
}
616698

617699
/* "Today" vertical line */

src/pages/roadmap.astro

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
import RoadmapLayout from '../layouts/RoadmapLayout.astro'
33
import RoadmapBoard from '../components/RoadmapBoard.astro'
4-
import type { ApiProject } from '../types/roadmap'
4+
import type { ApiProject, BoardTeam } from '../types/roadmap'
55
66
let projects: ApiProject[] = []
7+
let teams: BoardTeam[] = []
78
let fetchError = false
89
910
try {
@@ -12,6 +13,7 @@ try {
1213
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
1314
const data = await res.json()
1415
projects = data.projects ?? []
16+
teams = data.teams ?? []
1517
} catch {
1618
fetchError = true
1719
}
@@ -56,7 +58,7 @@ try {
5658
<p>No roadmap data available yet. Check back soon!</p>
5759
</div>
5860
) : (
59-
<RoadmapBoard projects={projects} />
61+
<RoadmapBoard projects={projects} teams={teams} />
6062
)
6163
}
6264
</section>

0 commit comments

Comments
 (0)