From 4f98ea3656565e8f0663e557c821107c2488e2bf Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 23 Sep 2024 22:44:32 -0500 Subject: [PATCH] Progress on course progress flow, lso fixes for course tracking --- .../content/lists/ProgressListItem.js | 66 +++++++++++++++++++ .../lists}/PurchasedListItem.js | 0 src/components/profile/UserProfile.js | 20 ++++-- src/components/profile/UserSettings.js | 2 +- .../profile/progress/UserProgress.js | 62 ++++++++++++----- src/hooks/tracking/useTrackCourse.js | 2 + src/pages/course/[slug]/index.js | 55 ++++++++-------- src/pages/index.js | 1 + 8 files changed, 156 insertions(+), 52 deletions(-) create mode 100644 src/components/content/lists/ProgressListItem.js rename src/components/{profile => content/lists}/PurchasedListItem.js (100%) diff --git a/src/components/content/lists/ProgressListItem.js b/src/components/content/lists/ProgressListItem.js new file mode 100644 index 0000000..2fef8ee --- /dev/null +++ b/src/components/content/lists/ProgressListItem.js @@ -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 ( + + {event.name} + + ); + } else if (category === "lessons") { + const lessonsLength = event.tags.filter(tag => tag[0] === "a").length; + return {lessonsLength}; + } + + return null; + } + + return !event || !ndk || !dTag ? ( + + ) : ( + renderContent() + ); +} + +export default ProgressListItem; \ No newline at end of file diff --git a/src/components/profile/PurchasedListItem.js b/src/components/content/lists/PurchasedListItem.js similarity index 100% rename from src/components/profile/PurchasedListItem.js rename to src/components/content/lists/PurchasedListItem.js diff --git a/src/components/profile/UserProfile.js b/src/components/profile/UserProfile.js index e66f1f6..7167222 100644 --- a/src/components/profile/UserProfile.js +++ b/src/components/profile/UserProfile.js @@ -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 = () => { ) : ( { } }} > - + ( + + )} + > { - console.log("rowData", rowData); - return + return }} header="Name" > - item.courseId) ? "course" : "resource"} header="Category"> + { + return + }} header="Lessons"> formatDateTime(rowData?.createdAt)} header="Date"> )} diff --git a/src/components/profile/UserSettings.js b/src/components/profile/UserSettings.js index bc04d4e..6d2e752 100644 --- a/src/components/profile/UserSettings.js +++ b/src/components/profile/UserSettings.js @@ -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"; diff --git a/src/components/profile/progress/UserProgress.js b/src/components/profile/progress/UserProgress.js index 4182e90..c34eb82 100644 --- a/src/components/profile/progress/UserProgress.js +++ b/src/components/profile/progress/UserProgress.js @@ -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 (

Your Dev Journey

diff --git a/src/hooks/tracking/useTrackCourse.js b/src/hooks/tracking/useTrackCourse.js index fb71d96..b4fd413 100644 --- a/src/hooks/tracking/useTrackCourse.js +++ b/src/hooks/tracking/useTrackCourse.js @@ -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}`, { diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index 690d5ef..f6b7044 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -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 (
); @@ -214,21 +213,23 @@ const Course = () => { return ( <> - - + )} + {uniqueLessons.length > 0 && uniqueLessons.map((lesson, index) => ( - { } >
- {lesson.type === 'video' ? - : + {lesson.type === 'video' ? + : }
diff --git a/src/pages/index.js b/src/pages/index.js index 31f1616..fe31c3b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -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 ( <>