diff --git a/apps/web/components/LessonView.tsx b/apps/web/components/LessonView.tsx index 5bc5d737..2284da70 100644 --- a/apps/web/components/LessonView.tsx +++ b/apps/web/components/LessonView.tsx @@ -5,6 +5,7 @@ import RedirectToLoginCard from "./RedirectToLoginCard"; import { getServerSession } from "next-auth"; import { authOptions } from "../lib/auth"; import { AppbarClient } from "./AppbarClient"; +import { setProgress } from "./utils"; export const LessonView = async ({ problem, @@ -34,6 +35,11 @@ export const LessonView = async ({ ); } + // Update progress when the lesson is viewed + if (session?.user) { + await setProgress(track.id, problem.id, true); + } + if (problem.type === "MCQ") { return ; } diff --git a/apps/web/components/TrackCard-2.tsx b/apps/web/components/TrackCard-2.tsx index 0238c882..59c8c7cf 100644 --- a/apps/web/components/TrackCard-2.tsx +++ b/apps/web/components/TrackCard-2.tsx @@ -1,8 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +"use client"; + +import { useRef, useState } from "react"; import { motion, useAnimation } from "framer-motion"; -import { Track, Problem } from "@prisma/client"; +import { Track, Problem, UserProgress } from "@prisma/client"; import { TrackPreview } from "./TrackPreview"; import { formatDistanceToNow } from "date-fns"; +import { useSession } from "next-auth/react"; interface TrackCardProps extends Track { problems: Problem[]; @@ -14,37 +17,26 @@ interface TrackCardProps extends Track { }[]; } -export function TrackCard2({ track }: { track: TrackCardProps }) { +export function TrackCard2({ track, userProgress }: { track: TrackCardProps; userProgress: UserProgress[] }) { const controls = useAnimation(); const ref = useRef(null); const [showPreview, setShowPreview] = useState(false); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry && entry.isIntersecting) { - controls.start("visible"); - } - }, - { threshold: 0.3 } - ); - - if (ref.current) { - observer.observe(ref.current); - } - - return () => { - if (ref.current) { - observer.unobserve(ref.current); - } - }; - }, [controls]); + const { data: session } = useSession(); const variants = { - hidden: { opacity: 0 }, + // hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.1, ease: "easeOut" } }, }; + const completedProblems = userProgress.filter((p) => p.completed).length; + const progressPercentage = (completedProblems / track.problems.length) * 100; + const isCompleted = progressPercentage === 100; + + const lastVisitedProblem = + userProgress.length > 0 + ? userProgress.reduce((prev, current) => (prev.lastVisited > current.lastVisited ? prev : current)) + : null; + return ( <> setShowPreview(true)} > - {track.title} -
+ {track.title} +
-

{track.title}

+

{track.title}

{track.categories.map((item) => (

{item.category.category}

))}
-
-

+

+

{track.problems.length} Chapters

-

+

{formatDistanceToNow(new Date(track.createdAt), { addSuffix: true })}

+ {(session?.user || userProgress.length > 0) && ( +
+
+
+
+ + {progressPercentage.toFixed(0)}% + +
+ )}
- + ); } diff --git a/apps/web/components/TrackPreview.tsx b/apps/web/components/TrackPreview.tsx index d3b85e0d..8d47ee05 100644 --- a/apps/web/components/TrackPreview.tsx +++ b/apps/web/components/TrackPreview.tsx @@ -1,16 +1,26 @@ "use client"; + import React, { useState, useEffect } from "react"; import Link from "next/link"; -import { Button } from "../../../packages/ui/src/shad/ui/button"; -import { Dialog, DialogContent } from "../../../packages/ui/src/shad/ui/dailog"; -import { ArrowRight } from "lucide-react"; +import { ArrowRight, Play, Redo, CheckCircle } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; +import { Track, Problem, UserProgress, Categories } from "@prisma/client"; +import { useSession } from "next-auth/react"; +import { Button, Dialog, DialogContent } from "@repo/ui"; -type TrackPreviewProps = { +interface TrackPreviewProps { showPreview: boolean; setShowPreview: (val: boolean) => void; - track: any; -}; + track: Track & { + problems: Problem[]; + categories: { + category: Categories; + }[]; + }; + isCompleted: boolean; + lastVisitedProblem: UserProgress | null; + userProgress: UserProgress[]; +} const truncateDescription = (text: string, wordLimit: number) => { const words = text.split(" "); @@ -20,74 +30,120 @@ const truncateDescription = (text: string, wordLimit: number) => { return text; }; -export function TrackPreview({ showPreview, setShowPreview, track }: TrackPreviewProps) { +export function TrackPreview({ + showPreview, + setShowPreview, + track, + isCompleted, + lastVisitedProblem, + userProgress, +}: TrackPreviewProps) { const [isMediumOrLarger, setIsMediumOrLarger] = useState(false); - - const updateScreenSize = () => { - setIsMediumOrLarger(window.innerWidth >= 768); - }; + const { data: session } = useSession(); + const [completedProblems, setCompletedProblems] = useState([]); + const [lastVisited, setLastVisited] = useState(null); useEffect(() => { - updateScreenSize(); // Set the initial state - window.addEventListener("resize", updateScreenSize); // Add resize listener + const updateScreenSize = () => { + setIsMediumOrLarger(window.innerWidth >= 768); + }; + + updateScreenSize(); + window.addEventListener("resize", updateScreenSize); return () => { - window.removeEventListener("resize", updateScreenSize); // Cleanup listener on unmount + window.removeEventListener("resize", updateScreenSize); }; }, []); + useEffect(() => { + if (session?.user) { + setCompletedProblems(userProgress.filter((p) => p.completed).map((p) => p.problemId)); + setLastVisited(lastVisitedProblem?.problemId || null); + } + }, [session, track.id, userProgress, lastVisitedProblem]); + const truncatedDescription = isMediumOrLarger ? track.description : truncateDescription(track.description, 15); + const progressPercentage = (completedProblems.length / track.problems.length) * 100; + return ( setShowPreview(false)}> -
- -
+
+ {track.title} +
-

{track.title}

+

{track.title}

- {track.categories.map((item: any, idx: number) => ( + {track.categories.map((item) => (

- {item.category.category}{" "} + {item.category.category}

))}
-

{truncatedDescription}

+

{truncatedDescription}

-
-
-

+

+
+

{track.problems.length} Chapters

-

+

{formatDistanceToNow(new Date(track.createdAt), { addSuffix: true })}

-
- {track.problems.map((topic: any, idx: number) => ( - -
+
+ {track.problems.map((topic) => ( + +
{topic.title} - + {completedProblems.includes(topic.id) ? ( + + ) : ( + + )}
))}
- - - +
+
+
+
+
+ + {progressPercentage.toFixed(0)}% Complete + +
+
+ + + + {lastVisited && lastVisited !== track.problems[0]?.id && ( + + + + )} + {isCompleted && ( + + )} +
+
diff --git a/apps/web/components/Tracks.tsx b/apps/web/components/Tracks.tsx index d9af482c..62704ec5 100644 --- a/apps/web/components/Tracks.tsx +++ b/apps/web/components/Tracks.tsx @@ -2,7 +2,7 @@ import { TrackCard2 } from "./TrackCard-2"; import { category } from "@repo/store"; import { useEffect, useState } from "react"; -import { Track, Problem } from "@prisma/client"; +import { Track, Problem, Categories, UserProgress } from "@prisma/client"; import { useRecoilState } from "recoil"; import { Select, @@ -36,7 +36,8 @@ export interface TrackPros extends Track { interface TracksWithCategoriesProps { tracks: TrackPros[]; - categories: { id: string; category: string }[]; + categories: Categories[]; + userProgress: UserProgress[]; } enum CohortGroup { @@ -45,7 +46,7 @@ enum CohortGroup { Three = 3, } -export const Tracks = ({ tracks, categories }: TracksWithCategoriesProps) => { +export const Tracks = ({ tracks, categories, userProgress }: TracksWithCategoriesProps) => { const [selectedCategory, setSelectedCategory] = useRecoilState(category); const [filteredTracks, setFilteredTracks] = useState(tracks); const [visibleTracks, setVisibleTracks] = useState([]); @@ -128,11 +129,11 @@ export const Tracks = ({ tracks, categories }: TracksWithCategoriesProps) => { initial={{ y: -20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.5, ease: "easeInOut", type: "spring", damping: 10, delay: 0.5 }} - className="flex max-w-5xl flex-col gap-4 w-full mx-auto p-4" + className="mx-auto flex w-full max-w-5xl flex-col gap-4 p-4" id="tracks" > -
-
+
+
- +
-
+
{/* Filter by Categories */} -
+