From 077fce74ec42cd6f2d402bad69e53d1b694468f5 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Thu, 25 Jun 2026 05:34:11 +0000
Subject: [PATCH] Implement functional Student Portal features and fix UI/logic
bugs
- Implemented functional QuizView component and integrated it into the course player.
- Updated Student Dashboard to display real course progress and correctly filter recommended courses.
- Implemented dynamic Arena Leaderboard by aggregating student XP from all organizations in Firebase.
- Refined gamification logic for points and XP calculation to match requirements.
- Fixed activity count calculation on Course Detail page.
- Normalized directory names to resolve case-sensitivity routing issues.
- Cleaned up redundant code and addressed code review feedback.
Co-authored-by: oshadhashiro404 <91681163+oshadhashiro404@users.noreply.github.com>
---
.../CoursePlayerWrapper.tsx | 21 +-
.../PDFViewModule.tsx | 0
.../activities/[activityId]/QuizView.tsx | 92 ++++++++
.../VideoPlayerModule.tsx | 0
.../activities/[activityId]/page.tsx | 105 +--------
app/StudentPortal/courses/[id]/page.tsx | 3 +-
app/StudentPortal/dashboard/page.tsx | 54 +++--
.../organizations/OrganizationContext.tsx | 21 +-
app/leaderboard/page.tsx | 213 +++++++++++++-----
9 files changed, 308 insertions(+), 201 deletions(-)
rename app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/{[activityid] => [activityId]}/CoursePlayerWrapper.tsx (89%)
rename app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/{[activityid] => [activityId]}/PDFViewModule.tsx (100%)
create mode 100644 app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/QuizView.tsx
rename app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/{[activityid] => [activityId]}/VideoPlayerModule.tsx (100%)
diff --git a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/CoursePlayerWrapper.tsx b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/CoursePlayerWrapper.tsx
similarity index 89%
rename from app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/CoursePlayerWrapper.tsx
rename to app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/CoursePlayerWrapper.tsx
index a0104c6..972391f 100644
--- a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/CoursePlayerWrapper.tsx
+++ b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/CoursePlayerWrapper.tsx
@@ -7,7 +7,7 @@ import { Loader2 } from "lucide-react";
// slow loading
const VideoPlayerModule = lazy(() => import("./VideoPlayerModule"));
const PDFViewerModule = lazy(() => import("./PDFViewModule"));
-// quiz view is inline btw
+const QuizView = lazy(() => import("./QuizView"));
interface CoursePlayerWrapperProps {
activity: {
type: string;
@@ -95,16 +95,15 @@ export default function CoursePlayerWrapper({ activity, onComplete, submitting }
);
case "quiz":
- // can import and use the exisiting quiz view
- const quizConfig = content as QuizBlockConfig;
- if (!quizConfig.quizQuestions?.length) {
- return (
-
-
No quiz questions have been added to this block yet.
-
- );
- }
- return Quiz renderer — wire in QuizView component here.
;
+ return (
+ }>
+
+
+ );
case "storyline":
return (
diff --git a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/PDFViewModule.tsx b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/PDFViewModule.tsx
similarity index 100%
rename from app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/PDFViewModule.tsx
rename to app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/PDFViewModule.tsx
diff --git a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/QuizView.tsx b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/QuizView.tsx
new file mode 100644
index 0000000..864404d
--- /dev/null
+++ b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/QuizView.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import React, { useState } from "react";
+import { CheckCircle, Loader2 } from "lucide-react";
+import { QuizBlockConfig } from "@/app/lib/course.types";
+
+interface QuizViewProps {
+ config: QuizBlockConfig;
+ onComplete: (points?: number) => void;
+ submitting: boolean;
+}
+
+export default function QuizView({ config, onComplete, submitting }: QuizViewProps) {
+ const [answers, setAnswers] = useState
>({});
+ const [submitted, setSubmitted] = useState(false);
+
+ const questions = config.quizQuestions || [];
+
+ const handleSubmit = () => {
+ setSubmitted(true);
+ const correct = questions.filter((q, i) =>
+ answers[i]?.trim().toLowerCase() === q.answer?.trim().toLowerCase()
+ ).length;
+
+ const accuracy = questions.length > 0 ? (correct / questions.length) * 100 : 0;
+ // accuracy is used by completeActivity via handleComplete's overridePoints mapping
+ // handleComplete in page.tsx: accuracy = overridePoints ? Math.round((overridePoints / 30) * 100) : 100
+ // So we pass points equivalent to accuracy out of 30.
+ // Wait, the page.tsx expects overridePoints out of 30 if we want to influence accuracy.
+ // If we pass 10 here, page.tsx will calculate accuracy as (10/30)*100 = 33%.
+ // So we should pass the accuracy percentage scaled to 30.
+ const accuracyScaledTo30 = Math.round((accuracy / 100) * 30);
+ onComplete(accuracyScaledTo30);
+ };
+
+ const allAnswered = questions.every((_, i) => answers[i]?.trim());
+
+ if (questions.length === 0) {
+ return (
+
+
No quiz questions have been added to this block yet.
+
+ );
+ }
+
+ return (
+
+ {questions.map((q, i) => (
+
+
{i + 1}. {q.question}
+ {q.options && q.options.length > 0 ? (
+
+ {q.options.map((opt: string, oi: number) => (
+
+ ))}
+
+ ) : (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/VideoPlayerModule.tsx b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/VideoPlayerModule.tsx
similarity index 100%
rename from app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityid]/VideoPlayerModule.tsx
rename to app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/activities/[activityId]/VideoPlayerModule.tsx
diff --git a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/page.tsx b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/page.tsx
index 2531fb1..af10847 100644
--- a/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/page.tsx
+++ b/app/StudentPortal/courses/[id]/modules/[moduleId]/activities/[activityId]/page.tsx
@@ -7,7 +7,7 @@ import { useOrganizations } from "@/app/components/organizations/OrganizationCon
import { useAuth } from "@/app/components/AuthCOntext";
import InteractiveSandbox from "@/app/components/InteractiveSandbox";
import { ArrowLeft, CheckCircle, Clock, Zap, Loader2 } from "lucide-react";
-import CoursePlayerWrapper from "./activities/[activityid]/CoursePlayerWrapper";
+import CoursePlayerWrapper from "./activities/[activityId]/CoursePlayerWrapper";
export default function StudentActivityPage() {
const params = useParams<{ id: string; moduleId: string; activityId: string }>();
@@ -129,104 +129,13 @@ export default function StudentActivityPage() {
{activity.durationMinutes || 10} min
- {activity.title}
- {activity.type === 'sandbox' && (
-
-
handleComplete(pts)} />
-
-
-
-
- )}
- {activity.type === 'quiz' && activity.content?.quizQuestions && (
-
- )}
- {(activity.type === 'video' || activity.type === 'storyline') && (
-
-
-
{activity.content?.description || 'Review the material below.'}
-
-
-
-
-
- )}
-
-
- );
-}
-
-function QuizView({ questions, onComplete, submitting }: { questions: any[]; onComplete: (pts?: number) => void; submitting: boolean }) {
- const [answers, setAnswers] = useState>({});
- const [submitted, setSubmitted] = useState(false);
-
- const handleSubmit = () => {
- setSubmitted(true);
- const correct = questions.filter((q, i) => answers[i]?.trim().toLowerCase() === q.answer?.trim().toLowerCase()).length;
- const accuracy = questions.length > 0 ? Math.round((correct / questions.length) * 100) : 0;
- const points = Math.round(accuracy / 10) * 3;
- onComplete(points);
- };
-
- const allAnswered = questions.every((_, i) => answers[i]?.trim());
+ {activity.title}
- return (
-
- {questions.map((q, i) => (
-
-
{i + 1}. {q.question}
- {q.options ? (
-
- {q.options.map((opt: string, oi: number) => (
-
- ))}
-
- ) : (
-
- ))}
-
-
+
);
diff --git a/app/StudentPortal/courses/[id]/page.tsx b/app/StudentPortal/courses/[id]/page.tsx
index ef6e836..60f5255 100644
--- a/app/StudentPortal/courses/[id]/page.tsx
+++ b/app/StudentPortal/courses/[id]/page.tsx
@@ -39,6 +39,7 @@ export default function StudentCourseDetail() {
}
const modules = course.modules ? Object.values(course.modules) : [];
+ const totalActivities = modules.reduce((acc: number, mod: any) => acc + Object.keys(mod.activityIds || {}).length, 0);
return (
@@ -63,7 +64,7 @@ export default function StudentCourseDetail() {
-
{Object.keys(course.activityIds || {}).length}
+
{totalActivities}
Activities
diff --git a/app/StudentPortal/dashboard/page.tsx b/app/StudentPortal/dashboard/page.tsx
index 8622675..990969f 100644
--- a/app/StudentPortal/dashboard/page.tsx
+++ b/app/StudentPortal/dashboard/page.tsx
@@ -29,10 +29,11 @@ interface RecommendedCourse {
export default function StudentDashboard() {
const { announce } = useAccessibility();
const { user, profile } = useAuth();
- const { getOrganizationsForStudent, getStudentXp, getXpProgress, getStreak, getOrgCoursesWithData } = useOrganizations();
+ const { getOrganizationsForStudent, getStudentXp, getXpProgress, getStreak, getOrgCoursesWithData, getCourseProgress } = useOrganizations();
const [orgLevelData, setOrgLevelData] = useState<{ orgName: string; orgId: string; level: number; currentXp: number; nextLevelXp: number } | null>(null);
const [streak, setStreak] = useState<{ current: number; longest: number } | null>(null);
const [allCourses, setAllCourses] = useState
([]);
+ const [courseProgress, setCourseProgress] = useState>({});
useEffect(() => {
if (!user?.uid) return;
@@ -44,31 +45,46 @@ export default function StudentDashboard() {
setOrgLevelData({ orgName: org.name, orgId: org.id, ...progress });
const courses = await getOrgCoursesWithData(org.id);
setAllCourses(courses);
+
+ const progressData: Record = {};
+ await Promise.all(courses.map(async (course) => {
+ if (course.id) {
+ progressData[course.id] = await getCourseProgress(course.id, user.uid);
+ }
+ }));
+ setCourseProgress(progressData);
});
getStreak(user.uid).then(data => setStreak(data));
- }, [user?.uid, getOrganizationsForStudent, getStudentXp, getXpProgress, getStreak, getOrgCoursesWithData]);
+ }, [user?.uid, getOrganizationsForStudent, getStudentXp, getXpProgress, getStreak, getOrgCoursesWithData, getCourseProgress]);
+
+ const inProgressCourses = allCourses.filter(c => courseProgress[c.id] > 0 && courseProgress[c.id] < 100);
+ const notStartedCourses = allCourses.filter(c => !courseProgress[c.id] || courseProgress[c.id] === 0);
+ const completedCourses = allCourses.filter(c => courseProgress[c.id] === 100);
- const continueLearningCourses: ContinueLearningCourse[] = allCourses.length > 0 ? allCourses.slice(0, 3).map(c => ({
+ const continueLearningCourses: ContinueLearningCourse[] = [...inProgressCourses, ...notStartedCourses].slice(0, 3).map(c => ({
id: c.id,
title: c.title,
- status: "in-progress" as const,
- progress: 0,
+ status: "in-progress",
+ progress: courseProgress[c.id] || 0,
metricText: `${Object.keys(c.modules || {}).length} Modules`,
- footerText: "Continue",
- })) : [];
+ footerText: courseProgress[c.id] > 0 ? "Continue" : "Start",
+ }));
- const recommendedCourses: RecommendedCourse[] = allCourses.length > 3 ? allCourses.slice(3).map((c, i) => ({
- id: c.id,
- title: c.title,
- category: (["SCIENCE", "DESIGN", "TECH", "ROBOTICS"])[i % 4] as "SCIENCE" | "DESIGN" | "TECH" | "ROBOTICS",
- rating: 4.8,
- lessonsCount: Object.keys(c.modules || {}).length,
- bgGradient: ["from-blue-900 to-indigo-950", "from-orange-950 to-amber-900", "from-slate-900 to-sky-950", "from-emerald-950 to-teal-900"][i % 4],
- })) : [];
+ const recommendedCourses: RecommendedCourse[] = allCourses
+ .filter(c => !courseProgress[c.id] || courseProgress[c.id] < 100)
+ .map((c, i) => ({
+ id: c.id,
+ title: c.title,
+ category: (["SCIENCE", "DESIGN", "TECH", "ROBOTICS"])[i % 4] as "SCIENCE" | "DESIGN" | "TECH" | "ROBOTICS",
+ rating: 4.8,
+ lessonsCount: Object.keys(c.modules || {}).length,
+ bgGradient: ["from-blue-900 to-indigo-950", "from-orange-950 to-amber-900", "from-slate-900 to-sky-950", "from-emerald-950 to-teal-900"][i % 4],
+ }));
const [carouselIndex, setCarouselIndex] = useState(0);
const handleNextCarousel = () => {
- if (carouselIndex < recommendedCourses.length - 1) {
+ const maxIndex = Math.max(0, recommendedCourses.length - 4);
+ if (carouselIndex < maxIndex) {
setCarouselIndex((prev) => prev + 1);
announce("Showing next recommended courses");
}
@@ -275,8 +291,8 @@ export default function StudentDashboard() {