Skip to content

Commit 5ce6c4d

Browse files
authored
UI and Logic changes
Changes : 1. Added streak and heatmap to the dashboard 2. updated modules styling 3. navbar styling fixed
2 parents 5843e7b + fc01bf0 commit 5ce6c4d

File tree

9 files changed

+379
-78
lines changed

9 files changed

+379
-78
lines changed

app/components/AuthModal.jsx

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useEffect, useState } from "react";
2+
import { supabase } from "@/lib/supabase";
3+
import StreakCounter from "@/app/components/dashboard/StreakCounter";
4+
import ActivityHeatmap from "@/app/components/dashboard/ActivityHeatmap";
5+
import {ChartNoAxesCombined} from "lucide-react";
6+
7+
function ActivityDashboard({ userId }) {
8+
const [activityDates, setActivityDates] = useState([]);
9+
const [loading, setLoading] = useState(true);
10+
11+
useEffect(() => {
12+
if (!userId) return;
13+
14+
async function fetchActivity() {
15+
setLoading(true);
16+
const { data, error } = await supabase
17+
.from("user_activity")
18+
.select("activity_date")
19+
.eq("user_id", userId);
20+
21+
if (!error && data) {
22+
const dates = data.map(
23+
(item) => new Date(item.activity_date).toISOString().split("T")[0]
24+
);
25+
setActivityDates(dates);
26+
}
27+
setLoading(false);
28+
}
29+
30+
fetchActivity();
31+
}, [userId]);
32+
33+
if (loading) {
34+
return (
35+
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-gray-600 dark:text-gray-300">
36+
Loading activity...
37+
</div>
38+
);
39+
}
40+
41+
return (
42+
<main className="rounded-xl bg-white border border-gray-200 dark:border-gray-700 dark:bg-neutral-950 p-4">
43+
<div className="flex items-center gap-2">
44+
<ChartNoAxesCombined className="text-black dark:text-white"/>
45+
<h1 className="font-poppins text-lg text-black dark:text-white">Your Stats</h1>
46+
</div>
47+
<div className="flex flex-col md:flex-row items-center justify-center md:gap-6">
48+
<StreakCounter activityDates={activityDates} />
49+
<ActivityHeatmap activityDates={activityDates} />
50+
</div>
51+
</main>
52+
);
53+
}
54+
55+
export default ActivityDashboard;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, { useMemo } from "react";
2+
3+
function ActivityHeatmap({ activityDates }) {
4+
// Generate last 90 days
5+
const last90Days = useMemo(() => {
6+
const dates = [];
7+
const today = new Date();
8+
for (let i = 89; i >= 0; i--) {
9+
const d = new Date();
10+
d.setDate(today.getDate() - i);
11+
dates.push(d);
12+
}
13+
return dates;
14+
}, []);
15+
16+
// Map activity dates to a Set for O(1) lookup
17+
const activitySet = useMemo(() => new Set(activityDates), [activityDates]);
18+
19+
// Helper to format date YYYY-MM-DD
20+
const formatDate = (date) => date.toISOString().split("T")[0];
21+
22+
// Weekday labels to show on the left (Sun, Mon, Wed, Fri)
23+
const weekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
24+
const visibleWeekdayLabels = ["Sun", "Mon", "Wed", "Fri"];
25+
26+
// Prepare weeks data (group dates by week starting on Sunday)
27+
// We want columns = weeks, rows = days (0=Sun to 6=Sat)
28+
const weeks = [];
29+
let currentWeek = [];
30+
let currentWeekStartDay = last90Days[0].getDay();
31+
// Pad first week with nulls if first day is not Sunday
32+
for (let i = 0; i < currentWeekStartDay; i++) {
33+
currentWeek.push(null);
34+
}
35+
last90Days.forEach((date) => {
36+
if (currentWeek.length === 7) {
37+
weeks.push(currentWeek);
38+
currentWeek = [];
39+
}
40+
currentWeek.push(date);
41+
});
42+
// Fill last week with nulls if needed
43+
while (currentWeek.length < 7) {
44+
currentWeek.push(null);
45+
}
46+
weeks.push(currentWeek);
47+
48+
// Get month labels for top row
49+
// For each week, show the month label only once when the month changes
50+
const monthLabels = [];
51+
let lastMonth = null;
52+
weeks.forEach((week) => {
53+
// Find first non-null date in week
54+
const firstDate = week.find((d) => d !== null);
55+
if (firstDate) {
56+
const month = firstDate.toLocaleString("default", { month: "short" });
57+
if (month !== lastMonth) {
58+
monthLabels.push(month);
59+
lastMonth = month;
60+
} else {
61+
monthLabels.push("");
62+
}
63+
} else {
64+
monthLabels.push("");
65+
}
66+
});
67+
68+
const totalContributions = activityDates.length;
69+
70+
return (
71+
<div className="overflow-x-auto">
72+
<div className="flex scale-90 sm:scale-100">
73+
{/* Left column with month label row height and weekday labels */}
74+
<div className="flex flex-col">
75+
{/* Contributions circle at top-left corner */}
76+
<div className="w-5 h-5 mb-1 flex items-center justify-center">
77+
<div className="w-5 h-5 text-sm rounded-full flex items-center justify-center bg-green-500 text-white font-bold">
78+
{totalContributions}
79+
</div>
80+
</div>
81+
{/* Weekday labels column */}
82+
<div className="grid grid-rows-7 gap-1 mr-1 text-xs md:text-xs text-gray-500 dark:text-gray-400" style={{height: "168px"}}>
83+
{weekdayLabels.map((day, idx) =>
84+
visibleWeekdayLabels.includes(day) ? (
85+
<div key={day} className="h-6 md:h-6 flex items-center justify-end pr-1 text-[10px] md:text-xs">
86+
{day}
87+
</div>
88+
) : (
89+
<div key={day} className="h-6 md:h-6"></div>
90+
)
91+
)}
92+
</div>
93+
</div>
94+
<div>
95+
{/* Month labels row */}
96+
<div className="grid grid-cols-[repeat(auto-fit,minmax(24px,1fr))] gap-1 mb-1" style={{gridTemplateColumns: `repeat(${weeks.length}, 24px)`}}>
97+
{monthLabels.map((month, idx) => (
98+
<div key={idx} className="text-[10px] md:text-xs font-medium text-gray-600 dark:text-gray-300 text-center">
99+
{month}
100+
</div>
101+
))}
102+
</div>
103+
{/* Heatmap grid */}
104+
<div className="grid grid-rows-7 grid-flow-col gap-1">
105+
{weeks.map((week, weekIdx) =>
106+
week.map((date, dayIdx) => {
107+
if (!date) {
108+
return <div key={`${weekIdx}-${dayIdx}`} className="w-6 h-6"></div>;
109+
}
110+
const dateStr = formatDate(date);
111+
const isActive = activitySet.has(dateStr);
112+
return (
113+
<div
114+
key={`${weekIdx}-${dayIdx}`}
115+
title={`${dateStr} - ${isActive ? "Active" : "No Activity"}`}
116+
className={`w-6 h-6 rounded-sm transition-colors duration-300 ${
117+
isActive
118+
? "bg-green-500"
119+
: "bg-gray-200 dark:bg-gray-700"
120+
}`}
121+
></div>
122+
);
123+
})
124+
)}
125+
</div>
126+
</div>
127+
</div>
128+
</div>
129+
);
130+
}
131+
132+
export default ActivityHeatmap;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { useMemo } from "react";
2+
import { Award } from "lucide-react";
3+
4+
function StreakCounter({ activityDates }) {
5+
const { currentStreak, highestStreak } = useMemo(() => {
6+
if (!activityDates || activityDates.length === 0) {
7+
return { currentStreak: 0, highestStreak: 0 };
8+
}
9+
10+
const dates = activityDates.map(d => new Date(d)).sort((a, b) => a - b);
11+
12+
let highest = 0;
13+
let tempStreak = 1;
14+
15+
for (let i = 1; i < dates.length; i++) {
16+
const diff = (dates[i] - dates[i - 1]) / (1000 * 60 * 60 * 24);
17+
if (diff === 1) tempStreak++;
18+
else tempStreak = 1;
19+
if (tempStreak > highest) highest = tempStreak;
20+
}
21+
22+
let streakCount = 0;
23+
const today = new Date();
24+
for (let i = dates.length - 1; i >= 0; i--) {
25+
const diff = (today - dates[i]) / (1000 * 60 * 60 * 24);
26+
if (diff === 0 || diff === 1) {
27+
streakCount++;
28+
today.setDate(today.getDate() - 1);
29+
} else break;
30+
}
31+
32+
return { currentStreak: streakCount, highestStreak: highest };
33+
}, [activityDates]);
34+
35+
return (
36+
<main className="md:p-12 p-4">
37+
<div className="flex flex-col items-center space-y-2">
38+
{/* Circle with fire icon and current streak */}
39+
<div className="w-24 h-24 rounded-full border-4 border-orange-500 flex flex-col items-center justify-center shadow-lg relative">
40+
<span className="text-3xl">
41+
<img src="/assets/fire.svg" className="w-10 h-10" alt="fire" />
42+
</span>
43+
<span className="text-xl font-bold text-gray-800 dark:text-gray-200">
44+
{currentStreak}
45+
</span>
46+
</div>
47+
48+
{/* Highest streak label */}
49+
<div className="text-sm text-black dark:text-white flex items-center gap-1">
50+
<Award size={24} color="#ff9300"/>Highest: {highestStreak} day{highestStreak !== 1 ? "s" : ""}
51+
</div>
52+
</div>
53+
</main>
54+
);
55+
}
56+
57+
export default StreakCounter;

app/components/navbar.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ const AboutItem = ({ title, description, icon, iconBg, href }) => (
193193
</Link>
194194
);
195195

196-
// Abut dropdown component for desktop
196+
// About dropdown component for desktop
197197
const AboutServicesDropdown = () => (
198198
<div className="absolute left-0 mt-2 w-64 origin-top-right dark:bg-black dark:ring-blue-400 bg-white rounded-lg shadow-xl ring-1 ring-gray-200 focus:outline-none opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-10 overflow-hidden">
199199
<div className="py-1">
@@ -315,7 +315,7 @@ const handleLogout = async () => {
315315
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
316316
/>
317317
{isUserMenuOpen && (
318-
<div className="absolute right-0 mt-2 w-36 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
318+
<div className="absolute right-0 mt-2 w-36 bg-white dark:bg-neutral-950 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
319319
<Link
320320
href="/dashboard"
321321
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"

0 commit comments

Comments
 (0)