Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions drizzle/0007_learning_platform.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
CREATE TABLE IF NOT EXISTS "learning_tracks" (
"id" text PRIMARY KEY NOT NULL,
"slug" text NOT NULL,
"title" text NOT NULL,
"description" text NOT NULL,
"difficulty" text DEFAULT 'beginner' NOT NULL,
"estimated_hours" integer DEFAULT 10 NOT NULL,
"is_published" boolean DEFAULT false NOT NULL,
"prerequisites" jsonb DEFAULT '[]'::jsonb NOT NULL,
"objectives" jsonb DEFAULT '[]'::jsonb NOT NULL,
"order_index" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "learning_tracks_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "learning_modules" (
"id" text PRIMARY KEY NOT NULL,
"track_id" text NOT NULL REFERENCES "learning_tracks"("id") ON DELETE cascade,
"slug" text NOT NULL,
"title" text NOT NULL,
"description" text NOT NULL,
"order_index" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "learning_lessons" (
"id" text PRIMARY KEY NOT NULL,
"module_id" text NOT NULL REFERENCES "learning_modules"("id") ON DELETE cascade,
"slug" text NOT NULL,
"title" text NOT NULL,
"content" text NOT NULL,
"estimated_minutes" integer DEFAULT 15 NOT NULL,
"order_index" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "learning_enrollments" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"track_id" text NOT NULL REFERENCES "learning_tracks"("id") ON DELETE cascade,
"status" text DEFAULT 'enrolled' NOT NULL,
"enrolled_at" timestamp with time zone DEFAULT now() NOT NULL,
"completed_at" timestamp with time zone,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "lesson_progress" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"lesson_id" text NOT NULL REFERENCES "learning_lessons"("id") ON DELETE cascade,
"completed" boolean DEFAULT true NOT NULL,
"completed_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "learning_assignments" (
"id" text PRIMARY KEY NOT NULL,
"lesson_id" text NOT NULL REFERENCES "learning_lessons"("id") ON DELETE cascade,
"title" text NOT NULL,
"description" text NOT NULL,
"acceptance_criteria" jsonb DEFAULT '[]'::jsonb NOT NULL,
"submission_type" text DEFAULT 'text_submission' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "assignment_submissions" (
"id" text PRIMARY KEY NOT NULL,
"assignment_id" text NOT NULL REFERENCES "learning_assignments"("id") ON DELETE cascade,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"content" text NOT NULL,
"status" text DEFAULT 'submitted' NOT NULL,
"reviewed_by" text REFERENCES "users"("id"),
"feedback" text,
"submitted_at" timestamp with time zone DEFAULT now() NOT NULL,
"reviewed_at" timestamp with time zone
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "learning_tracks_slug_idx" ON "learning_tracks" ("slug");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_tracks_is_published_idx" ON "learning_tracks" ("is_published");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_modules_track_id_idx" ON "learning_modules" ("track_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_modules_order_idx" ON "learning_modules" ("track_id", "order_index");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_lessons_module_id_idx" ON "learning_lessons" ("module_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_lessons_order_idx" ON "learning_lessons" ("module_id", "order_index");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "learning_enrollments_user_track_idx" ON "learning_enrollments" ("user_id", "track_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_enrollments_user_id_idx" ON "learning_enrollments" ("user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_enrollments_track_id_idx" ON "learning_enrollments" ("track_id");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "lesson_progress_user_lesson_idx" ON "lesson_progress" ("user_id", "lesson_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "lesson_progress_user_id_idx" ON "lesson_progress" ("user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "lesson_progress_lesson_id_idx" ON "lesson_progress" ("lesson_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "learning_assignments_lesson_id_idx" ON "learning_assignments" ("lesson_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "assignment_submissions_user_idx" ON "assignment_submissions" ("user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "assignment_submissions_assignment_idx" ON "assignment_submissions" ("assignment_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "assignment_submissions_status_idx" ON "assignment_submissions" ("status");
7 changes: 7 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
"when": 1793326000000,
"tag": "0006_privacy_and_governance",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1793327000000,
"tag": "0007_learning_platform",
"breakpoints": true
}
]
}
49 changes: 49 additions & 0 deletions src/app/api/learn/assignments/[id]/submit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
import { submitAssignment } from "@/lib/learning/service";

const submissionSchema = z.object({
content: z.string().min(1, "Submission content is required"),
});

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const token = request.cookies.get(COOKIE_NAME)?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const sessionUser = await verifySessionToken(token);
if (!sessionUser) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}

try {
const { id: assignmentId } = await params;
const body = await request.json();
const { content } = submissionSchema.parse(body);

const submission = await submitAssignment(
sessionUser.id,
assignmentId,
content
);

return NextResponse.json({ success: true, submission });
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.flatten() },
{ status: 400 }
);
}

return NextResponse.json(
{ error: error.message || "Failed to submit assignment" },
{ status: 400 }
);
}
}
41 changes: 41 additions & 0 deletions src/app/api/learn/enroll/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
import { enrollInTrack } from "@/lib/learning/service";

const enrollSchema = z.object({
trackId: z.string().min(1, "Track ID is required"),
});

export async function POST(request: NextRequest) {
const token = request.cookies.get(COOKIE_NAME)?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const sessionUser = await verifySessionToken(token);
if (!sessionUser) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}

try {
const body = await request.json();
const { trackId } = enrollSchema.parse(body);

const enrollment = await enrollInTrack(sessionUser.id, trackId);
return NextResponse.json({ success: true, enrollment });
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.flatten() },
{ status: 400 }
);
}

console.error("Failed to enroll in track:", error);
return NextResponse.json(
{ error: error.message || "Failed to enroll in track" },
{ status: 500 }
);
}
}
50 changes: 50 additions & 0 deletions src/app/api/learn/lessons/[id]/progress/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
import { toggleLessonProgress } from "@/lib/learning/service";

const progressSchema = z.object({
completed: z.boolean(),
});

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const token = request.cookies.get(COOKIE_NAME)?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const sessionUser = await verifySessionToken(token);
if (!sessionUser) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}

try {
const { id: lessonId } = await params;
const body = await request.json();
const { completed } = progressSchema.parse(body);

const result = await toggleLessonProgress(
sessionUser.id,
lessonId,
completed
);

return NextResponse.json({ success: true, ...result });
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.flatten() },
{ status: 400 }
);
}

console.error("Failed to update lesson progress:", error);
return NextResponse.json(
{ error: error.message || "Failed to update lesson progress" },
{ status: 500 }
);
}
}
Loading
Loading