Skip to content

Commit 1414cd4

Browse files
authored
progress with chart data (#257)
2 parents ee4c70d + 5521fb6 commit 1414cd4

File tree

12 files changed

+369
-202
lines changed

12 files changed

+369
-202
lines changed
Lines changed: 140 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,42 @@
11
'use server';
2-
import { StatsChartData } from '@/types/Stats';
2+
import { StatsChartData, StatsSteps } from '@/types/Stats';
33
import { prisma } from '@/utils/prisma';
4+
import { getRange } from '@/utils/stats/get-range';
45
import { revalidateTag } from 'next/cache';
56

67
/**
78
* Method to get all the question data for the user and return it
89
* in a format to display in a chart.
910
*
1011
* We return the data in the following format:
11-
* [month]: { totalQuestions: number, tags: string[], tagCounts: Record<string, number> }
12+
* [key]: { totalQuestions: number, tags: string[], tagCounts: Record<string, number> }
1213
*/
1314
export const getStatsChartData = async (opts: {
1415
userUid: string;
1516
to: string;
16-
from: string;
17+
from: StatsSteps;
18+
step: 'month' | 'week' | 'day';
1719
}) => {
18-
const { userUid, to, from } = opts;
20+
const { userUid, to, from, step } = opts;
1921

2022
if (!userUid) {
2123
return null;
2224
}
2325

26+
const toDate = new Date(to);
27+
const fromDate = getRange(from);
28+
2429
const questions = await prisma.answers.findMany({
2530
where: {
2631
userUid,
2732
createdAt: {
28-
gte: new Date(from),
29-
lte: new Date(to)
33+
gte: fromDate,
34+
lte: toDate
3035
}
3136
},
3237
include: {
3338
question: {
3439
select: {
35-
createdAt: true,
3640
tags: {
3741
include: {
3842
tag: true
@@ -46,20 +50,43 @@ export const getStatsChartData = async (opts: {
4650
const data: StatsChartData = {};
4751

4852
questions.forEach((answer) => {
49-
const month = answer.question.createdAt.toISOString().slice(0, 7);
53+
let key: string;
54+
switch (step) {
55+
case 'month':
56+
key = answer.createdAt.toISOString().slice(0, 7);
57+
break;
58+
case 'week':
59+
const weekStart = new Date(answer.createdAt);
60+
weekStart.setDate(
61+
answer.createdAt.getDate() - answer.createdAt.getDay()
62+
);
63+
key = weekStart.toISOString().slice(0, 10);
64+
break;
65+
case 'day':
66+
key = formatDayLabel(answer.createdAt);
67+
break;
68+
}
69+
5070
const tags = answer.question.tags.map((tag) => tag.tag.name);
5171

52-
if (data[month]) {
53-
data[month].totalQuestions++;
72+
if (data[key]) {
73+
data[key].totalQuestions++;
5474
tags.forEach((tag) => {
55-
data[month].tagCounts[tag] = (data[month].tagCounts[tag] || 0) + 1;
75+
data[key].tagCounts[tag] = (data[key].tagCounts[tag] || 0) + 1;
76+
if (!data[key].tags.includes(tag)) {
77+
data[key].tags.push(tag);
78+
}
5679
});
5780
} else {
5881
const tagCounts: Record<string, number> = {};
5982
tags.forEach((tag) => {
6083
tagCounts[tag] = 1;
6184
});
62-
data[month] = { totalQuestions: 1, tagCounts };
85+
data[key] = {
86+
totalQuestions: 1,
87+
tagCounts,
88+
tags: [...tags]
89+
};
6390
}
6491
});
6592

@@ -69,19 +96,27 @@ export const getStatsChartData = async (opts: {
6996
};
7097

7198
/**
72-
* Gets the total number of questions the user has answered.
73-
*
74-
* @param userUid
75-
* @returns
99+
* Gets the total number of questions the user has answered within a specific range.
76100
*/
77-
export const getTotalQuestionCount = async (userUid: string) => {
101+
export const getTotalQuestionCount = async (
102+
userUid: string,
103+
to: string,
104+
from: StatsSteps
105+
) => {
78106
if (!userUid) {
79107
return null;
80108
}
81109

110+
const toDate = new Date(to);
111+
const fromDate = getRange(from);
112+
82113
const questions = await prisma.answers.count({
83114
where: {
84-
userUid
115+
userUid,
116+
createdAt: {
117+
gte: fromDate,
118+
lte: toDate
119+
}
85120
}
86121
});
87122

@@ -90,34 +125,66 @@ export const getTotalQuestionCount = async (userUid: string) => {
90125
return questions;
91126
};
92127

93-
export const getTotalTimeTaken = async (userUid: string) => {
128+
/**
129+
* Gets the total time taken for questions answered within a specific range.
130+
*/
131+
export const getTotalTimeTaken = async (
132+
userUid: string,
133+
to: string,
134+
from: StatsSteps
135+
) => {
94136
if (!userUid) {
95137
return null;
96138
}
97139

140+
const toDate = new Date(to);
141+
const fromDate = getRange(from);
142+
98143
const answers = await prisma.answers.findMany({
99144
where: {
100-
userUid
145+
userUid,
146+
createdAt: {
147+
gte: fromDate,
148+
lte: toDate
149+
}
150+
},
151+
select: {
152+
timeTaken: true
101153
}
102154
});
103155

104-
const totalTime = answers.reduce((acc, answer) => {
105-
return acc + (answer.timeTaken || 0);
106-
}, 0);
156+
const totalTime = answers.reduce(
157+
(acc, answer) => acc + (answer.timeTaken || 0),
158+
0
159+
);
107160

108161
revalidateTag('statistics');
109162

110163
return totalTime;
111164
};
112165

113-
export const getHighestScoringTag = async (userUid: string) => {
166+
/**
167+
* Gets the highest scoring tag within a specific range.
168+
*/
169+
export const getHighestScoringTag = async (
170+
userUid: string,
171+
to: string,
172+
from: StatsSteps
173+
) => {
114174
if (!userUid) {
115175
return null;
116176
}
117177

178+
const toDate = new Date(to);
179+
const fromDate = getRange(from);
180+
118181
const answers = await prisma.answers.findMany({
119182
where: {
120-
userUid
183+
userUid,
184+
createdAt: {
185+
gte: fromDate,
186+
lte: toDate
187+
}
121188
},
122189
include: {
123190
question: {
@@ -141,9 +208,10 @@ export const getHighestScoringTag = async (userUid: string) => {
141208
});
142209
});
143210

144-
const highestScoringTag = Object.keys(tagCounts).reduce((acc, tag) => {
145-
return tagCounts[tag] > tagCounts[acc] ? tag : acc;
146-
}, '');
211+
const highestScoringTag = Object.keys(tagCounts).reduce(
212+
(a, b) => (tagCounts[a] > tagCounts[b] ? a : b),
213+
''
214+
);
147215

148216
revalidateTag('statistics');
149217

@@ -152,3 +220,47 @@ export const getHighestScoringTag = async (userUid: string) => {
152220
count: tagCounts[highestScoringTag]
153221
};
154222
};
223+
224+
export const getData = async (opts: {
225+
userUid: string;
226+
to: string;
227+
from: StatsSteps;
228+
step: 'month' | 'week' | 'day';
229+
}) => {
230+
const { userUid, to, from } = opts;
231+
232+
const [stats, totalQuestions, totalTimeTaken, highestScoringTag] =
233+
await Promise.all([
234+
getStatsChartData(opts),
235+
getTotalQuestionCount(userUid, to, from),
236+
getTotalTimeTaken(userUid, to, from),
237+
getHighestScoringTag(userUid, to, from)
238+
]);
239+
240+
return { stats, totalQuestions, totalTimeTaken, highestScoringTag };
241+
};
242+
243+
// Helper function to format day label
244+
const formatDayLabel = (date: Date) => {
245+
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
246+
const months = [
247+
'Jan',
248+
'Feb',
249+
'Mar',
250+
'Apr',
251+
'May',
252+
'Jun',
253+
'Jul',
254+
'Aug',
255+
'Sep',
256+
'Oct',
257+
'Nov',
258+
'Dec'
259+
];
260+
261+
const dayOfWeek = days[date.getDay()];
262+
const month = months[date.getMonth()];
263+
const dayOfMonth = date.getDate();
264+
265+
return `${dayOfWeek}, ${month} ${dayOfMonth}`;
266+
};

src/app/(dashboard)/(default_layout)/statistics/page.tsx

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,63 @@ import {
22
getStatsChartData,
33
getTotalQuestionCount,
44
getTotalTimeTaken,
5-
getHighestScoringTag
5+
getHighestScoringTag,
6+
getData
67
} from '@/actions/statistics/get-stats-chart-data';
8+
import StatsRangePicker from '@/components/statistics/range-picker';
79
import QuestionChart from '@/components/statistics/total-question-chart';
810
import TotalStatsCard from '@/components/statistics/total-stats-card';
911
import { Button } from '@/components/ui/button';
1012
import LoadingSpinner from '@/components/ui/loading';
1113
import { Separator } from '@/components/ui/separator';
1214
import { useUserServer } from '@/hooks/useUserServer';
15+
import { StatsSteps } from '@/types/Stats';
16+
import { STATISTICS } from '@/utils/constants/statistics-filters';
1317
import { formatSeconds } from '@/utils/time';
1418
import { Calendar, Stars } from 'lucide-react';
1519
import { redirect } from 'next/navigation';
1620

17-
export default async function StatisticsPage() {
21+
export default async function StatisticsPage({
22+
searchParams
23+
}: {
24+
searchParams: { [key: string]: string | string[] | undefined };
25+
}) {
1826
const user = await useUserServer();
19-
2027
if (!user) {
2128
return redirect('/login');
2229
}
2330

24-
const [stats, totalQuestions, totalTimeTaken, highestScoringTag] =
25-
await Promise.all([
26-
getStatsChartData({
27-
userUid: user.uid,
28-
from: '2024-01-01',
29-
to: '2024-12-31'
30-
}),
31-
getTotalQuestionCount(user.uid),
32-
getTotalTimeTaken(user.uid),
33-
getHighestScoringTag(user.uid)
34-
]);
31+
// get the date range from the search params
32+
let range = searchParams.range as StatsSteps;
33+
if (!range) range = '90d';
34+
35+
const { step } = STATISTICS[range];
36+
37+
const { stats, highestScoringTag, totalQuestions, totalTimeTaken } =
38+
await getData({
39+
userUid: user.uid,
40+
from: range,
41+
to: new Date().toISOString(),
42+
step
43+
});
3544

3645
return (
3746
<div>
38-
<div className="pt-14 pb-5 flex w-full justify-between items-center">
47+
<div className="pt-14 pb-5 flex flex-col gap-3 md:flex-row w-full justify-between md:items-center">
3948
<h1 className="text-3xl text-gradient from-white to-white/55">
4049
Statistics
4150
</h1>
4251
<div className="flex gap-3">
43-
<Button>
44-
<Calendar className="size-4 mr-2" />
45-
Date Range
46-
</Button>
47-
<Button variant="default">
52+
<StatsRangePicker selectedRange={STATISTICS[range].label} />
53+
{/* <Button variant="default">
4854
<Stars className="size-4 text-yellow-300 fill-yellow-300" />
49-
</Button>
55+
</Button> */}
5056
</div>
5157
</div>
5258
<div className="grid grid-cols-12 gap-y-4 gap-x-8">
5359
{totalQuestions ? (
5460
<TotalStatsCard
61+
className="-left-3 relative"
5562
header={Number(totalQuestions)}
5663
description="Total Questions Answered"
5764
/>
@@ -78,7 +85,6 @@ export default async function StatisticsPage() {
7885
{stats ? <QuestionChart questionData={stats} /> : <LoadingSpinner />}
7986
</div>
8087
</div>
81-
<pre>{JSON.stringify(stats, null, 2)}</pre>
8288
</div>
8389
);
8490
}

0 commit comments

Comments
 (0)