Skip to content

feat: init stats page refresh (WIP/ON HOLD) #547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 60 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
87e33ed
feat: init stats page refresh!
Logannford Mar 24, 2025
b3727f0
feat: work on stats page hero component
Logannford Mar 24, 2025
7f54d04
feat: stats hero chart tooltip work.
Logannford Mar 24, 2025
72a8e78
feat: stats page layout issues, minor changes to hero chart
Logannford Mar 24, 2025
77962e6
trying to fix chart svg height
Logannford Mar 24, 2025
0fa1973
feat: some minor progress
Logannford Mar 25, 2025
16c7dbc
feat: adds difficulty radial chart story
Logannford Mar 25, 2025
90667a4
feat: work getting RSC's working with storybook
Logannford Mar 25, 2025
d188bc4
feat: correctly rendering rsc's in storybook
Logannford Mar 25, 2025
15f3171
feat: function abstraction, stories created.
Logannford Mar 25, 2025
b22308d
feat: messing around with different styling for question history block
Logannford Mar 25, 2025
9413370
minor work
Logannford Mar 25, 2025
917a3aa
feat: styling work with question history component
Logannford Mar 26, 2025
8b433da
progress with question history block
Logannford Mar 26, 2025
e1143c5
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 26, 2025
31f17be
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 26, 2025
7ca3910
feat: adds new DifficultyRadialChart to homepage
Logannford Mar 26, 2025
c09a40e
minor changes to story and revert to rsc
Logannford Mar 26, 2025
17aee2d
chore: adds tremor area chart & new directory for charts
Logannford Mar 26, 2025
515b949
feat: adds story for total-question-chart
Logannford Mar 26, 2025
cb97c95
chore: minor cleanup
Logannford Mar 26, 2025
a866d8a
feat: some story work, adds new color to chart utils & custom tooltip
Logannford Mar 27, 2025
e8a52d0
progress with stories.
Logannford Mar 27, 2025
55240df
minor style changes
Logannford Mar 27, 2025
d4354f4
feat: adding spark charts (will be used elsewhere in the app)
Logannford Mar 27, 2025
29ba732
feat: tooltip style changes & total question chart style changes
Logannford Mar 27, 2025
72f1eb4
more work with total questions chart.
Logannford Mar 27, 2025
c5401c5
style amends
Logannford Mar 27, 2025
6f9f46f
feat: adds bar-list and story added.
Logannford Mar 27, 2025
b542d5d
feat: adding separators to stats charts
Logannford Mar 27, 2025
66f430f
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 27, 2025
b797a80
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 28, 2025
6042d90
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 28, 2025
7394872
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 29, 2025
bf8819a
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 29, 2025
1fc6ce7
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 30, 2025
436b3ad
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 30, 2025
eec0ca1
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 30, 2025
e2df550
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 31, 2025
961178a
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Mar 31, 2025
9b5b931
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 1, 2025
2fae587
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 2, 2025
b7fccbb
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 2, 2025
cfbf9d5
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 3, 2025
f796cce
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 4, 2025
be7a4fd
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 5, 2025
2732d94
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 5, 2025
2a330eb
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 6, 2025
c203d41
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 8, 2025
b381f69
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 9, 2025
5275e0d
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 9, 2025
441abb2
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 9, 2025
e59086e
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 10, 2025
2ec35bc
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 12, 2025
09dd2f2
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 18, 2025
74941d1
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 19, 2025
c16c723
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 20, 2025
3ec1695
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 20, 2025
7019c72
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 22, 2025
f7604d7
Merge branch 'main' into improvement/stats-page-ui-improvements
Logannford Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions src/app/(app)/(default_layout)/statistics/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import StatsRangePicker from '@/components/app/statistics/range-picker';
import QuestionChart from '@/components/app/statistics/total-question-chart';
import DifficultyRadialChart from '@/components/app/statistics/difficulty-radial-chart';

import { useUserServer } from '@/hooks/use-user-server';
import { StatsSteps } from '@/types/Stats';
Expand All @@ -12,11 +13,21 @@ import SuggestedQuestions from '@/components/app/statistics/suggested-questions'
import StatisticsReport from '@/components/app/statistics/statistics-report';
import StatisticsOverviewMenu from '@/components/app/statistics/statistics-overview-menu';
import QuestionTracker from '@/components/app/statistics/question-tracker';
import { createMetadata } from '@/utils/seo';
import { getUserDisplayName } from '@/utils/user';

export const metadata = {
title: 'Statistics | techblitz',
description: 'View your coding statistics and progress',
};
export async function generateMetadata() {
return createMetadata({
title: 'Statistics | techblitz',
description: 'View your coding statistics and progress',
image: {
text: 'Statistics | techblitz',
bgColor: '#000',
textColor: '#fff',
},
canonicalUrl: '/statistics',
});
}

export default async function StatisticsPage({
searchParams,
Expand All @@ -33,36 +44,31 @@ export default async function StatisticsPage({
const range = (searchParams.range as StatsSteps) || '7d';
const { step } = STATISTICS[range];

// Prefetch data
// Prefetch data with includeDifficultyData flag
const { stats } = await getData({
userUid: user.uid,
from: range,
to: new Date().toISOString(),
step,
includeDifficultyData: true, // Make sure to include difficulty data
});

return (
<div>
<div className="flex flex-col gap-3 md:flex-row w-full justify-between md:items-center">
<Hero
heading="Coding Journey"
heading={`${getUserDisplayName(user)}'s Statistics`}
container={false}
subheading="An overview of your coding journey on TechBlitz."
subheading="Dive into your current coding journey, track your progress, and gain insight on how to improve your skills."
/>
<div className="flex gap-3">
<StatsRangePicker selectedRange={STATISTICS[range].label} />
<StatisticsOverviewMenu user={user} />
</div>
{stats && (
<DifficultyRadialChart questionData={stats} step={step} backgroundColor="bg-card-dark" />
)}
</div>

<div className="grid grid-cols-12 gap-y-4 gap-x-8 mt-8 md:mt-0">
<div className="max-h-[28rem] col-span-12 mb-4">
{stats && <QuestionChart questionData={stats} step={step} />}
</div>
{stats && <QuestionTracker className="mb-4" stats={stats} step={step} range={range} />}
{/** suggested q's and analysis blocks TODO: CHANGE SUGGESTED QUESTIONS TO STREAK DATA (I THINK) */}
<SuggestedQuestions />
<StatisticsReport />
{/* Radial Difficulty Chart */}
<div className="col-span-12 lg:col-span-6 mb-4"></div>
</div>
</div>
);
Expand Down
147 changes: 147 additions & 0 deletions src/components/app/statistics/difficulty-radial-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use client';

import { useMemo } from 'react';
import { RadialBarChart, RadialBar, Legend, ResponsiveContainer, Tooltip } from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { StatsChartData, DifficultyRecord } from '@/types/Stats';
import { Button } from '@/components/ui/button';

// Define colors for each difficulty
const DIFFICULTY_COLORS = {
BEGINNER: 'hsl(var(--chart-1))', // blue
EASY: 'hsl(var(--chart-2))', // green
MEDIUM: 'hsl(var(--chart-3))', // yellow
HARD: 'hsl(var(--chart-4))', // red
};

// Map difficulty to friendly names
const DIFFICULTY_LABELS = {
BEGINNER: 'Beginner',
EASY: 'Easy',
MEDIUM: 'Medium',
HARD: 'Hard',
};

export default function DifficultyRadialChart({
questionData,
step,
backgroundColor,
}: {
questionData: StatsChartData;
step: 'day' | 'week' | 'month';
backgroundColor?: string;
}) {
// Calculate total questions by difficulty
const difficultyData = useMemo(() => {
// Create object to store totals by difficulty
const totalsByDifficulty: Record<string, number> = {};
let grandTotal = 0;

// Sum up all question counts by difficulty across all time periods
Object.values(questionData).forEach((data) => {
// Only process entries that have difficulties data
if (data.difficulties) {
Object.entries(data.difficulties).forEach(([difficulty, count]) => {
// Ensure count is treated as a number
const countValue = count ? Number(count) : 0;
totalsByDifficulty[difficulty] = (totalsByDifficulty[difficulty] || 0) + countValue;
grandTotal += countValue;
});
}
});

// Convert to array format for radial chart
// Sort from highest to lowest count for better visualization
const chartData = Object.entries(totalsByDifficulty)
.filter(([_, count]) => count > 0) // Only include non-zero counts
.sort((a, b) => b[1] - a[1]) // Sort by count (descending)
.map(([difficulty, count], index) => {
// Higher index means smaller inner radius for the radial bar
return {
name: DIFFICULTY_LABELS[difficulty as keyof typeof DIFFICULTY_LABELS] || difficulty,
value: count,
difficulty,
fill: DIFFICULTY_COLORS[difficulty as keyof typeof DIFFICULTY_COLORS] || '#888',
percentage: grandTotal > 0 ? ((count / grandTotal) * 100).toFixed(1) : '0',
};
});

return { chartData, grandTotal };
}, [questionData]);

// Custom Legend that shows the count and percentage
const CustomizedLegend = ({ payload }: any) => {
if (!payload || payload.length === 0) return null;

return (
<ul className="flex flex-col gap-2 text-sm mt-4">
{payload.map((entry: any, index: number) => (
<li key={`item-${index}`} className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-foreground">{entry.value}</span>
<span className="text-muted-foreground">
{difficultyData.chartData.find((item) => item.name === entry.value)?.value || 0}{' '}
questions (
{difficultyData.chartData.find((item) => item.name === entry.value)?.percentage || 0}
%)
</span>
</li>
))}
</ul>
);
};

return (
<>
{difficultyData.grandTotal > 0 ? (
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="20%"
outerRadius="80%"
barSize={20}
data={difficultyData.chartData}
startAngle={180}
endAngle={0}
>
<RadialBar
background
label={{ fill: 'hsl(var(--foreground))', position: 'insideStart' }}
dataKey="value"
/>
<Legend
iconSize={10}
layout="vertical"
verticalAlign="middle"
align="right"
content={<CustomizedLegend />}
/>
<Tooltip
formatter={(value: number) => [
`${value} (${((value / difficultyData.grandTotal) * 100).toFixed(1)}%)`,
'Questions',
]}
contentStyle={{
backgroundColor: 'hsl(var(--background))',
borderColor: 'hsl(var(--border))',
}}
itemStyle={{ color: 'hsl(var(--foreground))' }}
labelStyle={{ color: 'hsl(var(--foreground))' }}
/>
</RadialBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4">
<p className="text-lg text-muted-foreground text-center">
No difficulty data available due to lack of questions answered
</p>
<Button href="/questions">Start answering now</Button>
</div>
)}
</>
);
}
8 changes: 7 additions & 1 deletion src/types/Stats.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { STEPS } from '@/utils/constants';
import { StatisticsReport } from '@prisma/client';
import { StatisticsReport, QuestionDifficulty } from '@prisma/client';
import { Question } from '@/types/Questions';

// Record of difficulty types with their counts
export type DifficultyRecord = {
[K in QuestionDifficulty]?: number;
};

export type StatsChartData = {
[key: string]: {
totalQuestions: number;
tagCounts: Record<string, number>;
tags: string[];
difficulties?: DifficultyRecord;
};
};

Expand Down
31 changes: 27 additions & 4 deletions src/utils/data/statistics/get-stats-chart-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ const getStatsChartData = async (opts: {
to: string;
from: StatsSteps;
step: 'month' | 'week' | 'day';
separateByDifficulty?: boolean;
includeDifficultyData?: boolean;
}) => {
const { userUid, to, from, step } = opts;
const { userUid, to, from, step, separateByDifficulty, includeDifficultyData } = opts;

if (!userUid) {
return null;
Expand Down Expand Up @@ -91,7 +93,7 @@ const getStatsChartData = async (opts: {
}
}

// Fill in actual data
// fill in actual data
questions.forEach((answer) => {
let key: string;
const year = answer.createdAt.getFullYear();
Expand Down Expand Up @@ -122,6 +124,25 @@ const getStatsChartData = async (opts: {
}
});

if (separateByDifficulty) {
// separate by difficulty
const difficultyData: StatsChartData = {};
Object.keys(data).forEach((key) => {
const tags = data[key].tags;
tags.forEach((tag) => {
if (!difficultyData[tag]) {
difficultyData[tag] = {
totalQuestions: 0,
tagCounts: {},
tags: [],
};
}
difficultyData[tag].totalQuestions += data[key].totalQuestions;
});
});
return difficultyData;
}

revalidateTag('statistics');

return data;
Expand Down Expand Up @@ -252,12 +273,14 @@ export const getData = async (opts: {
to: string;
from: StatsSteps;
step: 'month' | 'week' | 'day';
separateByDifficulty?: boolean;
includeDifficultyData?: boolean;
}) => {
const { userUid, to, from } = opts;
const { userUid, to, from, separateByDifficulty, includeDifficultyData = false } = opts;

// run all in parallel as they do not depend on each other
const [stats, totalQuestions, totalTimeTaken, highestScoringTag] = await Promise.all([
getStatsChartData(opts),
getStatsChartData({ ...opts, includeDifficultyData }),
getTotalQuestionCount(userUid, to, from),
getTotalTimeTaken(userUid, to, from),
getHighestScoringTag(userUid, to, from),
Expand Down