mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
Full listing out of lesson and course starts / completions in progress, new styles for progress and purchase tables
This commit is contained in:
parent
32a9cd7cdc
commit
a3adee3467
@ -91,7 +91,7 @@ const GithubContributionChart = ({ username }) => {
|
|||||||
{/* Days of week labels */}
|
{/* Days of week labels */}
|
||||||
<div className="flex flex-col gap-[3px] text-[11px] text-gray-400 pr-3">
|
<div className="flex flex-col gap-[3px] text-[11px] text-gray-400 pr-3">
|
||||||
{weekDays.map((day, index) => (
|
{weekDays.map((day, index) => (
|
||||||
<div key={day} className="h-[12px] leading-[12px]">
|
<div key={day} className="h-[13px] leading-[13px]">
|
||||||
{index % 2 === 0 && day}
|
{index % 2 === 0 && day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -105,7 +105,7 @@ const GithubContributionChart = ({ username }) => {
|
|||||||
row[weekIndex] && (
|
row[weekIndex] && (
|
||||||
<div
|
<div
|
||||||
key={`${weekIndex}-${dayIndex}`}
|
key={`${weekIndex}-${dayIndex}`}
|
||||||
className={`w-[12px] h-[12px] ${getColor(row[weekIndex].count)} rounded-[2px] cursor-pointer transition-colors duration-100`}
|
className={`w-[13px] h-[13px] ${getColor(row[weekIndex].count)} rounded-[2px] cursor-pointer transition-colors duration-100`}
|
||||||
title={`${row[weekIndex].date.toDateString()}: ${row[weekIndex].count} contribution${row[weekIndex].count !== 1 ? 's' : ''}`}
|
title={`${row[weekIndex].date.toDateString()}: ${row[weekIndex].count} contribution${row[weekIndex].count !== 1 ? 's' : ''}`}
|
||||||
></div>
|
></div>
|
||||||
)
|
)
|
||||||
@ -130,11 +130,11 @@ const GithubContributionChart = ({ username }) => {
|
|||||||
<div className="text-[11px] text-gray-400 flex items-center justify-end">
|
<div className="text-[11px] text-gray-400 flex items-center justify-end">
|
||||||
<span className="mr-2">Less</span>
|
<span className="mr-2">Less</span>
|
||||||
<div className="flex gap-[3px]">
|
<div className="flex gap-[3px]">
|
||||||
<div className="w-[12px] h-[12px] bg-gray-100 rounded-[2px]"></div>
|
<div className="w-[13px] h-[13px] bg-gray-100 rounded-[2px]"></div>
|
||||||
<div className="w-[12px] h-[12px] bg-green-300 rounded-[2px]"></div>
|
<div className="w-[13px] h-[13px] bg-green-300 rounded-[2px]"></div>
|
||||||
<div className="w-[12px] h-[12px] bg-green-400 rounded-[2px]"></div>
|
<div className="w-[13px] h-[13px] bg-green-400 rounded-[2px]"></div>
|
||||||
<div className="w-[12px] h-[12px] bg-green-600 rounded-[2px]"></div>
|
<div className="w-[13px] h-[13px] bg-green-600 rounded-[2px]"></div>
|
||||||
<div className="w-[12px] h-[12px] bg-green-700 rounded-[2px]"></div>
|
<div className="w-[13px] h-[13px] bg-green-700 rounded-[2px]"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-2">More</span>
|
<span className="ml-2">More</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,16 +13,29 @@ const UserProgressTable = ({ session, ndk, windowWidth }) => {
|
|||||||
const progressData = [];
|
const progressData = [];
|
||||||
|
|
||||||
session.user.userCourses.forEach(courseProgress => {
|
session.user.userCourses.forEach(courseProgress => {
|
||||||
progressData.push({
|
// Add course start entry
|
||||||
id: courseProgress.id,
|
if (courseProgress.started) {
|
||||||
type: 'course',
|
progressData.push({
|
||||||
name: courseProgress.course?.name,
|
id: `${courseProgress.id}-start`,
|
||||||
started: courseProgress.started,
|
type: 'course',
|
||||||
startedAt: courseProgress.startedAt,
|
name: courseProgress.course?.name,
|
||||||
completed: courseProgress.completed,
|
eventType: 'started',
|
||||||
completedAt: courseProgress.completedAt,
|
date: courseProgress.startedAt,
|
||||||
courseId: courseProgress.courseId
|
courseId: courseProgress.courseId
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add course completion entry
|
||||||
|
if (courseProgress.completed) {
|
||||||
|
progressData.push({
|
||||||
|
id: `${courseProgress.id}-complete`,
|
||||||
|
type: 'course',
|
||||||
|
name: courseProgress.course?.name,
|
||||||
|
eventType: 'completed',
|
||||||
|
date: courseProgress.completedAt,
|
||||||
|
courseId: courseProgress.courseId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add lesson entries
|
// Add lesson entries
|
||||||
const courseLessons = session.user.userLessons?.filter(
|
const courseLessons = session.user.userLessons?.filter(
|
||||||
@ -30,22 +43,38 @@ const UserProgressTable = ({ session, ndk, windowWidth }) => {
|
|||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
courseLessons.forEach(lessonProgress => {
|
courseLessons.forEach(lessonProgress => {
|
||||||
progressData.push({
|
// Add lesson start entry
|
||||||
id: lessonProgress.id,
|
if (lessonProgress.opened) {
|
||||||
type: 'lesson',
|
progressData.push({
|
||||||
name: lessonProgress.lesson?.name,
|
id: `${lessonProgress.id}-start`,
|
||||||
started: lessonProgress.opened,
|
type: 'lesson',
|
||||||
startedAt: lessonProgress.openedAt,
|
name: lessonProgress.lesson?.name,
|
||||||
completed: lessonProgress.completed,
|
eventType: 'started',
|
||||||
completedAt: lessonProgress.completedAt,
|
date: lessonProgress.openedAt,
|
||||||
courseId: courseProgress.courseId,
|
courseId: courseProgress.courseId,
|
||||||
lessonId: lessonProgress.lessonId,
|
lessonId: lessonProgress.lessonId,
|
||||||
resourceId: lessonProgress.lesson?.resourceId
|
resourceId: lessonProgress.lesson?.resourceId
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lesson completion entry
|
||||||
|
if (lessonProgress.completed) {
|
||||||
|
progressData.push({
|
||||||
|
id: `${lessonProgress.id}-complete`,
|
||||||
|
type: 'lesson',
|
||||||
|
name: lessonProgress.lesson?.name,
|
||||||
|
eventType: 'completed',
|
||||||
|
date: lessonProgress.completedAt,
|
||||||
|
courseId: courseProgress.courseId,
|
||||||
|
lessonId: lessonProgress.lessonId,
|
||||||
|
resourceId: lessonProgress.lesson?.resourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return progressData;
|
// Sort by date, most recent first
|
||||||
|
return progressData.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
};
|
};
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
@ -54,6 +83,37 @@ const UserProgressTable = ({ session, ndk, windowWidth }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const typeTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className={`pi ${rowData.type === 'course' ? 'pi-book' : 'pi-file'} text-lg`}></i>
|
||||||
|
<span className="capitalize">{rowData.type}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className={`pi ${rowData.eventType === 'started' ? 'pi-play' : 'pi-check-circle'}
|
||||||
|
${rowData.eventType === 'started' ? 'text-blue-500' : 'text-green-500'} text-lg`}></i>
|
||||||
|
<span className="capitalize">{rowData.eventType}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{rowData.type === 'course'
|
||||||
|
? <ProgressListItem dTag={rowData.courseId} category="name" type="course" />
|
||||||
|
: <ProgressListItem dTag={rowData.resourceId} category="name" type="lesson" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className="pi pi-calendar text-gray-400"></i>
|
||||||
|
<span>{formatDateTime(rowData.date)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (!session || !session?.user || !ndk) {
|
if (!session || !session?.user || !ndk) {
|
||||||
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
|
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
|
||||||
}
|
}
|
||||||
@ -63,60 +123,45 @@ const UserProgressTable = ({ session, ndk, windowWidth }) => {
|
|||||||
emptyMessage="No Courses or Milestones completed"
|
emptyMessage="No Courses or Milestones completed"
|
||||||
value={prepareProgressData()}
|
value={prepareProgressData()}
|
||||||
header={header}
|
header={header}
|
||||||
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
|
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto" }}
|
||||||
pt={{
|
pt={{
|
||||||
wrapper: {
|
wrapper: {
|
||||||
className: "rounded-lg rounded-t-none"
|
className: "rounded-b-lg shadow-md"
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
className: "rounded-t-lg"
|
className: "rounded-t-lg border-b border-gray-700"
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
className: "text-gray-300 font-semibold"
|
||||||
|
},
|
||||||
|
bodyRow: {
|
||||||
|
className: "border-b border-gray-700"
|
||||||
|
},
|
||||||
|
bodyCell: {
|
||||||
|
className: "text-gray-200 p-4"
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
stripedRows
|
||||||
>
|
>
|
||||||
<Column
|
<Column
|
||||||
field="type"
|
field="type"
|
||||||
header="Type"
|
header="Type"
|
||||||
body={(rowData) => (
|
body={typeTemplate}
|
||||||
<span>{rowData.type}</span>
|
|
||||||
)}
|
|
||||||
></Column>
|
></Column>
|
||||||
<Column
|
<Column
|
||||||
field="started"
|
field="eventType"
|
||||||
header="Started"
|
header="Event"
|
||||||
body={(rowData) => (
|
body={eventTemplate}
|
||||||
<i className={classNames('pi', {
|
|
||||||
'pi-check-circle text-blue-500': rowData.started,
|
|
||||||
'pi-times-circle text-gray-500': !rowData.started
|
|
||||||
})}></i>
|
|
||||||
)}
|
|
||||||
></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
|
<Column
|
||||||
field="name"
|
field="name"
|
||||||
header="Name"
|
header="Name"
|
||||||
body={(rowData) => (
|
body={nameTemplate}
|
||||||
rowData.type === 'course'
|
|
||||||
? <ProgressListItem dTag={rowData.courseId} category="name" type="course" />
|
|
||||||
: <ProgressListItem dTag={rowData.resourceId} category="name" type="lesson" />
|
|
||||||
)}
|
|
||||||
></Column>
|
></Column>
|
||||||
<Column
|
<Column
|
||||||
body={rowData => {
|
field="date"
|
||||||
if (rowData.completed) {
|
body={dateTemplate}
|
||||||
return formatDateTime(rowData.completedAt);
|
header="Date"
|
||||||
}
|
|
||||||
return formatDateTime(rowData.startedAt) || formatDateTime(rowData.createdAt);
|
|
||||||
}}
|
|
||||||
header="Last Activity"
|
|
||||||
></Column>
|
></Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
|
95
src/components/profile/DataTables/UserPurchaseTable.js
Normal file
95
src/components/profile/DataTables/UserPurchaseTable.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataTable } from "primereact/datatable";
|
||||||
|
import { Column } from "primereact/column";
|
||||||
|
import PurchasedListItem from "@/components/content/lists/PurchasedListItem";
|
||||||
|
import { formatDateTime } from "@/utils/time";
|
||||||
|
|
||||||
|
const UserPurchaseTable = ({ session, windowWidth }) => {
|
||||||
|
const purchasesHeader = (
|
||||||
|
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<span className="text-xl text-gray-200 font-bold">Purchases</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const costTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className="pi pi-wallet text-yellow-500 text-lg"></i>
|
||||||
|
<span>{rowData.amountPaid} sats</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<PurchasedListItem
|
||||||
|
eventId={rowData?.resource?.noteId || rowData?.course?.noteId}
|
||||||
|
category={rowData?.course ? "courses" : "resources"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const categoryTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className={`pi ${rowData?.course ? 'pi-book' : 'pi-file'} text-lg`}></i>
|
||||||
|
<span className="capitalize">{rowData?.course ? 'course' : 'resource'}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateTemplate = (rowData) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<i className="pi pi-calendar text-gray-400"></i>
|
||||||
|
<span>{formatDateTime(rowData?.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
session && session?.user && (
|
||||||
|
<DataTable
|
||||||
|
emptyMessage="No purchases"
|
||||||
|
value={session.user?.purchased}
|
||||||
|
header={purchasesHeader}
|
||||||
|
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", marginTop: "2rem" }}
|
||||||
|
pt={{
|
||||||
|
wrapper: {
|
||||||
|
className: "rounded-b-lg shadow-md"
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
className: "rounded-t-lg border-b border-gray-700"
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
className: "text-gray-300 font-semibold"
|
||||||
|
},
|
||||||
|
bodyRow: {
|
||||||
|
className: "border-b border-gray-700"
|
||||||
|
},
|
||||||
|
bodyCell: {
|
||||||
|
className: "text-gray-200 p-4"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
stripedRows
|
||||||
|
>
|
||||||
|
<Column
|
||||||
|
field="amountPaid"
|
||||||
|
header="Cost"
|
||||||
|
body={costTemplate}
|
||||||
|
></Column>
|
||||||
|
<Column
|
||||||
|
field="name"
|
||||||
|
header="Name"
|
||||||
|
body={nameTemplate}
|
||||||
|
></Column>
|
||||||
|
<Column
|
||||||
|
field="category"
|
||||||
|
header="Category"
|
||||||
|
body={categoryTemplate}
|
||||||
|
></Column>
|
||||||
|
<Column
|
||||||
|
field="createdAt"
|
||||||
|
header="Date"
|
||||||
|
body={dateTemplate}
|
||||||
|
></Column>
|
||||||
|
</DataTable>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPurchaseTable;
|
@ -20,6 +20,7 @@ import { useToast } from "@/hooks/useToast";
|
|||||||
import UserProgress from "@/components/profile/progress/UserProgress";
|
import UserProgress from "@/components/profile/progress/UserProgress";
|
||||||
import { classNames } from "primereact/utils";
|
import { classNames } from "primereact/utils";
|
||||||
import UserProgressTable from '@/components/profile/DataTables/UserProgressTable';
|
import UserProgressTable from '@/components/profile/DataTables/UserProgressTable';
|
||||||
|
import UserPurchaseTable from '@/components/profile/DataTables/UserPurchaseTable';
|
||||||
|
|
||||||
const UserProfile = () => {
|
const UserProfile = () => {
|
||||||
const windowWidth = useWindowWidth();
|
const windowWidth = useWindowWidth();
|
||||||
@ -103,14 +104,14 @@ const UserProfile = () => {
|
|||||||
className="rounded-full my-4"
|
className="rounded-full my-4"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-8 right-80 max-tab:right-20 max-mob:left-0">
|
<div className="absolute top-8 right-80 max-tab:right-20 max-mob:left-0">
|
||||||
<i
|
<i
|
||||||
className="pi pi-ellipsis-h text-2xl cursor-pointer"
|
className="pi pi-ellipsis-h text-2xl cursor-pointer"
|
||||||
onClick={(e) => menu.current.toggle(e)}
|
onClick={(e) => menu.current.toggle(e)}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
model={menuItems}
|
model={menuItems}
|
||||||
popup
|
popup
|
||||||
ref={menu}
|
ref={menu}
|
||||||
id="profile-options-menu"
|
id="profile-options-menu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -143,37 +144,15 @@ const UserProfile = () => {
|
|||||||
)}
|
)}
|
||||||
<UserProgress />
|
<UserProgress />
|
||||||
</div>
|
</div>
|
||||||
<UserProgressTable
|
<UserProgressTable
|
||||||
session={session}
|
session={session}
|
||||||
ndk={ndk}
|
ndk={ndk}
|
||||||
windowWidth={windowWidth}
|
windowWidth={windowWidth}
|
||||||
/>
|
/>
|
||||||
{session && session?.user && (
|
<UserPurchaseTable
|
||||||
<DataTable
|
session={session}
|
||||||
emptyMessage="No purchases"
|
windowWidth={windowWidth}
|
||||||
value={session.user?.purchased}
|
/>
|
||||||
header={purchasesHeader}
|
|
||||||
style={{ maxWidth: windowWidth < 768 ? "100%" : "90%", margin: "0 auto", borderRadius: "10px" }}
|
|
||||||
pt={{
|
|
||||||
wrapper: {
|
|
||||||
className: "rounded-lg rounded-t-none"
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
className: "rounded-t-lg mt-4"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Column field="amountPaid" header="Cost"></Column>
|
|
||||||
<Column
|
|
||||||
body={(rowData) => {
|
|
||||||
return <PurchasedListItem eventId={rowData?.resource?.noteId || rowData?.course?.noteId} category={rowData?.course ? "courses" : "resources"} />
|
|
||||||
}}
|
|
||||||
header="Name"
|
|
||||||
></Column>
|
|
||||||
<Column body={session.user?.purchased?.some((item) => item.courseId) ? "course" : "resource"} header="Category"></Column>
|
|
||||||
<Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
|
|
||||||
</DataTable>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -85,7 +85,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed, pa
|
|||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
setTimeSpent(prevTime => {
|
setTimeSpent(prevTime => {
|
||||||
const newTime = prevTime + 1;
|
const newTime = prevTime + 1;
|
||||||
// console.log(`⏱️ Time spent: ${newTime}s / ${videoDuration}s (${((newTime/videoDuration)*100).toFixed(1)}%)`);
|
console.log(`⏱️ Time spent: ${newTime}s / ${videoDuration}s (${((newTime/videoDuration)*100).toFixed(1)}%)`);
|
||||||
return newTime;
|
return newTime;
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user