mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-22 15:35:05 +00:00
Created userCourse model, added endpoints, added basic useTrackcourse hook
This commit is contained in:
parent
1f8b69fb22
commit
96a6a29936
@ -178,6 +178,21 @@ CREATE TABLE "Purchase" (
|
|||||||
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserCourse" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"courseId" TEXT NOT NULL,
|
||||||
|
"started" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"startedAt" TIMESTAMP(3),
|
||||||
|
"completedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserCourse_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
|
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
|
||||||
|
|
||||||
@ -211,6 +226,9 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
|
|||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "UserLesson_userId_lessonId_key" ON "UserLesson"("userId", "lessonId");
|
CREATE UNIQUE INDEX "UserLesson_userId_lessonId_key" ON "UserLesson"("userId", "lessonId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserCourse_userId_courseId_key" ON "UserCourse"("userId", "courseId");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
@ -264,3 +282,9 @@ ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("cou
|
|||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserCourse" ADD CONSTRAINT "UserCourse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserCourse" ADD CONSTRAINT "UserCourse_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -28,6 +28,7 @@ model User {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
userLessons UserLesson[]
|
userLessons UserLesson[]
|
||||||
|
userCourses UserCourse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
@ -122,6 +123,7 @@ model Course {
|
|||||||
noteId String? @unique
|
noteId String? @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
userCourses UserCourse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CourseDraft {
|
model CourseDraft {
|
||||||
@ -192,4 +194,20 @@ model Purchase {
|
|||||||
amountPaid Int
|
amountPaid Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserCourse {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
courseId String
|
||||||
|
course Course @relation(fields: [courseId], references: [id])
|
||||||
|
started Boolean @default(false)
|
||||||
|
completed Boolean @default(false)
|
||||||
|
startedAt DateTime?
|
||||||
|
completedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([userId, courseId])
|
||||||
}
|
}
|
@ -16,6 +16,7 @@ import useWindowWidth from "@/hooks/useWindowWidth";
|
|||||||
import { useNDKContext } from "@/context/NDKContext";
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
import { findKind0Fields } from '@/utils/nostr';
|
import { findKind0Fields } from '@/utils/nostr';
|
||||||
import appConfig from "@/config/appConfig";
|
import appConfig from "@/config/appConfig";
|
||||||
|
import useTrackCourse from '@/hooks/tracking/useTrackCourse';
|
||||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
|
|
||||||
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
||||||
@ -33,6 +34,8 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
|
|||||||
const isMobileView = windowWidth <= 768;
|
const isMobileView = windowWidth <= 768;
|
||||||
const { ndk } = useNDKContext();
|
const { ndk } = useNDKContext();
|
||||||
|
|
||||||
|
const { isCompleted } = useTrackCourse({courseId: processedEvent?.d});
|
||||||
|
|
||||||
const fetchAuthor = useCallback(async (pubkey) => {
|
const fetchAuthor = useCallback(async (pubkey) => {
|
||||||
if (!pubkey) return;
|
if (!pubkey) return;
|
||||||
const author = await ndk.getUser({ pubkey });
|
const author = await ndk.getUser({ pubkey });
|
||||||
@ -126,6 +129,7 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
|
|||||||
<div className="w-full mx-auto px-4 py-8 -mt-32 relative z-10 max-mob:px-0 max-tab:px-0">
|
<div className="w-full mx-auto px-4 py-8 -mt-32 relative z-10 max-mob:px-0 max-tab:px-0">
|
||||||
<i className={`pi pi-arrow-left cursor-pointer hover:opacity-75 absolute top-0 left-4`} onClick={() => router.push('/')} />
|
<i className={`pi pi-arrow-left cursor-pointer hover:opacity-75 absolute top-0 left-4`} onClick={() => router.push('/')} />
|
||||||
<div className="mb-8 bg-gray-800/70 rounded-lg p-4 max-mob:rounded-t-none max-tab:rounded-t-none">
|
<div className="mb-8 bg-gray-800/70 rounded-lg p-4 max-mob:rounded-t-none max-tab:rounded-t-none">
|
||||||
|
{isCompleted && <Tag severity="success" value="Completed" />}
|
||||||
<div className="flex flex-row items-center justify-between w-full">
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
<h1 className='text-4xl font-bold text-white'>{processedEvent.name}</h1>
|
<h1 className='text-4xl font-bold text-white'>{processedEvent.name}</h1>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
@ -20,10 +20,9 @@ const MDDisplay = dynamic(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => {
|
||||||
const [zapAmount, setZapAmount] = useState(0);
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
const [nAddress, setNAddress] = useState(null);
|
const [nAddress, setNAddress] = useState(null);
|
||||||
const [completed, setCompleted] = useState(false);
|
|
||||||
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
const windowWidth = useWindowWidth();
|
const windowWidth = useWindowWidth();
|
||||||
@ -56,10 +55,10 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
|||||||
}, [lesson]);
|
}, [lesson]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCompleted) {
|
if (isCompleted && !isTracking) {
|
||||||
setCompleted(lesson.id);
|
setCompleted(lesson.id);
|
||||||
}
|
}
|
||||||
}, [isCompleted, lesson.id, setCompleted]);
|
}, [isCompleted, lesson.id, setCompleted, isTracking]);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (isPaid && decryptionPerformed) {
|
if (isPaid && decryptionPerformed) {
|
||||||
|
@ -89,10 +89,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCompleted) {
|
if (isCompleted && !isTracking) {
|
||||||
setCompleted(lesson.id);
|
setCompleted(lesson.id);
|
||||||
}
|
}
|
||||||
}, [isCompleted, lesson.id]); // Remove setCompleted from dependencies
|
}, [isCompleted, lesson.id, setCompleted, isTracking]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!zaps || zapsLoading || zapsError) return;
|
if (!zaps || zapsLoading || zapsError) return;
|
||||||
@ -115,7 +115,6 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
const timer = setTimeout(checkDuration, 500);
|
const timer = setTimeout(checkDuration, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
} else {
|
} else {
|
||||||
// For non-paid content, start checking after 3 seconds
|
|
||||||
const timer = setTimeout(checkDuration, 3000);
|
const timer = setTimeout(checkDuration, 3000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
84
src/db/models/userCourseModels.js
Normal file
84
src/db/models/userCourseModels.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import prisma from "@/db/prisma";
|
||||||
|
|
||||||
|
export const getUserCourses = async (userId) => {
|
||||||
|
return await prisma.userCourse.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: { course: true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserCourse = async (userId, courseId) => {
|
||||||
|
return await prisma.userCourse.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_courseId: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { course: true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createOrUpdateUserCourse = async (userId, courseId, data) => {
|
||||||
|
return await prisma.userCourse.upsert({
|
||||||
|
where: {
|
||||||
|
userId_courseId: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserCourse = async (userId, courseId) => {
|
||||||
|
return await prisma.userCourse.delete({
|
||||||
|
where: {
|
||||||
|
userId_courseId: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkCourseCompletion = async (userId, courseId) => {
|
||||||
|
const course = await prisma.course.findUnique({
|
||||||
|
where: { id: courseId },
|
||||||
|
include: {
|
||||||
|
lessons: {
|
||||||
|
include: {
|
||||||
|
userLessons: {
|
||||||
|
where: { userId: userId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
throw new Error("Course not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLessonsCompleted = course.lessons.every(lesson =>
|
||||||
|
lesson.userLessons.length > 0 && lesson.userLessons[0].completed
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allLessonsCompleted) {
|
||||||
|
await createOrUpdateUserCourse(userId, courseId, {
|
||||||
|
completed: true,
|
||||||
|
completedAt: new Date()
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
@ -20,7 +20,6 @@ export const getUserLesson = async (userId, lessonId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createOrUpdateUserLesson = async (userId, lessonId, data) => {
|
export const createOrUpdateUserLesson = async (userId, lessonId, data) => {
|
||||||
console.log(`Creating or updating user lesson for user ${userId} and lesson ${lessonId} with data:`, data);
|
|
||||||
return await prisma.userLesson.upsert({
|
return await prisma.userLesson.upsert({
|
||||||
where: {
|
where: {
|
||||||
userId_lessonId: {
|
userId_lessonId: {
|
||||||
|
45
src/hooks/tracking/useTrackCourse.js
Normal file
45
src/hooks/tracking/useTrackCourse.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, {useState, useEffect, useRef, useCallback} from 'react';
|
||||||
|
import {useSession} from 'next-auth/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const useTrackCourse = ({courseId}) => {
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
const {data: session} = useSession();
|
||||||
|
const completedRef = useRef(false);
|
||||||
|
|
||||||
|
const checkOrCreateUserCourse = useCallback(async () => {
|
||||||
|
if (!session?.user) return false;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`);
|
||||||
|
if (response.status === 200 && response?.data) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
completedRef.current = true;
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, {
|
||||||
|
completed: false,
|
||||||
|
started: true,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsCompleted(false);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.error('Error checking or creating UserCourse:', response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking or creating UserCourse:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [session, courseId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!completedRef.current && courseId) {
|
||||||
|
checkOrCreateUserCourse();
|
||||||
|
}
|
||||||
|
}, [checkOrCreateUserCourse, courseId]);
|
||||||
|
|
||||||
|
return {isCompleted};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTrackCourse;
|
@ -18,26 +18,20 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
// Check if the lesson is already completed or create a new UserLesson record
|
|
||||||
const checkOrCreateUserLesson = useCallback(async () => {
|
const checkOrCreateUserLesson = useCallback(async () => {
|
||||||
if (!session?.user) return false;
|
if (!session?.user) return false;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`);
|
const response = await axios.get(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`);
|
||||||
if (response.status === 200 && response?.data) {
|
if (response.status === 200 && response?.data) {
|
||||||
// Case 1: UserLesson record exists
|
|
||||||
if (response?.data?.completed) {
|
if (response?.data?.completed) {
|
||||||
// Lesson is already completed
|
|
||||||
setIsCompleted(true);
|
setIsCompleted(true);
|
||||||
completedRef.current = true;
|
completedRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Lesson exists but is not completed
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (response.status === 204) {
|
} else if (response.status === 204) {
|
||||||
// Case 2: UserLesson record doesn't exist, create a new one
|
|
||||||
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
|
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
|
||||||
// currently the only id we get is the resource id which associates to the lesson
|
|
||||||
resourceId: lessonId,
|
resourceId: lessonId,
|
||||||
opened: true,
|
opened: true,
|
||||||
openedAt: new Date().toISOString(),
|
openedAt: new Date().toISOString(),
|
||||||
@ -51,7 +45,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
console.error('Error checking or creating UserLesson:', error);
|
console.error('Error checking or creating UserLesson:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [session, lessonId]);
|
}, [session, lessonId, courseId]);
|
||||||
|
|
||||||
const markLessonAsCompleted = useCallback(async () => {
|
const markLessonAsCompleted = useCallback(async () => {
|
||||||
if (!session?.user || completedRef.current) return;
|
if (!session?.user || completedRef.current) return;
|
||||||
@ -72,15 +66,13 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error marking lesson as completed:', error);
|
console.error('Error marking lesson as completed:', error);
|
||||||
}
|
}
|
||||||
}, [lessonId, session]);
|
}, [lessonId, courseId, session]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeTracking = async () => {
|
const initializeTracking = async () => {
|
||||||
console.log('initializeTracking', videoDuration, !completedRef.current, videoPlayed);
|
if (isAdmin) return;
|
||||||
if (isAdmin) return; // Skip tracking for admin users
|
|
||||||
|
|
||||||
const alreadyCompleted = await checkOrCreateUserLesson();
|
const alreadyCompleted = await checkOrCreateUserLesson();
|
||||||
// Case 3: Start tracking if the lesson is not completed, video duration is available, and video has been played
|
|
||||||
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
|
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
|
||||||
console.log(`Tracking started for lesson ${lessonId}, video duration: ${videoDuration} seconds, video played: ${videoPlayed}`);
|
console.log(`Tracking started for lesson ${lessonId}, video duration: ${videoDuration} seconds, video played: ${videoPlayed}`);
|
||||||
setIsTracking(true);
|
setIsTracking(true);
|
||||||
@ -92,7 +84,6 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
|
|
||||||
initializeTracking();
|
initializeTracking();
|
||||||
|
|
||||||
// Cleanup function to clear the interval when the component unmounts
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
@ -101,9 +92,8 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
|
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAdmin) return; // Skip tracking for admin users
|
if (isAdmin) return;
|
||||||
|
|
||||||
// Case 4: Mark lesson as completed when 90% of the video is watched
|
|
||||||
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
|
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
|
||||||
markLessonAsCompleted();
|
markLessonAsCompleted();
|
||||||
}
|
}
|
||||||
|
25
src/pages/api/users/[slug]/courses/[courseSlug].js
Normal file
25
src/pages/api/users/[slug]/courses/[courseSlug].js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { checkCourseCompletion } from "@/db/models/userCourseModels";
|
||||||
|
|
||||||
|
// todo somehow make it to where we can get lesson slug in this endpoint
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const { method } = req;
|
||||||
|
const { slug, courseSlug } = req.query;
|
||||||
|
switch (method) {
|
||||||
|
case "GET":
|
||||||
|
try {
|
||||||
|
const courseCompletion = await checkCourseCompletion(slug, courseSlug);
|
||||||
|
if (courseCompletion) {
|
||||||
|
res.status(200).json(courseCompletion);
|
||||||
|
} else {
|
||||||
|
res.status(204).end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
|
||||||
|
res.status(405).end(`Method ${method} Not Allowed`);
|
||||||
|
}
|
||||||
|
}
|
21
src/pages/api/users/[slug]/courses/index.js
Normal file
21
src/pages/api/users/[slug]/courses/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { createOrUpdateUserCourse } from "@/db/models/userCourseModels";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const { method } = req;
|
||||||
|
const { slug, courseSlug } = req.query;
|
||||||
|
const userId = slug;
|
||||||
|
switch (method) {
|
||||||
|
case "POST":
|
||||||
|
try {
|
||||||
|
const userCourse = await createOrUpdateUserCourse(userId, courseSlug, req.body);
|
||||||
|
res.status(201).json(userCourse);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.setHeader("Allow", ["GET", "POST"]);
|
||||||
|
res.status(405).end(`Method ${method} Not Allowed`);
|
||||||
|
}
|
||||||
|
}
|
@ -143,10 +143,10 @@ const Course = () => {
|
|||||||
const [expandedIndex, setExpandedIndex] = useState(null);
|
const [expandedIndex, setExpandedIndex] = useState(null);
|
||||||
const [completedLessons, setCompletedLessons] = useState([]);
|
const [completedLessons, setCompletedLessons] = useState([]);
|
||||||
|
|
||||||
const setCompleted = (lessonId) => {
|
const setCompleted = useCallback((lessonId) => {
|
||||||
console.log('setting completed', lessonId);
|
console.log('setting completed', lessonId);
|
||||||
setCompletedLessons(prev => [...prev, lessonId]);
|
setCompletedLessons(prev => [...prev, lessonId]);
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
const fetchAuthor = useCallback(async (pubkey) => {
|
const fetchAuthor = useCallback(async (pubkey) => {
|
||||||
const author = await ndk.getUser({ pubkey });
|
const author = await ndk.getUser({ pubkey });
|
||||||
@ -247,7 +247,7 @@ const Course = () => {
|
|||||||
<div className="w-full py-4 rounded-b-lg">
|
<div className="w-full py-4 rounded-b-lg">
|
||||||
{lesson.type === 'video' ?
|
{lesson.type === 'video' ?
|
||||||
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
|
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
|
||||||
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />
|
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</AccordionTab>
|
</AccordionTab>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user