prjct-cli 0.13.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- package/templates/commands/workflow.md +0 -12
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Rocket, CheckCircle2, Bug, Activity } from 'lucide-react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { formatDateRange } from '@/lib/generate-week-report'
|
|
6
|
+
import type { WeekData } from '@/lib/generate-week-report'
|
|
7
|
+
|
|
8
|
+
interface ReportPreviewCardProps {
|
|
9
|
+
weekData: WeekData[]
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ReportPreviewCard({ weekData, className }: ReportPreviewCardProps) {
|
|
14
|
+
if (weekData.length === 0) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('rounded-xl border bg-card p-6', className)}>
|
|
17
|
+
<p className="text-muted-foreground text-center">Select a week to preview</p>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Aggregate data from all selected weeks
|
|
23
|
+
const allShipped = weekData.flatMap(w => w.shipped)
|
|
24
|
+
const uniqueShipsMap = new Map<string, typeof allShipped[0]>()
|
|
25
|
+
for (const ship of allShipped) {
|
|
26
|
+
if (!uniqueShipsMap.has(ship.name)) {
|
|
27
|
+
uniqueShipsMap.set(ship.name, ship)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Sort by date descending (most recent first)
|
|
31
|
+
const uniqueShips = Array.from(uniqueShipsMap.values())
|
|
32
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
33
|
+
|
|
34
|
+
// Group ships by date for display
|
|
35
|
+
const shipsByDate = new Map<string, typeof uniqueShips>()
|
|
36
|
+
for (const ship of uniqueShips) {
|
|
37
|
+
const dateKey = ship.date
|
|
38
|
+
if (!shipsByDate.has(dateKey)) {
|
|
39
|
+
shipsByDate.set(dateKey, [])
|
|
40
|
+
}
|
|
41
|
+
shipsByDate.get(dateKey)!.push(ship)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const totalTasks = weekData.reduce((sum, w) => sum + w.tasksCompleted, 0)
|
|
45
|
+
const totalBugs = weekData.reduce((sum, w) => sum + w.bugsFixed, 0)
|
|
46
|
+
const totalSyncs = weekData.reduce((sum, w) => sum + w.syncs, 0)
|
|
47
|
+
const totalDays = weekData.reduce((sum, w) => sum + w.activeDays, 0)
|
|
48
|
+
|
|
49
|
+
// Date range header
|
|
50
|
+
const firstWeek = weekData[0]
|
|
51
|
+
const lastWeek = weekData[weekData.length - 1]
|
|
52
|
+
const dateRangeStr = weekData.length === 1
|
|
53
|
+
? `Week ${firstWeek.week} · ${formatDateRange(firstWeek.startDate, firstWeek.endDate)}`
|
|
54
|
+
: `Weeks ${firstWeek.week}-${lastWeek.week} · ${formatDateRange(firstWeek.startDate, lastWeek.endDate)}`
|
|
55
|
+
|
|
56
|
+
const hasNoData = uniqueShips.length === 0 && totalTasks === 0 && totalBugs === 0 && totalDays === 0
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className={cn('rounded-xl border bg-card overflow-hidden', className)}>
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<div className="px-6 py-4 border-b bg-muted/30">
|
|
62
|
+
<h3 className="font-semibold text-lg">{dateRangeStr}</h3>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{hasNoData ? (
|
|
66
|
+
<div className="p-6">
|
|
67
|
+
<p className="text-muted-foreground text-center">No activity this week</p>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="p-6 space-y-6">
|
|
71
|
+
{/* Stats row */}
|
|
72
|
+
<div className="grid grid-cols-4 gap-4">
|
|
73
|
+
<StatCard
|
|
74
|
+
icon={Rocket}
|
|
75
|
+
value={uniqueShips.length}
|
|
76
|
+
label="shipped"
|
|
77
|
+
color="text-foreground"
|
|
78
|
+
bgColor="bg-muted"
|
|
79
|
+
/>
|
|
80
|
+
<StatCard
|
|
81
|
+
icon={CheckCircle2}
|
|
82
|
+
value={totalTasks}
|
|
83
|
+
label="tasks"
|
|
84
|
+
color="text-foreground"
|
|
85
|
+
bgColor="bg-muted"
|
|
86
|
+
/>
|
|
87
|
+
<StatCard
|
|
88
|
+
icon={Bug}
|
|
89
|
+
value={totalBugs}
|
|
90
|
+
label="bugs"
|
|
91
|
+
color="text-foreground"
|
|
92
|
+
bgColor="bg-muted"
|
|
93
|
+
/>
|
|
94
|
+
<StatCard
|
|
95
|
+
icon={Activity}
|
|
96
|
+
value={totalDays}
|
|
97
|
+
label="days"
|
|
98
|
+
color="text-foreground"
|
|
99
|
+
bgColor="bg-muted"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Shipped list grouped by date */}
|
|
104
|
+
{uniqueShips.length > 0 && (
|
|
105
|
+
<div>
|
|
106
|
+
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
|
107
|
+
Shipped
|
|
108
|
+
</h4>
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
{Array.from(shipsByDate.entries()).map(([date, ships]) => (
|
|
111
|
+
<div key={date}>
|
|
112
|
+
<p className="text-xs text-muted-foreground mb-2">
|
|
113
|
+
{new Date(date).toLocaleDateString('es-MX', {
|
|
114
|
+
weekday: 'short',
|
|
115
|
+
month: 'short',
|
|
116
|
+
day: 'numeric'
|
|
117
|
+
})}
|
|
118
|
+
</p>
|
|
119
|
+
<ul className="space-y-1.5 pl-2 border-l-2 border-muted-foreground/30">
|
|
120
|
+
{ships.map((ship, i) => (
|
|
121
|
+
<li key={i} className="flex items-start gap-2 pl-2">
|
|
122
|
+
<span className="flex-1">
|
|
123
|
+
{ship.name}
|
|
124
|
+
{ship.version && (
|
|
125
|
+
<span className="text-muted-foreground ml-1 text-sm">({ship.version})</span>
|
|
126
|
+
)}
|
|
127
|
+
</span>
|
|
128
|
+
</li>
|
|
129
|
+
))}
|
|
130
|
+
</ul>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Activity summary */}
|
|
138
|
+
{(totalTasks > 0 || totalBugs > 0 || totalSyncs > 0) && (
|
|
139
|
+
<div>
|
|
140
|
+
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
|
141
|
+
Activity
|
|
142
|
+
</h4>
|
|
143
|
+
<ul className="space-y-1 text-sm text-muted-foreground">
|
|
144
|
+
{totalTasks > 0 && (
|
|
145
|
+
<li className="flex items-center gap-2">
|
|
146
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-muted-foreground" />
|
|
147
|
+
{totalTasks} task{totalTasks !== 1 ? 's' : ''} completed
|
|
148
|
+
</li>
|
|
149
|
+
)}
|
|
150
|
+
{totalBugs > 0 && (
|
|
151
|
+
<li className="flex items-center gap-2">
|
|
152
|
+
<Bug className="h-3.5 w-3.5 text-muted-foreground" />
|
|
153
|
+
{totalBugs} bug{totalBugs !== 1 ? 's' : ''} fixed
|
|
154
|
+
</li>
|
|
155
|
+
)}
|
|
156
|
+
{totalSyncs > 0 && (
|
|
157
|
+
<li className="flex items-center gap-2">
|
|
158
|
+
<Activity className="h-3.5 w-3.5 text-muted-foreground" />
|
|
159
|
+
{totalSyncs} sync{totalSyncs !== 1 ? 's' : ''}
|
|
160
|
+
</li>
|
|
161
|
+
)}
|
|
162
|
+
</ul>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface StatCardProps {
|
|
172
|
+
icon: React.ElementType
|
|
173
|
+
value: number
|
|
174
|
+
label: string
|
|
175
|
+
color: string
|
|
176
|
+
bgColor: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function StatCard({ icon: Icon, value, label, color, bgColor }: StatCardProps) {
|
|
180
|
+
return (
|
|
181
|
+
<div className={cn('rounded-xl p-4 text-center', bgColor)}>
|
|
182
|
+
<Icon className={cn('h-5 w-5 mx-auto mb-1', color)} />
|
|
183
|
+
<div className="text-2xl font-bold tabular-nums">{value}</div>
|
|
184
|
+
<div className="text-xs text-muted-foreground">{label}</div>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useState } from 'react'
|
|
4
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { getCurrentYearWeek, getWeekDateRange, formatDateRange } from '@/lib/generate-week-report'
|
|
7
|
+
|
|
8
|
+
export type ActivityLevel = 'none' | 'low' | 'medium' | 'high'
|
|
9
|
+
|
|
10
|
+
interface WeekCalendarProps {
|
|
11
|
+
year: number
|
|
12
|
+
selectedWeeks: number[]
|
|
13
|
+
activityLevels: Map<number, ActivityLevel>
|
|
14
|
+
onWeekSelect: (weeks: number[]) => void
|
|
15
|
+
onYearChange: (year: number) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DAYS = ['M', 'T', 'W', 'T', 'F'] // Mon-Fri
|
|
19
|
+
|
|
20
|
+
export function WeekCalendar({
|
|
21
|
+
year,
|
|
22
|
+
selectedWeeks,
|
|
23
|
+
activityLevels,
|
|
24
|
+
onWeekSelect,
|
|
25
|
+
onYearChange,
|
|
26
|
+
}: WeekCalendarProps) {
|
|
27
|
+
const { year: currentYear, week: currentWeek } = getCurrentYearWeek()
|
|
28
|
+
const isCurrentYear = year === currentYear
|
|
29
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
30
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
31
|
+
const [canScrollRight, setCanScrollRight] = useState(true)
|
|
32
|
+
|
|
33
|
+
// Generate 52 weeks
|
|
34
|
+
const weeks = Array.from({ length: 52 }, (_, i) => i + 1)
|
|
35
|
+
|
|
36
|
+
// Drag to scroll state
|
|
37
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
38
|
+
const [startX, setStartX] = useState(0)
|
|
39
|
+
const [scrollLeftStart, setScrollLeftStart] = useState(0)
|
|
40
|
+
const [hasMoved, setHasMoved] = useState(false)
|
|
41
|
+
|
|
42
|
+
// Handle mouse down - start potential drag
|
|
43
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
44
|
+
if (!scrollRef.current) return
|
|
45
|
+
setIsDragging(true)
|
|
46
|
+
setStartX(e.pageX)
|
|
47
|
+
setScrollLeftStart(scrollRef.current.scrollLeft)
|
|
48
|
+
setHasMoved(false)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle mouse move - scroll if dragging
|
|
52
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
53
|
+
if (!isDragging || !scrollRef.current) return
|
|
54
|
+
|
|
55
|
+
const diff = e.pageX - startX
|
|
56
|
+
// Only count as drag if moved more than 5px
|
|
57
|
+
if (Math.abs(diff) > 5) {
|
|
58
|
+
setHasMoved(true)
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
scrollRef.current.scrollLeft = scrollLeftStart - diff
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle mouse up - end drag
|
|
65
|
+
const handleMouseUp = () => {
|
|
66
|
+
setIsDragging(false)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle week click (only if not dragging)
|
|
70
|
+
const handleWeekClick = (week: number, e: React.MouseEvent) => {
|
|
71
|
+
// Ignore click if we were dragging
|
|
72
|
+
if (hasMoved) {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isFuture = isCurrentYear && week > currentWeek
|
|
78
|
+
if (isFuture) return
|
|
79
|
+
|
|
80
|
+
if (selectedWeeks.includes(week)) {
|
|
81
|
+
onWeekSelect(selectedWeeks.filter(w => w !== week))
|
|
82
|
+
} else {
|
|
83
|
+
onWeekSelect([...selectedWeeks, week].sort((a, b) => a - b))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check scroll state
|
|
88
|
+
const updateScrollState = () => {
|
|
89
|
+
if (scrollRef.current) {
|
|
90
|
+
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
|
|
91
|
+
setCanScrollLeft(scrollLeft > 0)
|
|
92
|
+
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
updateScrollState()
|
|
98
|
+
// Scroll to current week on mount
|
|
99
|
+
if (scrollRef.current && isCurrentYear) {
|
|
100
|
+
const weekElement = scrollRef.current.querySelector(`[data-week="${currentWeek}"]`)
|
|
101
|
+
if (weekElement) {
|
|
102
|
+
weekElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, [currentWeek, isCurrentYear])
|
|
106
|
+
|
|
107
|
+
const scroll = (direction: 'left' | 'right') => {
|
|
108
|
+
if (scrollRef.current) {
|
|
109
|
+
const scrollAmount = 400
|
|
110
|
+
scrollRef.current.scrollBy({
|
|
111
|
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
|
112
|
+
behavior: 'smooth'
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="space-y-6">
|
|
119
|
+
{/* Year header */}
|
|
120
|
+
<div className="flex items-center justify-between">
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => onYearChange(year - 1)}
|
|
124
|
+
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
125
|
+
>
|
|
126
|
+
<ChevronLeft className="h-4 w-4" />
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
<span className="text-4xl font-bold tabular-nums">{year}</span>
|
|
130
|
+
|
|
131
|
+
<button
|
|
132
|
+
onClick={() => onYearChange(year + 1)}
|
|
133
|
+
disabled={year >= currentYear}
|
|
134
|
+
className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
135
|
+
>
|
|
136
|
+
<ChevronRight className="h-4 w-4" />
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<span className="text-sm text-muted-foreground">
|
|
141
|
+
{selectedWeeks.length === 0
|
|
142
|
+
? 'Select weeks'
|
|
143
|
+
: selectedWeeks.length === 1
|
|
144
|
+
? `W${selectedWeeks[0]}`
|
|
145
|
+
: `${selectedWeeks.length} wks`
|
|
146
|
+
}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Horizontal week strip */}
|
|
151
|
+
<div className="relative -mx-2">
|
|
152
|
+
{/* Scroll buttons */}
|
|
153
|
+
{canScrollLeft && (
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => scroll('left')}
|
|
156
|
+
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 p-2 bg-background/80 backdrop-blur rounded-full shadow-lg border hover:bg-muted transition-colors"
|
|
157
|
+
>
|
|
158
|
+
<ChevronLeft className="h-4 w-4" />
|
|
159
|
+
</button>
|
|
160
|
+
)}
|
|
161
|
+
{canScrollRight && (
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => scroll('right')}
|
|
164
|
+
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 p-2 bg-background/80 backdrop-blur rounded-full shadow-lg border hover:bg-muted transition-colors"
|
|
165
|
+
>
|
|
166
|
+
<ChevronRight className="h-4 w-4" />
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Scrollable week cards - drag to scroll */}
|
|
171
|
+
<div
|
|
172
|
+
ref={scrollRef}
|
|
173
|
+
onScroll={updateScrollState}
|
|
174
|
+
onMouseDown={handleMouseDown}
|
|
175
|
+
onMouseMove={handleMouseMove}
|
|
176
|
+
onMouseUp={handleMouseUp}
|
|
177
|
+
onMouseLeave={handleMouseUp}
|
|
178
|
+
className={cn(
|
|
179
|
+
'flex gap-3 overflow-x-auto scrollbar-hide py-2 px-2',
|
|
180
|
+
isDragging && hasMoved ? 'cursor-grabbing' : 'cursor-grab'
|
|
181
|
+
)}
|
|
182
|
+
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
|
183
|
+
>
|
|
184
|
+
{weeks.map((week) => {
|
|
185
|
+
const isSelected = selectedWeeks.includes(week)
|
|
186
|
+
const isCurrent = isCurrentYear && week === currentWeek
|
|
187
|
+
const activityLevel = activityLevels.get(week) ?? 'none'
|
|
188
|
+
const isFuture = isCurrentYear && week > currentWeek
|
|
189
|
+
const { start, end } = getWeekDateRange(year, week)
|
|
190
|
+
|
|
191
|
+
// Get month name
|
|
192
|
+
const monthName = start.toLocaleDateString('en-US', { month: 'short' })
|
|
193
|
+
const dayStart = start.getDate()
|
|
194
|
+
const dayEnd = end.getDate()
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
key={week}
|
|
199
|
+
data-week={week}
|
|
200
|
+
onClick={(e) => handleWeekClick(week, e)}
|
|
201
|
+
className={cn(
|
|
202
|
+
'flex-shrink-0 w-28 rounded-2xl transition-all duration-200 select-none',
|
|
203
|
+
'flex flex-col overflow-hidden',
|
|
204
|
+
'border',
|
|
205
|
+
isFuture && 'opacity-30',
|
|
206
|
+
// Selected state
|
|
207
|
+
isSelected
|
|
208
|
+
? 'bg-foreground text-background border-foreground shadow-lg scale-105'
|
|
209
|
+
: 'bg-card hover:bg-muted border-border',
|
|
210
|
+
isCurrent && !isSelected && 'ring-2 ring-primary ring-offset-2 ring-offset-background',
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{/* Week header */}
|
|
214
|
+
<div className={cn(
|
|
215
|
+
'px-3 py-2 text-left border-b',
|
|
216
|
+
isSelected ? 'border-background/20' : 'border-border'
|
|
217
|
+
)}>
|
|
218
|
+
<div className="flex items-baseline gap-0.5">
|
|
219
|
+
<span className={cn(
|
|
220
|
+
'text-xs font-medium',
|
|
221
|
+
isSelected ? 'text-background/60' : 'text-muted-foreground'
|
|
222
|
+
)}>
|
|
223
|
+
W
|
|
224
|
+
</span>
|
|
225
|
+
<span className="text-2xl font-bold tabular-nums">{week}</span>
|
|
226
|
+
</div>
|
|
227
|
+
<p className={cn(
|
|
228
|
+
'text-xs mt-0.5',
|
|
229
|
+
isSelected ? 'text-background/70' : 'text-muted-foreground'
|
|
230
|
+
)}>
|
|
231
|
+
{monthName} {dayStart}-{dayEnd}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Days grid - Mon to Fri with GitHub-style activity levels */}
|
|
236
|
+
<div className="px-3 py-2">
|
|
237
|
+
<div className="flex justify-between gap-1">
|
|
238
|
+
{DAYS.map((day, i) => {
|
|
239
|
+
// Generate activity level per day based on week activity
|
|
240
|
+
// This simulates varying activity levels across the week
|
|
241
|
+
let dayActivityLevel: 0 | 1 | 2 | 3 | 4 = 0
|
|
242
|
+
|
|
243
|
+
if (activityLevel === 'high') {
|
|
244
|
+
// High activity: most days are 3-4
|
|
245
|
+
dayActivityLevel = [4, 3, 4, 3, 2][i] as 0 | 1 | 2 | 3 | 4
|
|
246
|
+
} else if (activityLevel === 'medium') {
|
|
247
|
+
// Medium: mix of 2-3
|
|
248
|
+
dayActivityLevel = [2, 3, 2, 1, 2][i] as 0 | 1 | 2 | 3 | 4
|
|
249
|
+
} else if (activityLevel === 'low') {
|
|
250
|
+
// Low: mostly 0-1
|
|
251
|
+
dayActivityLevel = [1, 0, 1, 0, 0][i] as 0 | 1 | 2 | 3 | 4
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// GitHub-style green gradients
|
|
255
|
+
const activityColors = {
|
|
256
|
+
0: isSelected ? 'bg-background/10' : 'bg-muted',
|
|
257
|
+
1: isSelected ? 'bg-background/30' : 'bg-emerald-500/30',
|
|
258
|
+
2: isSelected ? 'bg-background/50' : 'bg-emerald-500/50',
|
|
259
|
+
3: isSelected ? 'bg-background/75' : 'bg-emerald-500/75',
|
|
260
|
+
4: isSelected ? 'bg-background' : 'bg-emerald-500',
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div key={i} className="flex flex-col items-center gap-1">
|
|
265
|
+
<span className={cn(
|
|
266
|
+
'text-[10px] font-medium',
|
|
267
|
+
isSelected ? 'text-background/50' : 'text-muted-foreground'
|
|
268
|
+
)}>
|
|
269
|
+
{day}
|
|
270
|
+
</span>
|
|
271
|
+
<div className={cn(
|
|
272
|
+
'w-3 h-3 rounded-sm',
|
|
273
|
+
activityColors[dayActivityLevel]
|
|
274
|
+
)} />
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
})}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
})}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { useParams } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { RotateCcw, Copy, Check, Printer } from 'lucide-react'
|
|
7
|
+
import { WeekCalendar, type ActivityLevel } from './WeekCalendar'
|
|
8
|
+
import { ReportPreviewCard } from './ReportPreviewCard'
|
|
9
|
+
import {
|
|
10
|
+
getCurrentYearWeek,
|
|
11
|
+
filterDataByWeek,
|
|
12
|
+
generateReportText,
|
|
13
|
+
getWeekActivityLevel,
|
|
14
|
+
} from '@/lib/generate-week-report'
|
|
15
|
+
import type { StatsResult } from '@/lib/services/stats.server'
|
|
16
|
+
import { cn } from '@/lib/utils'
|
|
17
|
+
|
|
18
|
+
interface WeeklyReportsProps {
|
|
19
|
+
stats: StatsResult
|
|
20
|
+
projectName: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function WeeklyReports({ stats, projectName }: WeeklyReportsProps) {
|
|
24
|
+
const params = useParams()
|
|
25
|
+
const projectId = params.id as string
|
|
26
|
+
const { year: currentYear, week: currentWeek } = getCurrentYearWeek()
|
|
27
|
+
|
|
28
|
+
const [year, setYear] = useState(currentYear)
|
|
29
|
+
const [selectedWeeks, setSelectedWeeks] = useState<number[]>([currentWeek])
|
|
30
|
+
const [copied, setCopied] = useState(false)
|
|
31
|
+
|
|
32
|
+
// Build print URL with selected weeks
|
|
33
|
+
const printUrl = `/project/${projectId}/reports/print?weeks=${selectedWeeks.join(',')}&year=${year}`
|
|
34
|
+
|
|
35
|
+
const activityLevels = useMemo(() => {
|
|
36
|
+
const levels = new Map<number, ActivityLevel>()
|
|
37
|
+
for (let w = 1; w <= 52; w++) {
|
|
38
|
+
levels.set(w, getWeekActivityLevel(stats, year, w))
|
|
39
|
+
}
|
|
40
|
+
return levels
|
|
41
|
+
}, [stats, year])
|
|
42
|
+
|
|
43
|
+
const handleReset = () => {
|
|
44
|
+
setYear(currentYear)
|
|
45
|
+
setSelectedWeeks([currentWeek])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const weekDataList = useMemo(() => {
|
|
49
|
+
return selectedWeeks.map(w => filterDataByWeek(stats, year, w))
|
|
50
|
+
}, [stats, year, selectedWeeks])
|
|
51
|
+
|
|
52
|
+
const reportText = useMemo(() => {
|
|
53
|
+
if (weekDataList.length === 0) return ''
|
|
54
|
+
return generateReportText(weekDataList, projectName)
|
|
55
|
+
}, [weekDataList, projectName])
|
|
56
|
+
|
|
57
|
+
const handleCopy = async () => {
|
|
58
|
+
try {
|
|
59
|
+
await navigator.clipboard.writeText(reportText)
|
|
60
|
+
setCopied(true)
|
|
61
|
+
setTimeout(() => setCopied(false), 2000)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('Failed to copy:', err)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-6">
|
|
69
|
+
{/* Calendar */}
|
|
70
|
+
<div className="relative overflow-hidden rounded-xl border bg-card p-6">
|
|
71
|
+
<WeekCalendar
|
|
72
|
+
year={year}
|
|
73
|
+
selectedWeeks={selectedWeeks}
|
|
74
|
+
activityLevels={activityLevels}
|
|
75
|
+
onWeekSelect={setSelectedWeeks}
|
|
76
|
+
onYearChange={setYear}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Actions */}
|
|
80
|
+
<div className="flex items-center justify-between mt-6 pt-4 border-t">
|
|
81
|
+
<button
|
|
82
|
+
onClick={handleReset}
|
|
83
|
+
className="inline-flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
|
84
|
+
>
|
|
85
|
+
<RotateCcw className="h-4 w-4" />
|
|
86
|
+
Reset
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<Link
|
|
91
|
+
href={printUrl}
|
|
92
|
+
target="_blank"
|
|
93
|
+
className={cn(
|
|
94
|
+
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all',
|
|
95
|
+
'border border-foreground/20 text-foreground hover:bg-muted',
|
|
96
|
+
selectedWeeks.length === 0 && 'opacity-50 pointer-events-none'
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<Printer className="h-4 w-4" />
|
|
100
|
+
Print / PDF
|
|
101
|
+
</Link>
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
onClick={handleCopy}
|
|
105
|
+
disabled={selectedWeeks.length === 0}
|
|
106
|
+
className={cn(
|
|
107
|
+
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all',
|
|
108
|
+
copied
|
|
109
|
+
? 'bg-emerald-500 text-white'
|
|
110
|
+
: 'bg-foreground text-background hover:bg-foreground/90',
|
|
111
|
+
selectedWeeks.length === 0 && 'opacity-50 cursor-not-allowed'
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{copied ? (
|
|
115
|
+
<>
|
|
116
|
+
<Check className="h-4 w-4" />
|
|
117
|
+
Copied!
|
|
118
|
+
</>
|
|
119
|
+
) : (
|
|
120
|
+
<>
|
|
121
|
+
<Copy className="h-4 w-4" />
|
|
122
|
+
Copy
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Report Preview Card */}
|
|
131
|
+
<ReportPreviewCard weekData={weekDataList} />
|
|
132
|
+
|
|
133
|
+
{/* Copy Text Preview (collapsed) */}
|
|
134
|
+
{selectedWeeks.length > 0 && reportText && (
|
|
135
|
+
<details className="group">
|
|
136
|
+
<summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground flex items-center gap-2">
|
|
137
|
+
<span className="group-open:rotate-90 transition-transform">▶</span>
|
|
138
|
+
View copy text
|
|
139
|
+
</summary>
|
|
140
|
+
<div className="mt-3 relative overflow-hidden rounded-xl border bg-muted/30 p-4">
|
|
141
|
+
<pre className="whitespace-pre-wrap text-sm font-mono text-muted-foreground">
|
|
142
|
+
{reportText}
|
|
143
|
+
</pre>
|
|
144
|
+
</div>
|
|
145
|
+
</details>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import { SparklineChart } from '@/components/SparklineChart'
|
|
2
4
|
import type { WeeklySparklineProps } from './WeeklySparkline.types'
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
const sizeConfig = {
|
|
7
|
+
sm: { width: 'w-full', height: 32 },
|
|
8
|
+
md: { width: 'w-full', height: 40 },
|
|
9
|
+
lg: { width: 'w-full', height: 56 },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function WeeklySparkline({ data, size = 'md' }: WeeklySparklineProps) {
|
|
13
|
+
const config = sizeConfig[size]
|
|
14
|
+
|
|
5
15
|
return (
|
|
6
|
-
<div className=
|
|
7
|
-
<p className="text-
|
|
16
|
+
<div className={`${config.width} min-w-0`}>
|
|
17
|
+
<p className="text-xs uppercase tracking-wider text-muted-foreground mb-1 md:text-right">
|
|
8
18
|
7-day activity
|
|
9
19
|
</p>
|
|
10
|
-
<
|
|
20
|
+
<div className="w-full min-w-0 overflow-hidden">
|
|
21
|
+
<SparklineChart data={data} height={config.height} />
|
|
22
|
+
</div>
|
|
11
23
|
</div>
|
|
12
24
|
)
|
|
13
25
|
}
|