clean up badge reward flow and session update, added badge awards to user progress table

This commit is contained in:
austinkelsay 2024-12-30 11:07:33 -06:00
parent 4437f7f929
commit e4a8b01eec
No known key found for this signature in database
GPG Key ID: 44CB4EC6D9F2FA02
6 changed files with 113 additions and 214 deletions

View File

@ -8,6 +8,7 @@ const CombinedContributionChart = ({ session }) => {
const [contributionData, setContributionData] = useState({});
const [totalContributions, setTotalContributions] = useState(0);
const windowWidth = useWindowWidth();
const [retryCount, setRetryCount] = useState(0);
const prepareProgressData = useCallback(() => {
if (!session?.user?.userCourses) return {};
@ -85,7 +86,45 @@ const CombinedContributionChart = ({ session }) => {
setTotalContributions(totalCommits + Object.values(activityData).reduce((a, b) => a + b, 0));
}, [prepareProgressData]);
const { data, isLoading, isFetching } = useFetchGithubCommits(session, handleNewCommit);
const {
data,
isLoading,
isFetching,
error,
refetch
} = useFetchGithubCommits(session, handleNewCommit);
// Add recovery logic
useEffect(() => {
if (error && retryCount < 3) {
const timer = setTimeout(() => {
setRetryCount(prev => prev + 1);
refetch();
}, 1000 * (retryCount + 1)); // Exponential backoff
return () => clearTimeout(timer);
}
}, [error, retryCount, refetch]);
// Reset retry count on successful data fetch
useEffect(() => {
if (data) {
setRetryCount(0);
}
}, [data]);
// Add loading state check
useEffect(() => {
if (isLoading || isFetching) {
const loadingTimeout = setTimeout(() => {
if (!data) {
refetch();
}
}, 5000); // Timeout after 5 seconds
return () => clearTimeout(loadingTimeout);
}
}, [isLoading, isFetching, data, refetch]);
// Initialize from cached data if available
useEffect(() => {
@ -256,6 +295,16 @@ const CombinedContributionChart = ({ session }) => {
</div>
<span className="ml-2">More</span>
</div>
{error && retryCount >= 3 && (
<div className="text-red-400 text-sm px-4">
Error loading data. <button onClick={() => {
setRetryCount(0);
refetch();
}} className="text-blue-400 hover:text-blue-300">
Try again
</button>
</div>
)}
</div>
</div>
</div>

View File

@ -1,145 +0,0 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useFetchGithubCommits } from '@/hooks/githubQueries/useFetchGithubCommits';
import { Tooltip } from 'primereact/tooltip';
const GithubContributionChart = ({ username }) => {
const [contributionData, setContributionData] = useState({});
const [totalCommits, setTotalCommits] = useState(0);
const handleNewCommit = useCallback(({ contributionData, totalCommits }) => {
setContributionData(contributionData);
setTotalCommits(totalCommits);
}, []);
const { data, isLoading, isFetching } = useFetchGithubCommits(username, handleNewCommit);
// Initialize from cached data if available
useEffect(() => {
if (data && !isLoading) {
setContributionData(data.contributionData);
setTotalCommits(data.totalCommits);
}
}, [data, isLoading]);
const getColor = useCallback((count) => {
if (count === 0) return 'bg-gray-100';
if (count < 5) return 'bg-green-300';
if (count < 10) return 'bg-green-400';
if (count < 20) return 'bg-green-600';
return 'bg-green-700';
}, []);
const generateCalendar = useCallback(() => {
const today = new Date();
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
const calendar = [];
// Create 7 rows for days of the week
for (let i = 0; i < 7; i++) {
calendar[i] = [];
}
// Fill in the dates
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
const dateString = d.toISOString().split('T')[0];
const count = contributionData[dateString] || 0;
const dayOfWeek = d.getDay();
calendar[dayOfWeek].push({ date: new Date(d), count });
}
return calendar;
}, [contributionData]);
const calendar = generateCalendar();
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const getMonthLabels = useCallback(() => {
const today = new Date();
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
const months = [];
let currentMonth = -1;
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
const month = d.getMonth();
if (month !== currentMonth) {
months.push({
name: d.toLocaleString('default', { month: 'short' }),
index: calendar[0].findIndex(
(_, weekIndex) => calendar[0][weekIndex]?.date.getMonth() === month
)
});
currentMonth = month;
}
}
return months;
}, [calendar]);
return (
<div className="mx-auto py-2 px-8 max-w-[1000px] bg-gray-800 rounded-lg">
{(isLoading || isFetching) && <p>Loading contribution data... ({totalCommits} commits fetched)</p>}
{!isLoading && !isFetching &&
<div className="flex justify-between items-center mb-3">
<h4 className="text-base font-semibold text-gray-200">
{totalCommits} contributions in the last year
</h4>
<i className="pi pi-question-circle text-lg cursor-pointer text-gray-400 hover:text-gray-200"
data-pr-tooltip="Total number of commits made to GitHub repositories over the last year. (may not be 100% accurate)" />
<Tooltip target=".pi-question-circle" position="top" />
</div>
}
<div className="flex">
{/* Days of week labels */}
<div className="flex flex-col gap-[3px] text-[11px] text-gray-400 pr-3">
{weekDays.map((day, index) => (
<div key={day} className="h-[13px] leading-[13px]">
{index % 2 === 0 && day}
</div>
))}
</div>
<div className="flex flex-col">
{/* Calendar grid */}
<div className="flex gap-[3px]">
{calendar[0].map((_, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-[3px]">
{calendar.map((row, dayIndex) => (
row[weekIndex] && (
<div
key={`${weekIndex}-${dayIndex}`}
className={`w-[13px] h-[13px] ${getColor(row[weekIndex].count)} rounded-[2px] cursor-pointer transition-colors duration-100`}
title={`${row[weekIndex].date.toDateString()}: ${row[weekIndex].count} contribution${row[weekIndex].count !== 1 ? 's' : ''}`}
></div>
)
))}
</div>
))}
</div>
{/* Month labels moved to bottom */}
<div className="flex text-[11px] text-gray-400 h-[20px] mt-1">
{getMonthLabels().map((month, index) => (
<div
key={index}
className="absolute"
style={{ marginLeft: `${month.index * 15}px` }}
>
{month.name}
</div>
))}
</div>
</div>
</div>
<div className="text-[11px] text-gray-400 flex items-center justify-end">
<span className="mr-2">Less</span>
<div className="flex gap-[3px]">
<div className="w-[13px] h-[13px] bg-gray-100 rounded-[2px]"></div>
<div className="w-[13px] h-[13px] bg-green-300 rounded-[2px]"></div>
<div className="w-[13px] h-[13px] bg-green-400 rounded-[2px]"></div>
<div className="w-[13px] h-[13px] bg-green-600 rounded-[2px]"></div>
<div className="w-[13px] h-[13px] bg-green-700 rounded-[2px]"></div>
</div>
<span className="ml-2">More</span>
</div>
</div>
);
};
export default GithubContributionChart;

View File

@ -1,55 +0,0 @@
import React, { useMemo } from 'react';
const GithubContributionChartDisabled = () => {
const getRandomColor = () => {
const random = Math.random();
if (random < 0.4) return 'bg-gray-100';
if (random < 0.6) return 'bg-green-300';
if (random < 0.75) return 'bg-green-400';
if (random < 0.9) return 'bg-green-600';
return 'bg-green-700';
};
const calendar = useMemo(() => {
const today = new Date();
const sixMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
const calendar = [];
for (let d = new Date(sixMonthsAgo); d <= today; d.setDate(d.getDate() + 1)) {
calendar.push({ date: new Date(d), color: getRandomColor() });
}
return calendar;
}, []);
return (
<div className="relative mx-auto py-2 px-4 max-w-[900px] bg-gray-800 rounded-lg">
<div className="opacity-30">
<div className="flex flex-wrap gap-1">
{calendar.map((day, index) => (
<div
key={index}
className={`w-3 h-3 ${day.color} rounded-sm`}
></div>
))}
</div>
<div className="mt-2 text-sm text-gray-400 flex items-center">
<span className="mr-2">Less</span>
<div className="flex gap-1">
<div className="w-3 h-3 bg-gray-100 rounded-sm"></div>
<div className="w-3 h-3 bg-green-300 rounded-sm"></div>
<div className="w-3 h-3 bg-green-400 rounded-sm"></div>
<div className="w-3 h-3 bg-green-600 rounded-sm"></div>
<div className="w-3 h-3 bg-green-700 rounded-sm"></div>
</div>
<span className="ml-2">More</span>
</div>
</div>
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-lg">
<p className="text-white text-xl font-semibold">Connect to GitHub (Coming Soon)</p>
</div>
</div>
);
};
export default GithubContributionChartDisabled;

View File

@ -5,14 +5,28 @@ import useWindowWidth from "@/hooks/useWindowWidth";
import ProgressListItem from "@/components/content/lists/ProgressListItem";
import { formatDateTime } from "@/utils/time";
import { ProgressSpinner } from "primereact/progressspinner";
import Link from 'next/link';
const UserProgressTable = ({ session, ndk }) => {
const windowWidth = useWindowWidth();
const prepareProgressData = () => {
if (!session?.user?.userCourses) return [];
const progressData = [];
// Add badge awards
session.user.userBadges?.forEach(userBadge => {
progressData.push({
id: `badge-${userBadge.id}`,
type: 'badge',
name: userBadge.badge?.name,
eventType: 'awarded',
date: userBadge.awardedAt,
courseId: userBadge.badge?.courseId,
badgeId: userBadge.badgeId,
noteId: userBadge.badge?.noteId
});
});
session.user.userCourses.forEach(courseProgress => {
// Add course start entry
if (courseProgress.started) {
@ -86,25 +100,45 @@ const UserProgressTable = ({ session, ndk }) => {
const typeTemplate = (rowData) => (
<div className="flex items-center gap-2">
<i className={`pi ${rowData.type === 'course' ? 'pi-book' : 'pi-file'} text-lg`}></i>
<i className={`pi ${
rowData.type === 'course' ? 'pi-book' :
rowData.type === 'lesson' ? 'pi-file' :
'pi-star' // Badge icon
} text-lg`}></i>
<span className="capitalize">{rowData.type}</span>
</div>
);
const eventTemplate = (rowData) => (
<div className="flex items-center gap-2">
<i className={`pi ${rowData.eventType === 'started' ? 'pi-play' : 'pi-check-circle'}
${rowData.eventType === 'started' ? 'text-blue-500' : 'text-green-500'} text-lg`}></i>
<i className={`pi ${
rowData.eventType === 'started' ? 'pi-play' :
rowData.eventType === 'completed' ? 'pi-check-circle' :
'pi-trophy' // Badge award icon
} ${
rowData.eventType === 'started' ? 'text-blue-500' :
rowData.eventType === 'completed' ? 'text-green-500' :
'text-yellow-500' // Badge award color
} text-lg`}></i>
<span className="capitalize">{rowData.eventType}</span>
</div>
);
const nameTemplate = (rowData) => (
<div className="flex items-center">
{rowData.type === 'course'
? <ProgressListItem dTag={rowData.courseId} category="name" type="course" />
: <ProgressListItem dTag={rowData.resourceId} category="name" type="lesson" />
}
{rowData.type === 'badge' ? (
<Link
href={`https://badges.page/a/${rowData.noteId}`}
target="_blank"
className="text-purple-400 hover:text-purple-300 transition-colors"
>
{rowData.name}
</Link>
) : rowData.type === 'course' ? (
<ProgressListItem dTag={rowData.courseId} category="name" type="course" />
) : (
<ProgressListItem dTag={rowData.resourceId} category="name" type="lesson" />
)}
</div>
);

View File

@ -1,9 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useSession } from 'next-auth/react';
import { useState } from 'react';
export function useCompletedCoursesQuery() {
const { data: session } = useSession();
const [retryCount, setRetryCount] = useState(0);
const fetchCompletedCourses = async () => {
if (!session?.user?.id) return [];
@ -17,7 +19,7 @@ export function useCompletedCoursesQuery() {
return response.data;
} catch (error) {
console.error('Error fetching completed courses:', error);
return [];
throw error; // Let React Query handle the retry
}
};
@ -29,5 +31,7 @@ export function useCompletedCoursesQuery() {
cacheTime: 1000 * 60 * 30, // 30 minutes
refetchOnWindowFocus: false,
refetchOnMount: false,
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});
}

View File

@ -48,10 +48,21 @@ export const useBadge = () => {
}
if (badgesAwarded) {
// Update session silently
await update({ revalidate: false });
// Invalidate the completed courses query to trigger a clean refetch
// First invalidate the queries
await queryClient.invalidateQueries(['completedCourses']);
await queryClient.invalidateQueries(['githubCommits']);
// Wait a brief moment before updating session
await new Promise(resolve => setTimeout(resolve, 100));
// Update session last
await update({ revalidate: false });
// Force a refetch of the invalidated queries
await Promise.all([
queryClient.refetchQueries(['completedCourses']),
queryClient.refetchQueries(['githubCommits'])
]);
}
} catch (error) {
console.error('Error checking badge eligibility:', error);
@ -62,7 +73,8 @@ export const useBadge = () => {
};
const timeoutId = setTimeout(checkForBadgeEligibility, 0);
const interval = setInterval(checkForBadgeEligibility, 300000);
// Reduce the frequency of checks to avoid potential race conditions
const interval = setInterval(checkForBadgeEligibility, 600000); // 10 minutes
return () => {
clearTimeout(timeoutId);