Skip to content

Commit 06a81c3

Browse files
committed
feat: gannt view
1 parent 33e9379 commit 06a81c3

3 files changed

Lines changed: 83 additions & 125 deletions

File tree

src/components/Projects/GanttView.stories.tsx

Lines changed: 68 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@ const meta: Meta<typeof GanttView> = {
1414
export default meta
1515
type Story = StoryObj<typeof GanttView>
1616

17-
// Epics grouped by name (same name = same row) with different phases
17+
const dateFromOffset = (days: number): string => {
18+
const d = new Date()
19+
d.setDate(d.getDate() + days)
20+
const year = d.getFullYear()
21+
const month = String(d.getMonth() + 1).padStart(2, '0')
22+
const day = String(d.getDate()).padStart(2, '0')
23+
return `${year}-${month}-${day}`
24+
}
25+
26+
const createdAt = dateFromOffset(-60)
27+
28+
// Epics grouped by name (same name = same row) with different phases.
29+
// Spans 2 months back to 12 months forward, relative to today.
1830
const mockEpics = [
1931
{
2032
id: 1,
@@ -23,10 +35,10 @@ const mockEpics = [
2335
description: 'Implement secure user authentication system',
2436
confidence: 'high' as const,
2537
phase: 1,
26-
startDate: '2024-01-01',
27-
endDate: '2024-01-28',
28-
updatedAt: '2024-01-01',
29-
createdAt: '2024-01-01',
38+
startDate: dateFromOffset(-60),
39+
endDate: dateFromOffset(-20),
40+
updatedAt: createdAt,
41+
createdAt,
3042
},
3143
{
3244
id: 2,
@@ -35,10 +47,10 @@ const mockEpics = [
3547
description: 'OAuth integration and session management',
3648
confidence: 'high' as const,
3749
phase: 2,
38-
startDate: '2024-02-01',
39-
endDate: '2024-03-15',
40-
updatedAt: '2024-01-01',
41-
createdAt: '2024-01-01',
50+
startDate: dateFromOffset(-19),
51+
endDate: dateFromOffset(90),
52+
updatedAt: createdAt,
53+
createdAt,
4254
},
4355
{
4456
id: 3,
@@ -47,10 +59,10 @@ const mockEpics = [
4759
description: 'Build comprehensive dashboard functionality',
4860
confidence: 'medium' as const,
4961
phase: 1,
50-
startDate: '2024-01-15',
51-
endDate: '2024-02-28',
52-
updatedAt: '2024-01-01',
53-
createdAt: '2024-01-01',
62+
startDate: dateFromOffset(-30),
63+
endDate: dateFromOffset(60),
64+
updatedAt: createdAt,
65+
createdAt,
5466
},
5567
{
5668
id: 4,
@@ -59,10 +71,10 @@ const mockEpics = [
5971
description: 'Analytics widgets and reporting',
6072
confidence: 'medium' as const,
6173
phase: 2,
62-
startDate: '2024-03-01',
63-
endDate: '2024-04-15',
64-
updatedAt: '2024-01-01',
65-
createdAt: '2024-01-01',
74+
startDate: dateFromOffset(61),
75+
endDate: dateFromOffset(180),
76+
updatedAt: createdAt,
77+
createdAt,
6678
},
6779
{
6880
id: 5,
@@ -71,10 +83,10 @@ const mockEpics = [
7183
description: 'Optimize application for mobile devices',
7284
confidence: 'low' as const,
7385
phase: 1,
74-
startDate: '2024-02-15',
75-
endDate: '2024-04-01',
76-
updatedAt: '2024-01-01',
77-
createdAt: '2024-01-01',
86+
startDate: dateFromOffset(120),
87+
endDate: dateFromOffset(300),
88+
updatedAt: createdAt,
89+
createdAt,
7890
},
7991
{
8092
id: 6,
@@ -83,10 +95,10 @@ const mockEpics = [
8395
description: 'Enhance application performance and speed',
8496
confidence: 'high' as const,
8597
phase: 1,
86-
startDate: '2024-01-10',
87-
endDate: '2024-02-10',
88-
updatedAt: '2024-01-01',
89-
createdAt: '2024-01-01',
98+
startDate: dateFromOffset(30),
99+
endDate: dateFromOffset(150),
100+
updatedAt: createdAt,
101+
createdAt,
90102
},
91103
{
92104
id: 7,
@@ -95,85 +107,42 @@ const mockEpics = [
95107
description: 'Database query optimisation',
96108
confidence: 'high' as const,
97109
phase: 2,
98-
startDate: '2024-02-15',
99-
endDate: '2024-03-20',
100-
updatedAt: '2024-01-01',
101-
createdAt: '2024-01-01',
110+
startDate: dateFromOffset(180),
111+
endDate: dateFromOffset(365),
112+
updatedAt: createdAt,
113+
createdAt,
102114
},
103115
]
104116

105-
const mockSprints = [
106-
{
107-
id: 1,
108-
name: 'Sprint 1',
109-
description: 'Initial development',
110-
startDate: '2024-01-01',
111-
endDate: '2024-01-14',
112-
updatedAt: '2024-01-01',
113-
createdAt: '2024-01-01',
114-
},
115-
{
116-
id: 2,
117-
name: 'Sprint 2',
118-
description: 'Feature development',
119-
startDate: '2024-01-15',
120-
endDate: '2024-01-28',
121-
updatedAt: '2024-01-01',
122-
createdAt: '2024-01-01',
123-
},
124-
{
125-
id: 3,
126-
name: 'Sprint 3',
127-
description: 'Integration phase',
128-
startDate: '2024-01-29',
129-
endDate: '2024-02-11',
130-
updatedAt: '2024-01-01',
131-
createdAt: '2024-01-01',
132-
},
133-
{
134-
id: 4,
135-
name: 'Sprint 4',
136-
description: 'Polish and QA',
137-
startDate: '2024-02-12',
138-
endDate: '2024-02-25',
139-
updatedAt: '2024-01-01',
140-
createdAt: '2024-01-01',
141-
},
142-
{
143-
id: 5,
144-
name: 'Sprint 5',
145-
description: 'Release preparation',
146-
startDate: '2024-02-26',
147-
endDate: '2024-03-10',
148-
updatedAt: '2024-01-01',
149-
createdAt: '2024-01-01',
150-
},
151-
{
152-
id: 6,
153-
name: 'Sprint 6',
154-
description: 'Post-launch fixes',
155-
startDate: '2024-03-11',
156-
endDate: '2024-03-24',
157-
updatedAt: '2024-01-01',
158-
createdAt: '2024-01-01',
159-
},
160-
]
117+
// 2-week sprints covering the full 14-month window.
118+
const mockSprints = Array.from({ length: 30 }, (_, i) => {
119+
const startOffset = -60 + i * 14
120+
return {
121+
id: i + 1,
122+
name: `Sprint ${i + 1}`,
123+
description: `Sprint ${i + 1}`,
124+
startDate: dateFromOffset(startOffset),
125+
endDate: dateFromOffset(startOffset + 13),
126+
updatedAt: createdAt,
127+
createdAt,
128+
}
129+
})
161130

162131
const mockTasks = [
163-
{ id: 1, epic: 1, status: 'done' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
164-
{ id: 2, epic: 1, status: 'done' as const, index: 1, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
165-
{ id: 3, epic: 1, status: 'review' as const, index: 2, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
166-
{ id: 4, epic: 1, status: 'in-progress' as const, index: 3, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
167-
{ id: 5, epic: 2, status: 'done' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
168-
{ id: 6, epic: 2, status: 'todo' as const, index: 1, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
169-
{ id: 7, epic: 3, status: 'done' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
170-
{ id: 8, epic: 3, status: 'done' as const, index: 1, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
171-
{ id: 9, epic: 3, status: 'done' as const, index: 2, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
172-
{ id: 10, epic: 4, status: 'todo' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
173-
{ id: 11, epic: 5, status: 'in-progress' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
174-
{ id: 12, epic: 6, status: 'done' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
175-
{ id: 13, epic: 6, status: 'done' as const, index: 1, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
176-
{ id: 14, epic: 7, status: 'todo' as const, index: 0, dateLogged: '2024-01-01', updatedAt: '2024-01-01', createdAt: '2024-01-01' },
132+
{ id: 1, epic: 1, status: 'done' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
133+
{ id: 2, epic: 1, status: 'done' as const, index: 1, dateLogged: createdAt, updatedAt: createdAt, createdAt },
134+
{ id: 3, epic: 1, status: 'review' as const, index: 2, dateLogged: createdAt, updatedAt: createdAt, createdAt },
135+
{ id: 4, epic: 1, status: 'in-progress' as const, index: 3, dateLogged: createdAt, updatedAt: createdAt, createdAt },
136+
{ id: 5, epic: 2, status: 'done' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
137+
{ id: 6, epic: 2, status: 'todo' as const, index: 1, dateLogged: createdAt, updatedAt: createdAt, createdAt },
138+
{ id: 7, epic: 3, status: 'done' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
139+
{ id: 8, epic: 3, status: 'done' as const, index: 1, dateLogged: createdAt, updatedAt: createdAt, createdAt },
140+
{ id: 9, epic: 3, status: 'done' as const, index: 2, dateLogged: createdAt, updatedAt: createdAt, createdAt },
141+
{ id: 10, epic: 4, status: 'todo' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
142+
{ id: 11, epic: 5, status: 'in-progress' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
143+
{ id: 12, epic: 6, status: 'done' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
144+
{ id: 13, epic: 6, status: 'done' as const, index: 1, dateLogged: createdAt, updatedAt: createdAt, createdAt },
145+
{ id: 14, epic: 7, status: 'todo' as const, index: 0, dateLogged: createdAt, updatedAt: createdAt, createdAt },
177146
]
178147

179148
export const Default: Story = {

src/components/Projects/GanttView.tsx

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -274,23 +274,7 @@ export const GanttView: React.FC<GanttViewProps> = ({
274274
)
275275
})}
276276

277-
{/* Sprint vertical lines */}
278-
{realSprints.map((sprint) => {
279-
const left = differenceInDays(new Date(sprint.startDate), timelineStart) * dayWidth
280-
if (left < 0 || left > totalWidth) return null
281-
const active = isSprintActive(sprint)
282-
return (
283-
<div
284-
key={`line-${sprint.id}`}
285-
className={`absolute inset-y-0 pointer-events-none ${
286-
active ? 'border-l border-primary/40' : 'border-l border-dashed border-border/60'
287-
}`}
288-
style={{ left }}
289-
/>
290-
)
291-
})}
292-
293-
{/* Today line */}
277+
{/* Today line */}
294278
{todayLeft >= 0 && todayLeft <= totalWidth && (
295279
<div
296280
className="absolute inset-y-0 pointer-events-none z-10"
@@ -340,7 +324,7 @@ export const GanttView: React.FC<GanttViewProps> = ({
340324
return (
341325
<div
342326
key={epic.id}
343-
className="absolute rounded overflow-hidden cursor-pointer hover:ring-2 hover:ring-white/40 transition-all"
327+
className="absolute rounded overflow-hidden cursor-pointer ring-1 ring-inset ring-foreground/30 hover:ring-2 hover:ring-white/40 transition-all"
344328
style={{ left, width, height: 32, top: 8 }}
345329
title={tooltip}
346330
>
@@ -392,14 +376,16 @@ export const GanttView: React.FC<GanttViewProps> = ({
392376
const inner = (
393377
<div className={`flex flex-col ${isFullscreen ? 'h-screen' : 'h-full'} overflow-hidden`}>
394378
{/* Hero */}
395-
<div ref={heroRef} className="flex-shrink-0">
396-
<DashboardHero
397-
title="Gantt Chart"
398-
description="Epic timeline with sprint alignment and phase-based grouping."
399-
gradient="bg-gradient-to-r from-indigo-600 via-blue-600 to-cyan-600"
400-
badge={`${sortedKeys.length} epic${sortedKeys.length !== 1 ? 's' : ''}`}
401-
/>
402-
</div>
379+
{!isFullscreen && (
380+
<div ref={heroRef} className="flex-shrink-0">
381+
<DashboardHero
382+
title="Gantt Chart"
383+
description="Epic timeline with sprint alignment and phase-based grouping."
384+
gradient="bg-gradient-to-r from-indigo-600 via-blue-600 to-cyan-600"
385+
badge={`${sortedKeys.length} epic${sortedKeys.length !== 1 ? 's' : ''}`}
386+
/>
387+
</div>
388+
)}
403389

404390
{/* Toolbar */}
405391
<div className="flex items-center gap-2 px-4 py-2 border-b bg-background flex-shrink-0">

src/globals.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
src: url('fonts/Manrope-Regular.ttf') format('ttf');
1111
}
1212

13+
1314

1415
:root {
1516
--background: 20 14.3% 4.1%;
@@ -55,6 +56,8 @@
5556
--sidebar-accent-foreground: 240 4.8% 95.9%;
5657
--sidebar-border: 240 3.7% 15.9%;
5758
--sidebar-ring: 217.2 91.2% 59.8%;
59+
60+
--app-header-height: 0rem;
5861
}
5962
.light {
6063
--background: 0 0% 100%;

0 commit comments

Comments
 (0)