mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
fixed signup / login flow for github, added lessons and courses starting and completing in progress table
This commit is contained in:
parent
8dc95e92bb
commit
32a9cd7cdc
145
src/components/charts/CombinedContributionChart.js
Normal file
145
src/components/charts/CombinedContributionChart.js
Normal file
@ -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 (
|
||||
<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-[12px] leading-[12px]">
|
||||
{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-[12px] h-[12px] ${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-[12px] h-[12px] bg-gray-100 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-300 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-400 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-600 rounded-[2px]"></div>
|
||||
<div className="w-[12px] h-[12px] bg-green-700 rounded-[2px]"></div>
|
||||
</div>
|
||||
<span className="ml-2">More</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubContributionChart;
|
@ -75,13 +75,13 @@ const GithubContributionChart = ({ username }) => {
|
||||
}, [calendar]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto py-4 px-8 max-w-[1000px] bg-gray-800 rounded-lg">
|
||||
<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-4">
|
||||
<h3 className="text-base font-semibold text-gray-200">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="text-base font-semibold text-gray-200">
|
||||
{totalCommits} contributions in the last year
|
||||
</h3>
|
||||
</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" />
|
||||
|
@ -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 (
|
||||
<a className="text-blue-500 underline hover:text-blue-600" href={`/course/${encodeNaddr()}`}>
|
||||
{event.name}
|
||||
<a className="text-blue-500 underline hover:text-blue-600" href={href}>
|
||||
{event.name || event.title}
|
||||
</a>
|
||||
);
|
||||
} else if (category === "lessons") {
|
||||
|
125
src/components/profile/DataTables/UserProgressTable.js
Normal file
125
src/components/profile/DataTables/UserProgressTable.js
Normal file
@ -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 = (
|
||||
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<span className="text-xl text-900 font-bold text-[#f8f8ff]">Progress</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!session || !session?.user || !ndk) {
|
||||
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
emptyMessage="No Courses or Milestones completed"
|
||||
value={prepareProgressData()}
|
||||
header={header}
|
||||
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
|
||||
pt={{
|
||||
wrapper: {
|
||||
className: "rounded-lg rounded-t-none"
|
||||
},
|
||||
header: {
|
||||
className: "rounded-t-lg"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Column
|
||||
field="type"
|
||||
header="Type"
|
||||
body={(rowData) => (
|
||||
<span>{rowData.type}</span>
|
||||
)}
|
||||
></Column>
|
||||
<Column
|
||||
field="started"
|
||||
header="Started"
|
||||
body={(rowData) => (
|
||||
<i className={classNames('pi', {
|
||||
'pi-check-circle text-blue-500': rowData.started,
|
||||
'pi-times-circle text-gray-500': !rowData.started
|
||||
})}></i>
|
||||
)}
|
||||
></Column>
|
||||
<Column
|
||||
field="completed"
|
||||
header="Completed"
|
||||
body={(rowData) => (
|
||||
<i className={classNames('pi', {
|
||||
'pi-check-circle text-green-500': rowData.completed,
|
||||
'pi-times-circle text-red-500': !rowData.completed
|
||||
})}></i>
|
||||
)}
|
||||
></Column>
|
||||
<Column
|
||||
field="name"
|
||||
header="Name"
|
||||
body={(rowData) => (
|
||||
rowData.type === 'course'
|
||||
? <ProgressListItem dTag={rowData.courseId} category="name" type="course" />
|
||||
: <ProgressListItem dTag={rowData.resourceId} category="name" type="lesson" />
|
||||
)}
|
||||
></Column>
|
||||
<Column
|
||||
body={rowData => {
|
||||
if (rowData.completed) {
|
||||
return formatDateTime(rowData.completedAt);
|
||||
}
|
||||
return formatDateTime(rowData.startedAt) || formatDateTime(rowData.createdAt);
|
||||
}}
|
||||
header="Last Activity"
|
||||
></Column>
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProgressTable;
|
@ -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 = () => {
|
||||
)}
|
||||
<UserProgress />
|
||||
</div>
|
||||
{!session || !session?.user || !ndk ? (
|
||||
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
|
||||
) : (
|
||||
<DataTable
|
||||
emptyMessage="No Courses or Milestones completed"
|
||||
value={session.user?.userCourses}
|
||||
header={header}
|
||||
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
|
||||
pt={{
|
||||
wrapper: {
|
||||
className: "rounded-lg rounded-t-none"
|
||||
},
|
||||
header: {
|
||||
className: "rounded-t-lg"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Column
|
||||
field="completed"
|
||||
header="Completed"
|
||||
body={(rowData) => (
|
||||
<i className={classNames('pi', { 'pi-check-circle text-green-500': rowData.completed, 'pi-times-circle text-red-500': !rowData.completed })}></i>
|
||||
)}
|
||||
></Column>
|
||||
<Column
|
||||
body={(rowData) => {
|
||||
return <ProgressListItem dTag={rowData.courseId} category="name" />
|
||||
}}
|
||||
header="Name"
|
||||
></Column>
|
||||
<Column body={(rowData) => {
|
||||
return <ProgressListItem dTag={rowData.courseId} category="lessons" />
|
||||
}} header="Lessons"></Column>
|
||||
<Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
|
||||
</DataTable>
|
||||
)}
|
||||
<UserProgressTable
|
||||
session={session}
|
||||
ndk={ndk}
|
||||
windowWidth={windowWidth}
|
||||
/>
|
||||
{session && session?.user && (
|
||||
<DataTable
|
||||
emptyMessage="No purchases"
|
||||
|
@ -11,7 +11,7 @@ const appConfig = {
|
||||
"wss://purplerelay.com/",
|
||||
"wss://relay.devs.tools/"
|
||||
],
|
||||
authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345"],
|
||||
authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345", "468f729dd409053dac5e7470622c3996aad88db6ed1de9165cb1921b5ab4fd5e"],
|
||||
customLightningAddresses: [
|
||||
{
|
||||
// todo remove need for lowercase
|
||||
|
@ -7,7 +7,7 @@ import nodemailer from 'nodemailer';
|
||||
import { findKind0Fields } from "@/utils/nostr";
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import { updateUser, getUserByPubkey, createUser } from "@/db/models/userModels";
|
||||
import { updateUser, getUserByPubkey, createUser, getUserById } from "@/db/models/userModels";
|
||||
import { createRole } from "@/db/models/roleModels";
|
||||
import appConfig from "@/config/appConfig";
|
||||
import GithubProvider from "next-auth/providers/github";
|
||||
@ -132,11 +132,9 @@ export const authOptions = {
|
||||
clientId: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
profile: async (profile) => {
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user