Super commit - repo selection for course submission, course submission required field, course submission link field, badge issuance flow fixed

This commit is contained in:
austinkelsay 2025-01-01 14:24:58 -06:00
parent 12693c6637
commit 991a732f4e
No known key found for this signature in database
GPG Key ID: 44CB4EC6D9F2FA02
14 changed files with 230 additions and 51 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "UserCourse" ADD COLUMN "submittedRepoLink" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Course" ADD COLUMN "submissionRequired" BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,14 +1,14 @@
// datasource db {
// provider = "postgresql"
// url = env("DATABASE_URL")
// }
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
provider = "postgresql"
url = env("DATABASE_URL")
}
// datasource db {
// provider = "postgresql"
// url = env("POSTGRES_PRISMA_URL")
// directUrl = env("POSTGRES_URL_NON_POOLING")
// }
generator client {
provider = "prisma-client-js"
}
@ -130,6 +130,7 @@ model Course {
lessons Lesson[]
purchases Purchase[]
noteId String? @unique
submissionRequired Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userCourses UserCourse[]
@ -218,6 +219,7 @@ model UserCourse {
completed Boolean @default(false)
startedAt DateTime?
completedAt DateTime?
submittedRepoLink String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@ -53,7 +53,7 @@ const UserPurchaseTable = ({ session, windowWidth }) => {
emptyMessage="No purchases"
value={session.user?.purchased}
header={purchasesHeader}
className="m-2 max-lap:m-0"
className="m-2 max-lap:m-0 max-lap:mt-2"
style={{ width: "100%", borderRadius: "8px", border: "1px solid #333", boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.1)" }}
pt={{
wrapper: {

View File

@ -0,0 +1,66 @@
import React, { useMemo } from 'react';
import { Dropdown } from 'primereact/dropdown';
import { useFetchGithubRepos } from '@/hooks/githubQueries/useFetchGithubRepos';
import { useSession } from 'next-auth/react';
import axios from 'axios';
import { useToast } from '@/hooks/useToast';
const RepoSelector = ({ courseId, onSubmit }) => {
const { data: session } = useSession();
const accessToken = session?.account?.access_token;
const { data: repos, isLoading } = useFetchGithubRepos(accessToken);
const { showToast } = useToast();
// Find the existing submission for this course
const existingSubmission = useMemo(() => {
return session?.user?.userCourses?.find(
course => course.courseId === courseId
)?.submittedRepoLink;
}, [session, courseId]);
const repoOptions = repos?.map(repo => ({
label: repo.name,
value: repo.html_url
})) || [];
const handleRepoSelect = async (repoLink) => {
try {
await axios.post(`/api/users/${session.user.id}/courses/${courseId}/submit-repo`, {
repoLink
});
onSubmit(repoLink);
showToast('success', 'Success', 'Repository submitted successfully');
} catch (error) {
console.error('Error submitting repo:', error);
showToast('error', 'Error', 'Failed to submit repository');
}
};
if (!accessToken) {
return (
<div className="pl-[28px] mt-2 text-gray-400">
GitHub connection required
</div>
);
}
return (
<div className="pl-[28px] mt-2">
<Dropdown
value={existingSubmission}
options={repoOptions}
onChange={(e) => handleRepoSelect(e.value)}
placeholder={isLoading ? "Loading repositories..." : "Select a repository"}
className="w-full max-w-[300px]"
loading={isLoading}
/>
{existingSubmission && (
<div className="text-sm text-gray-400 mt-1">
Repository submitted
</div>
)}
</div>
);
};
export default RepoSelector;

View File

@ -61,6 +61,8 @@ const UserBadges = ({ visible, onHide }) => {
})
};
console.log("Current Badge: ", currentBadge);
// Only update if this is the first instance or if it's newer than the existing one
if (!latestBadgeMap.has(defId) ||
new Date(currentBadge.awardedOn) > new Date(latestBadgeMap.get(defId).awardedOn)) {
@ -118,7 +120,7 @@ const UserBadges = ({ visible, onHide }) => {
>
<div className="relative w-32 h-32 mb-4">
<Image
src={badge.thumbnail}
src={badge.thumbnail || badge.image}
alt={badge.name}
layout="fill"
objectFit="contain"

View File

@ -7,6 +7,7 @@ import { useBadge } from '@/hooks/badges/useBadge';
import GenericButton from '@/components/buttons/GenericButton';
import UserProgressFlow from './UserProgressFlow';
import { Tooltip } from 'primereact/tooltip';
import RepoSelector from '@/components/profile/RepoSelector';
const allTasks = [
{
@ -32,10 +33,11 @@ const allTasks = [
status: 'Frontend Course',
completed: false,
tier: 'Frontend Dev',
courseId: 'f73c37f4-df2e-4f7d-a838-dce568c76136',
// courseId: 'f73c37f4-df2e-4f7d-a838-dce568c76136',
courseId: 'af98e096-3136-4d8b-aafe-17677f0266d0',
subTasks: [
{ status: 'Complete the course', completed: false },
// { status: 'Select your completed project', completed: false },
{ status: 'Submit your project repository', completed: false },
]
},
{
@ -45,7 +47,7 @@ const allTasks = [
courseId: 'f6825391-831c-44da-904a-9ac3d149b7be',
subTasks: [
{ status: 'Complete the course', completed: false },
// { status: 'Select your completed project', completed: false },
{ status: 'Submit your project repository', completed: false },
]
},
];
@ -87,20 +89,28 @@ const UserProgress = () => {
return {
...task,
completed: session?.account?.provider === 'github' ? true : false,
subTasks: task.subTasks ? task.subTasks.map(subTask => ({
subTasks: task.subTasks.map(subTask => ({
...subTask,
completed: session?.account?.provider === 'github' ? true : false
})) : undefined
}))
};
}
const userCourse = session?.user?.userCourses?.find(uc => uc.courseId === task.courseId);
const courseCompleted = completedCourseIds.includes(task.courseId);
const repoSubmitted = userCourse?.submittedRepoLink ? true : false;
return {
...task,
completed: task.courseId === null || completedCourseIds.includes(task.courseId),
subTasks: task.subTasks ? task.subTasks.map(subTask => ({
completed: courseCompleted && (task.courseId === null || repoSubmitted),
subTasks: task.subTasks.map(subTask => ({
...subTask,
completed: completedCourseIds.includes(task.courseId)
})) : undefined
completed: subTask.status.includes('Complete')
? courseCompleted
: subTask.status.includes('repository')
? repoSubmitted
: false
}))
};
});
@ -164,7 +174,7 @@ const UserProgress = () => {
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedSession = await getSession();
if (updatedSession?.account?.provider === 'github') {
router.push('/'); // Accounts linked successfully
router.push('/profile'); // Accounts linked successfully
}
}
} else {
@ -253,19 +263,42 @@ const UserProgress = () => {
{task.subTasks && (
<ul className="space-y-2">
{task.subTasks.map((subTask, subIndex) => (
<li key={subIndex} className="flex items-center pl-[28px]">
{subTask.completed ? (
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center mr-3">
<i className="pi pi-check text-white text-sm"></i>
</div>
) : (
<div className="w-4 h-4 bg-gray-700 rounded-full flex items-center justify-center mr-3">
<i className="pi pi-info-circle text-white text-sm"></i>
</div>
<li key={subIndex}>
<div className="flex items-center pl-[28px]">
{subTask.completed ? (
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center mr-3">
<i className="pi pi-check text-white text-sm"></i>
</div>
) : (
<div className="w-4 h-4 bg-gray-700 rounded-full flex items-center justify-center mr-3">
<i className="pi pi-info-circle text-white text-sm"></i>
</div>
)}
<span className={`${subTask.completed ? 'text-white' : 'text-gray-400'}`}>
{subTask.status}
</span>
</div>
{subTask.status.includes('repository') && !subTask.completed && (
<RepoSelector
courseId={task.courseId}
onSubmit={() => {
const updatedTasks = tasks.map(t =>
t.courseId === task.courseId
? {
...t,
subTasks: t.subTasks.map(st =>
st.status === subTask.status
? { ...st, completed: true }
: st
)
}
: t
);
setTasks(updatedTasks);
router.push('/profile');
}}
/>
)}
<span className={`${subTask.completed ? 'text-white' : 'text-gray-400'}`}>
{subTask.status}
</span>
</li>
))}
</ul>

View File

@ -44,6 +44,7 @@ export const createCourse = async (data) => {
id: courseData.id,
noteId: courseData.noteId,
price: courseData.price,
submissionRequired: courseData.submissionRequired || false,
user: { connect: { id: courseData.user.connect.id } },
lessons: {
connect: courseData.lessons.connect
@ -71,6 +72,7 @@ export const updateCourse = async (id, data) => {
where: { id },
data: {
...otherData,
submissionRequired: otherData.submissionRequired || false,
lessons: {
deleteMany: {},
create: lessons.map((lesson, index) => ({

View File

@ -65,6 +65,20 @@ export const deleteUserCourse = async (userId, courseId) => {
});
};
export const submitCourseRepo = async (userId, courseSlug, repoLink) => {
return await prisma.userCourse.update({
where: {
userId_courseId: {
userId,
courseId: courseSlug
}
},
data: {
submittedRepoLink: repoLink
}
});
};
export const checkCourseCompletion = async (userId, courseId) => {
const course = await prisma.course.findUnique({
where: { id: courseId },

View File

@ -25,13 +25,13 @@ export const useBadge = () => {
// Check for GitHub connection badge
if (session?.account?.provider === 'github') {
const hasPlebBadge = userBadges?.some(
userBadge => userBadge.badge?.id === '3664e78f-b618-420d-a7cc-f3393b0211df'
userBadge => userBadge.badge?.id === '4664e73f-c618-41dd-a7cc-f3393b031fdf'
);
if (!hasPlebBadge) {
try {
const response = await axios.post('/api/badges/issue', {
badgeId: '3664e78f-b618-420d-a7cc-f3393b0211df',
badgeId: '4664e73f-c618-41dd-a7cc-f3393b031fdf',
userId: session.user.id,
});
@ -51,8 +51,12 @@ export const useBadge = () => {
userBadge => userBadge.badge?.courseId === userCourse.courseId
);
const hasBadgeDefined = !!userCourse.course?.badge;
// Check if course requires repo submission
const requiresRepo = userCourse.course?.submissionRequired ?? false;
const hasRepoIfRequired = requiresRepo ? !!userCourse.submittedRepoLink : true;
return isCompleted && hasNoBadge && hasBadgeDefined;
return isCompleted && hasNoBadge && hasBadgeDefined && hasRepoIfRequired;
});
for (const course of eligibleCourses || []) {

View File

@ -8,26 +8,46 @@ export function useFetchGithubRepos(accessToken) {
return useQuery({
queryKey: ['githubRepos', accessToken],
queryFn: async () => {
if (!accessToken) return [];
if (!accessToken) {
console.log('No access token provided');
return [];
}
const octokit = new ThrottledOctokit({
auth: accessToken,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
if (retryCount < 2) return true;
try {
const octokit = new ThrottledOctokit({
auth: accessToken,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
console.log(`Rate limit exceeded, retrying after ${retryAfter} seconds`);
if (retryCount < 2) return true;
return false;
},
onSecondaryRateLimit: (retryAfter, options, octokit) => {
console.log(`Secondary rate limit hit, retrying after ${retryAfter} seconds`);
return true;
},
},
onSecondaryRateLimit: (retryAfter, options, octokit) => true,
},
});
});
const { data } = await octokit.repos.listForAuthenticatedUser({
sort: 'updated',
per_page: 100
});
console.log('Fetching repositories...');
const { data } = await octokit.repos.listForAuthenticatedUser({
sort: 'updated',
per_page: 100
});
console.log(`Found ${data.length} repositories`);
return data;
return data.map(repo => ({
id: repo.id,
name: repo.name,
html_url: repo.html_url
}));
} catch (error) {
console.error('Error fetching GitHub repos:', error);
throw error;
}
},
staleTime: 1000 * 60 * 5, // 5 minutes
enabled: !!accessToken
enabled: !!accessToken,
retry: 3,
});
}

View File

@ -51,6 +51,14 @@ export default async function handler(req, res) {
return res.status(400).json({ error: 'Course not completed' });
}
// Check if course requires repo submission
if (userCourse.course.submissionRequired && !userCourse.submittedRepoLink) {
return res.status(400).json({
error: 'Repository submission required',
message: 'You must submit a project repository to earn this badge'
});
}
badge = userCourse.course.badge;
} else if (badgeId) {
// Direct badge lookup for non-course badges

View File

@ -0,0 +1,24 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth].js";
import { submitCourseRepo } from "@/db/models/userCourseModels";
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { courseSlug } = req.query;
const { repoLink } = req.body;
try {
await submitCourseRepo(session.user.id, courseSlug, repoLink);
return res.status(200).json({ success: true });
} catch (error) {
return res.status(500).json({ error: 'Failed to submit repo' });
}
}