mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
clean up badge reward flow and session update, added badge awards to user progress table
This commit is contained in:
parent
4437f7f929
commit
e4a8b01eec
@ -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>
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user