mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +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 { 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>
|
||||||
)}
|
)}
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
@ -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}`, {
|
||||||
|
@ -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}
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user