Skip to content

Commit 377dc29

Browse files
authored
Merge pull request #14 from youknowom/TD-60
feat: enhance exercise navigation and completion tracking with update…
2 parents af94399 + 1510113 commit 377dc29

8 files changed

Lines changed: 305 additions & 41 deletions

File tree

app/(routes)/courses/[courseId]/[chapterId]/[exerciseslug]/page.tsx

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import React, { useEffect, useState } from "react";
44
import dynamic from "next/dynamic";
55
import "react-splitter-layout/lib/index.css";
66
import axios from "axios";
7-
import { exercises } from "../../../_components/CourseList";
7+
import {
8+
completedExcercises,
9+
exercises,
10+
} from "../../../_components/CourseList";
811
import ContentSection from "../_components/ContentSection";
912
import CodeEditor from "../_components/CodeEditor";
13+
import { Button } from "@/components/ui/button";
14+
import Image from "next/image";
15+
import Link from "next/link";
1016

1117
export type CourseExercise = {
1218
chapterId: number;
@@ -15,6 +21,7 @@ export type CourseExercise = {
1521
name: string;
1622
exercises: exercises[];
1723
ExerciseData: ExerciseData;
24+
completedExercise: completedExcercises[];
1825
};
1926
type ExerciseData = {
2027
chapterId: number;
@@ -42,6 +49,9 @@ function playground() {
4249

4350
const [courseExerciseData, setCourseExerciseData] =
4451
useState<CourseExercise>();
52+
const [nextButtonRoute, setNextButtonRoute] = useState<string>();
53+
const [prevButtonRoute, setPrevButtonRoute] = useState<string>();
54+
const [exerciseInfo, setexerciseInfo] = useState<exercises>();
4555
useEffect(() => {
4656
GetExerciseCourseDetail();
4757
}, [courseId, chapterId, exerciseslug]);
@@ -56,12 +66,58 @@ function playground() {
5666
});
5767
console.log("Exercise data:", result.data);
5868
setCourseExerciseData(result.data);
69+
const exerciseInfo = result.data?.exercises?.find(
70+
(item: exercises) => item.slug == exerciseslug
71+
);
72+
setexerciseInfo(exerciseInfo);
5973
} catch (error) {
6074
console.error("Error fetching exercise:", error);
6175
} finally {
6276
setLoading(false);
6377
}
6478
};
79+
80+
useEffect(() => {
81+
document.body.style.overflow = "hidden";
82+
83+
return () => {
84+
document.body.style.overflow = "empty";
85+
};
86+
}, []);
87+
88+
const GetExerciseDetail = () => {
89+
const exerciseInfo = courseExerciseData?.exercises?.find(
90+
(item) => item.slug == exerciseslug
91+
);
92+
setexerciseInfo(exerciseInfo);
93+
};
94+
95+
const GetPrevNextButtonRoute = () => {
96+
//cuurent index of exe
97+
98+
const CurrentExerciseIndex =
99+
courseExerciseData?.exercises?.findIndex(
100+
(item) => item.slug == exerciseslug
101+
) ?? 0;
102+
103+
const NextExercise =
104+
courseExerciseData?.exercises[CurrentExerciseIndex + 1].slug;
105+
106+
const PrevExercise =
107+
courseExerciseData?.exercises[CurrentExerciseIndex - 1].slug;
108+
109+
setNextButtonRoute(
110+
NextExercise
111+
? "/courses/" + courseId + "/" + chapterId + "/" + NextExercise
112+
: undefined
113+
);
114+
115+
setPrevButtonRoute(
116+
PrevExercise
117+
? "/courses/" + courseId + "/" + chapterId + "/" + PrevExercise
118+
: undefined
119+
);
120+
};
65121
return (
66122
<div className="border-t-4">
67123
<SplitterLayout percentage primaryMinSize={40} secondaryInitialSize={60}>
@@ -78,6 +134,25 @@ function playground() {
78134
/>
79135
</div>
80136
</SplitterLayout>
137+
138+
<div className="font-game fixed bottom-0 w-full bg-zinc-900 flex p-4 justify-between items-center">
139+
<Link href={prevButtonRoute ?? "/courses/" + courseId}>
140+
<Button variant={"pixel"} className="text-xl">
141+
Previos
142+
</Button>
143+
</Link>
144+
<div className="flex gap-3 items-center">
145+
<Image src={"/star.png"} alt="start" width={40} height={40} />
146+
<h2 className="text-2xl">
147+
You can earn{exerciseInfo?.xp} <span className="text-3xl">XP</span>
148+
</h2>
149+
</div>
150+
<Link href={nextButtonRoute ?? "/courses" + courseId}>
151+
<Button variant={"pixel"} className="text-xl">
152+
Next
153+
</Button>
154+
</Link>
155+
</div>
81156
</div>
82157
);
83158
}

app/(routes)/courses/[courseId]/[chapterId]/_components/CodeEditor.tsx

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,106 @@ import {
44
SandpackLayout,
55
SandpackCodeEditor,
66
SandpackPreview,
7+
useSandpack,
78
} from "@codesandbox/sandpack-react";
8-
import SplitterLayout from "react-splitter-layout";
9+
import dynamic from "next/dynamic";
910
import "react-splitter-layout/lib/index.css";
1011
import { CourseExercise } from "../[exerciseslug]/page";
1112
import { Button } from "@/components/ui/button";
13+
import { nightOwl } from "@codesandbox/sandpack-themes";
14+
import { useParams } from "next/navigation";
15+
import axios from "axios";
16+
import { toast } from "sonner";
17+
18+
const SplitterLayout = dynamic(() => import("react-splitter-layout"), {
19+
ssr: false,
20+
});
21+
1222
type Props = {
1323
courseExerciseData: CourseExercise | undefined;
1424
loading: boolean;
1525
};
1626

17-
const CodeEditorChildren = () => {
27+
const CodeEditorChildren = ({ onCompleteExercise, IsCompleted }: any) => {
28+
const { sandpack } = useSandpack();
1829
return (
19-
<div className="font-game absolute bottom-20 flex gap-5 right-5">
20-
<Button variant={"pixel"} className="text-xl" size={"lg"}>
30+
<div className="font-game absolute bottom-40 flex gap-5 right-5">
31+
<Button
32+
variant={"pixel"}
33+
className="text-xl"
34+
onClick={() => sandpack.runSandpack()}
35+
size={"lg"}
36+
>
2137
Run code
2238
</Button>
23-
<Button variant={"pixel"} size={"lg"} className="bg-[#a3e534] text-xl">
24-
Mark Completed
39+
<Button
40+
variant={"pixel"}
41+
size={"lg"}
42+
className="bg-[#a3e534] text-xl"
43+
onClick={() => onCompleteExercise()}
44+
disabled={IsCompleted}
45+
>
46+
{IsCompleted ? "Already completed !" : "mark completed"}
2547
</Button>
2648
</div>
2749
);
2850
};
51+
2952
function CodeEditor({ courseExerciseData, loading }: Props) {
53+
const { exerciseslug } = useParams();
54+
const exerciseIndex = courseExerciseData?.exercises?.findIndex(
55+
(item) => item.slug == exerciseslug
56+
);
57+
58+
const IsCompleted = courseExerciseData?.completedExercise?.find(
59+
(item) => item?.exerciseId == Number(exerciseIndex) + 1
60+
);
61+
62+
const onCompleteExercise = async () => {
63+
if (exerciseIndex === undefined || !courseExerciseData) {
64+
return;
65+
}
66+
67+
try {
68+
const result = await axios.post("/api/exercise/complete", {
69+
courseId: courseExerciseData.courseId,
70+
chapterId: courseExerciseData.chapterId,
71+
exerciseId: exerciseIndex + 1,
72+
xpEarned: courseExerciseData.exercises[exerciseIndex].xp,
73+
});
74+
75+
toast.success("Exercise Completed!");
76+
} catch (error) {
77+
console.error("Error completing exercise:", error);
78+
toast.error("Failed to complete exercise");
79+
}
80+
};
81+
3082
return (
3183
<div className="relative h-full">
3284
<SandpackProvider
85+
theme={nightOwl}
3386
template="static"
34-
style={{
35-
height: "100vh",
36-
}}
87+
style={{ height: "100vh" }}
3788
files={
3889
courseExerciseData?.ExerciseData?.exerciseContent?.startCode || {}
3990
}
91+
options={{ autorun: false, autoReload: false }}
4092
>
41-
<SandpackLayout
42-
style={{
43-
height: "100%",
44-
}}
45-
>
93+
<SandpackLayout style={{ height: "100%" }}>
4694
<SplitterLayout>
4795
<div className="relative">
48-
<SandpackCodeEditor
49-
style={{
50-
height: "100%",
51-
}}
96+
<SandpackCodeEditor showTabs style={{ height: "100%" }} />
97+
<CodeEditorChildren
98+
onCompleteExercise={onCompleteExercise}
99+
IsCompleted={IsCompleted}
52100
/>
53-
<CodeEditorChildren />
54101
</div>
55102
<SandpackPreview
56-
style={{
57-
height: "100%",
58-
}}
103+
showNavigator
104+
showOpenInCodeSandbox={false}
105+
showOpenNewtab
106+
style={{ height: "100%" }}
59107
/>
60108
</SplitterLayout>
61109
</SandpackLayout>

app/(routes)/courses/[courseId]/[chapterId]/_components/ContentSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function ContentSection({ courseExerciseData, loading }: Props) {
1111
const contentInfo = courseExerciseData?.ExerciseData;
1212

1313
return (
14-
<div className="p-10">
14+
<div className="p-10 mb-28">
1515
{loading || !contentInfo ? (
1616
<Skeleton className="h-full w-full m-10 rounded-2xl" />
1717
) : (

app/api/exercise/complete/route.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { db } from "@/config/db";
2+
import {
3+
completedExercisesTable,
4+
usersTable,
5+
courseChaptersTable,
6+
enrolledCoursesTable,
7+
} from "@/config/schema";
8+
import { currentUser } from "@clerk/nextjs/server";
9+
import { and, eq, sql } from "drizzle-orm";
10+
import { NextRequest, NextResponse } from "next/server";
11+
12+
export async function POST(req: NextRequest) {
13+
try {
14+
const { courseId, chapterId, exerciseId, xpEarned } = await req.json();
15+
const user = await currentUser();
16+
17+
if (!user?.primaryEmailAddress?.emailAddress) {
18+
return NextResponse.json(
19+
{ error: "User not authenticated" },
20+
{ status: 401 }
21+
);
22+
}
23+
24+
// Get or create user in database
25+
let dbUser = await db
26+
.select()
27+
.from(usersTable)
28+
.where(eq(usersTable.email, user.primaryEmailAddress.emailAddress))
29+
.limit(1);
30+
31+
if (!dbUser || dbUser.length === 0) {
32+
const newUser = await db
33+
.insert(usersTable)
34+
.values({
35+
email: user.primaryEmailAddress.emailAddress,
36+
name: user.fullName || user.firstName || "User",
37+
})
38+
.returning();
39+
dbUser = newUser;
40+
}
41+
42+
// Get the actual chapter table ID (not logical chapterId)
43+
const chapter = await db
44+
.select()
45+
.from(courseChaptersTable)
46+
.where(
47+
and(
48+
eq(courseChaptersTable.courseId, courseId),
49+
eq(courseChaptersTable.chapterId, chapterId)
50+
)
51+
)
52+
.limit(1);
53+
54+
if (!chapter || chapter.length === 0) {
55+
return NextResponse.json({ error: "Chapter not found" }, { status: 404 });
56+
}
57+
58+
// Check if already completed
59+
const existing = await db
60+
.select()
61+
.from(completedExercisesTable)
62+
.where(
63+
and(
64+
eq(completedExercisesTable.userId, dbUser[0].id),
65+
eq(completedExercisesTable.courseId, courseId),
66+
eq(completedExercisesTable.chapterId, chapter[0].id),
67+
eq(completedExercisesTable.exerciseId, exerciseId)
68+
)
69+
)
70+
.limit(1);
71+
72+
if (existing && existing.length > 0) {
73+
return NextResponse.json({
74+
alreadyCompleted: true,
75+
message: "Exercise already completed",
76+
});
77+
}
78+
79+
// Insert completion record
80+
const result = await db
81+
.insert(completedExercisesTable)
82+
.values({
83+
chapterId: chapter[0].id, // Use actual DB id, not logical chapterId
84+
courseId: courseId,
85+
exerciseId: exerciseId,
86+
userId: dbUser[0].id,
87+
})
88+
.returning();
89+
90+
//Update Course XP Earned
91+
await db
92+
.update(enrolledCoursesTable)
93+
.set({ xpEarned: sql`${enrolledCoursesTable.xpEarned}+${xpEarned}` })
94+
.where(eq(enrolledCoursesTable?.courseId, courseId));
95+
96+
//Update User XP Earn Points
97+
await db
98+
.update(usersTable)
99+
.set({
100+
points: sql`${usersTable.points}+${xpEarned}`,
101+
})
102+
.where(eq(usersTable.email, user?.primaryEmailAddress?.emailAddress));
103+
return NextResponse.json({
104+
success: true,
105+
alreadyCompleted: false,
106+
data: result,
107+
});
108+
} catch (error: any) {
109+
console.error("Error completing exercise:", error);
110+
return NextResponse.json(
111+
{
112+
error: "Failed to complete exercise",
113+
details: error?.message || "Unknown error",
114+
},
115+
{ status: 500 }
116+
);
117+
}
118+
}

0 commit comments

Comments
 (0)