diff --git a/app/(routes)/courses/[courseId]/_components/CourseChapters.tsx b/app/(routes)/courses/[courseId]/_components/CourseChapters.tsx
new file mode 100644
index 0000000..ba7022a
--- /dev/null
+++ b/app/(routes)/courses/[courseId]/_components/CourseChapters.tsx
@@ -0,0 +1,47 @@
+'use client'
+import React from 'react'
+import { Course } from '../../_components/CourseList'
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion"
+import { Button } from '@/components/ui/button'
+
+
+type Props = {
+ loading: boolean
+ courseDetail: Course | undefined
+}
+
+function CourseChapters({loading,courseDetail}:Props) {
+ return (
+
+
+ {courseDetail?.chapters?.map((chapter,index)=>(
+
+
+ {chapter?.name}
+
+
+ {chapter?.exercises.map((exc,index)=>(
+
+
+
Exercise {index +1}
+ {exc.name}
+
+
+
+ ))}
+
+
+
+
+ ))}
+
+
+ )
+}
+
+export default CourseChapters
\ No newline at end of file
diff --git a/app/(routes)/courses/[courseId]/_components/CourseDetailBanner.tsx b/app/(routes)/courses/[courseId]/_components/CourseDetailBanner.tsx
new file mode 100644
index 0000000..fc2a253
--- /dev/null
+++ b/app/(routes)/courses/[courseId]/_components/CourseDetailBanner.tsx
@@ -0,0 +1,40 @@
+import React from 'react'
+import { Course } from '../../_components/CourseList'
+import Image from 'next/image'
+import { Skeleton } from '@/components/ui/skeleton'
+import { Button } from '@/components/ui/button'
+
+type Props = {
+ loading: boolean
+ courseDetail: Course | undefined
+}
+
+function CourseDetailBanner({ loading, courseDetail }: Props) {
+ return (
+
+ {!courseDetail ? (
+
+ ) : (
+
+
+
+
{courseDetail?.title}
+
{courseDetail?.desc}
+
+
+
+
+ )}
+
+ )
+}
+
+export default CourseDetailBanner
+
diff --git a/app/(routes)/courses/[courseId]/_components/CourseStatus.tsx b/app/(routes)/courses/[courseId]/_components/CourseStatus.tsx
new file mode 100644
index 0000000..bb49d8e
--- /dev/null
+++ b/app/(routes)/courses/[courseId]/_components/CourseStatus.tsx
@@ -0,0 +1,27 @@
+import React from 'react'
+import Image from 'next/image'
+import { Progress } from '@/components/ui/progress'
+function CourseStatus() {
+ return (
+
+
Course Progress
+
+
+ Exercises-
+ 1/72
+
+
+
+
+
+ XP Earned
+ 1/340
+
+
+
+
+
+ )
+}
+
+export default CourseStatus
\ No newline at end of file
diff --git a/app/(routes)/courses/[courseId]/page.tsx b/app/(routes)/courses/[courseId]/page.tsx
new file mode 100644
index 0000000..c98c17e
--- /dev/null
+++ b/app/(routes)/courses/[courseId]/page.tsx
@@ -0,0 +1,64 @@
+'use client'
+import { useParams } from 'next/navigation'
+import React, { useEffect, useState } from 'react'
+import CourseDetailBanner from './_components/CourseDetailBanner'
+import axios from 'axios'
+import { Course } from '../_components/CourseList'
+import CourseChapters from './_components/CourseChapters'
+import CourseStatus from './_components/CourseStatus'
+import UpgradeToPro from '../../dashboard/_components/Upgrade-To-Pro'
+import Header from '@/app/_components/Header'
+
+function CourseDetails() {
+
+ const { courseId } = useParams()
+ const [courseDetail, setCourseDetail] = useState()
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ courseId && GetCourseDetails();
+ }, [courseId])
+
+ const GetCourseDetails = async () => {
+ setLoading(true);
+ const result = await axios.get('/api/course?courseid=' + courseId);
+ setCourseDetail(result.data)
+ setLoading(false)
+ };
+
+ return (
+
+
+ {/* HEADER */}
+
+
+ {/* BANNER */}
+
+
+ {/* PAGE CONTENT */}
+
+
+
+ )
+}
+
+export default CourseDetails
diff --git a/app/(routes)/courses/_components/CourseList.tsx b/app/(routes)/courses/_components/CourseList.tsx
index c7f2db3..a77b708 100644
--- a/app/(routes)/courses/_components/CourseList.tsx
+++ b/app/(routes)/courses/_components/CourseList.tsx
@@ -3,16 +3,34 @@ import React, { useEffect, useState } from 'react'
import axios from 'axios'
import Image from 'next/image'
import { ChartNoAxesColumnIncreasing } from 'lucide-react'
+import Link from 'next/link'
-type Course = {
- id: number
- courseId: number
- title: string
- desc: string
- level: string
- bannerImage: string
- tag: string
+export type Course = {
+ id: number,
+ courseId: number,
+ title: string,
+ desc: string,
+ level: string,
+ bannerImage: string,
+ tag: string,
+ chapters?:Chapter[]
}
+type Chapter={
+ chapterId: number,
+ courseId: number,
+ desc: string,
+ name:string ,
+ id:number ,
+ exercises:exercise[]
+}
+
+type exercise={
+ name:string,
+ slug:string,
+ xp:number ,
+ difficulty: string
+}
+
function CourseList() {
const [courseList, setCourseList] = useState([])
@@ -33,8 +51,9 @@ function CourseList() {
{courseList?.map((course, index) => (
+
+
))}
)
diff --git a/app/api/admin/save-chapters/route.ts b/app/api/admin/save-chapters/route.ts
new file mode 100644
index 0000000..9efa4ba
--- /dev/null
+++ b/app/api/admin/save-chapters/route.ts
@@ -0,0 +1,188 @@
+import { db } from "@/config/db";
+import { CourseChapterTable } from "@/config/schema";
+// import { CourseChapterTable } from "@/config/schema";
+import { NextRequest, NextResponse } from "next/server";
+
+const DATA = [
+ {
+ "id": 1,
+ "name": "Introduction to HTML",
+ "desc": "Discover the foundation of every webpage and learn how HTML shapes the digital world.",
+ "exercises": [
+ {"name": "Explore the Web Skeleton", "slug": "explore-the-web-skeleton", "xp": 20, "difficulty": "easy"},
+ {"name": "Build Your Base Camp", "slug": "build-your-base-camp", "xp": 25, "difficulty": "easy"},
+ {"name": "Name Your World", "slug": "name-your-world", "xp": 15, "difficulty": "easy"},
+ {"name": "Break & Repair", "slug": "break-and-repair", "xp": 20, "difficulty": "easy"},
+ {"name": "HTML Detective", "slug": "html-detective", "xp": 20, "difficulty": "easy"},
+ {"name": "Element Collector", "slug": "element-collector", "xp": 25, "difficulty": "easy"}
+ ]
+ },
+ {
+ "id": 2,
+ "name": "HTML Boilerplate",
+ "desc": "Understand the core structure that every HTML document begins with.",
+ "exercises": [
+ {"name": "Build the Core Structure", "slug": "build-the-core-structure", "xp": 35, "difficulty": "medium"},
+ {"name": "Fix the Broken Blueprint", "slug": "fix-the-broken-blueprint", "xp": 30, "difficulty": "easy"},
+ {"name": "Boost Meta Power", "slug": "boost-meta-power", "xp": 20, "difficulty": "easy"},
+ {"name": "Add Language Identity", "slug": "add-language-identity", "xp": 10, "difficulty": "easy"},
+ {"name": "Viewport Setup", "slug": "viewport-setup", "xp": 20, "difficulty": "easy"},
+ {"name": "Author Credit", "slug": "author-credit", "xp": 15, "difficulty": "easy"}
+ ]
+ },
+ {
+ "id": 3,
+ "name": "Head & Body Tags",
+ "desc": "Learn the difference between behind-the-scenes metadata and visible page content.",
+ "exercises": [
+ {"name": "Mind vs Body", "slug": "mind-vs-body", "xp": 20, "difficulty": "easy"},
+ {"name": "Activate Styles", "slug": "activate-styles", "xp": 30, "difficulty": "medium"},
+ {"name": "Display Your Content", "slug": "display-your-content", "xp": 15, "difficulty": "easy"},
+ {"name": "Add External Script", "slug": "add-external-script", "xp": 20, "difficulty": "easy"},
+ {"name": "Meta Collection", "slug": "meta-collection", "xp": 25, "difficulty": "easy"},
+ {"name": "Body Structure Challenge", "slug": "body-structure-challenge", "xp": 25, "difficulty": "easy"}
+ ]
+ },
+ {
+ "id": 4,
+ "name": "Text Formatting",
+ "desc": "Format your content with headings, paragraphs, bold, italic, and more.",
+ "exercises": [
+ {"name": "Create the Text Realm", "slug": "create-the-text-realm", "xp": 30, "difficulty": "easy"},
+ {"name": "Power Words", "slug": "power-words", "xp": 20, "difficulty": "easy"},
+ {"name": "Build a Story Block", "slug": "build-a-story-block", "xp": 30, "difficulty": "medium"},
+ {"name": "Line Break Mastery", "slug": "line-break-mastery", "xp": 15, "difficulty": "easy"},
+ {"name": "Quote Chamber", "slug": "quote-chamber", "xp": 25, "difficulty": "easy"},
+ {"name": "Code Snippet Display", "slug": "code-snippet-display", "xp": 30, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 5,
+ "name": "Links & Navigation",
+ "desc": "Create portals between pages and build simple navigation.",
+ "exercises": [
+ {"name": "Create a Warp Gate", "slug": "create-a-warp-gate", "xp": 20, "difficulty": "easy"},
+ {"name": "Open a New Dimension", "slug": "open-a-new-dimension", "xp": 25, "difficulty": "easy"},
+ {"name": "Navigation Builder", "slug": "navigation-builder", "xp": 40, "difficulty": "medium"},
+ {"name": "Anchor Teleport", "slug": "anchor-teleport", "xp": 20, "difficulty": "easy"},
+ {"name": "Email Portal", "slug": "email-portal", "xp": 20, "difficulty": "easy"},
+ {"name": "Button Link Trick", "slug": "button-link-trick", "xp": 25, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 6,
+ "name": "Images",
+ "desc": "Display images, control sizing, and optimize accessibility.",
+ "exercises": [
+ {"name": "Summon an Image", "slug": "summon-an-image", "xp": 20, "difficulty": "easy"},
+ {"name": "Vision for All", "slug": "vision-for-all", "xp": 15, "difficulty": "easy"},
+ {"name": "Image Grid Challenge", "slug": "image-grid-challenge", "xp": 35, "difficulty": "medium"},
+ {"name": "Resize Hero", "slug": "resize-hero", "xp": 20, "difficulty": "easy"},
+ {"name": "Caption Creator", "slug": "caption-creator", "xp": 25, "difficulty": "medium"},
+ {"name": "Broken Image Test", "slug": "broken-image-test", "xp": 15, "difficulty": "easy"}
+ ]
+ },
+ {
+ "id": 7,
+ "name": "Lists",
+ "desc": "Structure grouped information using ordered, unordered, and description lists.",
+ "exercises": [
+ {"name": "Bullet Creator", "slug": "bullet-creator", "xp": 20, "difficulty": "easy"},
+ {"name": "Number Builder", "slug": "number-builder", "xp": 20, "difficulty": "easy"},
+ {"name": "Nested List Challenge", "slug": "nested-list-challenge", "xp": 35, "difficulty": "medium"},
+ {"name": "Description Vault", "slug": "description-vault", "xp": 25, "difficulty": "easy"},
+ {"name": "Task Checklist", "slug": "task-checklist", "xp": 20, "difficulty": "easy"},
+ {"name": "Navigation with Lists", "slug": "navigation-with-lists", "xp": 35, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 8,
+ "name": "Tables",
+ "desc": "Represent information in structured grid format.",
+ "exercises": [
+ {"name": "Table Blueprint", "slug": "table-blueprint", "xp": 30, "difficulty": "medium"},
+ {"name": "Add Column Headers", "slug": "add-column-headers", "xp": 20, "difficulty": "easy"},
+ {"name": "Merge the Cells", "slug": "merge-the-cells", "xp": 35, "difficulty": "medium"},
+ {"name": "Student Report Table", "slug": "student-report-table", "xp": 25, "difficulty": "easy"},
+ {"name": "Border Styling", "slug": "border-styling", "xp": 20, "difficulty": "easy"},
+ {"name": "Header Footer Rows", "slug": "header-footer-rows", "xp": 30, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 9,
+ "name": "Forms Basics",
+ "desc": "Collect user input using form controls like input, labels, and buttons.",
+ "exercises": [
+ {"name": "Create a Login Portal", "slug": "create-a-login-portal", "xp": 40, "difficulty": "medium"},
+ {"name": "Design a Contact Form", "slug": "design-a-contact-form", "xp": 45, "difficulty": "medium"},
+ {"name": "Placeholder Magic", "slug": "placeholder-magic", "xp": 15, "difficulty": "easy"},
+ {"name": "Label Linker", "slug": "label-linker", "xp": 20, "difficulty": "easy"},
+ {"name": "Choose Wisely", "slug": "choose-wisely", "xp": 25, "difficulty": "easy"},
+ {"name": "Dropdown Selector", "slug": "dropdown-selector", "xp": 30, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 10,
+ "name": "Semantic HTML",
+ "desc": "Use meaningful HTML elements to improve page structure and accessibility.",
+ "exercises": [
+ {"name": "Build the Layout", "slug": "build-the-layout", "xp": 35, "difficulty": "medium"},
+ {"name": "Blog Structure", "slug": "blog-structure", "xp": 40, "difficulty": "medium"},
+ {"name": "Sidebar Creator", "slug": "sidebar-creator", "xp": 25, "difficulty": "easy"},
+ {"name": "Navigation Map", "slug": "navigation-map", "xp": 35, "difficulty": "medium"},
+ {"name": "Figure & Caption", "slug": "figure-and-caption", "xp": 25, "difficulty": "easy"},
+ {"name": "Semantic Rebuild", "slug": "semantic-rebuild", "xp": 40, "difficulty": "medium"}
+ ]
+ },
+ {
+ "id": 11,
+ "name": "Audio & Video",
+ "desc": "Add multimedia components for richer experiences.",
+ "exercises": [
+ {"name": "Play the Sound", "slug": "play-the-sound", "xp": 25, "difficulty": "easy"},
+ {"name": "Video Portal", "slug": "video-portal", "xp": 30, "difficulty": "medium"},
+ {"name": "Autoplay Test", "slug": "autoplay-test", "xp": 20, "difficulty": "easy"},
+ {"name": "Add Subtitles", "slug": "add-subtitles", "xp": 40, "difficulty": "medium"},
+ {"name": "Audio Playlist", "slug": "audio-playlist", "xp": 20, "difficulty": "easy"},
+ {"name": "Thumbnail Setup", "slug": "thumbnail-setup", "xp": 25, "difficulty": "easy"}
+ ]
+ },
+ {
+ "id": 12,
+ "name": "HTML Best Practices",
+ "desc": "Write clear, clean, and accessible HTML optimized for real-world use.",
+ "exercises": [
+ {"name": "Code Cleanup", "slug": "code-cleanup", "xp": 20, "difficulty": "easy"},
+ {"name": "Accessibility Upgrade", "slug": "accessibility-upgrade", "xp": 35, "difficulty": "medium"},
+ {"name": "Alt Text Review", "slug": "alt-text-review", "xp": 20, "difficulty": "easy"},
+ {"name": "Heading Order Fix", "slug": "heading-order-fix", "xp": 25, "difficulty": "easy"},
+ {"name": "Link Check", "slug": "link-check", "xp": 20, "difficulty": "easy"},
+ {"name": "Semantic Improvement", "slug": "semantic-improvement", "xp": 40, "difficulty": "medium"}
+ ]
+ }
+]
+
+
+
+ // ✨ Add all other chapters exactly as you already have them
+ // (I trimmed here because message length is huge)
+ // Just paste the remaining objects below.
+;
+
+// -------------------------------------
+
+
+export async function GET(req: NextRequest) {
+ DATA.forEach(async (item) => {
+ await db.insert(CourseChapterTable).values({
+ courseId: 2, //Change Course ID depends on course info,
+ desc: item?.desc,
+ exercises: item.exercises,
+ name: item?.name,
+ chapterId: item?.id
+ })
+ })
+ return NextResponse.json('Success')
+}
+
+
diff --git a/app/api/course/route.ts b/app/api/course/route.ts
index 8b010ba..6253a72 100644
--- a/app/api/course/route.ts
+++ b/app/api/course/route.ts
@@ -1,10 +1,32 @@
import {db} from '@/config/db';
-import { CourseTable } from '@/config/schema';
+import { CourseChapterTable, CourseTable } from '@/config/schema';
+import { asc, eq } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req:NextRequest){
- const result = await db.select().from(CourseTable);
+
+ const {searchParams} = new URL(req.url);
+ const courseId = searchParams.get('courseid')
+
+if (courseId){
+ const result = await db.select().from(CourseTable)
+ //@ts-ignore
+ .where(eq(CourseTable.courseId,courseId))
+
+ const chapterResult = await db.select().from(CourseChapterTable)
+ //@ts-ignore
+ .where(eq(CourseChapterTable.courseId,courseId))
+
+ return NextResponse.json({
+ ...result[0],
+ chapters:chapterResult
+ })
+}
+else{
+ const result = await db.select().from(CourseTable).orderBy();
return NextResponse.json(result)
+}
+
}
\ No newline at end of file
diff --git a/config/db.js b/config/db.js
new file mode 100644
index 0000000..d2ffac9
--- /dev/null
+++ b/config/db.js
@@ -0,0 +1,3 @@
+import { drizzle } from 'drizzle-orm/neon-http';
+
+export const db = drizzle(process.env.DATABASE_URL);
\ No newline at end of file
diff --git a/config/schema.js b/config/schema.js
new file mode 100644
index 0000000..5ce7f4b
--- /dev/null
+++ b/config/schema.js
@@ -0,0 +1,31 @@
+import { integer, json, jsonb, pgTable, varchar } from "drizzle-orm/pg-core";
+
+
+export const usersTable = pgTable("users", {
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
+ name: varchar({ length: 255 }).notNull(),
+ email: varchar({ length: 255 }).notNull().unique(),
+ points: integer().default(0),
+ subscription: varchar()
+});
+
+
+export const CourseTable = pgTable("courses",{
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
+ courseId: integer().notNull().unique(),
+ title: varchar().notNull(),
+ desc: varchar().notNull(),
+ bannerImage: varchar().notNull(),
+ level: varchar().default('Beginner'),
+ tags: varchar()
+})
+
+export const CourseChapterTable = pgTable('courseChapters',{
+ id : integer().primaryKey().generatedAlwaysAsIdentity(),
+ chapterId: integer(),
+ courseId : integer().notNull(),
+ name: varchar(),
+ desc: varchar(),
+ exercises : json(),
+
+})
diff --git a/config/schema.tsx b/config/schema.tsx
index 49f2dbe..ce23053 100644
--- a/config/schema.tsx
+++ b/config/schema.tsx
@@ -1,4 +1,6 @@
-import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
+import { integer, json, jsonb, pgTable, varchar } from "drizzle-orm/pg-core";
+
+
export const usersTable = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
@@ -18,3 +20,13 @@ export const CourseTable = pgTable("courses",{
level: varchar().default('Beginner'),
tags: varchar()
})
+
+export const CourseChapterTable = pgTable('courseChapters',{
+ id : integer().primaryKey().generatedAlwaysAsIdentity(),
+ chapterId: integer(),
+ courseId : integer().notNull(),
+ name: varchar(),
+ desc: varchar(),
+ exercises : json(),
+
+})