mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Super commit - repo selection for course submission, course submission required field, course submission link field, badge issuance flow fixed
This commit is contained in:
parent
12693c6637
commit
991a732f4e
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserCourse" ADD COLUMN "submittedRepoLink" TEXT;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Course" ADD COLUMN "submissionRequired" BOOLEAN NOT NULL DEFAULT false;
|
@ -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
|
||||
|
||||
|
@ -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: {
|
||||
|
66
src/components/profile/RepoSelector.js
Normal file
66
src/components/profile/RepoSelector.js
Normal 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;
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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) => ({
|
||||
|
@ -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 },
|
||||
|
@ -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 || []) {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
@ -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
|
||||
|
@ -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' });
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user