mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Progress on course progress flow, lso fixes for course tracking
This commit is contained in:
parent
215a00e593
commit
4f98ea3656
66
src/components/content/lists/ProgressListItem.js
Normal file
66
src/components/content/lists/ProgressListItem.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { parseCourseEvent } from "@/utils/nostr";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import appConfig from "@/config/appConfig";
|
||||
|
||||
const ProgressListItem = ({ dTag, category }) => {
|
||||
const { ndk } = useNDKContext();
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
if (!dTag) return;
|
||||
|
||||
try {
|
||||
await ndk.connect();
|
||||
const filter = {
|
||||
kinds: [30004],
|
||||
"#d": [dTag]
|
||||
}
|
||||
const event = await ndk.fetchEvent(filter);
|
||||
if (event) {
|
||||
setEvent(parseCourseEvent(event));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching event:", error);
|
||||
}
|
||||
}
|
||||
fetchEvent();
|
||||
}, [dTag, ndk]);
|
||||
|
||||
const encodeNaddr = () => {
|
||||
return nip19.naddrEncode({
|
||||
pubkey: event.pubkey,
|
||||
identifier: dTag,
|
||||
kind: 30004,
|
||||
relayUrls: appConfig.defaultRelayUrls
|
||||
})
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (!event) return null;
|
||||
|
||||
if (category === "name") {
|
||||
return (
|
||||
<a className="text-blue-500 underline hover:text-blue-600" href={`/course/${encodeNaddr()}`}>
|
||||
{event.name}
|
||||
</a>
|
||||
);
|
||||
} else if (category === "lessons") {
|
||||
const lessonsLength = event.tags.filter(tag => tag[0] === "a").length;
|
||||
return <span>{lessonsLength}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return !event || !ndk || !dTag ? (
|
||||
<ProgressSpinner className="w-[40px] h-[40px]" />
|
||||
) : (
|
||||
renderContent()
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressListItem;
|
@ -5,7 +5,7 @@ import { Column } from "primereact/column";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import PurchasedListItem from "@/components/profile/PurchasedListItem";
|
||||
import ProgressListItem from "@/components/content/lists/ProgressListItem";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { formatDateTime } from "@/utils/time";
|
||||
import { Tooltip } from "primereact/tooltip";
|
||||
@ -15,6 +15,7 @@ import GithubContributionChart from "@/components/charts/GithubContributionChart
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import UserProgress from "@/components/profile/progress/UserProgress";
|
||||
import { classNames } from "primereact/utils";
|
||||
|
||||
const UserProfile = () => {
|
||||
const windowWidth = useWindowWidth();
|
||||
@ -93,7 +94,7 @@ const UserProfile = () => {
|
||||
) : (
|
||||
<DataTable
|
||||
emptyMessage="No Courses or Milestones completed"
|
||||
value={session.user?.purchased}
|
||||
value={session.user?.userCourses}
|
||||
header={header}
|
||||
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
|
||||
pt={{
|
||||
@ -105,15 +106,22 @@ const UserProfile = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Column field="amountPaid" header="Cost"></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
|
||||
body={(rowData) => {
|
||||
console.log("rowData", rowData);
|
||||
return <PurchasedListItem eventId={rowData?.resource?.noteId || rowData?.course?.noteId} category={rowData?.course ? "courses" : "resources"} />
|
||||
return <ProgressListItem dTag={rowData.courseId} category="name" />
|
||||
}}
|
||||
header="Name"
|
||||
></Column>
|
||||
<Column body={session.user?.purchased?.some((item) => item.courseId) ? "course" : "resource"} header="Category"></Column>
|
||||
<Column body={(rowData) => {
|
||||
return <ProgressListItem dTag={rowData.courseId} category="lessons" />
|
||||
}} header="Lessons"></Column>
|
||||
<Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
|
||||
</DataTable>
|
||||
)}
|
||||
|
@ -8,7 +8,7 @@ import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import Image from "next/image";
|
||||
import PurchasedListItem from "@/components/profile/PurchasedListItem";
|
||||
import PurchasedListItem from "@/components/content/lists/PurchasedListItem";
|
||||
import { formatDateTime } from "@/utils/time";
|
||||
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
|
||||
import { Panel } from "primereact/panel";
|
||||
|
@ -1,11 +1,55 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
const UserProgress = () => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [currentTier, setCurrentTier] = useState('Pleb');
|
||||
const [expanded, setExpanded] = useState(null);
|
||||
const [completedCourses, setCompletedCourses] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
const user = session.user;
|
||||
const ids = user?.userCourses.map(course => course?.completed ? course.courseId : null).filter(id => id !== null);
|
||||
setCompletedCourses(ids);
|
||||
generateTasks(ids);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const generateTasks = (completedCourseIds) => {
|
||||
const allTasks = [
|
||||
{ status: 'Create Account', completed: true, tier: 'Pleb', courseId: null },
|
||||
{
|
||||
status: 'Complete PlebDevs Starter',
|
||||
completed: false,
|
||||
tier: 'New Dev',
|
||||
courseId: null,
|
||||
subTasks: [
|
||||
{ status: 'Connect GitHub', completed: false },
|
||||
{ status: 'Create First GitHub Repo', completed: false },
|
||||
{ status: 'Push Commit', completed: false }
|
||||
]
|
||||
},
|
||||
{ status: 'Complete PlebDevs Course 1', completed: false, tier: 'Junior Dev', courseId: 'd20e2e9b-5123-4a91-b27f-d75ea1d5811e' },
|
||||
{ status: 'Complete PlebDevs Course 2', completed: false, tier: 'Plebdev', courseId: 'aa3b1641-ad2b-4ef4-9f0f-38951ae307b7' },
|
||||
];
|
||||
|
||||
const updatedTasks = allTasks.map(task => ({
|
||||
...task,
|
||||
completed: task.courseId === null || completedCourseIds.includes(task.courseId),
|
||||
subTasks: task.subTasks ? task.subTasks.map(subTask => ({
|
||||
...subTask,
|
||||
completed: completedCourseIds.includes(task.courseId)
|
||||
})) : undefined
|
||||
}));
|
||||
|
||||
setTasks(updatedTasks);
|
||||
};
|
||||
|
||||
const getProgress = async () => {
|
||||
return 10;
|
||||
@ -15,8 +59,6 @@ const UserProgress = () => {
|
||||
return 'Pleb';
|
||||
};
|
||||
useEffect(() => {
|
||||
// Fetch progress and current tier from backend
|
||||
// For now, let's assume we have a function to get these values
|
||||
const fetchProgress = async () => {
|
||||
const progress = await getProgress();
|
||||
const currentTier = await getCurrentTier();
|
||||
@ -27,22 +69,6 @@ const UserProgress = () => {
|
||||
fetchProgress();
|
||||
}, []);
|
||||
|
||||
const tasks = [
|
||||
{ status: 'Create Account', completed: true, tier: 'Pleb' },
|
||||
{
|
||||
status: 'Complete PlebDevs Starter',
|
||||
completed: false,
|
||||
tier: 'New Dev',
|
||||
subTasks: [
|
||||
{ status: 'Connect GitHub', completed: false },
|
||||
{ status: 'Create First GitHub Repo', completed: false },
|
||||
{ status: 'Push Commit', completed: false }
|
||||
]
|
||||
},
|
||||
{ status: 'Complete PlebDevs Course 1', completed: false, tier: 'Junior Dev' },
|
||||
{ status: 'Complete PlebDevs Course 2', completed: false, tier: 'Plebdev' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-3xl p-6 w-[500px] mx-auto my-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Your Dev Journey</h1>
|
||||
|
@ -11,11 +11,13 @@ const useTrackCourse = ({courseId, paidCourse, decryptionPerformed}) => {
|
||||
if (!session?.user || !courseId) return false;
|
||||
try {
|
||||
const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`);
|
||||
// fix this condition?
|
||||
if (response.status === 200 && response?.data) {
|
||||
setIsCompleted(true);
|
||||
completedRef.current = true;
|
||||
} else if (response.status === 204) {
|
||||
// Only create a new UserCourse entry if it's a free course or if decryption has been performed for a paid course
|
||||
console.log("about to create new UserCourse entry", paidCourse, decryptionPerformed);
|
||||
if (paidCourse === false || (paidCourse && decryptionPerformed)) {
|
||||
console.log("creating new UserCourse entry");
|
||||
await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, {
|
||||
|
@ -19,6 +19,8 @@ const MDDisplay = dynamic(() => import("@uiw/react-markdown-preview"), { ssr: fa
|
||||
const useCourseData = (ndk, fetchAuthor, router) => {
|
||||
const [course, setCourse] = useState(null);
|
||||
const [lessonIds, setLessonIds] = useState([]);
|
||||
const [paidCourse, setPaidCourse] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
@ -26,6 +28,7 @@ const useCourseData = (ndk, fetchAuthor, router) => {
|
||||
const { data } = nip19.decode(slug);
|
||||
if (!data) {
|
||||
showToast('error', 'Error', 'Course not found');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const id = data?.identifier;
|
||||
@ -41,9 +44,12 @@ const useCourseData = (ndk, fetchAuthor, router) => {
|
||||
setLessonIds(lessonIds);
|
||||
const parsedCourse = { ...parseCourseEvent(event), author };
|
||||
setCourse(parsedCourse);
|
||||
setPaidCourse(parsedCourse.price && parsedCourse.price > 0);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching event:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (ndk && id) {
|
||||
@ -52,7 +58,7 @@ const useCourseData = (ndk, fetchAuthor, router) => {
|
||||
}
|
||||
}, [router.isReady, router.query, ndk, fetchAuthor]);
|
||||
|
||||
return { course, lessonIds };
|
||||
return { course, lessonIds, paidCourse, loading };
|
||||
};
|
||||
|
||||
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
|
||||
@ -64,7 +70,7 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
|
||||
const fetchLesson = async (lessonId) => {
|
||||
try {
|
||||
await ndk.connect();
|
||||
const filter = { "#d": [lessonId], kinds:[30023, 30402], authors: [pubkey] };
|
||||
const filter = { "#d": [lessonId], kinds: [30023, 30402], authors: [pubkey] };
|
||||
const event = await ndk.fetchEvent(filter);
|
||||
if (event) {
|
||||
const author = await fetchAuthor(event.pubkey);
|
||||
@ -107,7 +113,7 @@ const useDecryption = (session, paidCourse, course, lessons, setLessons) => {
|
||||
const decrypt = async () => {
|
||||
if (session?.user && paidCourse && !decryptionPerformed) {
|
||||
setLoading(true);
|
||||
const canAccess =
|
||||
const canAccess =
|
||||
session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
|
||||
session.user?.role?.subscribed ||
|
||||
session.user?.pubkey === course?.pubkey;
|
||||
@ -139,7 +145,6 @@ const Course = () => {
|
||||
const { ndk, addSigner } = useNDKContext();
|
||||
const { data: session, update } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const [paidCourse, setPaidCourse] = useState(null);
|
||||
const [expandedIndex, setExpandedIndex] = useState(null);
|
||||
const [completedLessons, setCompletedLessons] = useState([]);
|
||||
|
||||
@ -155,20 +160,14 @@ const Course = () => {
|
||||
return fields;
|
||||
}, [ndk]);
|
||||
|
||||
const { course, lessonIds } = useCourseData(ndk, fetchAuthor, router);
|
||||
const { course, lessonIds, paidCourse, loading: courseLoading } = useCourseData(ndk, fetchAuthor, router);
|
||||
const { lessons, uniqueLessons, setLessons } = useLessons(ndk, fetchAuthor, lessonIds, course?.pubkey);
|
||||
const { decryptionPerformed, loading } = useDecryption(session, paidCourse, course, lessons, setLessons);
|
||||
const { decryptionPerformed, loading: decryptionLoading } = useDecryption(session, paidCourse, course, lessons, setLessons);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('lessonIds', lessonIds);
|
||||
}, [lessonIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course?.price && course?.price > 0) {
|
||||
setPaidCourse(true);
|
||||
}
|
||||
}, [course]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
const { active } = router.query;
|
||||
@ -183,7 +182,7 @@ const Course = () => {
|
||||
const handleAccordionChange = (e) => {
|
||||
const newIndex = e.index === expandedIndex ? null : e.index;
|
||||
setExpandedIndex(newIndex);
|
||||
|
||||
|
||||
if (newIndex !== null) {
|
||||
router.push(`/course/${router.query.slug}?active=${newIndex}`, undefined, { shallow: true });
|
||||
} else {
|
||||
@ -206,7 +205,7 @@ const Course = () => {
|
||||
showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (courseLoading || decryptionLoading) {
|
||||
return (
|
||||
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
|
||||
);
|
||||
@ -214,21 +213,23 @@ const Course = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CourseDetailsNew
|
||||
processedEvent={course}
|
||||
paidCourse={paidCourse}
|
||||
lessons={uniqueLessons}
|
||||
decryptionPerformed={decryptionPerformed}
|
||||
handlePaymentSuccess={handlePaymentSuccess}
|
||||
handlePaymentError={handlePaymentError}
|
||||
/>
|
||||
<Accordion
|
||||
activeIndex={expandedIndex}
|
||||
{course && paidCourse !== null && (
|
||||
<CourseDetailsNew
|
||||
processedEvent={course}
|
||||
paidCourse={paidCourse}
|
||||
lessons={uniqueLessons}
|
||||
decryptionPerformed={decryptionPerformed}
|
||||
handlePaymentSuccess={handlePaymentSuccess}
|
||||
handlePaymentError={handlePaymentError}
|
||||
/>
|
||||
)}
|
||||
<Accordion
|
||||
activeIndex={expandedIndex}
|
||||
onTabChange={handleAccordionChange}
|
||||
className="mt-4 px-4 max-mob:px-0 max-tab:px-0"
|
||||
>
|
||||
{uniqueLessons.length > 0 && uniqueLessons.map((lesson, index) => (
|
||||
<AccordionTab
|
||||
<AccordionTab
|
||||
key={index}
|
||||
pt={{
|
||||
root: { className: 'border-none' },
|
||||
@ -245,8 +246,8 @@ const Course = () => {
|
||||
}
|
||||
>
|
||||
<div className="w-full py-4 rounded-b-lg">
|
||||
{lesson.type === 'video' ?
|
||||
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
|
||||
{lesson.type === 'video' ?
|
||||
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
|
||||
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />
|
||||
}
|
||||
</div>
|
||||
|
@ -5,6 +5,7 @@ import VideosCarousel from '@/components/content/carousels/VideosCarousel';
|
||||
import DocumentsCarousel from '@/components/content/carousels/DocumentsCarousel';
|
||||
import InteractivePromotionalCarousel from '@/components/content/carousels/InteractivePromotionalCarousel';
|
||||
|
||||
// todo: make paid course videos and documents not appear in carousels
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
|
Loading…
x
Reference in New Issue
Block a user