Progress on course progress flow, lso fixes for course tracking

This commit is contained in:
austinkelsay 2024-09-23 22:44:32 -05:00
parent 215a00e593
commit 4f98ea3656
8 changed files with 156 additions and 52 deletions

View 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;

View File

@ -5,7 +5,7 @@ import { Column } from "primereact/column";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { ProgressSpinner } from "primereact/progressspinner"; import { ProgressSpinner } from "primereact/progressspinner";
import PurchasedListItem from "@/components/profile/PurchasedListItem"; import ProgressListItem from "@/components/content/lists/ProgressListItem";
import { useNDKContext } from "@/context/NDKContext"; import { useNDKContext } from "@/context/NDKContext";
import { formatDateTime } from "@/utils/time"; import { formatDateTime } from "@/utils/time";
import { Tooltip } from "primereact/tooltip"; import { Tooltip } from "primereact/tooltip";
@ -15,6 +15,7 @@ import GithubContributionChart from "@/components/charts/GithubContributionChart
import useWindowWidth from "@/hooks/useWindowWidth"; import useWindowWidth from "@/hooks/useWindowWidth";
import { useToast } from "@/hooks/useToast"; import { useToast } from "@/hooks/useToast";
import UserProgress from "@/components/profile/progress/UserProgress"; import UserProgress from "@/components/profile/progress/UserProgress";
import { classNames } from "primereact/utils";
const UserProfile = () => { const UserProfile = () => {
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
@ -93,7 +94,7 @@ const UserProfile = () => {
) : ( ) : (
<DataTable <DataTable
emptyMessage="No Courses or Milestones completed" emptyMessage="No Courses or Milestones completed"
value={session.user?.purchased} value={session.user?.userCourses}
header={header} header={header}
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }} style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
pt={{ 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 <Column
body={(rowData) => { body={(rowData) => {
console.log("rowData", rowData); return <ProgressListItem dTag={rowData.courseId} category="name" />
return <PurchasedListItem eventId={rowData?.resource?.noteId || rowData?.course?.noteId} category={rowData?.course ? "courses" : "resources"} />
}} }}
header="Name" header="Name"
></Column> ></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> <Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
</DataTable> </DataTable>
)} )}

View File

@ -8,7 +8,7 @@ import { ProgressSpinner } from "primereact/progressspinner";
import { useNDKContext } from "@/context/NDKContext"; import { useNDKContext } from "@/context/NDKContext";
import useWindowWidth from "@/hooks/useWindowWidth"; import useWindowWidth from "@/hooks/useWindowWidth";
import Image from "next/image"; import Image from "next/image";
import PurchasedListItem from "@/components/profile/PurchasedListItem"; import PurchasedListItem from "@/components/content/lists/PurchasedListItem";
import { formatDateTime } from "@/utils/time"; import { formatDateTime } from "@/utils/time";
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect"; import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
import { Panel } from "primereact/panel"; import { Panel } from "primereact/panel";

View File

@ -1,11 +1,55 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ProgressBar } from 'primereact/progressbar'; import { ProgressBar } from 'primereact/progressbar';
import { Accordion, AccordionTab } from 'primereact/accordion'; import { Accordion, AccordionTab } from 'primereact/accordion';
import { useSession } from 'next-auth/react';
const UserProgress = () => { const UserProgress = () => {
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [currentTier, setCurrentTier] = useState('Pleb'); const [currentTier, setCurrentTier] = useState('Pleb');
const [expanded, setExpanded] = useState(null); 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 () => { const getProgress = async () => {
return 10; return 10;
@ -15,8 +59,6 @@ const UserProgress = () => {
return 'Pleb'; return 'Pleb';
}; };
useEffect(() => { 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 fetchProgress = async () => {
const progress = await getProgress(); const progress = await getProgress();
const currentTier = await getCurrentTier(); const currentTier = await getCurrentTier();
@ -27,22 +69,6 @@ const UserProgress = () => {
fetchProgress(); 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 ( return (
<div className="bg-gray-800 rounded-3xl p-6 w-[500px] mx-auto my-8"> <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> <h1 className="text-3xl font-bold text-white mb-2">Your Dev Journey</h1>

View File

@ -11,11 +11,13 @@ const useTrackCourse = ({courseId, paidCourse, decryptionPerformed}) => {
if (!session?.user || !courseId) return false; if (!session?.user || !courseId) return false;
try { try {
const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`); const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`);
// fix this condition?
if (response.status === 200 && response?.data) { if (response.status === 200 && response?.data) {
setIsCompleted(true); setIsCompleted(true);
completedRef.current = true; completedRef.current = true;
} else if (response.status === 204) { } 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 // 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)) { if (paidCourse === false || (paidCourse && decryptionPerformed)) {
console.log("creating new UserCourse entry"); console.log("creating new UserCourse entry");
await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, { await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, {

View File

@ -19,6 +19,8 @@ const MDDisplay = dynamic(() => import("@uiw/react-markdown-preview"), { ssr: fa
const useCourseData = (ndk, fetchAuthor, router) => { const useCourseData = (ndk, fetchAuthor, router) => {
const [course, setCourse] = useState(null); const [course, setCourse] = useState(null);
const [lessonIds, setLessonIds] = useState([]); const [lessonIds, setLessonIds] = useState([]);
const [paidCourse, setPaidCourse] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
@ -26,6 +28,7 @@ const useCourseData = (ndk, fetchAuthor, router) => {
const { data } = nip19.decode(slug); const { data } = nip19.decode(slug);
if (!data) { if (!data) {
showToast('error', 'Error', 'Course not found'); showToast('error', 'Error', 'Course not found');
setLoading(false);
return; return;
} }
const id = data?.identifier; const id = data?.identifier;
@ -41,9 +44,12 @@ const useCourseData = (ndk, fetchAuthor, router) => {
setLessonIds(lessonIds); setLessonIds(lessonIds);
const parsedCourse = { ...parseCourseEvent(event), author }; const parsedCourse = { ...parseCourseEvent(event), author };
setCourse(parsedCourse); setCourse(parsedCourse);
setPaidCourse(parsedCourse.price && parsedCourse.price > 0);
} }
setLoading(false);
} catch (error) { } catch (error) {
console.error('Error fetching event:', error); console.error('Error fetching event:', error);
setLoading(false);
} }
}; };
if (ndk && id) { if (ndk && id) {
@ -52,7 +58,7 @@ const useCourseData = (ndk, fetchAuthor, router) => {
} }
}, [router.isReady, router.query, ndk, fetchAuthor]); }, [router.isReady, router.query, ndk, fetchAuthor]);
return { course, lessonIds }; return { course, lessonIds, paidCourse, loading };
}; };
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
@ -64,7 +70,7 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
const fetchLesson = async (lessonId) => { const fetchLesson = async (lessonId) => {
try { try {
await ndk.connect(); 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); const event = await ndk.fetchEvent(filter);
if (event) { if (event) {
const author = await fetchAuthor(event.pubkey); const author = await fetchAuthor(event.pubkey);
@ -139,7 +145,6 @@ const Course = () => {
const { ndk, addSigner } = useNDKContext(); const { ndk, addSigner } = useNDKContext();
const { data: session, update } = useSession(); const { data: session, update } = useSession();
const { showToast } = useToast(); const { showToast } = useToast();
const [paidCourse, setPaidCourse] = useState(null);
const [expandedIndex, setExpandedIndex] = useState(null); const [expandedIndex, setExpandedIndex] = useState(null);
const [completedLessons, setCompletedLessons] = useState([]); const [completedLessons, setCompletedLessons] = useState([]);
@ -155,20 +160,14 @@ const Course = () => {
return fields; return fields;
}, [ndk]); }, [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 { 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(() => { useEffect(() => {
console.log('lessonIds', lessonIds); console.log('lessonIds', lessonIds);
}, [lessonIds]); }, [lessonIds]);
useEffect(() => {
if (course?.price && course?.price > 0) {
setPaidCourse(true);
}
}, [course]);
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
const { active } = router.query; const { active } = router.query;
@ -206,7 +205,7 @@ const Course = () => {
showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`); showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`);
} }
if (loading) { if (courseLoading || decryptionLoading) {
return ( return (
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div> <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
); );
@ -214,14 +213,16 @@ const Course = () => {
return ( return (
<> <>
<CourseDetailsNew {course && paidCourse !== null && (
processedEvent={course} <CourseDetailsNew
paidCourse={paidCourse} processedEvent={course}
lessons={uniqueLessons} paidCourse={paidCourse}
decryptionPerformed={decryptionPerformed} lessons={uniqueLessons}
handlePaymentSuccess={handlePaymentSuccess} decryptionPerformed={decryptionPerformed}
handlePaymentError={handlePaymentError} handlePaymentSuccess={handlePaymentSuccess}
/> handlePaymentError={handlePaymentError}
/>
)}
<Accordion <Accordion
activeIndex={expandedIndex} activeIndex={expandedIndex}
onTabChange={handleAccordionChange} onTabChange={handleAccordionChange}

View File

@ -5,6 +5,7 @@ import VideosCarousel from '@/components/content/carousels/VideosCarousel';
import DocumentsCarousel from '@/components/content/carousels/DocumentsCarousel'; import DocumentsCarousel from '@/components/content/carousels/DocumentsCarousel';
import InteractivePromotionalCarousel from '@/components/content/carousels/InteractivePromotionalCarousel'; import InteractivePromotionalCarousel from '@/components/content/carousels/InteractivePromotionalCarousel';
// todo: make paid course videos and documents not appear in carousels
export default function Home() { export default function Home() {
return ( return (
<> <>