mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: perf improvements and bug fixes
This commit is contained in:
parent
93d0e2a104
commit
59bae82ee7
10 changed files with 200 additions and 112 deletions
|
|
@ -17,7 +17,7 @@ from src.db.courses.courses import (
|
||||||
CourseCreate,
|
CourseCreate,
|
||||||
CourseRead,
|
CourseRead,
|
||||||
CourseUpdate,
|
CourseUpdate,
|
||||||
FullCourseReadWithTrail,
|
FullCourseRead,
|
||||||
AuthorWithRole,
|
AuthorWithRole,
|
||||||
)
|
)
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
|
|
@ -129,7 +129,7 @@ async def get_course_meta(
|
||||||
with_unpublished_activities: bool,
|
with_unpublished_activities: bool,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
) -> FullCourseReadWithTrail:
|
) -> FullCourseRead:
|
||||||
# Avoid circular import
|
# Avoid circular import
|
||||||
from src.services.courses.chapters import get_course_chapters
|
from src.services.courses.chapters import get_course_chapters
|
||||||
|
|
||||||
|
|
@ -156,30 +156,10 @@ async def get_course_meta(
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||||
|
|
||||||
# Start async tasks concurrently
|
# Get course chapters
|
||||||
tasks = []
|
chapters = []
|
||||||
|
if course.id is not None:
|
||||||
# Task 1: Get course chapters
|
chapters = await get_course_chapters(request, course.id, db_session, current_user, with_unpublished_activities)
|
||||||
async def get_chapters():
|
|
||||||
# Ensure course.id is not None
|
|
||||||
if course.id is None:
|
|
||||||
return []
|
|
||||||
return await get_course_chapters(request, course.id, db_session, current_user, with_unpublished_activities)
|
|
||||||
|
|
||||||
# Task 2: Get user trail (only for authenticated users)
|
|
||||||
async def get_trail():
|
|
||||||
if isinstance(current_user, AnonymousUser):
|
|
||||||
return None
|
|
||||||
return await get_user_trail_with_orgid(
|
|
||||||
request, current_user, course.org_id, db_session
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add tasks to the list
|
|
||||||
tasks.append(get_chapters())
|
|
||||||
tasks.append(get_trail())
|
|
||||||
|
|
||||||
# Run all tasks concurrently
|
|
||||||
chapters, trail = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
# Convert to AuthorWithRole objects
|
# Convert to AuthorWithRole objects
|
||||||
authors = [
|
authors = [
|
||||||
|
|
@ -193,15 +173,15 @@ async def get_course_meta(
|
||||||
for resource_author, user in author_results
|
for resource_author, user in author_results
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create course read model
|
# Create course read model with chapters
|
||||||
course_read = CourseRead(**course.model_dump(), authors=authors)
|
course_read = FullCourseRead(
|
||||||
|
**course.model_dump(),
|
||||||
return FullCourseReadWithTrail(
|
authors=authors,
|
||||||
**course_read.model_dump(),
|
chapters=chapters
|
||||||
chapters=chapters,
|
|
||||||
trail=trail,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return course_read
|
||||||
|
|
||||||
|
|
||||||
async def get_courses_orgslug(
|
async def get_courses_orgslug(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export async function generateMetadata(props: MetadataProps): Promise<Metadata>
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const course_meta = await getCourseMetadata(
|
const course_meta = await getCourseMetadata(
|
||||||
params.courseid,
|
params.courseid,
|
||||||
{ revalidate: 0, tags: ['courses'] },
|
{ revalidate: 60, tags: ['courses'] },
|
||||||
access_token ? access_token : null
|
access_token ? access_token : null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/Assign
|
||||||
import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
|
import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { mutate } from 'swr'
|
import { mutate } from 'swr'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal'
|
import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import { useMediaQuery } from 'usehooks-ts'
|
import { useMediaQuery } from 'usehooks-ts'
|
||||||
import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer'
|
import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer'
|
||||||
|
|
@ -32,6 +34,7 @@ import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/Ge
|
||||||
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests'
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import UserAvatar from '@components/Objects/UserAvatar'
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
import CoursesActions from '@components/Objects/Courses/CourseActions/CoursesActions'
|
||||||
|
|
||||||
// Lazy load heavy components
|
// Lazy load heavy components
|
||||||
const Canva = lazy(() => import('@components/Objects/Activities/DynamicCanva/DynamicCanva'))
|
const Canva = lazy(() => import('@components/Objects/Activities/DynamicCanva/DynamicCanva'))
|
||||||
|
|
@ -94,8 +97,18 @@ function useActivityPosition(course: any, activityId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActivityActions({ activity, activityid, course, orgslug, assignment, showNavigation = true }: ActivityActionsProps) {
|
function ActivityActions({ activity, activityid, course, orgslug, assignment, showNavigation = true }: ActivityActionsProps) {
|
||||||
const session = useLHSession() as any;
|
|
||||||
const { contributorStatus } = useContributorStatus(course.course_uuid);
|
const { contributorStatus } = useContributorStatus(course.course_uuid);
|
||||||
|
const org = useOrg() as any;
|
||||||
|
const session = useLHSession() as any;
|
||||||
|
const access_token = session?.data?.tokens?.access_token;
|
||||||
|
|
||||||
|
// Add SWR for trail data
|
||||||
|
const { data: trailData } = useSWR(
|
||||||
|
`${getAPIUrl()}trail/org/${org?.id}/trail`,
|
||||||
|
(url) => swrFetcher(url, access_token)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
|
|
@ -108,6 +121,7 @@ function ActivityActions({ activity, activityid, course, orgslug, assignment, sh
|
||||||
activityid={activityid}
|
activityid={activityid}
|
||||||
course={course}
|
course={course}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
|
trailData={trailData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -171,6 +185,12 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
const { contributorStatus } = useContributorStatus(courseuuid);
|
const { contributorStatus } = useContributorStatus(courseuuid);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Add SWR for trail data
|
||||||
|
const { data: trailData, error: error } = useSWR(
|
||||||
|
`${getAPIUrl()}trail/org/${org?.id}/trail`,
|
||||||
|
(url) => swrFetcher(url, access_token)
|
||||||
|
)
|
||||||
|
|
||||||
// Memoize activity position calculation
|
// Memoize activity position calculation
|
||||||
const { allActivities, currentIndex } = useActivityPosition(course, activityid);
|
const { allActivities, currentIndex } = useActivityPosition(course, activityid);
|
||||||
|
|
||||||
|
|
@ -331,17 +351,17 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeDasharray={2 * Math.PI * 14}
|
strokeDasharray={2 * Math.PI * 14}
|
||||||
strokeDashoffset={2 * Math.PI * 14 * (1 - (course.trail?.runs?.find((run: any) => run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1))}
|
strokeDashoffset={2 * Math.PI * 14 * (1 - (trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1))}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<span className="text-xs font-bold text-gray-800">
|
<span className="text-xs font-bold text-gray-800">
|
||||||
{Math.round(((course.trail?.runs?.find((run: any) => run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1)) * 100)}%
|
{Math.round(((trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1)) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600">
|
<div className="text-xs text-gray-600">
|
||||||
{course.trail?.runs?.find((run: any) => run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0} of {course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0}
|
{trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.steps?.filter((step: any) => step.complete)?.length || 0} of {course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
@ -386,6 +406,7 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
course={course}
|
course={course}
|
||||||
currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')}
|
currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
|
trailData={trailData}
|
||||||
/>
|
/>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
|
|
@ -542,6 +563,7 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
course={course}
|
course={course}
|
||||||
enableNavigation={true}
|
enableNavigation={true}
|
||||||
|
trailData={trailData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
|
|
@ -639,6 +661,7 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
course={course}
|
course={course}
|
||||||
currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')}
|
currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
|
trailData={trailData}
|
||||||
/>
|
/>
|
||||||
{contributorStatus === 'ACTIVE' && activity.activity_type == 'TYPE_DYNAMIC' && (
|
{contributorStatus === 'ACTIVE' && activity.activity_type == 'TYPE_DYNAMIC' && (
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -746,15 +769,18 @@ export function MarkStatus(props: {
|
||||||
activity: any
|
activity: any
|
||||||
activityid: string
|
activityid: string
|
||||||
course: any
|
course: any
|
||||||
orgslug: string
|
orgslug: string,
|
||||||
|
trailData: any
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const session = useLHSession() as any;
|
const session = useLHSession() as any;
|
||||||
|
const org = useOrg() as any;
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [showMarkedTooltip, setShowMarkedTooltip] = React.useState(false);
|
const [showMarkedTooltip, setShowMarkedTooltip] = React.useState(false);
|
||||||
const [showUnmarkedTooltip, setShowUnmarkedTooltip] = React.useState(false);
|
const [showUnmarkedTooltip, setShowUnmarkedTooltip] = React.useState(false);
|
||||||
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const markedTooltipCount = localStorage.getItem('activity_marked_tooltip_count');
|
const markedTooltipCount = localStorage.getItem('activity_marked_tooltip_count');
|
||||||
|
|
@ -799,8 +825,8 @@ export function MarkStatus(props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
const areAllActivitiesCompleted = () => {
|
const areAllActivitiesCompleted = () => {
|
||||||
const run = props.course.trail.runs.find(
|
const run = props.trailData?.runs?.find(
|
||||||
(run: any) => run.course_id == props.course.id
|
(run: any) => run.course_uuid === props.course.course_uuid
|
||||||
);
|
);
|
||||||
if (!run) return false;
|
if (!run) return false;
|
||||||
|
|
||||||
|
|
@ -811,7 +837,7 @@ export function MarkStatus(props: {
|
||||||
chapter.activities.forEach((activity: any) => {
|
chapter.activities.forEach((activity: any) => {
|
||||||
totalActivities++;
|
totalActivities++;
|
||||||
const isCompleted = run.steps.find(
|
const isCompleted = run.steps.find(
|
||||||
(step: any) => step.activity_id === activity.id && step.complete === true
|
(step: any) => step.activity_uuid === activity.activity_uuid && step.complete === true
|
||||||
);
|
);
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
completedActivities++;
|
completedActivities++;
|
||||||
|
|
@ -826,9 +852,7 @@ export function MarkStatus(props: {
|
||||||
try {
|
try {
|
||||||
const willCompleteAll = areAllActivitiesCompleted();
|
const willCompleteAll = areAllActivitiesCompleted();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// refresh the page after marking the activity as complete
|
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
|
||||||
router.refresh();
|
|
||||||
await markActivityAsComplete(
|
await markActivityAsComplete(
|
||||||
props.orgslug,
|
props.orgslug,
|
||||||
props.course.course_uuid,
|
props.course.course_uuid,
|
||||||
|
|
@ -836,13 +860,11 @@ export function MarkStatus(props: {
|
||||||
session.data?.tokens?.access_token
|
session.data?.tokens?.access_token
|
||||||
);
|
);
|
||||||
|
|
||||||
await mutate(`${getAPIUrl()}courses/${props.course.course_uuid}/meta`);
|
await mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`);
|
||||||
|
|
||||||
if (willCompleteAll) {
|
if (willCompleteAll) {
|
||||||
const cleanCourseUuid = props.course.course_uuid.replace('course_', '');
|
const cleanCourseUuid = props.course.course_uuid.replace('course_', '');
|
||||||
router.push(getUriWithOrg(props.orgslug, '') + `/course/${cleanCourseUuid}/activity/end`);
|
router.push(getUriWithOrg(props.orgslug, '') + `/course/${cleanCourseUuid}/activity/end`);
|
||||||
} else {
|
|
||||||
router.refresh();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error marking activity as complete:', error);
|
console.error('Error marking activity as complete:', error);
|
||||||
|
|
@ -855,15 +877,15 @@ export function MarkStatus(props: {
|
||||||
async function unmarkActivityAsCompleteFront() {
|
async function unmarkActivityAsCompleteFront() {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
await unmarkActivityAsComplete(
|
await unmarkActivityAsComplete(
|
||||||
props.orgslug,
|
props.orgslug,
|
||||||
props.course.course_uuid,
|
props.course.course_uuid,
|
||||||
props.activity.activity_uuid,
|
props.activity.activity_uuid,
|
||||||
session.data?.tokens?.access_token
|
session.data?.tokens?.access_token
|
||||||
);
|
);
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
|
||||||
await mutate(`${getAPIUrl()}courses/${props.course.course_uuid}/meta`);
|
await mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`);
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to unmark activity as complete');
|
toast.error('Failed to unmark activity as complete');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -872,14 +894,28 @@ export function MarkStatus(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActivityCompleted = () => {
|
const isActivityCompleted = () => {
|
||||||
let run = props.course.trail.runs.find(
|
// Clean up course UUID by removing 'course_' prefix if it exists
|
||||||
(run: any) => run.course_id == props.course.id
|
const cleanCourseUuid = props.course.course_uuid?.replace('course_', '');
|
||||||
)
|
|
||||||
|
let run = props.trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (run) {
|
if (run) {
|
||||||
|
// Find the step that matches the current activity
|
||||||
return run.steps.find(
|
return run.steps.find(
|
||||||
(step: any) => (step.activity_id == props.activity.id) && (step.complete == true)
|
(step: any) => step.activity_id === props.activity.id && step.complete === true
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render until we have trail data
|
||||||
|
if (!props.trailData) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -931,24 +967,41 @@ export function MarkStatus(props: {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className={`${isLoading ? 'opacity-75 cursor-not-allowed' : ''} bg-gray-800 rounded-md px-4 nice-shadow flex flex-col p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out`}
|
className={`${isLoading ? 'opacity-90' : ''} bg-gray-800 rounded-md px-4 nice-shadow flex flex-col p-2.5 text-white hover:cursor-pointer transition-all duration-200 ${isLoading ? 'cursor-not-allowed' : 'hover:bg-gray-700'}`}
|
||||||
onClick={!isLoading ? markActivityAsCompleteFront : undefined}
|
onClick={!isLoading ? markActivityAsCompleteFront : undefined}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-bold mb-1 uppercase">Status</span>
|
<span className="text-[10px] font-bold mb-1 uppercase">Status</span>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<svg
|
{isLoading ? (
|
||||||
width="17"
|
<div className="animate-spin">
|
||||||
height="17"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
width="17"
|
||||||
fill="none"
|
height="17"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="2"
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeWidth="2"
|
||||||
>
|
strokeLinecap="round"
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
strokeLinejoin="round"
|
||||||
</svg>
|
>
|
||||||
<span className="text-xs font-bold">{isLoading ? 'Marking...' : 'Mark as complete'}</span>
|
<path d="M21 12a9 9 0 11-6.219-8.56" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="17"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<span className="text-xs font-bold min-w-[90px]">{isLoading ? 'Marking...' : 'Mark as complete'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showUnmarkedTooltip && (
|
{showUnmarkedTooltip && (
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ type Session = {
|
||||||
async function fetchCourseMetadata(courseuuid: string, access_token: string | null | undefined) {
|
async function fetchCourseMetadata(courseuuid: string, access_token: string | null | undefined) {
|
||||||
return await getCourseMetadata(
|
return await getCourseMetadata(
|
||||||
courseuuid,
|
courseuuid,
|
||||||
{ revalidate: 0, tags: ['courses'] },
|
{ revalidate: 60, tags: ['courses'] },
|
||||||
access_token || null
|
access_token || null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
'use client'
|
'use client'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg, getAPIUrl } from '@services/config/config'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests'
|
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests'
|
||||||
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper'
|
import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper'
|
||||||
|
|
@ -18,6 +18,8 @@ import CoursesActions from '@components/Objects/Courses/CourseActions/CoursesAct
|
||||||
import CourseActionsMobile from '@components/Objects/Courses/CourseActions/CourseActionsMobile'
|
import CourseActionsMobile from '@components/Objects/Courses/CourseActions/CourseActionsMobile'
|
||||||
import CourseAuthors from '@components/Objects/Courses/CourseAuthors/CourseAuthors'
|
import CourseAuthors from '@components/Objects/Courses/CourseAuthors/CourseAuthors'
|
||||||
import CourseBreadcrumbs from '@components/Pages/Courses/CourseBreadcrumbs'
|
import CourseBreadcrumbs from '@components/Pages/Courses/CourseBreadcrumbs'
|
||||||
|
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
const CourseClient = (props: any) => {
|
const CourseClient = (props: any) => {
|
||||||
const [learnings, setLearnings] = useState<any>([])
|
const [learnings, setLearnings] = useState<any>([])
|
||||||
|
|
@ -28,6 +30,14 @@ const CourseClient = (props: any) => {
|
||||||
const org = useOrg() as any
|
const org = useOrg() as any
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||||
|
const session = useLHSession() as any;
|
||||||
|
const access_token = session?.data?.tokens?.access_token;
|
||||||
|
|
||||||
|
// Add SWR for trail data
|
||||||
|
const { data: trailData } = useSWR(
|
||||||
|
`${getAPIUrl()}trail/org/${org?.id}/trail`,
|
||||||
|
(url) => swrFetcher(url, access_token)
|
||||||
|
);
|
||||||
|
|
||||||
console.log(course)
|
console.log(course)
|
||||||
|
|
||||||
|
|
@ -178,7 +188,7 @@ const CourseClient = (props: any) => {
|
||||||
|
|
||||||
<div className='course_metadata_right w-full md:w-1/4 space-y-4'>
|
<div className='course_metadata_right w-full md:w-1/4 space-y-4'>
|
||||||
{/* Actions Box */}
|
{/* Actions Box */}
|
||||||
<CoursesActions courseuuid={courseuuid} orgslug={orgslug} course={course} />
|
<CoursesActions courseuuid={courseuuid} orgslug={orgslug} course={course} trailData={trailData} />
|
||||||
|
|
||||||
{/* Authors & Updates Box */}
|
{/* Authors & Updates Box */}
|
||||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden p-4">
|
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden p-4">
|
||||||
|
|
@ -329,10 +339,9 @@ const CourseClient = (props: any) => {
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
|
|
||||||
|
{/* Mobile Actions Box */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="fixed bottom-0 left-0 right-0 p-4 z-50">
|
<CourseActionsMobile courseuuid={courseuuid} orgslug={orgslug} course={course} trailData={trailData} />
|
||||||
<CourseActionsMobile courseuuid={courseuuid} orgslug={orgslug} course={course} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export async function generateMetadata(props: MetadataProps): Promise<Metadata>
|
||||||
})
|
})
|
||||||
const course_meta = await getCourseMetadata(
|
const course_meta = await getCourseMetadata(
|
||||||
params.courseuuid,
|
params.courseuuid,
|
||||||
{ revalidate: 0, tags: ['courses'] },
|
{ revalidate: 60, tags: ['courses'] },
|
||||||
access_token ? access_token : null
|
access_token ? access_token : null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,17 +69,20 @@ const CoursePage = async (params: any) => {
|
||||||
const session = await getServerSession(nextAuthOptions)
|
const session = await getServerSession(nextAuthOptions)
|
||||||
const access_token = session?.tokens?.access_token
|
const access_token = session?.tokens?.access_token
|
||||||
|
|
||||||
|
// Await params before using them
|
||||||
|
const { courseuuid, orgslug } = await params.params
|
||||||
|
|
||||||
// Fetch course metadata once
|
// Fetch course metadata once
|
||||||
const course_meta = await getCourseMetadata(
|
const course_meta = await getCourseMetadata(
|
||||||
params.params.courseuuid,
|
courseuuid,
|
||||||
{ revalidate: 0, tags: ['courses'] },
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
access_token ? access_token : null
|
access_token ? access_token : null
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CourseClient
|
<CourseClient
|
||||||
courseuuid={params.params.courseuuid}
|
courseuuid={courseuuid}
|
||||||
orgslug={params.params.orgslug}
|
orgslug={orgslug}
|
||||||
course={course_meta}
|
course={course_meta}
|
||||||
access_token={access_token}
|
access_token={access_token}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ interface CourseRun {
|
||||||
|
|
||||||
interface Course {
|
interface Course {
|
||||||
id: string
|
id: string
|
||||||
|
course_uuid: string
|
||||||
authors: Author[]
|
authors: Author[]
|
||||||
trail?: {
|
trail?: {
|
||||||
runs: CourseRun[]
|
runs: CourseRun[]
|
||||||
|
|
@ -51,6 +52,7 @@ interface CourseActionsMobileProps {
|
||||||
course: Course & {
|
course: Course & {
|
||||||
org_id: number
|
org_id: number
|
||||||
}
|
}
|
||||||
|
trailData?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component for displaying multiple authors
|
// Component for displaying multiple authors
|
||||||
|
|
@ -122,7 +124,7 @@ const MultipleAuthors = ({ authors }: { authors: Author[] }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CourseActionsMobile = ({ courseuuid, orgslug, course }: CourseActionsMobileProps) => {
|
const CourseActionsMobile = ({ courseuuid, orgslug, course, trailData }: CourseActionsMobileProps) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const session = useLHSession() as any
|
const session = useLHSession() as any
|
||||||
const [linkedProducts, setLinkedProducts] = useState<any[]>([])
|
const [linkedProducts, setLinkedProducts] = useState<any[]>([])
|
||||||
|
|
@ -131,9 +133,15 @@ const CourseActionsMobile = ({ courseuuid, orgslug, course }: CourseActionsMobil
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [hasAccess, setHasAccess] = useState<boolean | null>(null)
|
const [hasAccess, setHasAccess] = useState<boolean | null>(null)
|
||||||
|
|
||||||
const isStarted = course.trail?.runs?.some(
|
// Clean up course UUID by removing 'course_' prefix if it exists
|
||||||
(run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
|
const cleanCourseUuid = course.course_uuid?.replace('course_', '');
|
||||||
) ?? false
|
|
||||||
|
const isStarted = trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
) ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLinkedProducts = async () => {
|
const fetchLinkedProducts = async () => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { removeCourse, startCourse } from '@services/courses/activity'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests'
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||||
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
|
import { getAPIUrl, getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
|
||||||
import { getProductsByCourse } from '@services/payments/products'
|
import { getProductsByCourse } from '@services/payments/products'
|
||||||
import { LogIn, LogOut, ShoppingCart, AlertCircle, UserPen, ClockIcon, ArrowRight, Sparkles, BookOpen } from 'lucide-react'
|
import { LogIn, LogOut, ShoppingCart, AlertCircle, UserPen, ClockIcon, ArrowRight, Sparkles, BookOpen } from 'lucide-react'
|
||||||
import Modal from '@components/Objects/StyledElements/Modal/Modal'
|
import Modal from '@components/Objects/StyledElements/Modal/Modal'
|
||||||
|
|
@ -14,6 +14,8 @@ import toast from 'react-hot-toast'
|
||||||
import { useContributorStatus } from '../../../../hooks/useContributorStatus'
|
import { useContributorStatus } from '../../../../hooks/useContributorStatus'
|
||||||
import CourseProgress from '../CourseProgress/CourseProgress'
|
import CourseProgress from '../CourseProgress/CourseProgress'
|
||||||
import UserAvatar from '@components/Objects/UserAvatar'
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
interface CourseRun {
|
interface CourseRun {
|
||||||
status: string
|
status: string
|
||||||
|
|
@ -26,6 +28,7 @@ interface CourseRun {
|
||||||
|
|
||||||
interface Course {
|
interface Course {
|
||||||
id: string
|
id: string
|
||||||
|
course_uuid: string
|
||||||
trail?: {
|
trail?: {
|
||||||
runs: CourseRun[]
|
runs: CourseRun[]
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +49,10 @@ interface CourseActionsProps {
|
||||||
course: Course & {
|
course: Course & {
|
||||||
org_id: number
|
org_id: number
|
||||||
}
|
}
|
||||||
|
trailData?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) {
|
function CoursesActions({ courseuuid, orgslug, course, trailData }: CourseActionsProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const session = useLHSession() as any
|
const session = useLHSession() as any
|
||||||
const [linkedProducts, setLinkedProducts] = useState<any[]>([])
|
const [linkedProducts, setLinkedProducts] = useState<any[]>([])
|
||||||
|
|
@ -59,10 +63,17 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) {
|
||||||
const [hasAccess, setHasAccess] = useState<boolean | null>(null)
|
const [hasAccess, setHasAccess] = useState<boolean | null>(null)
|
||||||
const { contributorStatus, refetch } = useContributorStatus(courseuuid)
|
const { contributorStatus, refetch } = useContributorStatus(courseuuid)
|
||||||
const [isProgressOpen, setIsProgressOpen] = useState(false)
|
const [isProgressOpen, setIsProgressOpen] = useState(false)
|
||||||
|
const org = useOrg() as any
|
||||||
|
|
||||||
const isStarted = course.trail?.runs?.some(
|
// Clean up course UUID by removing 'course_' prefix if it exists
|
||||||
(run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
|
const cleanCourseUuid = course.course_uuid?.replace('course_', '');
|
||||||
) ?? false
|
|
||||||
|
const isStarted = trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
) ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLinkedProducts = async () => {
|
const fetchLinkedProducts = async () => {
|
||||||
|
|
@ -120,12 +131,11 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) {
|
||||||
try {
|
try {
|
||||||
if (isStarted) {
|
if (isStarted) {
|
||||||
await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
|
await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
|
||||||
await revalidateTags(['courses'], orgslug)
|
mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`)
|
||||||
toast.success('Successfully left the course', { id: loadingToast })
|
toast.success('Successfully left the course', { id: loadingToast })
|
||||||
router.refresh()
|
|
||||||
} else {
|
} else {
|
||||||
await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
|
await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
|
||||||
await revalidateTags(['courses'], orgslug)
|
mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`)
|
||||||
toast.success('Successfully started the course', { id: loadingToast })
|
toast.success('Successfully started the course', { id: loadingToast })
|
||||||
|
|
||||||
// Get the first activity from the first chapter
|
// Get the first activity from the first chapter
|
||||||
|
|
@ -139,7 +149,7 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) {
|
||||||
`/course/${courseuuid}/activity/${firstActivity.activity_uuid.replace('activity_', '')}`
|
`/course/${courseuuid}/activity/${firstActivity.activity_uuid.replace('activity_', '')}`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
router.refresh()
|
mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -262,10 +272,16 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) {
|
||||||
|
|
||||||
const renderProgressSection = () => {
|
const renderProgressSection = () => {
|
||||||
const totalActivities = course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0;
|
const totalActivities = course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0;
|
||||||
const completedActivities = course.trail?.runs?.find(
|
|
||||||
(run: CourseRun) => run.course_id === course.id
|
|
||||||
)?.steps?.filter((step) => step.complete)?.length || 0;
|
|
||||||
|
|
||||||
|
// Find the correct run using the cleaned UUID
|
||||||
|
const run = trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const completedActivities = run?.steps?.filter((step: any) => step.complete)?.length || 0;
|
||||||
const progressPercentage = Math.round((completedActivities / totalActivities) * 100);
|
const progressPercentage = Math.round((completedActivities / totalActivities) * 100);
|
||||||
|
|
||||||
if (!isStarted) {
|
if (!isStarted) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface ActivityChapterDropdownProps {
|
||||||
course: any
|
course: any
|
||||||
currentActivityId: string
|
currentActivityId: string
|
||||||
orgslug: string
|
orgslug: string
|
||||||
|
trailData?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActivityChapterDropdown(props: ActivityChapterDropdownProps): React.ReactNode {
|
export default function ActivityChapterDropdown(props: ActivityChapterDropdownProps): React.ReactNode {
|
||||||
|
|
@ -16,6 +17,9 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr
|
||||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
|
// Clean up course UUID by removing 'course_' prefix if it exists
|
||||||
|
const cleanCourseUuid = props.course.course_uuid?.replace('course_', '');
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
|
@ -100,9 +104,20 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr
|
||||||
<div className="py-0.5">
|
<div className="py-0.5">
|
||||||
{chapter.activities.map((activity: any) => {
|
{chapter.activities.map((activity: any) => {
|
||||||
const cleanActivityUuid = activity.activity_uuid?.replace('activity_', '');
|
const cleanActivityUuid = activity.activity_uuid?.replace('activity_', '');
|
||||||
const cleanCourseUuid = props.course.course_uuid?.replace('course_', '');
|
|
||||||
const isCurrent = cleanActivityUuid === props.currentActivityId.replace('activity_', '');
|
const isCurrent = cleanActivityUuid === props.currentActivityId.replace('activity_', '');
|
||||||
|
|
||||||
|
// Find the correct run and check if activity is complete
|
||||||
|
const run = props.trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isComplete = run?.steps?.find(
|
||||||
|
(step: any) => step.activity_id === activity.id && step.complete === true
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={activity.id}
|
key={activity.id}
|
||||||
|
|
@ -117,11 +132,7 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr
|
||||||
>
|
>
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{props.course.trail?.runs?.find(
|
{isComplete ? (
|
||||||
(run: any) => run.course_id === props.course.id
|
|
||||||
)?.steps?.find(
|
|
||||||
(step: any) => (step.activity_id === activity.id || step.activity_id === activity.activity_uuid) && step.complete === true
|
|
||||||
) ? (
|
|
||||||
<div className="relative cursor-pointer">
|
<div className="relative cursor-pointer">
|
||||||
<Check size={14} className="stroke-[2.5] text-teal-600" />
|
<Check size={14} className="stroke-[2.5] text-teal-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface Props {
|
||||||
course_uuid: string
|
course_uuid: string
|
||||||
current_activity?: string
|
current_activity?: string
|
||||||
enableNavigation?: boolean
|
enableNavigation?: boolean
|
||||||
|
trailData?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
@ -109,8 +110,6 @@ function ActivityIndicators(props: Props) {
|
||||||
const black_activity_style = 'bg-zinc-300 hover:bg-zinc-400'
|
const black_activity_style = 'bg-zinc-300 hover:bg-zinc-400'
|
||||||
const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700'
|
const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700'
|
||||||
|
|
||||||
const trail = props.course.trail
|
|
||||||
|
|
||||||
// Flatten all activities for navigation and rendering
|
// Flatten all activities for navigation and rendering
|
||||||
const allActivities = useMemo(() => {
|
const allActivities = useMemo(() => {
|
||||||
return course.chapters.flatMap((chapter: any) =>
|
return course.chapters.flatMap((chapter: any) =>
|
||||||
|
|
@ -131,14 +130,23 @@ function ActivityIndicators(props: Props) {
|
||||||
|
|
||||||
// Memoize activity status checks
|
// Memoize activity status checks
|
||||||
const isActivityDone = useMemo(() => (activity: any) => {
|
const isActivityDone = useMemo(() => (activity: any) => {
|
||||||
let run = props.course.trail?.runs.find(
|
// Clean up course UUID by removing 'course_' prefix if it exists
|
||||||
(run: any) => run.course_id == props.course.id
|
const cleanCourseUuid = course.course_uuid?.replace('course_', '');
|
||||||
)
|
|
||||||
|
let run = props.trailData?.runs?.find(
|
||||||
|
(run: any) => {
|
||||||
|
const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', '');
|
||||||
|
return cleanRunCourseUuid === cleanCourseUuid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (run) {
|
if (run) {
|
||||||
return run.steps.find((step: any) => step.activity_id == activity.id)
|
return run.steps.find(
|
||||||
|
(step: any) => step.activity_id === activity.id && step.complete === true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}, [props.course]);
|
}, [props.trailData, course.course_uuid]);
|
||||||
|
|
||||||
const isActivityCurrent = useMemo(() => (activity: any) => {
|
const isActivityCurrent = useMemo(() => (activity: any) => {
|
||||||
let activity_uuid = activity.activity_uuid.replace('activity_', '')
|
let activity_uuid = activity.activity_uuid.replace('activity_', '')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue