Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 0 additions & 20 deletions app/components/AuthModal.jsx

This file was deleted.

55 changes: 55 additions & 0 deletions app/components/dashboard/ActivityDashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect, useState } from "react";
import { supabase } from "@/lib/supabase";
import StreakCounter from "@/app/components/dashboard/StreakCounter";
import ActivityHeatmap from "@/app/components/dashboard/ActivityHeatmap";
import {ChartNoAxesCombined} from "lucide-react";

function ActivityDashboard({ userId }) {
const [activityDates, setActivityDates] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
if (!userId) return;

async function fetchActivity() {
setLoading(true);
const { data, error } = await supabase
.from("user_activity")
.select("activity_date")
.eq("user_id", userId);

if (!error && data) {
const dates = data.map(
(item) => new Date(item.activity_date).toISOString().split("T")[0]
);
setActivityDates(dates);
}
setLoading(false);
}

fetchActivity();
}, [userId]);

if (loading) {
return (
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-gray-600 dark:text-gray-300">
Loading activity...
</div>
);
}

return (
<main className="rounded-xl bg-white border border-gray-200 dark:border-gray-700 dark:bg-neutral-950 p-4">
<div className="flex items-center gap-2">
<ChartNoAxesCombined className="text-black dark:text-white"/>
<h1 className="font-poppins text-lg text-black dark:text-white">Your Stats</h1>
</div>
<div className="flex flex-col md:flex-row items-center justify-center md:gap-6">
<StreakCounter activityDates={activityDates} />
<ActivityHeatmap activityDates={activityDates} />
</div>
</main>
);
}

export default ActivityDashboard;
132 changes: 132 additions & 0 deletions app/components/dashboard/ActivityHeatmap.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useMemo } from "react";

function ActivityHeatmap({ activityDates }) {
// Generate last 90 days
const last90Days = useMemo(() => {
const dates = [];
const today = new Date();
for (let i = 89; i >= 0; i--) {
const d = new Date();
d.setDate(today.getDate() - i);
dates.push(d);
}
return dates;
}, []);

// Map activity dates to a Set for O(1) lookup
const activitySet = useMemo(() => new Set(activityDates), [activityDates]);

// Helper to format date YYYY-MM-DD
const formatDate = (date) => date.toISOString().split("T")[0];

// Weekday labels to show on the left (Sun, Mon, Wed, Fri)
const weekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const visibleWeekdayLabels = ["Sun", "Mon", "Wed", "Fri"];

// Prepare weeks data (group dates by week starting on Sunday)
// We want columns = weeks, rows = days (0=Sun to 6=Sat)
const weeks = [];
let currentWeek = [];
let currentWeekStartDay = last90Days[0].getDay();
// Pad first week with nulls if first day is not Sunday
for (let i = 0; i < currentWeekStartDay; i++) {
currentWeek.push(null);
}
last90Days.forEach((date) => {
if (currentWeek.length === 7) {
weeks.push(currentWeek);
currentWeek = [];
}
currentWeek.push(date);
});
// Fill last week with nulls if needed
while (currentWeek.length < 7) {
currentWeek.push(null);
}
weeks.push(currentWeek);

// Get month labels for top row
// For each week, show the month label only once when the month changes
const monthLabels = [];
let lastMonth = null;
weeks.forEach((week) => {
// Find first non-null date in week
const firstDate = week.find((d) => d !== null);
if (firstDate) {
const month = firstDate.toLocaleString("default", { month: "short" });
if (month !== lastMonth) {
monthLabels.push(month);
lastMonth = month;
} else {
monthLabels.push("");
}
} else {
monthLabels.push("");
}
});

const totalContributions = activityDates.length;

return (
<div className="overflow-x-auto">
<div className="flex scale-90 sm:scale-100">
{/* Left column with month label row height and weekday labels */}
<div className="flex flex-col">
{/* Contributions circle at top-left corner */}
<div className="w-5 h-5 mb-1 flex items-center justify-center">
<div className="w-5 h-5 text-sm rounded-full flex items-center justify-center bg-green-500 text-white font-bold">
{totalContributions}
</div>
</div>
{/* Weekday labels column */}
<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"}}>
{weekdayLabels.map((day, idx) =>
visibleWeekdayLabels.includes(day) ? (
<div key={day} className="h-6 md:h-6 flex items-center justify-end pr-1 text-[10px] md:text-xs">
{day}
</div>
) : (
<div key={day} className="h-6 md:h-6"></div>
)
)}
</div>
</div>
<div>
{/* Month labels row */}
<div className="grid grid-cols-[repeat(auto-fit,minmax(24px,1fr))] gap-1 mb-1" style={{gridTemplateColumns: `repeat(${weeks.length}, 24px)`}}>
{monthLabels.map((month, idx) => (
<div key={idx} className="text-[10px] md:text-xs font-medium text-gray-600 dark:text-gray-300 text-center">
{month}
</div>
))}
</div>
{/* Heatmap grid */}
<div className="grid grid-rows-7 grid-flow-col gap-1">
{weeks.map((week, weekIdx) =>
week.map((date, dayIdx) => {
if (!date) {
return <div key={`${weekIdx}-${dayIdx}`} className="w-6 h-6"></div>;
}
const dateStr = formatDate(date);
const isActive = activitySet.has(dateStr);
return (
<div
key={`${weekIdx}-${dayIdx}`}
title={`${dateStr} - ${isActive ? "Active" : "No Activity"}`}
className={`w-6 h-6 rounded-sm transition-colors duration-300 ${
isActive
? "bg-green-500"
: "bg-gray-200 dark:bg-gray-700"
}`}
></div>
);
})
)}
</div>
</div>
</div>
</div>
);
}

export default ActivityHeatmap;
57 changes: 57 additions & 0 deletions app/components/dashboard/StreakCounter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useMemo } from "react";
import { Award } from "lucide-react";

function StreakCounter({ activityDates }) {
const { currentStreak, highestStreak } = useMemo(() => {
if (!activityDates || activityDates.length === 0) {
return { currentStreak: 0, highestStreak: 0 };
}

const dates = activityDates.map(d => new Date(d)).sort((a, b) => a - b);

let highest = 0;
let tempStreak = 1;

for (let i = 1; i < dates.length; i++) {
const diff = (dates[i] - dates[i - 1]) / (1000 * 60 * 60 * 24);
if (diff === 1) tempStreak++;
else tempStreak = 1;
if (tempStreak > highest) highest = tempStreak;
}

let streakCount = 0;
const today = new Date();
for (let i = dates.length - 1; i >= 0; i--) {
const diff = (today - dates[i]) / (1000 * 60 * 60 * 24);
if (diff === 0 || diff === 1) {
streakCount++;
today.setDate(today.getDate() - 1);
} else break;
}

return { currentStreak: streakCount, highestStreak: highest };
}, [activityDates]);

return (
<main className="md:p-12 p-4">
<div className="flex flex-col items-center space-y-2">
{/* Circle with fire icon and current streak */}
<div className="w-24 h-24 rounded-full border-4 border-orange-500 flex flex-col items-center justify-center shadow-lg relative">
<span className="text-3xl">
<img src="/assets/fire.svg" className="w-10 h-10" alt="fire" />
</span>
<span className="text-xl font-bold text-gray-800 dark:text-gray-200">
{currentStreak}
</span>
</div>

{/* Highest streak label */}
<div className="text-sm text-black dark:text-white flex items-center gap-1">
<Award size={24} color="#ff9300"/>Highest: {highestStreak} day{highestStreak !== 1 ? "s" : ""}
</div>
</div>
</main>
);
}

export default StreakCounter;
4 changes: 2 additions & 2 deletions app/components/navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ const AboutItem = ({ title, description, icon, iconBg, href }) => (
</Link>
);

// Abut dropdown component for desktop
// About dropdown component for desktop
const AboutServicesDropdown = () => (
<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">
<div className="py-1">
Expand Down Expand Up @@ -315,7 +315,7 @@ const handleLogout = async () => {
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
/>
{isUserMenuOpen && (
<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">
<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">
<Link
href="/dashboard"
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
Expand Down
Loading
Loading