From 32a9cd7cdc0c1d470b49180e93ca3e297b62f870 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Tue, 19 Nov 2024 16:02:11 -0600 Subject: [PATCH] fixed signup / login flow for github, added lessons and courses starting and completing in progress table --- .../charts/CombinedContributionChart.js | 145 ++++++++++++++++++ .../charts/GithubContributionChart.js | 8 +- .../content/lists/ProgressListItem.js | 25 +-- .../profile/DataTables/UserProgressTable.js | 125 +++++++++++++++ src/components/profile/UserProfile.js | 42 +---- src/config/appConfig.js | 2 +- src/pages/api/auth/[...nextauth].js | 56 +++++-- 7 files changed, 338 insertions(+), 65 deletions(-) create mode 100644 src/components/charts/CombinedContributionChart.js create mode 100644 src/components/profile/DataTables/UserProgressTable.js diff --git a/src/components/charts/CombinedContributionChart.js b/src/components/charts/CombinedContributionChart.js new file mode 100644 index 0000000..1ef3c36 --- /dev/null +++ b/src/components/charts/CombinedContributionChart.js @@ -0,0 +1,145 @@ +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 ( +
+ {(isLoading || isFetching) &&

Loading contribution data... ({totalCommits} commits fetched)

} + {!isLoading && !isFetching && +
+

+ {totalCommits} contributions in the last year +

+ + +
+ } +
+ {/* Days of week labels */} +
+ {weekDays.map((day, index) => ( +
+ {index % 2 === 0 && day} +
+ ))} +
+
+ {/* Calendar grid */} +
+ {calendar[0].map((_, weekIndex) => ( +
+ {calendar.map((row, dayIndex) => ( + row[weekIndex] && ( +
+ ) + ))} +
+ ))} +
+ {/* Month labels moved to bottom */} +
+ {getMonthLabels().map((month, index) => ( +
+ {month.name} +
+ ))} +
+
+
+
+ Less +
+
+
+
+
+
+
+ More +
+
+ ); +}; + +export default GithubContributionChart; diff --git a/src/components/charts/GithubContributionChart.js b/src/components/charts/GithubContributionChart.js index 6a8bc35..1ef3c36 100644 --- a/src/components/charts/GithubContributionChart.js +++ b/src/components/charts/GithubContributionChart.js @@ -75,13 +75,13 @@ const GithubContributionChart = ({ username }) => { }, [calendar]); return ( -
+
{(isLoading || isFetching) &&

Loading contribution data... ({totalCommits} commits fetched)

} {!isLoading && !isFetching && -
-

+
+

{totalCommits} contributions in the last year -

+

diff --git a/src/components/content/lists/ProgressListItem.js b/src/components/content/lists/ProgressListItem.js index 90baf77..3e9cb0f 100644 --- a/src/components/content/lists/ProgressListItem.js +++ b/src/components/content/lists/ProgressListItem.js @@ -1,11 +1,11 @@ import React, { useEffect, useState } from "react"; import { useNDKContext } from "@/context/NDKContext"; -import { parseCourseEvent } from "@/utils/nostr"; +import { parseCourseEvent, parseEvent } from "@/utils/nostr"; import { ProgressSpinner } from "primereact/progressspinner"; import { nip19 } from "nostr-tools"; import appConfig from "@/config/appConfig"; -const ProgressListItem = ({ dTag, category }) => { +const ProgressListItem = ({ dTag, category, type = 'course' }) => { const { ndk } = useNDKContext(); const [event, setEvent] = useState(null); @@ -16,25 +16,28 @@ const ProgressListItem = ({ dTag, category }) => { try { await ndk.connect(); const filter = { - kinds: [30004], - "#d": [dTag] + kinds: type === 'course' ? [30004] : [30023, 30402], + authors: appConfig.authorPubkeys, + "#d": [dTag], } + console.log("filter", filter); const event = await ndk.fetchEvent(filter); + console.log("event", event); if (event) { - setEvent(parseCourseEvent(event)); + setEvent(type === 'course' ? parseCourseEvent(event) : parseEvent(event)); } } catch (error) { console.error("Error fetching event:", error); } } fetchEvent(); - }, [dTag, ndk]); + }, [dTag, ndk, type]); const encodeNaddr = () => { return nip19.naddrEncode({ pubkey: event.pubkey, identifier: event.d, - kind: 30004, + kind: type === 'course' ? 30004 : event.kind, relays: appConfig.defaultRelayUrls }) } @@ -43,9 +46,13 @@ const ProgressListItem = ({ dTag, category }) => { if (!event) return null; if (category === "name") { + const href = type === 'course' + ? `/course/${encodeNaddr()}` + : `/details/${encodeNaddr()}`; + return ( - - {event.name} + + {event.name || event.title} ); } else if (category === "lessons") { diff --git a/src/components/profile/DataTables/UserProgressTable.js b/src/components/profile/DataTables/UserProgressTable.js new file mode 100644 index 0000000..431aa96 --- /dev/null +++ b/src/components/profile/DataTables/UserProgressTable.js @@ -0,0 +1,125 @@ +import React from "react"; +import { DataTable } from "primereact/datatable"; +import { Column } from "primereact/column"; +import { classNames } from "primereact/utils"; +import ProgressListItem from "@/components/content/lists/ProgressListItem"; +import { formatDateTime } from "@/utils/time"; +import { ProgressSpinner } from "primereact/progressspinner"; + +const UserProgressTable = ({ session, ndk, windowWidth }) => { + const prepareProgressData = () => { + if (!session?.user?.userCourses) return []; + + const progressData = []; + + session.user.userCourses.forEach(courseProgress => { + progressData.push({ + id: courseProgress.id, + type: 'course', + name: courseProgress.course?.name, + started: courseProgress.started, + startedAt: courseProgress.startedAt, + completed: courseProgress.completed, + completedAt: courseProgress.completedAt, + courseId: courseProgress.courseId + }); + + // Add lesson entries + const courseLessons = session.user.userLessons?.filter( + lesson => lesson.lesson?.courseId === courseProgress.courseId + ) || []; + + courseLessons.forEach(lessonProgress => { + progressData.push({ + id: lessonProgress.id, + type: 'lesson', + name: lessonProgress.lesson?.name, + started: lessonProgress.opened, + startedAt: lessonProgress.openedAt, + completed: lessonProgress.completed, + completedAt: lessonProgress.completedAt, + courseId: courseProgress.courseId, + lessonId: lessonProgress.lessonId, + resourceId: lessonProgress.lesson?.resourceId + }); + }); + }); + + return progressData; + }; + + const header = ( +
+ Progress +
+ ); + + if (!session || !session?.user || !ndk) { + return
; + } + + return ( + + ( + {rowData.type} + )} + > + ( + + )} + > + ( + + )} + > + ( + rowData.type === 'course' + ? + : + )} + > + { + if (rowData.completed) { + return formatDateTime(rowData.completedAt); + } + return formatDateTime(rowData.startedAt) || formatDateTime(rowData.createdAt); + }} + header="Last Activity" + > + + ); +}; + +export default UserProgressTable; diff --git a/src/components/profile/UserProfile.js b/src/components/profile/UserProfile.js index 3a922b8..c7251f8 100644 --- a/src/components/profile/UserProfile.js +++ b/src/components/profile/UserProfile.js @@ -19,6 +19,7 @@ import useWindowWidth from "@/hooks/useWindowWidth"; import { useToast } from "@/hooks/useToast"; import UserProgress from "@/components/profile/progress/UserProgress"; import { classNames } from "primereact/utils"; +import UserProgressTable from '@/components/profile/DataTables/UserProgressTable'; const UserProfile = () => { const windowWidth = useWindowWidth(); @@ -142,42 +143,11 @@ const UserProfile = () => { )}
- {!session || !session?.user || !ndk ? ( -
- ) : ( - - ( - - )} - > - { - return - }} - header="Name" - > - { - return - }} header="Lessons"> - formatDateTime(rowData?.createdAt)} header="Date"> - - )} + {session && session?.user && ( { - console.log("GitHub Profile Data:", profile); - return { id: profile.id.toString(), - name: profile.login, // Using login instead of name + name: profile.login, email: profile.email, avatar: profile.avatar_url } @@ -180,10 +178,18 @@ export const authOptions = { }), ], callbacks: { - async jwt({ token, user, account, trigger }) { - // Add account to token if it exists + async jwt({ token, user, account, trigger, profile }) { + // Add account and profile to token if they exist if (account) { token.account = account; + // Store GitHub-specific information + if (account.provider === 'github') { + token.githubProfile = { + login: profile?.login, + avatar_url: profile?.avatar_url, + email: profile?.email, + }; + } } if (trigger === "update" && account?.provider !== "anonymous") { @@ -194,8 +200,6 @@ export const authOptions = { // if we sign up with email and we don't have a pubkey or privkey, we need to generate them if (trigger === "signUp" && account?.provider === "email" && !user.pubkey && !user.privkey) { - console.log("signUp", user); - console.log("account", account); const sk = generateSecretKey(); const pubkey = getPublicKey(sk); const privkey = bytesToHex(sk); @@ -217,8 +221,8 @@ export const authOptions = { const pubkey = getPublicKey(sk); const privkey = bytesToHex(sk); - // Prioritize login as the username and name - const githubUsername = token?.login || token.name; // GitHub login is the @username + // Use GitHub login (username) from the profile stored in token + const githubUsername = token.githubProfile?.login; // Update the user in the database with all GitHub details await prisma.user.update({ @@ -228,8 +232,8 @@ export const authOptions = { privkey, name: githubUsername, username: githubUsername, - avatar: user.image || null, - email: user.email, + avatar: token.githubProfile?.avatar_url || null, + email: token.githubProfile?.email, } }); @@ -238,7 +242,8 @@ export const authOptions = { user.privkey = privkey; user.name = githubUsername; user.username = githubUsername; - user.githubUsername = token?.login || null; + user.avatar = token.githubProfile?.avatar_url; + user.email = token.githubProfile?.email; } if (user) { @@ -254,10 +259,31 @@ export const authOptions = { return token; }, async session({ session, token }) { - session.user = token.user; - // Add account from token to session + // If this is a GitHub session, get the full user data from DB first + if (token.account?.provider === 'github') { + const dbUser = await getUserById(token.user.id); + + // Start with the complete DB user as the base + session.user = dbUser; + + // Override only the GitHub-specific fields + session.user = { + ...dbUser, // This includes role, purchases, userCourses, userLessons, etc. + username: token.githubProfile.login, + avatar: token.githubProfile.avatar_url, + email: token.githubProfile.email + }; + } else { + // For non-GitHub sessions, use the existing token.user + session.user = token.user; + } + + // Keep the rest of the session data if (token.account) { session.account = token.account; + if (token.account.provider === 'github') { + session.githubProfile = token.githubProfile; + } } if (token.pubkey && token.privkey) { session.pubkey = token.pubkey;