Skip to content

Commit 276f7d1

Browse files
committed
feat: enhance analytics and dashboard components
1 parent 98a3735 commit 276f7d1

File tree

5 files changed

+219
-103
lines changed

5 files changed

+219
-103
lines changed

convex/model/analytics.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface SessionSummary {
1010
issuesCompleted: number;
1111
totalStoryPoints: number | null; // null if non-numeric scale
1212
averageAgreement: number | null;
13+
participantCount: number;
1314
}
1415

1516
export interface AgreementDataPoint {
@@ -129,6 +130,11 @@ export async function getUserSessions(
129130
)
130131
: null;
131132

133+
const roomMembers = await ctx.db
134+
.query("roomMemberships")
135+
.withIndex("by_room", (q) => q.eq("roomId", room._id))
136+
.collect();
137+
132138
return {
133139
roomId: room._id,
134140
roomName: room.name,
@@ -137,6 +143,7 @@ export async function getUserSessions(
137143
issuesCompleted: completedIssues.length,
138144
totalStoryPoints,
139145
averageAgreement,
146+
participantCount: roomMembers.length,
140147
};
141148
})
142149
);

src/app/dashboard/dashboard-content.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,9 @@ export function DashboardContent() {
127127
agreementTrend={predictability?.agreementTrend ?? "stable"}
128128
isLoading={predictabilityLoading}
129129
/>
130-
<VelocityTrend
131-
sessions={predictability?.sessions ?? []}
132-
velocityTrend={predictability?.velocityTrend ?? "stable"}
133-
isLoading={predictabilityLoading}
130+
<VoterAlignmentChart
131+
data={voterAlignment?.scatterPoints ?? []}
132+
isLoading={alignmentLoading}
134133
/>
135134
</div>
136135

@@ -153,9 +152,10 @@ export function DashboardContent() {
153152
data={timeToConsensus?.trendBySession ?? []}
154153
isLoading={consensusLoading}
155154
/>
156-
<VoterAlignmentChart
157-
data={voterAlignment?.scatterPoints ?? []}
158-
isLoading={alignmentLoading}
155+
<VelocityTrend
156+
sessions={predictability?.sessions ?? []}
157+
velocityTrend={predictability?.velocityTrend ?? "stable"}
158+
isLoading={predictabilityLoading}
159159
/>
160160
<VoteDistribution
161161
data={voteDistribution ?? []}

src/app/globals.css

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@
8282
--border: oklch(0.922 0 0);
8383
--input: oklch(0.922 0 0);
8484
--ring: oklch(0.708 0 0);
85-
--chart-1: var(--color-blue-300);
86-
--chart-2: var(--color-blue-500);
87-
--chart-3: var(--color-blue-600);
88-
--chart-4: var(--color-blue-700);
89-
--chart-5: var(--color-blue-800);
85+
--chart-1: var(--color-zinc-900);
86+
--chart-2: var(--color-zinc-700);
87+
--chart-3: var(--color-zinc-500);
88+
--chart-4: var(--color-zinc-300);
89+
--chart-5: var(--color-zinc-100);
9090
--radius: 0.5rem;
9191
--sidebar: oklch(0.985 0 0);
9292
--sidebar-foreground: oklch(0.145 0 0);
@@ -134,11 +134,11 @@
134134
--border: oklch(1 0 0 / 10%);
135135
--input: oklch(1 0 0 / 15%);
136136
--ring: oklch(0.556 0 0);
137-
--chart-1: var(--color-blue-300);
138-
--chart-2: var(--color-blue-500);
139-
--chart-3: var(--color-blue-600);
140-
--chart-4: var(--color-blue-700);
141-
--chart-5: var(--color-blue-800);
137+
--chart-1: var(--color-zinc-100);
138+
--chart-2: var(--color-zinc-300);
139+
--chart-3: var(--color-zinc-500);
140+
--chart-4: var(--color-zinc-700);
141+
--chart-5: var(--color-zinc-900);
142142
--sidebar: oklch(0.205 0 0);
143143
--sidebar-foreground: oklch(0.985 0 0);
144144
--sidebar-primary: oklch(0.488 0.243 264.376);
Lines changed: 129 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
22

33
import { formatDistanceToNow } from "date-fns";
4-
import { ExternalLink, Users, Target, TrendingUp } from "lucide-react";
4+
import { Users, Target, TrendingUp, Calendar, ArrowRight, Activity, Clock } from "lucide-react";
55
import Link from "next/link";
6-
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
6+
import { Badge } from "@/components/ui/badge";
7+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
78

89
interface SessionSummary {
910
roomId: string;
@@ -13,6 +14,7 @@ interface SessionSummary {
1314
issuesCompleted: number;
1415
totalStoryPoints: number | null;
1516
averageAgreement: number | null;
17+
participantCount: number;
1618
}
1719

1820
interface SessionHistoryProps {
@@ -23,103 +25,152 @@ interface SessionHistoryProps {
2325
export function SessionHistory({ sessions, isLoading }: SessionHistoryProps) {
2426
if (isLoading) {
2527
return (
26-
<Card className="flex flex-col h-full">
27-
<CardHeader>
28-
<CardTitle className="text-base font-semibold">Session History</CardTitle>
29-
<CardDescription>Recent planning rooms joined</CardDescription>
30-
</CardHeader>
31-
<CardContent className="flex-1">
32-
<div className="space-y-3">
33-
{[1, 2, 3].map((i) => (
34-
<div key={i} className="animate-pulse">
35-
<div className="h-16 rounded-lg bg-muted" />
28+
<div className="space-y-4">
29+
<div>
30+
<h2 className="text-xl font-semibold tracking-tight">Recent Sessions</h2>
31+
<p className="text-sm text-muted-foreground">Continue where you left off or review past estimates.</p>
32+
</div>
33+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
34+
{[1, 2, 3, 4].map((i) => (
35+
<div key={i} className="animate-pulse rounded-2xl border bg-card p-5 h-[200px]">
36+
<div className="h-6 w-2/3 bg-muted rounded-md mb-4" />
37+
<div className="space-y-3">
38+
<div className="h-4 w-1/2 bg-muted rounded-md" />
39+
<div className="h-4 w-3/4 bg-muted rounded-md" />
3640
</div>
37-
))}
38-
</div>
39-
</CardContent>
40-
</Card>
41+
</div>
42+
))}
43+
</div>
44+
</div>
4145
);
4246
}
4347

4448
if (sessions.length === 0) {
4549
return (
46-
<Card className="flex flex-col h-full">
47-
<CardHeader>
48-
<CardTitle className="text-base font-semibold">Session History</CardTitle>
49-
<CardDescription>Recent planning rooms joined</CardDescription>
50-
</CardHeader>
51-
<CardContent className="flex-1">
52-
<div className="flex h-full min-h-[200px] flex-col items-center justify-center text-muted-foreground">
53-
<Users className="mb-2 h-8 w-8 opacity-50" />
54-
<p>No sessions yet</p>
55-
<p className="text-sm">
56-
Join a planning room to start tracking your sessions
57-
</p>
58-
</div>
59-
</CardContent>
60-
</Card>
50+
<div className="flex flex-col items-center justify-center min-h-[300px] rounded-2xl border border-dashed p-8 text-center bg-muted/20">
51+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4">
52+
<Activity className="h-6 w-6 text-primary" />
53+
</div>
54+
<h2 className="text-xl font-semibold mb-2">No active sessions</h2>
55+
<p className="text-sm text-muted-foreground max-w-sm mb-6">
56+
You haven't joined any planning rooms yet. Create or join a room to start estimating with your team.
57+
</p>
58+
</div>
6159
);
6260
}
6361

6462
return (
65-
<Card className="flex flex-col h-full">
66-
<CardHeader>
67-
<CardTitle className="text-base font-semibold">Session History</CardTitle>
68-
<CardDescription>Recent planning rooms joined</CardDescription>
69-
</CardHeader>
70-
<CardContent className="flex-1">
71-
<div className="space-y-2">
72-
{sessions.map((session) => (
63+
<div className="space-y-6">
64+
<div className="flex items-center justify-between">
65+
<div>
66+
<h2 className="text-xl font-semibold tracking-tight">Recent Sessions</h2>
67+
<p className="text-sm text-muted-foreground mt-1">Pick up where you left off or review past estimates.</p>
68+
</div>
69+
</div>
70+
71+
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
72+
{sessions.map((session) => {
73+
const isHighConsensus = session.averageAgreement !== null && session.averageAgreement >= 80;
74+
const isMedConsensus = session.averageAgreement !== null && session.averageAgreement >= 60 && session.averageAgreement < 80;
75+
76+
return (
7377
<Link
7478
key={session.roomId}
7579
href={`/room/${session.roomId}`}
76-
className="group flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50"
80+
className="group relative flex flex-col justify-between overflow-hidden rounded-2xl border bg-card p-5 transition-all duration-300 hover:shadow-md hover:border-primary/30"
7781
>
78-
<div className="min-w-0 flex-1">
79-
<div className="flex items-center gap-2">
80-
<span className="truncate font-medium">
81-
{session.roomName}
82-
</span>
83-
<ExternalLink className="h-3 w-3 opacity-0 transition-opacity group-hover:opacity-50" />
84-
</div>
85-
<div className="mt-1 text-xs text-muted-foreground">
86-
{formatDistanceToNow(session.lastActivityAt, {
87-
addSuffix: true,
88-
})}
82+
<div className="absolute inset-0 bg-linear-to-br from-primary/5 via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
83+
84+
<div className="relative z-10 mb-6">
85+
<div className="flex items-start justify-between mb-4">
86+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
87+
<Activity className="h-5 w-5" />
88+
</div>
89+
{session.averageAgreement !== null && (
90+
<Tooltip>
91+
<TooltipTrigger render={<div className="cursor-default" onClick={(e) => e.preventDefault()} />}>
92+
<Badge
93+
variant={isHighConsensus ? "default" : isMedConsensus ? "secondary" : "destructive"}
94+
className="font-medium"
95+
>
96+
<Users className="h-3 w-3 mr-1" />
97+
{session.averageAgreement}% Match
98+
</Badge>
99+
</TooltipTrigger>
100+
<TooltipContent>
101+
<p>Average team consensus matching</p>
102+
</TooltipContent>
103+
</Tooltip>
104+
)}
89105
</div>
106+
107+
<h3 className="line-clamp-2 text-lg font-semibold leading-tight group-hover:text-primary transition-colors">
108+
{session.roomName}
109+
</h3>
90110
</div>
91111

92-
<div className="flex items-center gap-4 text-sm">
93-
<div className="flex items-center gap-1 text-muted-foreground">
94-
<Target className="h-4 w-4" />
95-
<span>{session.issuesCompleted}</span>
96-
</div>
112+
<div className="relative z-10 mt-auto">
113+
<div className={`grid gap-4 mb-5 pb-5 border-b border-border/50 ${session.totalStoryPoints !== null ? "grid-cols-3" : "grid-cols-2"}`}>
114+
<Tooltip>
115+
<TooltipTrigger render={<div className="text-left cursor-default" onClick={(e) => e.preventDefault()} />}>
116+
<p className="text-xs text-muted-foreground flex items-center mb-1 whitespace-nowrap">
117+
<Target className="h-3 w-3 mr-1 shrink-0" /> Issues
118+
</p>
119+
<p className="text-base font-medium">{session.issuesCompleted}</p>
120+
</TooltipTrigger>
121+
<TooltipContent>
122+
<p>Total issues estimated</p>
123+
</TooltipContent>
124+
</Tooltip>
97125

98-
{session.totalStoryPoints !== null && (
99-
<div className="flex items-center gap-1 text-muted-foreground">
100-
<TrendingUp className="h-4 w-4" />
101-
<span>{session.totalStoryPoints} pts</span>
102-
</div>
103-
)}
126+
<Tooltip>
127+
<TooltipTrigger render={<div className="text-left cursor-default" onClick={(e) => e.preventDefault()} />}>
128+
<p className="text-xs text-muted-foreground flex items-center mb-1 whitespace-nowrap">
129+
<Users className="h-3 w-3 mr-1 shrink-0" /> Members
130+
</p>
131+
<p className="text-base font-medium">{session.participantCount}</p>
132+
</TooltipTrigger>
133+
<TooltipContent>
134+
<p>Total room participants</p>
135+
</TooltipContent>
136+
</Tooltip>
137+
138+
{session.totalStoryPoints !== null && (
139+
<Tooltip>
140+
<TooltipTrigger render={<div className="text-left cursor-default" onClick={(e) => e.preventDefault()} />}>
141+
<p className="text-xs text-muted-foreground flex items-center mb-1 whitespace-nowrap">
142+
<TrendingUp className="h-3 w-3 mr-1 shrink-0" /> Points
143+
</p>
144+
<p className="text-base font-medium">{session.totalStoryPoints}</p>
145+
</TooltipTrigger>
146+
<TooltipContent>
147+
<p>Total story points estimated</p>
148+
</TooltipContent>
149+
</Tooltip>
150+
)}
151+
</div>
104152

105-
{session.averageAgreement !== null && (
106-
<div
107-
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
108-
session.averageAgreement >= 80
109-
? "bg-green-100 text-green-700 dark:bg-status-success-bg dark:text-status-success-fg"
110-
: session.averageAgreement >= 60
111-
? "bg-amber-100 text-amber-700 dark:bg-status-warning-bg dark:text-status-warning-fg"
112-
: "bg-red-100 text-red-700 dark:bg-status-error-bg dark:text-status-error-fg"
113-
}`}
114-
>
115-
{session.averageAgreement}%
153+
<div className="flex items-center justify-between">
154+
<Tooltip>
155+
<TooltipTrigger render={<div className="flex items-center text-xs text-muted-foreground cursor-default" onClick={(e) => e.preventDefault()} />}>
156+
<Clock className="h-3.5 w-3.5 mr-1.5" />
157+
<time dateTime={new Date(session.lastActivityAt).toISOString()}>
158+
{formatDistanceToNow(session.lastActivityAt, { addSuffix: true })}
159+
</time>
160+
</TooltipTrigger>
161+
<TooltipContent>
162+
<p>Time since last activity</p>
163+
</TooltipContent>
164+
</Tooltip>
165+
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-foreground opacity-0 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0 group-hover:opacity-100">
166+
<ArrowRight className="h-4 w-4" />
116167
</div>
117-
)}
168+
</div>
118169
</div>
119170
</Link>
120-
))}
121-
</div>
122-
</CardContent>
123-
</Card>
171+
);
172+
})}
173+
</div>
174+
</div>
124175
);
125176
}

0 commit comments

Comments
 (0)