From 928311bc3ec1972a3ae3aef8e0d068e5de8b629e Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Sat, 13 Dec 2025 15:52:24 +0100 Subject: [PATCH 1/7] -new progress bar system structure --- app/courses/[courseId]/progress/page.tsx | 292 ++++++++++++----------- components/ChapterHeader.tsx | 2 +- components/CompetencyProgressbar.tsx | 44 +++- 3 files changed, 188 insertions(+), 150 deletions(-) diff --git a/app/courses/[courseId]/progress/page.tsx b/app/courses/[courseId]/progress/page.tsx index 6966e3da..2c8502f3 100644 --- a/app/courses/[courseId]/progress/page.tsx +++ b/app/courses/[courseId]/progress/page.tsx @@ -1,20 +1,15 @@ "use client"; -import { useState } from "react"; -import * as React from "react"; -import { RewardScores } from "@/components/RewardScores"; -import { RewardScoresHelpButton } from "@/components/RewardScoresHelpButton"; -import { Button, IconButton, Typography } from "@mui/material"; -import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import { StudentCourseLayoutCourseIdQuery$data } from "@/__generated__/StudentCourseLayoutCourseIdQuery.graphql"; +import { stringToColor } from "@/components/ChapterHeader"; +import CompetencyProgressbar from "@/components/CompetencyProgressbar"; import { LightTooltip } from "@/components/LightTooltip"; +import { Suggestion } from "@/components/Suggestion"; import { Info } from "@mui/icons-material"; -import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; -import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; -import CompetencyProgressbar from "@/components/CompetencyProgressbar"; -import { stringToColor } from "@/components/ChapterHeader"; -import { useCourseData } from "../../../../components/courses/context/CourseDataContext"; +import { IconButton, Typography } from "@mui/material"; import { useRouter } from "next/navigation"; -import { StudentCourseLayoutCourseIdQuery$data } from "@/__generated__/StudentCourseLayoutCourseIdQuery.graphql"; +import { useState } from "react"; +import { useCourseData } from "../../../../components/courses/context/CourseDataContext"; export default function LearningProgress() { const router = useRouter(); @@ -22,15 +17,13 @@ export default function LearningProgress() { // Get data from context const data = useCourseData() as StudentCourseLayoutCourseIdQuery$data; const course = data.coursesByIds[0]; - const id = course.id; - - const [currentPage, setCurrentPage] = useState(0); - const categoriesPerPage = 3; const uniqueSkillCategories = Array.from( new Map(course.skills.map((skill) => [skill.skillCategory, skill])).values() ); + const [selectedCategory, setSelectedCategory] = useState(0); + // Sort categories by total progress const sortedSkillCategories = [...uniqueSkillCategories].sort((a, b) => { const getTotalProgress = (category: typeof a) => { @@ -53,136 +46,153 @@ export default function LearningProgress() { return getTotalProgress(b) - getTotalProgress(a); }); - const totalPages = Math.ceil( - sortedSkillCategories.length / categoriesPerPage - ); - - const currentCategorySlice = sortedSkillCategories.slice( - currentPage * categoriesPerPage, - (currentPage + 1) * categoriesPerPage - ); - - const handlePrevPage = () => { - if (currentPage > 0) { - setCurrentPage(currentPage - 1); - } - }; - - const handleNextPage = () => { - if (currentPage < totalPages - 1) { - setCurrentPage(currentPage + 1); - } - }; return ( -
-
-
- Skill progress - - -

Information Skillprogress

-

- { - "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." - } -

- - } - > - - - -
- - {totalPages > 1 && ( -
- - - - - {currentPage + 1} / {totalPages} - - = totalPages - 1} +
+
+
+ Knowledge Area + +

Knowledge Area

+

+ { + "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." + } +

+ + } > - - + + + +
- )} -
- -
- {currentCategorySlice.map((uniqueSkill) => { - const skillsInCategory = course.skills.filter( - (skill) => skill.skillCategory === uniqueSkill.skillCategory - ); - const uniqueSkillsInCategory = Array.from( - new Map( - skillsInCategory.map((skill) => [skill.skillName, skill]) - ).values() - ); - - const totalCategoryProgress = uniqueSkillsInCategory.reduce( - (acc, skill) => - acc + - Object.values(skill.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), +
+ {sortedSkillCategories.map((category) => { + const skillsInCategory = course.skills.filter( + (skill) => skill.skillCategory === category.skillCategory + ); + const uniqueSkillsInCategory = Array.from( + new Map( + skillsInCategory.map((skill) => [skill.skillName, skill]) + ).values() + ); + + const totalCategoryProgress = uniqueSkillsInCategory.reduce( + (acc, skill) => + acc + + Object.values(skill.skillLevels || {}).reduce( + (sum, level) => sum + (level?.value || 0), + 0 + ), 0 - ), - 0 - ); - const categoryProgressValue = Math.floor( - Math.min( - (totalCategoryProgress * 100) / uniqueSkillsInCategory.length, - 100 - ) - ); - - return ( -
-
- -
-
- {uniqueSkillsInCategory.map((skill) => { - const rawValue = Object.values( - skill?.skillLevels || {} - ).reduce((sum, level) => sum + (level?.value || 0), 0); - const clamped = Math.min(rawValue, 1); - const skillProgressPercent = Math.floor(clamped * 100); - - return ( -
+ ); + let categoryProgressValue = Math.floor( + Math.min( + (totalCategoryProgress * 100) / uniqueSkillsInCategory.length, + 100 + ) + ); + return ( +
+
+ setSelectedCategory(sortedSkillCategories.findIndex((s)=> s.skillCategory === category.skillCategory))} + /> +
+
+ ); + })} +
+
+ +
+
+ Competencies + +

Competency

+

+ { + "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." + } +

+ + } + > + + + +
+
+
+ {course.skills.filter( + (skill) => skill.skillCategory === sortedSkillCategories[selectedCategory].skillCategory + ).map((skill) =>{ + const rawValue = Object.values( + skill?.skillLevels || {} + ).reduce((sum, level) => sum + (level?.value || 0), 0); + const clamped = Math.min(rawValue, 1); + const skillProgressPercent = Math.floor(clamped * 100); + return ( +
-
- ); - })} -
-
- ); - })} +
+ ); + }) + } +
+
+ +
+
+ Task Recommendation + + +

Task Recommendation

+

+ { + "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." + } +

+ + } + > + + + +
+
+
+ {course.suggestions.map((x) => ( + + ))} +
+
+
-
-
); -} +} \ No newline at end of file diff --git a/components/ChapterHeader.tsx b/components/ChapterHeader.tsx index ca1c039e..5323ec3b 100644 --- a/components/ChapterHeader.tsx +++ b/components/ChapterHeader.tsx @@ -29,7 +29,7 @@ export function stringToColor(string: string): string { "Networking and Communication": "#E0FFFF", // light cyan "Operating Systems": "#AEC6CF", // pastel blue "Parallel and Distributed Computing": "#FFDAB9", // pastel peach - Security: "#FDFD96", // pastel yellow + "Security": "#FDFD96", // pastel yellow "Society, Ethics, and the Profession": "#FFFACD", // pastel lemon "Software Development Fundamentals": "#D5E8D4", // pastel mint "Software Engineering": "#C1E1C1", // pastel light green diff --git a/components/CompetencyProgressbar.tsx b/components/CompetencyProgressbar.tsx index 0eb4cc90..be606a1b 100644 --- a/components/CompetencyProgressbar.tsx +++ b/components/CompetencyProgressbar.tsx @@ -1,30 +1,58 @@ -import * as React from "react"; import Box from "@mui/material/Box"; import LinearProgress from "@mui/material/LinearProgress"; +import { useEffect, useState } from "react"; type CompetencyProgressbarProps = { competencyName: string; - progressValue: number; - heightValue: number; + startProgress?:number; + endProgress: number; + height: number; color: string; + onClick?: () => void; }; export default function CompetencyProgressbar( props: CompetencyProgressbarProps ) { const { competencyName } = props; - const { progressValue } = props; - const { heightValue } = props; + const { startProgress } = props; + const { endProgress } = props; + const { height } = props; const { color } = props; + const { onClick } = props; + + const [progress, setProgress] = useState(startProgress ?? endProgress); + + useEffect(() => { + if (startProgress == null || startProgress === endProgress) return; + + const steps = 25; + const totalMs = 500; + const intervalMs = Math.floor(totalMs / steps); + const increment = (endProgress - startProgress) / steps; + + let current = startProgress; + setProgress(current); + + const timer = setInterval(() => { + current = Math.min(endProgress, current + increment); + setProgress(current); + if (current >= endProgress) { + clearInterval(timer); + } + }, intervalMs); + + return () => clearInterval(timer); + }, [startProgress, endProgress]); return ( - + Date: Wed, 17 Dec 2025 18:07:50 +0100 Subject: [PATCH 2/7] -wip form finalized + run prettier --- app/courses/[courseId]/progress/page.tsx | 486 +++++++++++++++-------- components/ChapterOverview.tsx | 6 - components/CompetencyProgressbar.tsx | 95 +++-- components/content-link/ContentLink.tsx | 9 +- components/leaderboard/Leaderboard.tsx | 5 +- 5 files changed, 385 insertions(+), 216 deletions(-) diff --git a/app/courses/[courseId]/progress/page.tsx b/app/courses/[courseId]/progress/page.tsx index 2c8502f3..4c578c10 100644 --- a/app/courses/[courseId]/progress/page.tsx +++ b/app/courses/[courseId]/progress/page.tsx @@ -1,198 +1,332 @@ "use client"; -import { StudentCourseLayoutCourseIdQuery$data } from "@/__generated__/StudentCourseLayoutCourseIdQuery.graphql"; +import { pageLearningProgressQuery } from "@/__generated__/pageLearningProgressQuery.graphql"; import { stringToColor } from "@/components/ChapterHeader"; import CompetencyProgressbar from "@/components/CompetencyProgressbar"; import { LightTooltip } from "@/components/LightTooltip"; import { Suggestion } from "@/components/Suggestion"; import { Info } from "@mui/icons-material"; -import { IconButton, Typography } from "@mui/material"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { useCourseData } from "../../../../components/courses/context/CourseDataContext"; +import { Chip, IconButton, Typography } from "@mui/material"; +import { useParams } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import { graphql, useLazyLoadQuery } from "react-relay"; export default function LearningProgress() { - const router = useRouter(); + const { courseId } = useParams(); + const data = useLazyLoadQuery( + graphql` + query pageLearningProgressQuery($id: UUID!) { + coursesByIds(ids: [$id]) { + id + suggestions(amount: 100) { + ...SuggestionFragment + content { + id + ... on Assessment { + items { + associatedSkills { + skillName + skillCategory + } + } + } + } + } + chapters { + elements { + id + contents { + ...ContentLinkFragment + userProgressData { + nextLearnDate + lastLearnDate + } + id + metadata { + type + } + } + } + } + skills { + skillName + skillCategory + skillLevels { + remember { + value + } + understand { + value + } + apply { + value + } + analyze { + value + } + evaluate { + value + } + create { + value + } + } + } + } + } + `, + { id: courseId } + ); - // Get data from context - const data = useCourseData() as StudentCourseLayoutCourseIdQuery$data; const course = data.coursesByIds[0]; - const uniqueSkillCategories = Array.from( - new Map(course.skills.map((skill) => [skill.skillCategory, skill])).values() - ); + const skillsByCategory = useMemo(() => { + return course.skills.reduce< + Record + >((acc, skill) => { + acc[skill.skillCategory] ??= []; + acc[skill.skillCategory].push(skill); + return acc; + }, {}); + }, [course]); + + const uniqueCategories = Object.keys(skillsByCategory); const [selectedCategory, setSelectedCategory] = useState(0); - // Sort categories by total progress - const sortedSkillCategories = [...uniqueSkillCategories].sort((a, b) => { - const getTotalProgress = (category: typeof a) => { - const skillsInCategory = course.skills.filter( - (skill) => skill.skillCategory === category.skillCategory - ); - const uniqueSkills = Array.from( - new Map(skillsInCategory.map((s) => [s.skillName, s])).values() - ); - return uniqueSkills.reduce( - (acc, skill) => - acc + - Object.values(skill.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), - 0 - ), - 0 - ); - }; - return getTotalProgress(b) - getTotalProgress(a); - }); + const sortedCategories = useMemo(() => { + return [...uniqueCategories].sort((a, b) => { + const getTotalProgress = (category: typeof a) => { + const uniqueSkills = Array.from( + new Map( + skillsByCategory[category].map((s) => [s.skillName, s]) + ).values() + ); + return uniqueSkills.reduce( + (acc, skill) => + acc + + Object.values(skill.skillLevels || {}).reduce( + (sum, level) => sum + (level?.value || 0), + 0 + ), + 0 + ); + }; + return getTotalProgress(b) - getTotalProgress(a); + }); + }, [skillsByCategory, uniqueCategories]); + + const currentUniqueSkills = useMemo(() => { + return skillsByCategory[sortedCategories[selectedCategory]].reduce( + (acc, skillA) => { + if (!acc.some((skillB) => skillA.skillName === skillB.skillName)) { + acc.push(skillA); + } + return acc; + }, + [] as (typeof skillsByCategory)[string] + ); + }, [selectedCategory, skillsByCategory, sortedCategories]); + + const filteredSuggestions = useMemo(() => { + return (course.suggestions ?? []).filter((suggestion) => + suggestion.content.items?.some((item) => + item.associatedSkills.some( + (skill) => skill.skillCategory === sortedCategories[selectedCategory] + ) + ) + ); + }, [course.suggestions, selectedCategory, sortedCategories]); + + //const stored = sessionStorage.getItem("previousProgress"); + //console.log("stored", stored); + + //const previousProgress = stored ? new Map(JSON.parse(stored)) : new Map(); + //const updatedProgress = new Map(); + + useEffect(() => { + //const updatedStore = JSON.stringify(Array.from(updatedProgress.entries())); + //sessionStorage.setItem("previousProgress", updatedStore); + //const storedUseEffect = sessionStorage.getItem("previousProgress"); + //console.log("storedUseEffect", storedUseEffect); + }, []); return ( -
-
-
- Knowledge Area - -

Knowledge Area

-

- { - "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." - } -

- - } - > - - - -
-
-
- {sortedSkillCategories.map((category) => { - const skillsInCategory = course.skills.filter( - (skill) => skill.skillCategory === category.skillCategory - ); - const uniqueSkillsInCategory = Array.from( - new Map( - skillsInCategory.map((skill) => [skill.skillName, skill]) - ).values() - ); - - const totalCategoryProgress = uniqueSkillsInCategory.reduce( - (acc, skill) => - acc + - Object.values(skill.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), - 0 - ), +
+
+
+ Knowledge Area + +

Knowledge Area

+

+ { + "A knowledge area is defined as a thematic field of study that groups together related competencies in order to represent the fundamental knowledge of a given subject area." + } +

+ + } + > + + + +
+
+
+ {sortedCategories.map((category) => { + const skillsInCategory = course.skills.filter( + (skill) => skill.skillCategory === category + ); + const uniqueSkillsInCategory = Array.from( + new Map( + skillsInCategory.map((skill) => [skill.skillName, skill]) + ).values() + ); + + const totalCategoryProgress = uniqueSkillsInCategory.reduce( + (acc, skill) => + acc + + Object.values(skill.skillLevels || {}).reduce( + (sum, level) => sum + (level?.value || 0), 0 - ); - let categoryProgressValue = Math.floor( - Math.min( - (totalCategoryProgress * 100) / uniqueSkillsInCategory.length, - 100 - ) - ); - return ( -
-
- setSelectedCategory(sortedSkillCategories.findIndex((s)=> s.skillCategory === category.skillCategory))} - /> -
-
- ); - })} -
-
- -
-
- Competencies - -

Competency

-

- { - "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." - } -

- - } - > - - - -
-
-
- {course.skills.filter( - (skill) => skill.skillCategory === sortedSkillCategories[selectedCategory].skillCategory - ).map((skill) =>{ - const rawValue = Object.values( - skill?.skillLevels || {} - ).reduce((sum, level) => sum + (level?.value || 0), 0); - const clamped = Math.min(rawValue, 1); - const skillProgressPercent = Math.floor(clamped * 100); - return ( -
- -
- ); - }) - } -
-
- -
-
- Task Recommendation - - -

Task Recommendation

-

- { - "Here you can see your personal progress for this course, splitted up in every skill category that is assigned to this course. Every skill category consists of unique skills. These skills are assigned to the different exercises. If you complete an exercise your skill progress will increase." - } -

- - } - > - - - -
-
-
- {course.suggestions.map((x) => ( - +
+ + setSelectedCategory( + sortedCategories.findIndex((c) => c === category) + ) + } + isSelected={category === sortedCategories[selectedCategory]} + disabled={true} /> - ))} -
-
+
+
+ ); + })} +
+
+ +
+
+ Competencies + + +

Competency

+

+ { + "Each task has assigned Competencies to represent the content of the task. The following competencies are part of the selected Knowledge Area." + } +

+ + } + > + + + +
+
+
+ {currentUniqueSkills.map((skill) => { + const rawValue = Object.values(skill?.skillLevels || {}).reduce( + (sum, level) => sum + (level?.value || 0), + 0 + ); + const clamped = Math.min(rawValue, 1); + const skillProgressValue = Math.floor(clamped * 100); + + //const startProgress = previousProgress.has(skill.skillName) ? previousProgress.get(skill.skillName)! : skillProgressPercent; + //updatedProgress.set(skill.skillName, skillProgressPercent); + return ( +
+ +
+ ); + })} +
+
+ +
+
+ Task Recommendation + + +

Task Recommendation

+

+ { + "Here are some recommended tasks to improve your progress in the selected Knowledge Area." + } +

+ + } + > + + + +
+
+
+ {filteredSuggestions.length > 0 ? ( + filteredSuggestions.map((suggestion) => ( + + )) + ) : ( + + {" "} + No task recommendations available for this knowledge area. + + )}
+
+
); -} \ No newline at end of file +} diff --git a/components/ChapterOverview.tsx b/components/ChapterOverview.tsx index 58b972e9..91b2ae13 100644 --- a/components/ChapterOverview.tsx +++ b/components/ChapterOverview.tsx @@ -9,12 +9,6 @@ import { StudentChapter } from "./StudentChapter"; const ChapterFragment = graphql` fragment ChapterOverviewFragment on Course { id - suggestions(amount: 4) { - ...SuggestionFragment - content { - id - } - } chapters { elements { id diff --git a/components/CompetencyProgressbar.tsx b/components/CompetencyProgressbar.tsx index be606a1b..ba3d5893 100644 --- a/components/CompetencyProgressbar.tsx +++ b/components/CompetencyProgressbar.tsx @@ -1,33 +1,33 @@ -import Box from "@mui/material/Box"; +import { Box, useTheme } from "@mui/material"; import LinearProgress from "@mui/material/LinearProgress"; import { useEffect, useState } from "react"; -type CompetencyProgressbarProps = { +export default function CompetencyProgressbar({ + competencyName, + startProgress, + endProgress, + height, + color, + onClick, + isSelected, + disabled, +}: { competencyName: string; - startProgress?:number; + startProgress: number; endProgress: number; height: number; color: string; onClick?: () => void; -}; - -export default function CompetencyProgressbar( - props: CompetencyProgressbarProps -) { - const { competencyName } = props; - const { startProgress } = props; - const { endProgress } = props; - const { height } = props; - const { color } = props; - const { onClick } = props; - - const [progress, setProgress] = useState(startProgress ?? endProgress); + isSelected: boolean; + disabled?: boolean; +}) { + const [progress, setProgress] = useState(startProgress); useEffect(() => { - if (startProgress == null || startProgress === endProgress) return; + if (startProgress === endProgress) return; - const steps = 25; - const totalMs = 500; + const steps = 60; + const totalMs = 3000; const intervalMs = Math.floor(totalMs / steps); const increment = (endProgress - startProgress) / steps; @@ -45,21 +45,54 @@ export default function CompetencyProgressbar( return () => clearInterval(timer); }, [startProgress, endProgress]); + const theme = useTheme(); + return ( - - - + + > + {competencyName + " - " + Math.floor(progress) + "%"} + + {!disabled ? ( + "No progress possible yet." + ) : ( + + )} ); } diff --git a/components/content-link/ContentLink.tsx b/components/content-link/ContentLink.tsx index fd056479..a5d07c97 100644 --- a/components/content-link/ContentLink.tsx +++ b/components/content-link/ContentLink.tsx @@ -266,7 +266,7 @@ export function ContentLink({ }} >
-
+
{content.metadata.name} diff --git a/components/leaderboard/Leaderboard.tsx b/components/leaderboard/Leaderboard.tsx index 9daae102..57fff974 100644 --- a/components/leaderboard/Leaderboard.tsx +++ b/components/leaderboard/Leaderboard.tsx @@ -216,7 +216,10 @@ export default function Leaderboard({ } } } - allTime: getAllTimeCourseLeaderboards(courseID: $courseID, date: "1970-01-01") { + allTime: getAllTimeCourseLeaderboards( + courseID: $courseID + date: "1970-01-01" + ) { id title startDate From 7aa4ea5aca8babf687c6eb46f0e0c8380c9fd8ee Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Tue, 23 Dec 2025 15:08:19 +0100 Subject: [PATCH 3/7] add storing temperory progress data through sessional storage for animation, categorization in locked and urgent, filtered suggestion etc. --- app/courses/[courseId]/progress/page.tsx | 278 ++++++++++++++++------- components/Navbar.tsx | 1 + 2 files changed, 196 insertions(+), 83 deletions(-) diff --git a/app/courses/[courseId]/progress/page.tsx b/app/courses/[courseId]/progress/page.tsx index 4c578c10..b2a279e6 100644 --- a/app/courses/[courseId]/progress/page.tsx +++ b/app/courses/[courseId]/progress/page.tsx @@ -30,21 +30,23 @@ export default function LearningProgress() { } } } + metadata { + chapter { + id + } + } } } chapters { elements { id - contents { - ...ContentLinkFragment - userProgressData { - nextLearnDate - lastLearnDate - } - id - metadata { - type - } + suggestedStartDate + suggestedEndDate + startDate + endDate + skills { + skillName + skillCategory } } } @@ -92,67 +94,138 @@ export default function LearningProgress() { const uniqueCategories = Object.keys(skillsByCategory); + const progressBySkill = useMemo(() => { + const progressBySkillValues = new Map(); + uniqueCategories.forEach((category) => { + skillsByCategory[category].forEach((skill) => { + const currentProgressTuple = progressBySkillValues.get( + skill.skillName + ) ?? [0, 0]; + + const levels = Object.values(skill.skillLevels || {}); + const sumLevels = levels.reduce( + (sum, level) => sum + (level?.value || 0), + 0 + ); + const notZeroValues = levels.filter( + (level) => level?.value !== 0 + ).length; + + const contribution = notZeroValues > 0 ? sumLevels / notZeroValues : 0; + const newSum = currentProgressTuple[0] + contribution; + const newCount = currentProgressTuple[1] + 1; + + progressBySkillValues.set(skill.skillName, [newSum, newCount]); + }); + }); + return progressBySkillValues; + }, [skillsByCategory, uniqueCategories]); + const [selectedCategory, setSelectedCategory] = useState(0); const sortedCategories = useMemo(() => { + if (uniqueCategories.length === 0) return []; return [...uniqueCategories].sort((a, b) => { const getTotalProgress = (category: typeof a) => { - const uniqueSkills = Array.from( + const uniqueSkillsInCategory = Array.from( new Map( - skillsByCategory[category].map((s) => [s.skillName, s]) + skillsByCategory[category].map((skill) => [skill.skillName, skill]) ).values() ); - return uniqueSkills.reduce( - (acc, skill) => - acc + - Object.values(skill.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), - 0 - ), - 0 - ); + const progressSum = uniqueSkillsInCategory.reduce((sum, skill) => { + const tuple = progressBySkill.get(skill.skillName); + if (!tuple || tuple[1] === 0) return sum; + const avg = tuple[0] / tuple[1]; + return sum + avg; + }, 0); + return (progressSum / uniqueSkillsInCategory.length) * 100; }; return getTotalProgress(b) - getTotalProgress(a); }); - }, [skillsByCategory, uniqueCategories]); + }, [progressBySkill, skillsByCategory, uniqueCategories]); const currentUniqueSkills = useMemo(() => { - return skillsByCategory[sortedCategories[selectedCategory]].reduce( - (acc, skillA) => { + if (sortedCategories.length === 0) return []; + return skillsByCategory[sortedCategories[selectedCategory]] + .reduce((acc, skillA) => { if (!acc.some((skillB) => skillA.skillName === skillB.skillName)) { acc.push(skillA); } return acc; - }, - [] as (typeof skillsByCategory)[string] + }, [] as (typeof skillsByCategory)[string]) + .sort((skillA, skillB) => { + const progressA = progressBySkill.get(skillA.skillName)?.[0] ?? 0; + const progressB = progressBySkill.get(skillB.skillName)?.[0] ?? 0; + return progressB - progressA; + }); + }, [progressBySkill, selectedCategory, skillsByCategory, sortedCategories]); + + const urgentChapters = useMemo(() => { + return course.chapters.elements.filter((chapter) => { + const suggestedEndDate = Date.parse( + chapter.suggestedEndDate ?? chapter.endDate + ); + return suggestedEndDate.valueOf() < Date.now(); + }); + }, [course.chapters.elements]); + + const lockedChapters = useMemo(() => { + return course.chapters.elements.filter((chapter) => { + const startDate = Date.parse(chapter.startDate); + return startDate.valueOf() > Date.now(); + }); + }, [course.chapters.elements]); + + const filteredSuggestionsByCategory = (category: string) => { + return (course.suggestions ?? []).filter((suggestion) => + suggestion.content.items?.some((item) => + item.associatedSkills.some((skill) => skill.skillCategory === category) + ) ); - }, [selectedCategory, skillsByCategory, sortedCategories]); + }; - const filteredSuggestions = useMemo(() => { + const filteredSuggestionsBySkill = (skillName: string) => { return (course.suggestions ?? []).filter((suggestion) => suggestion.content.items?.some((item) => - item.associatedSkills.some( - (skill) => skill.skillCategory === sortedCategories[selectedCategory] - ) + item.associatedSkills.some((skill) => skill.skillName === skillName) ) ); - }, [course.suggestions, selectedCategory, sortedCategories]); + }; - //const stored = sessionStorage.getItem("previousProgress"); - //console.log("stored", stored); + const [previousProgress] = useState>(() => { + if (typeof window === "undefined") return new Map(); - //const previousProgress = stored ? new Map(JSON.parse(stored)) : new Map(); - //const updatedProgress = new Map(); + const stored = sessionStorage.getItem("previousProgress"); + return stored + ? new Map(JSON.parse(stored)) + : new Map(); + }); useEffect(() => { - //const updatedStore = JSON.stringify(Array.from(updatedProgress.entries())); - //sessionStorage.setItem("previousProgress", updatedStore); - //const storedUseEffect = sessionStorage.getItem("previousProgress"); - //console.log("storedUseEffect", storedUseEffect); - }, []); + if (uniqueCategories.length === 0) return; + + const tempMap = new Map(); + + progressBySkill.forEach((progressValue, skill) => { + if(!progressValue) return; + const tempProgress = progressValue[0] / progressValue[1] * 100; + tempMap.set(skill, tempProgress); + }) + + sessionStorage.setItem("previousProgress", JSON.stringify([...tempMap])); + }, [progressBySkill, uniqueCategories.length]); + + if (uniqueCategories.length === 0) { + return ( + + {" "} + No Skills in this course to display progress. + + ); + } return ( -
+
Knowledge Area @@ -173,35 +246,53 @@ export default function LearningProgress() {
-
+
{sortedCategories.map((category) => { - const skillsInCategory = course.skills.filter( - (skill) => skill.skillCategory === category - ); const uniqueSkillsInCategory = Array.from( new Map( - skillsInCategory.map((skill) => [skill.skillName, skill]) + skillsByCategory[category].map((skill) => [ + skill.skillName, + skill, + ]) ).values() ); - const totalCategoryProgress = uniqueSkillsInCategory.reduce( - (acc, skill) => - acc + - Object.values(skill.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), - 0 - ), - 0 + const chaptersWithThisCategory = course.chapters.elements.filter( + (chapter) => { + return chapter.skills?.some( + (skill) => skill?.skillCategory === category + ); + } ); - const categoryProgressValue = Math.floor( - Math.min( - (totalCategoryProgress * 100) / uniqueSkillsInCategory.length, - 100 - ) + + const disabled = chaptersWithThisCategory.every((chapter) => + lockedChapters.includes(chapter) + ); + + const urgent = filteredSuggestionsByCategory(category).some( + (suggestion) => + urgentChapters.some( + (chapter) => + chapter.id === suggestion.content.metadata.chapter.id + ) + ); + + const progressSum = uniqueSkillsInCategory.reduce((sum, skill) => { + const tuple = progressBySkill.get(skill.skillName); + if (!tuple || tuple[1] === 0) return sum; + const avg = tuple[0] / tuple[1]; + return sum + avg; + }, 0); + + const categoryProgressValue = + (progressSum / uniqueSkillsInCategory.length) * 100; + + const tempSumPreviousProgress = uniqueSkillsInCategory.reduce((sum, skill) => + sum + (previousProgress.get(skill.skillName) ?? 0), + 0 ); - //const startProgress = previousProgress.has(category) ? previousProgress.get(category)! : categoryProgressValue; - //updatedProgress.set(category, categoryProgressValue); + const previousCategoryProgressValue = previousProgress.size === 0 ? categoryProgressValue : tempSumPreviousProgress / uniqueSkillsInCategory.length; return (
@@ -209,16 +300,17 @@ export default function LearningProgress() { setSelectedCategory( sortedCategories.findIndex((c) => c === category) ) } + isDisabled={disabled} isSelected={category === sortedCategories[selectedCategory]} - disabled={true} + isUrgent={urgent} />
@@ -261,27 +353,44 @@ export default function LearningProgress() {
-
- {currentUniqueSkills.map((skill) => { - const rawValue = Object.values(skill?.skillLevels || {}).reduce( - (sum, level) => sum + (level?.value || 0), - 0 +
+ {currentUniqueSkills.map((currentSkill) => { + const chaptersWithThisSkill = course.chapters.elements.filter( + (chapter) => { + return chapter.skills?.some( + (skill) => skill?.skillName === currentSkill.skillName + ); + } ); - const clamped = Math.min(rawValue, 1); - const skillProgressValue = Math.floor(clamped * 100); - //const startProgress = previousProgress.has(skill.skillName) ? previousProgress.get(skill.skillName)! : skillProgressPercent; - //updatedProgress.set(skill.skillName, skillProgressPercent); + const disabled = chaptersWithThisSkill.every((chapter) => + lockedChapters.includes(chapter) + ); + + const urgent = filteredSuggestionsBySkill( + currentSkill.skillName + ).some((suggestion) => + urgentChapters.some( + (chapter) => + chapter.id === suggestion.content.metadata.chapter.id + ) + ); + + const tuple = progressBySkill.get(currentSkill.skillName) ?? [0, 1]; + const skillProgressValue = (tuple[0] / tuple[1]) * 100; + + const previousSkillProgressValue = previousProgress.get(currentSkill.skillName) ?? skillProgressValue; return ( -
+
); @@ -310,9 +419,12 @@ export default function LearningProgress() {
-
- {filteredSuggestions.length > 0 ? ( - filteredSuggestions.map((suggestion) => ( +
+ {filteredSuggestionsByCategory(sortedCategories[selectedCategory]) + .length > 0 ? ( + filteredSuggestionsByCategory( + sortedCategories[selectedCategory] + ).map((suggestion) => ( { + sessionStorage.clear(); window.localStorage.removeItem("meitrex-welcome-shown"); clearChat(); auth.signoutRedirect({ From a3c2c80164f4a6b0d8513f288d66d6e56f2b6864 Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Tue, 23 Dec 2025 15:10:29 +0100 Subject: [PATCH 4/7] add urgent mode animation, progress increasing animation and complete mode --- components/CompetencyProgressbar.tsx | 133 +++++++++++++++++---------- 1 file changed, 85 insertions(+), 48 deletions(-) diff --git a/components/CompetencyProgressbar.tsx b/components/CompetencyProgressbar.tsx index ba3d5893..af173e93 100644 --- a/components/CompetencyProgressbar.tsx +++ b/components/CompetencyProgressbar.tsx @@ -1,5 +1,5 @@ -import { Box, useTheme } from "@mui/material"; -import LinearProgress from "@mui/material/LinearProgress"; +import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; +import { Box, LinearProgress, Typography, useTheme } from "@mui/material"; import { useEffect, useState } from "react"; export default function CompetencyProgressbar({ @@ -9,8 +9,9 @@ export default function CompetencyProgressbar({ height, color, onClick, - isSelected, - disabled, + isSelected = false, + isDisabled = false, + isUrgent = false, }: { competencyName: string; startProgress: number; @@ -18,80 +19,116 @@ export default function CompetencyProgressbar({ height: number; color: string; onClick?: () => void; - isSelected: boolean; - disabled?: boolean; + isSelected?: boolean; + isDisabled?: boolean; + isUrgent?: boolean; }) { const [progress, setProgress] = useState(startProgress); + const [showBar, setShowBar] = useState(startProgress !== 100); + const [isMoving, setMoving] = useState(startProgress !== endProgress); useEffect(() => { - if (startProgress === endProgress) return; - - const steps = 60; - const totalMs = 3000; - const intervalMs = Math.floor(totalMs / steps); - const increment = (endProgress - startProgress) / steps; - - let current = startProgress; - setProgress(current); - - const timer = setInterval(() => { - current = Math.min(endProgress, current + increment); - setProgress(current); - if (current >= endProgress) { - clearInterval(timer); - } - }, intervalMs); - - return () => clearInterval(timer); + if (startProgress !== endProgress) { + setProgress(endProgress); + setMoving(true); + setShowBar(true); + } else { + setProgress(endProgress); + setMoving(false); + setShowBar(endProgress !== 100); + } }, [startProgress, endProgress]); + const theme = useTheme(); return ( - {competencyName + " - " + Math.floor(progress) + "%"} + {competencyName} {!isDisabled && "-"} + + {isMoving && ( + {`${startProgress}% →`} + )} + + {!isDisabled && ( + + {endProgress}% + {!showBar && ( + + )} + + )} + - {!disabled ? ( - "No progress possible yet." + + {isDisabled ? ( + + No progress possible yet. + ) : ( - + showBar && ( + { + if (e.propertyName === "transform" && (e.target as HTMLElement).classList.contains("MuiLinearProgress-bar")) { + setMoving(false); + if (endProgress === 100) { + setShowBar(false); + } + } + }} + /> + ) )} ); From 5cdc22be46658f0a2079b666a0fea725cb5f7319 Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Fri, 2 Jan 2026 20:33:32 +0100 Subject: [PATCH 5/7] wip --- app/courses/[courseId]/progress/page.tsx | 55 ++++++++++++++++++------ components/CompetencyProgressbar.tsx | 38 ++++++++-------- src/schema.graphql | 18 ++++++++ 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/app/courses/[courseId]/progress/page.tsx b/app/courses/[courseId]/progress/page.tsx index b2a279e6..eca56f42 100644 --- a/app/courses/[courseId]/progress/page.tsx +++ b/app/courses/[courseId]/progress/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { pageAverageSkillValuesQuery } from "@/__generated__/pageAverageSkillValuesQuery.graphql"; import { pageLearningProgressQuery } from "@/__generated__/pageLearningProgressQuery.graphql"; import { stringToColor } from "@/components/ChapterHeader"; import CompetencyProgressbar from "@/components/CompetencyProgressbar"; @@ -51,6 +52,7 @@ export default function LearningProgress() { } } skills { + id skillName skillCategory skillLevels { @@ -82,6 +84,31 @@ export default function LearningProgress() { const course = data.coursesByIds[0]; + const skillIds = useMemo(() => { + return course.skills.map(skill => skill.id).filter((id) => id !== undefined); + }, [course.skills]); + + console.log(skillIds); + console.log(course.skills.map((skill) => skill.id)); + + const averageData = useLazyLoadQuery( + graphql` + query pageAverageSkillValuesQuery($skillIds: [UUID!]!) { + averageSkillValues(skillIds: $skillIds) { + skillId + averageValue + } + } + `, + { skillIds: skillIds} + ); + + const averageSkillValues = useMemo(() => { + return new Map(averageData.averageSkillValues.map(v => [v.skillId, v.averageValue])); + }, [averageData]); + + averageSkillValues.forEach((value, skillId) => console.log(`${skillId} ${value} ${course.skills.filter((skill)=> skill.id === skillId).map((skill)=> skill.skillName)}`)); + const skillsByCategory = useMemo(() => { return course.skills.reduce< Record @@ -196,21 +223,19 @@ export default function LearningProgress() { if (typeof window === "undefined") return new Map(); const stored = sessionStorage.getItem("previousProgress"); - return stored - ? new Map(JSON.parse(stored)) - : new Map(); + return stored ? new Map(JSON.parse(stored)) : new Map(); }); useEffect(() => { if (uniqueCategories.length === 0) return; - const tempMap = new Map(); + const tempMap = new Map(); progressBySkill.forEach((progressValue, skill) => { - if(!progressValue) return; - const tempProgress = progressValue[0] / progressValue[1] * 100; + if (!progressValue) return; + const tempProgress = (progressValue[0] / progressValue[1]) * 100; tempMap.set(skill, tempProgress); - }) + }); sessionStorage.setItem("previousProgress", JSON.stringify([...tempMap])); }, [progressBySkill, uniqueCategories.length]); @@ -287,12 +312,16 @@ export default function LearningProgress() { const categoryProgressValue = (progressSum / uniqueSkillsInCategory.length) * 100; - const tempSumPreviousProgress = uniqueSkillsInCategory.reduce((sum, skill) => - sum + (previousProgress.get(skill.skillName) ?? 0), + const tempSumPreviousProgress = uniqueSkillsInCategory.reduce( + (sum, skill) => + sum + (previousProgress.get(skill.skillName) ?? 0), 0 ); - const previousCategoryProgressValue = previousProgress.size === 0 ? categoryProgressValue : tempSumPreviousProgress / uniqueSkillsInCategory.length; + const previousCategoryProgressValue = + previousProgress.size === 0 + ? categoryProgressValue + : tempSumPreviousProgress / uniqueSkillsInCategory.length; return (
@@ -379,12 +408,14 @@ export default function LearningProgress() { const tuple = progressBySkill.get(currentSkill.skillName) ?? [0, 1]; const skillProgressValue = (tuple[0] / tuple[1]) * 100; - const previousSkillProgressValue = previousProgress.get(currentSkill.skillName) ?? skillProgressValue; + const previousSkillProgressValue = + previousProgress.get(currentSkill.skillName) ?? + skillProgressValue; return (
void; @@ -28,18 +30,16 @@ export default function CompetencyProgressbar({ const [isMoving, setMoving] = useState(startProgress !== endProgress); useEffect(() => { - if (startProgress !== endProgress) { - setProgress(endProgress); + setProgress(endProgress); + if (startProgress !== endProgress && endProgress - startProgress > 1) { setMoving(true); setShowBar(true); } else { - setProgress(endProgress); setMoving(false); setShowBar(endProgress !== 100); } }, [startProgress, endProgress]); - const theme = useTheme(); return ( @@ -51,9 +51,7 @@ export default function CompetencyProgressbar({ mb: 1, borderRadius: "14px", cursor: "pointer", - outline: isSelected - ? `4px solid ${color}` - : "2px solid #E5E7EB", + outline: isSelected ? `4px solid ${color}` : "2px solid #E5E7EB", backgroundColor: "#FFFFFF", boxShadow: isUrgent ? "0 0 15px rgba(211, 47, 47, 0.7)" : "none", animation: isUrgent ? "pulse-red 2s infinite" : "none", @@ -82,21 +80,20 @@ export default function CompetencyProgressbar({ : theme.palette.text.primary, }} > - {competencyName} {!isDisabled && "-"} + + {competencyName} {!isDisabled && "-"} + - {isMoving && ( - {`${startProgress}% →`} - )} + {isMoving && {`${startProgress}% →`}} {!isDisabled && ( - + {endProgress}% {!showBar && ( )} )} - {isDisabled ? ( @@ -120,11 +117,16 @@ export default function CompetencyProgressbar({ borderColor: "gold", }} onTransitionEnd={(e) => { - if (e.propertyName === "transform" && (e.target as HTMLElement).classList.contains("MuiLinearProgress-bar")) { - setMoving(false); - if (endProgress === 100) { - setShowBar(false); - } + if ( + e.propertyName === "transform" && + (e.target as HTMLElement).classList.contains( + "MuiLinearProgress-bar" + ) + ) { + setMoving(false); + if (endProgress === 100) { + setShowBar(false); + } } }} /> diff --git a/src/schema.graphql b/src/schema.graphql index 55e0c25f..41d22dc4 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1273,6 +1273,15 @@ type Post { upvotedByUsers: [UUID]! } +type ProactiveFeedback { + assessmentId: UUID! + correctness: Float! + createdAt: DateTime! + feedbackText: String! + id: UUID! + success: Boolean! +} + type ProgressLogItem { correctness: Float! hintsUsed: Int! @@ -1292,6 +1301,8 @@ type Query { _empty: String achievementsByCourseId(courseId: UUID!): [Achievement!]! achievementsByUserId(userId: UUID): [Achievement!]! + allProactiveFeedback: [ProactiveFeedback!]! + averageSkillValues(skillIds: [UUID!]!): [SkillAverageValue!]! contentsByChapterIds(chapterIds: [UUID!]!): [[Content!]!]! contentsByCourseIds(courseIds: [UUID!]!): [[Content!]!] contentsByIds(ids: [UUID!]!): [Content!]! @@ -1340,6 +1351,7 @@ type Query { openQuestionByCourseId(id: UUID!): [Thread!]! otherUserForumActivityByUserId(otherUserId: UUID!): [ForumActivityEntry!]! PlayerHexadScoreExists(userId: UUID!): Boolean! + proactiveFeedback(assessmentId: UUID!): ProactiveFeedback scoreboard(courseId: UUID!): [ScoreboardItem!]! semanticSearch(count: Int! = 10, courseWhitelist: [UUID!], queryText: String!): [SemanticSearchResult!]! submissionExerciseByUser(assessmentId: UUID!): SubmissionExercise! @@ -1597,6 +1609,11 @@ type Skill { skillName: String! } +type SkillAverageValue { + averageValue: Float! + skillId: UUID! +} + input SkillInput { id: UUID isCustomSkill: Boolean! @@ -1744,6 +1761,7 @@ type SubmissionSolution { type Subscription { notificationAdded(userId: UUID!): NotificationData! + proactiveFeedbackAdded(userId: UUID!): ProactiveFeedback! } type Suggestion { From 6a7a95ea88c67f721162e042fa97b7e1eb2f0c2d Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Fri, 9 Jan 2026 15:39:36 +0100 Subject: [PATCH 6/7] use of skillValue and skillAllUsersStats to display average progress --- app/courses/[courseId]/progress/page.tsx | 321 ++++++++++++++--------- components/CompetencyProgressbar.tsx | 168 +++++++++--- src/schema.graphql | 15 +- 3 files changed, 332 insertions(+), 172 deletions(-) diff --git a/app/courses/[courseId]/progress/page.tsx b/app/courses/[courseId]/progress/page.tsx index eca56f42..41e31b7e 100644 --- a/app/courses/[courseId]/progress/page.tsx +++ b/app/courses/[courseId]/progress/page.tsx @@ -1,13 +1,20 @@ "use client"; -import { pageAverageSkillValuesQuery } from "@/__generated__/pageAverageSkillValuesQuery.graphql"; import { pageLearningProgressQuery } from "@/__generated__/pageLearningProgressQuery.graphql"; import { stringToColor } from "@/components/ChapterHeader"; import CompetencyProgressbar from "@/components/CompetencyProgressbar"; import { LightTooltip } from "@/components/LightTooltip"; import { Suggestion } from "@/components/Suggestion"; import { Info } from "@mui/icons-material"; -import { Chip, IconButton, Typography } from "@mui/material"; +import { + Checkbox, + Chip, + FormControlLabel, + IconButton, + Slide, + Typography, + useTheme, +} from "@mui/material"; import { useParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { graphql, useLazyLoadQuery } from "react-relay"; @@ -19,6 +26,7 @@ export default function LearningProgress() { query pageLearningProgressQuery($id: UUID!) { coursesByIds(ids: [$id]) { id + numberOfCourseMemberships suggestions(amount: 100) { ...SuggestionFragment content { @@ -52,28 +60,13 @@ export default function LearningProgress() { } } skills { - id skillName skillCategory - skillLevels { - remember { - value - } - understand { - value - } - apply { - value - } - analyze { - value - } - evaluate { - value - } - create { - value - } + skillValue + skillAllUsersStats { + skillValueSum + participantCount + averageSkillValue } } } @@ -84,31 +77,6 @@ export default function LearningProgress() { const course = data.coursesByIds[0]; - const skillIds = useMemo(() => { - return course.skills.map(skill => skill.id).filter((id) => id !== undefined); - }, [course.skills]); - - console.log(skillIds); - console.log(course.skills.map((skill) => skill.id)); - - const averageData = useLazyLoadQuery( - graphql` - query pageAverageSkillValuesQuery($skillIds: [UUID!]!) { - averageSkillValues(skillIds: $skillIds) { - skillId - averageValue - } - } - `, - { skillIds: skillIds} - ); - - const averageSkillValues = useMemo(() => { - return new Map(averageData.averageSkillValues.map(v => [v.skillId, v.averageValue])); - }, [averageData]); - - averageSkillValues.forEach((value, skillId) => console.log(`${skillId} ${value} ${course.skills.filter((skill)=> skill.id === skillId).map((skill)=> skill.skillName)}`)); - const skillsByCategory = useMemo(() => { return course.skills.reduce< Record @@ -121,34 +89,10 @@ export default function LearningProgress() { const uniqueCategories = Object.keys(skillsByCategory); - const progressBySkill = useMemo(() => { - const progressBySkillValues = new Map(); - uniqueCategories.forEach((category) => { - skillsByCategory[category].forEach((skill) => { - const currentProgressTuple = progressBySkillValues.get( - skill.skillName - ) ?? [0, 0]; - - const levels = Object.values(skill.skillLevels || {}); - const sumLevels = levels.reduce( - (sum, level) => sum + (level?.value || 0), - 0 - ); - const notZeroValues = levels.filter( - (level) => level?.value !== 0 - ).length; - - const contribution = notZeroValues > 0 ? sumLevels / notZeroValues : 0; - const newSum = currentProgressTuple[0] + contribution; - const newCount = currentProgressTuple[1] + 1; - - progressBySkillValues.set(skill.skillName, [newSum, newCount]); - }); - }); - return progressBySkillValues; - }, [skillsByCategory, uniqueCategories]); - const [selectedCategory, setSelectedCategory] = useState(0); + const [selectedSkill, setSelectedSkill] = useState(null); + + const [showAverageProgress, setAverageProgress] = useState(false); const sortedCategories = useMemo(() => { if (uniqueCategories.length === 0) return []; @@ -160,16 +104,14 @@ export default function LearningProgress() { ).values() ); const progressSum = uniqueSkillsInCategory.reduce((sum, skill) => { - const tuple = progressBySkill.get(skill.skillName); - if (!tuple || tuple[1] === 0) return sum; - const avg = tuple[0] / tuple[1]; - return sum + avg; + const progress = skill.skillValue; + return sum + progress; }, 0); return (progressSum / uniqueSkillsInCategory.length) * 100; }; return getTotalProgress(b) - getTotalProgress(a); }); - }, [progressBySkill, skillsByCategory, uniqueCategories]); + }, [skillsByCategory, uniqueCategories]); const currentUniqueSkills = useMemo(() => { if (sortedCategories.length === 0) return []; @@ -181,11 +123,9 @@ export default function LearningProgress() { return acc; }, [] as (typeof skillsByCategory)[string]) .sort((skillA, skillB) => { - const progressA = progressBySkill.get(skillA.skillName)?.[0] ?? 0; - const progressB = progressBySkill.get(skillB.skillName)?.[0] ?? 0; - return progressB - progressA; + return skillB.skillValue - skillA.skillValue; }); - }, [progressBySkill, selectedCategory, skillsByCategory, sortedCategories]); + }, [selectedCategory, skillsByCategory, sortedCategories]); const urgentChapters = useMemo(() => { return course.chapters.elements.filter((chapter) => { @@ -231,14 +171,14 @@ export default function LearningProgress() { const tempMap = new Map(); - progressBySkill.forEach((progressValue, skill) => { - if (!progressValue) return; - const tempProgress = (progressValue[0] / progressValue[1]) * 100; - tempMap.set(skill, tempProgress); + course.skills.forEach((skill) => { + tempMap.set(skill.skillName, skill.skillValue * 100); }); sessionStorage.setItem("previousProgress", JSON.stringify([...tempMap])); - }, [progressBySkill, uniqueCategories.length]); + }, [course.skills, uniqueCategories.length]); + + const theme = useTheme(); if (uniqueCategories.length === 0) { return ( @@ -252,7 +192,7 @@ export default function LearningProgress() { return (
-
+
Knowledge Area +
+ setAverageProgress(!showAverageProgress)} + /> + } + label="show average Progress" + /> +
{sortedCategories.map((category) => { @@ -303,14 +257,29 @@ export default function LearningProgress() { ); const progressSum = uniqueSkillsInCategory.reduce((sum, skill) => { - const tuple = progressBySkill.get(skill.skillName); - if (!tuple || tuple[1] === 0) return sum; - const avg = tuple[0] / tuple[1]; - return sum + avg; + const progress = skill.skillValue * 100; + return sum + progress; }, 0); + const averageProgressSum = uniqueSkillsInCategory.reduce( + (sum, skill) => { + const averageProgress = + skill.skillAllUsersStats.averageSkillValue * 100; + return sum + averageProgress; + }, + 0 + ); + + const maxParticipantCountForaSkill = Math.max( + ...uniqueSkillsInCategory.map( + (skill) => skill.skillAllUsersStats.participantCount + ) + ); + const categoryProgressValue = - (progressSum / uniqueSkillsInCategory.length) * 100; + progressSum / uniqueSkillsInCategory.length; + const categoryAverageProgressValue = + averageProgressSum / uniqueSkillsInCategory.length; const tempSumPreviousProgress = uniqueSkillsInCategory.reduce( (sum, skill) => @@ -328,18 +297,25 @@ export default function LearningProgress() {
+ onClick={() => { setSelectedCategory( sortedCategories.findIndex((c) => c === category) - ) - } + ); + setSelectedSkill(null); + }} isDisabled={disabled} isSelected={category === sortedCategories[selectedCategory]} isUrgent={urgent} + showAverageProgress={showAverageProgress} + participantCount={maxParticipantCountForaSkill} + courseMemberCount={course.numberOfCourseMemberships} + openTaskCount={ + filteredSuggestionsByCategory(category).length + } />
@@ -405,25 +381,55 @@ export default function LearningProgress() { ) ); - const tuple = progressBySkill.get(currentSkill.skillName) ?? [0, 1]; - const skillProgressValue = (tuple[0] / tuple[1]) * 100; + const skillProgressValue = currentSkill.skillValue * 100; + const skillAverageProgressValue = + currentSkill.skillAllUsersStats.averageSkillValue * 100; const previousSkillProgressValue = previousProgress.get(currentSkill.skillName) ?? skillProgressValue; return ( -
- -
+ +
+ { + const currentIndex = currentUniqueSkills.findIndex( + (skill) => skill === currentSkill + ); + currentIndex === selectedSkill + ? setSelectedSkill(null) + : setSelectedSkill(currentIndex); + }} + isDisabled={disabled} + isSelected={ + selectedSkill === null + ? false + : currentUniqueSkills.at(selectedSkill) === currentSkill + } + isUrgent={urgent} + showAverageProgress={showAverageProgress} + participantCount={ + currentSkill.skillAllUsersStats.participantCount + } + courseMemberCount={course.numberOfCourseMemberships} + openTaskCount={ + filteredSuggestionsBySkill(currentSkill.skillName).length + } + /> +
+
); })}
@@ -450,25 +456,80 @@ export default function LearningProgress() {
-
- {filteredSuggestionsByCategory(sortedCategories[selectedCategory]) - .length > 0 ? ( - filteredSuggestionsByCategory( - sortedCategories[selectedCategory] - ).map((suggestion) => ( - - )) - ) : ( - - {" "} - No task recommendations available for this knowledge area. - - )} -
+ {selectedSkill === null ? ( +
+ {filteredSuggestionsByCategory(sortedCategories[selectedCategory]) + .length > 0 ? ( + filteredSuggestionsByCategory( + sortedCategories[selectedCategory] + ).map((suggestion) => ( + +
+ +
+
+ )) + ) : ( + + {" "} + No task recommendations available for this knowledge area. + + )} +
+ ) : ( +
+ Urgent Tasks: +
+ {filteredSuggestionsBySkill( + currentUniqueSkills.at(selectedSkill)!.skillName + ) + .filter((suggestion) => + urgentChapters.some( + (urgentChapter) => + urgentChapter.id === + suggestion.content.metadata.chapter.id + ) + ) + .map((urgentSuggestion) => ( + + ))} +
+ Other Tasks: +
+ {filteredSuggestionsBySkill( + currentUniqueSkills.at(selectedSkill)!.skillName + ) + .filter( + (suggestion) => + !urgentChapters.some( + (urgentChapter) => + urgentChapter.id === + suggestion.content.metadata.chapter.id + ) + ) + .map((notUrgentSuggestion) => ( + + ))} +
+
+ )}
); diff --git a/components/CompetencyProgressbar.tsx b/components/CompetencyProgressbar.tsx index bc108ab0..98e8d04d 100644 --- a/components/CompetencyProgressbar.tsx +++ b/components/CompetencyProgressbar.tsx @@ -1,29 +1,44 @@ import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; -import { Box, LinearProgress, Typography, useTheme } from "@mui/material"; +import GroupsIcon from "@mui/icons-material/Groups"; +import { + Box, + LinearProgress, + Tooltip, + Typography, + useTheme, +} from "@mui/material"; import { useEffect, useState } from "react"; export default function CompetencyProgressbar({ competencyName, startProgress, endProgress, - averageProgress, - height, + averageProgress = 0, + small = false, color, onClick, isSelected = false, isDisabled = false, isUrgent = false, + showAverageProgress = false, + participantCount, + courseMemberCount, + openTaskCount, }: { competencyName: string; startProgress: number; endProgress: number; averageProgress?: number; - height: number; + small?: boolean; color: string; onClick?: () => void; isSelected?: boolean; isDisabled?: boolean; isUrgent?: boolean; + showAverageProgress?: boolean; + participantCount: number; + courseMemberCount: number; + openTaskCount: number; }) { const [progress, setProgress] = useState(startProgress); const [showBar, setShowBar] = useState(startProgress !== 100); @@ -44,14 +59,24 @@ export default function CompetencyProgressbar({ return ( + + {openTaskCount} open Tasks + + @@ -87,7 +134,14 @@ export default function CompetencyProgressbar({ {isMoving && {`${startProgress}% →`}} {!isDisabled && ( - + {endProgress}% {!showBar && ( @@ -102,34 +156,70 @@ export default function CompetencyProgressbar({ ) : ( showBar && ( - { - if ( - e.propertyName === "transform" && - (e.target as HTMLElement).classList.contains( - "MuiLinearProgress-bar" - ) - ) { - setMoving(false); - if (endProgress === 100) { - setShowBar(false); + + { + if ( + e.propertyName === "transform" && + (e.target as HTMLElement).classList.contains( + "MuiLinearProgress-bar" + ) + ) { + setMoving(false); + if (endProgress === 100) { + setShowBar(false); + } } - } - }} - /> + }} + /> + + {showAverageProgress && ( + + + Average Progress: {averageProgress} % + + + + {participantCount} / {courseMemberCount} students + started working on this. + + + + } + > + + + + + )} + ) )} diff --git a/src/schema.graphql b/src/schema.graphql index 41d22dc4..58904921 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -299,6 +299,7 @@ type Course { id: UUID! mediaRecords: [MediaRecord!]! memberships: [CourseMembership!]! + numberOfCourseMemberships: Int! published: Boolean! rewardScores: RewardScores! scoreboard: [ScoreboardItem!]! @@ -1302,7 +1303,6 @@ type Query { achievementsByCourseId(courseId: UUID!): [Achievement!]! achievementsByUserId(userId: UUID): [Achievement!]! allProactiveFeedback: [ProactiveFeedback!]! - averageSkillValues(skillIds: [UUID!]!): [SkillAverageValue!]! contentsByChapterIds(chapterIds: [UUID!]!): [[Content!]!]! contentsByCourseIds(courseIds: [UUID!]!): [[Content!]!] contentsByIds(ids: [UUID!]!): [Content!]! @@ -1604,14 +1604,18 @@ directive @Size(min: Int = 0, max: Int = 2147483647, message: String = "graphql. type Skill { id: UUID! isCustomSkill: Boolean! + skillAllUsersStats: SkillAllUsersStats! skillCategory: String! skillLevels: SkillLevels skillName: String! + skillValue: Float! } -type SkillAverageValue { - averageValue: Float! +type SkillAllUsersStats { + averageSkillValue: Float! + participantCount: Int! skillId: UUID! + skillValueSum: Float! } input SkillInput { @@ -1655,6 +1659,11 @@ enum SkillType { UNDERSTAND } +type SkillValue { + skillId: UUID! + skillValue: Float! +} + enum SortDirection { ASC DESC From f310926082a45d8d32750e1ef20bd6fd36c8545f Mon Sep 17 00:00:00 2001 From: Sangaran Ramesch Date: Thu, 15 Jan 2026 19:23:07 +0100 Subject: [PATCH 7/7] prettier style fix --- components/ChapterHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ChapterHeader.tsx b/components/ChapterHeader.tsx index 5323ec3b..ca1c039e 100644 --- a/components/ChapterHeader.tsx +++ b/components/ChapterHeader.tsx @@ -29,7 +29,7 @@ export function stringToColor(string: string): string { "Networking and Communication": "#E0FFFF", // light cyan "Operating Systems": "#AEC6CF", // pastel blue "Parallel and Distributed Computing": "#FFDAB9", // pastel peach - "Security": "#FDFD96", // pastel yellow + Security: "#FDFD96", // pastel yellow "Society, Ethics, and the Profession": "#FFFACD", // pastel lemon "Software Development Fundamentals": "#D5E8D4", // pastel mint "Software Engineering": "#C1E1C1", // pastel light green