From b4377507759b389901264483dd088369f4ae0b20 Mon Sep 17 00:00:00 2001 From: pushkar Date: Thu, 4 Dec 2025 23:45:09 +0530 Subject: [PATCH] Course Deatil --- .../[courseId]/_components/CourseChapters.tsx | 47 +++++ .../_components/CourseDetailBanner.tsx | 40 ++++ .../[courseId]/_components/CourseStatus.tsx | 27 +++ app/(routes)/courses/[courseId]/page.tsx | 64 ++++++ .../courses/_components/CourseList.tsx | 38 +++- app/api/admin/save-chapters/route.ts | 188 ++++++++++++++++++ app/api/course/route.ts | 26 ++- config/db.js | 3 + config/schema.js | 31 +++ config/schema.tsx | 14 +- 10 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 app/(routes)/courses/[courseId]/_components/CourseChapters.tsx create mode 100644 app/(routes)/courses/[courseId]/_components/CourseDetailBanner.tsx create mode 100644 app/(routes)/courses/[courseId]/_components/CourseStatus.tsx create mode 100644 app/(routes)/courses/[courseId]/page.tsx create mode 100644 app/api/admin/save-chapters/route.ts create mode 100644 config/db.js create mode 100644 config/schema.js 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?.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

+
+ image +

Exercises- + 1/72

+ +
+ +
+ image +

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(), + +})