Seats
+12 / 20 used
+diff --git a/.env.local.example b/.env.local.example
old mode 100644
new mode 100755
index bc808a2..f3304ba
--- a/.env.local.example
+++ b/.env.local.example
@@ -1,3 +1,5 @@
NEXT_PUBLIC_SUPABASE_URL=YOUR_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_ANON_KEY
TEACHER_EMAILS=teacher@example.com
+DATABASE_URL=
+DIRECT_URL=
diff --git a/.eslintrc.json b/.eslintrc.json
old mode 100644
new mode 100755
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index caae059..7302da5
--- a/.gitignore
+++ b/.gitignore
@@ -2,9 +2,12 @@ node_modules
.next
out
dist
+.env
.env.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
+
+/generated/prisma
diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts
old mode 100644
new mode 100755
diff --git a/app/(auth)/auth/login/page.tsx b/app/(auth)/auth/login/page.tsx
old mode 100644
new mode 100755
index ac05314..bd4b43b
--- a/app/(auth)/auth/login/page.tsx
+++ b/app/(auth)/auth/login/page.tsx
@@ -3,6 +3,8 @@ import LoginForm from "@/components/auth/LoginForm";
type LoginPageProps = {
searchParams: Promise<{
redirectTo?: string | string[];
+ role?: string | string[];
+ forgot?: string | string[];
}>;
};
@@ -10,6 +12,10 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
const params = await searchParams;
const redirectValue = params.redirectTo;
const redirectTo = Array.isArray(redirectValue) ? redirectValue[0] : redirectValue;
+ const roleValue = params.role;
+ const role = Array.isArray(roleValue) ? roleValue[0] : roleValue;
+ const forgotValue = params.forgot;
+ const forgot = Array.isArray(forgotValue) ? forgotValue[0] : forgotValue;
- return
+ Are you a teacher?{" "} + + Login here + +
++ Teacher accounts are invite-only. If you received an invite, use the email provided. +
); } diff --git a/app/(protected)/courses/[slug]/learn/actions.ts b/app/(protected)/courses/[slug]/learn/actions.ts new file mode 100644 index 0000000..70ff816 --- /dev/null +++ b/app/(protected)/courses/[slug]/learn/actions.ts @@ -0,0 +1,92 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { requireUser } from "@/lib/auth/requireUser"; +import { db } from "@/lib/prisma"; + +type ToggleLessonCompleteInput = { + courseSlug: string; + lessonId: string; +}; + +export async function toggleLessonComplete({ courseSlug, lessonId }: ToggleLessonCompleteInput) { + const user = await requireUser(); + + if (!user?.id) { + return { success: false, error: "Unauthorized" }; + } + + const lesson = await db.lesson.findUnique({ + where: { id: lessonId }, + select: { + id: true, + module: { + select: { + courseId: true, + course: { + select: { + slug: true, + }, + }, + }, + }, + }, + }); + + if (!lesson || lesson.module.course.slug !== courseSlug) { + return { success: false, error: "Lesson not found" }; + } + + const enrollment = await db.enrollment.findUnique({ + where: { + userId_courseId: { + userId: user.id, + courseId: lesson.module.courseId, + }, + }, + select: { id: true }, + }); + + if (!enrollment) { + return { success: false, error: "Not enrolled in this course" }; + } + + const existingProgress = await db.userProgress.findUnique({ + where: { + userId_lessonId: { + userId: user.id, + lessonId, + }, + }, + select: { + id: true, + isCompleted: true, + }, + }); + + const nextCompleted = !existingProgress?.isCompleted; + + if (existingProgress) { + await db.userProgress.update({ + where: { id: existingProgress.id }, + data: { + isCompleted: nextCompleted, + finishedAt: nextCompleted ? new Date() : null, + lastPlayedAt: new Date(), + }, + }); + } else { + await db.userProgress.create({ + data: { + userId: user.id, + lessonId, + isCompleted: true, + finishedAt: new Date(), + }, + }); + } + + revalidatePath(`/courses/${courseSlug}/learn`); + + return { success: true, isCompleted: nextCompleted }; +} diff --git a/app/(protected)/courses/[slug]/learn/page.tsx b/app/(protected)/courses/[slug]/learn/page.tsx old mode 100644 new mode 100755 index 1b1cb16..4cd0923 --- a/app/(protected)/courses/[slug]/learn/page.tsx +++ b/app/(protected)/courses/[slug]/learn/page.tsx @@ -1,214 +1,130 @@ -"use client"; +import { notFound, redirect } from "next/navigation"; +import { db } from "@/lib/prisma"; +import { requireUser } from "@/lib/auth/requireUser"; +import StudentClassroomClient from "@/components/courses/StudentClassroomClient"; -import { useEffect, useState } from "react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import LessonRow from "@/components/LessonRow"; -import ProgressBar from "@/components/ProgressBar"; -import { getCourseBySlug } from "@/lib/data/courseCatalog"; -import { - getCourseProgress, - getCourseProgressPercent, - markLessonComplete, - setLastLesson, -} from "@/lib/progress/localProgress"; -import { teacherCoursesUpdatedEventName } from "@/lib/data/teacherCourses"; -import { supabaseBrowser } from "@/lib/supabase/browser"; -import type { Course, Lesson } from "@/types/course"; - -const lessonContent = (lesson: Lesson) => { - if (lesson.type === "video") { - return ( -- Demo video URL: {lesson.videoUrl} -
- ) : null} -- Reading placeholder content for lesson: {lesson.title}. Replace with full lesson text and references in Phase 2. -
-Interactive placeholder for lesson: {lesson.title}.
- -Loading course...
-The requested course slug does not exist in mock data.
- - Back to courses - -This course currently has no lessons configured.
- - Back to course - -Completed
- ) : null} -- {selectedLesson.type} | {selectedLesson.minutes} min -
-+ Org admin tools are frontend placeholders for seat management, usage analytics, and assignment flows. +
++ Org admin only (access model pending backend) +
+Seats
+12 / 20 used
+Active learners
+9
+Courses assigned
+4
+- Final score: {score}/{total} -
- +Practice Results
++ Final score: {score}/{total} +
+No attempts recorded yet.
+ ) : ( +Practice Session
+{practiceModule.description}
+Questions
+{total}
+Estimated time
+{Math.max(3, total * 2)} min
+Difficulty
+Intermediate
+No attempts recorded yet for this module.
+ ) : ( +Practice and Exercises
-+ {practiceModule.title} | Question {index + 1} / {total} +
+Score: {score}/{total}
+{module.description}
+{module.description}
- Question {index + 1} / {total} -
-Score: {score}/{total}
-No attempts recorded yet.
+ ) : ( +Spanish Term
-Prompt
+