├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── SECURITY.md ├── actions ├── get-chapter.ts ├── get-courses.ts ├── get-dashboard-courses.ts ├── get-progress.ts └── getAnalytics.ts ├── app ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (course) │ └── courses │ │ └── [courseId] │ │ ├── _components │ │ ├── course-mobile-sidebar.tsx │ │ ├── course-navbar.tsx │ │ ├── course-sidebar-item.tsx │ │ └── course-sidebar.tsx │ │ ├── chapters │ │ └── [chapterId] │ │ │ ├── _components │ │ │ ├── course-enroll-button.tsx │ │ │ ├── course-progress-button.tsx │ │ │ └── video-player.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx ├── api │ ├── courses │ │ ├── [courseId] │ │ │ ├── attachments │ │ │ │ ├── [attachmentId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── chapters │ │ │ │ ├── [chapterId] │ │ │ │ │ ├── progress │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── publish │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── unpublish │ │ │ │ │ │ └── route.ts │ │ │ │ ├── reorder │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── checkout │ │ │ │ └── route.ts │ │ │ ├── publish │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── unpublish │ │ │ │ └── route.ts │ │ └── route.ts │ ├── uploadthing │ │ ├── core.ts │ │ └── route.ts │ └── webhook │ │ └── route.ts ├── components │ ├── Banner │ │ ├── Banner.tsx │ │ ├── Dropdownone.tsx │ │ └── Dropdowntwo.tsx │ ├── Companies │ │ └── Companies.tsx │ ├── Courses │ │ └── Courses.tsx │ ├── Footer │ │ └── Footer.tsx │ ├── Mentor │ │ └── Mentor.tsx │ ├── Navbar │ │ ├── Contactus.tsx │ │ ├── Drawer.tsx │ │ ├── Drawerdata.tsx │ │ ├── Navbar.tsx │ │ ├── Registerdialog.tsx │ │ └── index.tsx │ ├── Newsletter │ │ └── Newsletter.tsx │ └── Students │ │ └── Students.tsx ├── dashboard │ ├── (routes) │ │ ├── (root) │ │ │ ├── _components │ │ │ │ └── info-card.tsx │ │ │ └── page.tsx │ │ ├── search │ │ │ ├── _components │ │ │ │ ├── Categories.tsx │ │ │ │ └── Category-items.tsx │ │ │ └── page.tsx │ │ └── teacher │ │ │ ├── analytics │ │ │ ├── _components │ │ │ │ ├── chart.tsx │ │ │ │ └── data-card.tsx │ │ │ └── page.tsx │ │ │ ├── courses │ │ │ ├── [courseId] │ │ │ │ ├── _components │ │ │ │ │ ├── actions.tsx │ │ │ │ │ ├── attachment-form.tsx │ │ │ │ │ ├── category-form.tsx │ │ │ │ │ ├── chapters-form.tsx │ │ │ │ │ ├── chapters-list.tsx │ │ │ │ │ ├── description-form.tsx │ │ │ │ │ ├── image-form.tsx │ │ │ │ │ ├── price-form.tsx │ │ │ │ │ └── title-form.tsx │ │ │ │ ├── chapters │ │ │ │ │ └── [chapterId] │ │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── chapter-access-form.tsx │ │ │ │ │ │ ├── chapter-actions.tsx │ │ │ │ │ │ ├── chapter-description-form.tsx │ │ │ │ │ │ ├── chapter-title-form.tsx │ │ │ │ │ │ └── chapter-video-form.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── _components │ │ │ │ ├── columns.tsx │ │ │ │ └── data-table.tsx │ │ │ └── page.tsx │ │ │ ├── create │ │ │ └── page.tsx │ │ │ └── layout.tsx │ ├── _components │ │ ├── logo.tsx │ │ ├── mobile-sidebar.tsx │ │ ├── navbar.tsx │ │ ├── sidebar-item.tsx │ │ ├── sidebar-routes.tsx │ │ └── sidebar.tsx │ └── layout.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── providers.js ├── bun.lockb ├── components.json ├── components ├── banner.tsx ├── course-card.tsx ├── course-progress.tsx ├── courses-list.tsx ├── editor.tsx ├── file-upload.tsx ├── icon-badge.tsx ├── modals │ └── confirm-modal.tsx ├── navbar-routes.tsx ├── preview.tsx ├── providers │ ├── confetti-provider.tsx │ └── toaster-provider.tsx ├── search-input.tsx └── ui │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── combobox.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── table.tsx │ └── textarea.tsx ├── hooks ├── use-confetti-store.ts └── use-debounce.ts ├── lib ├── db.ts ├── format.ts ├── stripe.ts ├── teacher.ts ├── uploadthing.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20240424012306_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── assets │ ├── banner │ │ ├── Stars.svg │ │ └── background.png │ ├── courses │ │ ├── Star.svg │ │ ├── account.svg │ │ ├── coursesFour.svg │ │ ├── coursesOne.svg │ │ ├── coursesThree.svg │ │ └── coursesTwo.svg │ ├── footer │ │ ├── dribble.svg │ │ ├── inputIcon.svg │ │ ├── insta.svg │ │ ├── twitter.svg │ │ └── youtube.svg │ ├── logo │ │ ├── Logo.svg │ │ └── Logo2.svg │ ├── mentor │ │ ├── boy1.svg │ │ ├── boy2.svg │ │ ├── boy3.svg │ │ ├── boy4.svg │ │ ├── boy5.svg │ │ └── girl1.svg │ ├── newsletter │ │ ├── Free.svg │ │ ├── hands.svg │ │ └── pinkBackground.svg │ ├── slickCompany │ │ ├── airbnb.svg │ │ ├── fedex.svg │ │ ├── google.svg │ │ ├── hubspot.svg │ │ ├── microsoft.svg │ │ └── walmart.svg │ └── students │ │ ├── austin.svg │ │ ├── gaby.svg │ │ ├── greenpic.svg │ │ ├── smallAvatar.svg │ │ ├── stars.png │ │ ├── user-1.jpg │ │ ├── user-2.jpg │ │ └── user-3.jpg ├── ellipse.svg ├── favicon.ico ├── logo.svg ├── next.svg └── vercel.svg ├── scripts ├── seed.js └── seed.ts ├── stripe.exe ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # PostgreSQL Database URL 2 | DATABASE_URL= 3 | 4 | # Cloud API Key and Secret Key 5 | CLOUD_API_KEY= 6 | CLOUD_SECRET_KEY= 7 | 8 | # Redis URL 9 | REDIS_URL= 10 | 11 | # Access Token Private Key 12 | ACESS_TOKEN_PRIVATE_KEY= 13 | 14 | # Next.js Public Clerk Keys 15 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 16 | CLERK_SECRET_KEY= 17 | NEXT_PUBLIC_CLERK_SIGN_IN_URL= 18 | NEXT_PUBLIC_CLERK_SIGN_UP_URL= 19 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL= 20 | 21 | # Hygraph Key 22 | NEXT_PUBLIC_HYGRAPH_KEY= 23 | 24 | # Mux Token ID and Secret 25 | MUX_TOKEN_ID= 26 | MUX_TOKEN_SECRET= 27 | 28 | # Next.js Public Teacher ID 29 | NEXT_PUBLIC_TEACHER_ID= 30 | 31 | # UploadThing Secret and App ID 32 | UPLOADTHING_SECRET= 33 | UPLOADTHING_APP_ID= 34 | 35 | # Stripe API Key 36 | STRIPE_API_KEY= 37 | 38 | # Next.js Public App URL 39 | NEXT_PUBLIC_APP_URL= 40 | NEXT_PUBLIC_POSTHOG_KEY= 41 | NEXT_PUBLIC_POSTHOG_HOST= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | certificates 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | certificates -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Educational Platform 2 | 3 | 4 | ## Landing Page 5 | ![image](https://github.com/Iamanshuaditya/lul/assets/87059568/5335e96d-0c5f-4fa3-a146-4b4f1d8b9af9) 6 | ## Login Page 7 | ![image](https://github.com/Iamanshuaditya/lul/assets/87059568/320782a4-bfb2-43fb-b67d-f368714c2cd3) 8 | ## UserPage 9 | ![App Screenshot](https://pbs.twimg.com/media/GL8_R0cbAAAdyl2?format=jpg&name=large) 10 | -- 11 | ## Admin dashboard to add Courses 12 | ![](https://pbs.twimg.com/media/GL8_clmaYAAjf2n?format=jpg&name=large) 13 | 14 | ![](https://pbs.twimg.com/media/GL8_WZxbMAM3Iyo?format=jpg&name=large) 15 | ## Stripe for Payment 16 | ![](https://github.com/Iamanshuaditya/lul/assets/87059568/e6953e65-9e22-4dfe-b4e4-42c7daeec956) 17 | # SidePanel for user to watch courses 18 | ![image](https://github.com/Iamanshuaditya/lul/assets/87059568/c6860f9b-ef04-4269-9f3a-f33c20c3a01a) 19 | 20 | ## Teacher Dashboard to Watch course activity 21 | 22 | ![image](https://github.com/Iamanshuaditya/lul/assets/87059568/0f5d8b01-52cb-4a0f-a4de-049545dcc00e) 23 | 24 | 25 | 26 | ## Your Comprehensive Learning Solution 27 | 28 | Our platform offers a seamless solution for managing and delivering educational content, catering to both educators and learners. Whether you're eager to explore new subjects or share knowledge, our platform has you covered. 29 | 30 | ## Features: 31 | 32 | ### 1. Landing Page: 33 | - Engaging introduction to the platform and its offerings. 34 | - Easy navigation to different sections of the platform. 35 | 36 | ### 2. Blog Section: 37 | - Stay updated with the latest educational articles, news, and insights. 38 | - Access valuable resources to enhance your learning experience. 39 | 40 | ### 3. User Dashboard: 41 | - Personalized dashboard for managing your learning journey. 42 | - Effortlessly purchase courses using integrated payment solutions. 43 | - Track course progress with visual indicators. 44 | - Convenient access to course content and materials. 45 | 46 | ### 4. Course Creation: 47 | - Create, manage, and update courses seamlessly. 48 | - Set course details including pricing, descriptions, and availability. 49 | - Monitor course completion progress. 50 | 51 | ### 5. Educator Dashboard: 52 | - Dedicated dashboard for educators to manage courses. 53 | - Easily update course details, pricing, and availability. 54 | - Gain insights into course performance through analytics. 55 | 56 | ### 6. Analytics: 57 | - Access comprehensive analytics to track course performance. 58 | - Monitor sales, revenue, and enrollment data for informed decisions. 59 | 60 | ## Getting Started: 61 | 1. Clone the repository to your local machine. 62 | 2. Install dependencies using `npm install`. 63 | 3. Set up your database and configure environment variables. 64 | 4. Run the application using `npm run dev`. 65 | 5. Access the application via your web browser. 66 | 67 | ## Technologies Used: 68 | - **Frontend:** Next.js 14 69 | - **Backend:** Prisma, MySQL 70 | - **Payment Integration:** Stripe 71 | 72 | 73 | 74 | ## Support: 75 | For inquiries or assistance, contact us on [Twitter](https://twitter.com/AnshuAd14312398). 76 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /actions/get-chapter.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { Attachment, Chapter } from "@prisma/client"; 3 | 4 | interface GetChapterProps { 5 | userId: string; 6 | courseId: string; 7 | chapterId: string; 8 | } 9 | 10 | export const getChapter = async ({ 11 | userId, 12 | courseId, 13 | chapterId, 14 | }: GetChapterProps) => { 15 | try { 16 | const purchase = await db.purchase.findUnique({ 17 | where: { 18 | userId_courseId: { 19 | userId, 20 | courseId, 21 | }, 22 | }, 23 | }); 24 | 25 | const course = await db.course.findUnique({ 26 | where: { 27 | isPublished: true, 28 | id: courseId, 29 | }, 30 | select: { 31 | price: true, 32 | }, 33 | }); 34 | 35 | const chapter = await db.chapter.findUnique({ 36 | where: { 37 | id: chapterId, 38 | isPublished: true, 39 | }, 40 | }); 41 | 42 | if (!chapter || !course) { 43 | throw new Error("Chapter or course not found"); 44 | } 45 | 46 | let muxData = null; 47 | let attachments: Attachment[] = []; 48 | let nextChapter: Chapter | null = null; 49 | 50 | if (purchase) { 51 | attachments = await db.attachment.findMany({ 52 | where: { 53 | courseId: courseId, 54 | }, 55 | }); 56 | } 57 | 58 | if (chapter.isFree || purchase) { 59 | muxData = await db.muxData.findUnique({ 60 | where: { 61 | chapterId: chapterId, 62 | }, 63 | }); 64 | 65 | nextChapter = await db.chapter.findFirst({ 66 | where: { 67 | courseId: courseId, 68 | isPublished: true, 69 | position: { 70 | gt: chapter?.position, 71 | }, 72 | }, 73 | orderBy: { 74 | position: "asc", 75 | }, 76 | }); 77 | } 78 | 79 | const userProgress = await db.userProgress.findUnique({ 80 | where: { 81 | userId_chapterId: { 82 | userId, 83 | chapterId, 84 | }, 85 | }, 86 | }); 87 | 88 | return { 89 | chapter, 90 | course, 91 | muxData, 92 | attachments, 93 | nextChapter, 94 | userProgress, 95 | purchase, 96 | }; 97 | } catch (error) { 98 | console.log("[GET_CHAPTER]", error); 99 | return { 100 | chapter: null, 101 | course: null, 102 | muxData: null, 103 | attachments: [], 104 | nextChapter: null, 105 | userProgress: null, 106 | purchase: null, 107 | }; 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /actions/get-courses.ts: -------------------------------------------------------------------------------- 1 | import { Category, Course } from "@prisma/client"; 2 | import { getProgress } from "./get-progress"; 3 | import { db } from "@/lib/db"; 4 | 5 | type CourseWithProgressWithCategory = Course & { 6 | category: Category | null; 7 | chapters: { id: string }[]; 8 | progress: number | null; 9 | }; 10 | 11 | type GetCourses = { 12 | userId: string; 13 | title?: string; 14 | categoryId?: string; 15 | }; 16 | 17 | export const getCourses = async ({ 18 | userId, 19 | title, 20 | categoryId, 21 | }: GetCourses): Promise => { 22 | try { 23 | const courses = await db.course.findMany({ 24 | where: { 25 | isPublished: true, 26 | title: { 27 | contains: title, 28 | }, 29 | categoryId, 30 | }, 31 | include: { 32 | category: true, 33 | chapters: { 34 | where: { 35 | isPublished: true, 36 | }, 37 | select: { 38 | id: true, 39 | }, 40 | }, 41 | purchases: { 42 | where: { 43 | userId, 44 | }, 45 | }, 46 | }, 47 | orderBy: { 48 | createdAt: "desc", 49 | }, 50 | }); 51 | 52 | const coursesWithProgress: CourseWithProgressWithCategory[] = 53 | await Promise.all( 54 | courses.map(async (course) => { 55 | if (course.purchases.length === 0) { 56 | return { 57 | ...course, 58 | progress: null, 59 | }; 60 | } 61 | const progressPercentage = await getProgress(userId, course.id); 62 | return { 63 | ...course, 64 | progress: progressPercentage, 65 | }; 66 | }) 67 | ); 68 | return coursesWithProgress; 69 | } catch (error) { 70 | console.log("GET_COURSES", error); 71 | return []; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /actions/get-dashboard-courses.ts: -------------------------------------------------------------------------------- 1 | import { Category, Chapter, Course } from "@prisma/client"; 2 | 3 | import { db } from "@/lib/db"; 4 | import { getProgress } from "@/actions/get-progress"; 5 | 6 | type CourseWithProgressWithCategory = Course & { 7 | category: Category; 8 | chapters: Chapter[]; 9 | progress: number | null; 10 | }; 11 | 12 | type DashboardCourses = { 13 | completedCourses: CourseWithProgressWithCategory[]; 14 | coursesInProgress: CourseWithProgressWithCategory[]; 15 | }; 16 | 17 | export const getDashboardCourses = async ( 18 | userId: string 19 | ): Promise => { 20 | try { 21 | const purchasedCourses = await db.purchase.findMany({ 22 | where: { 23 | userId: userId, 24 | }, 25 | select: { 26 | course: { 27 | include: { 28 | category: true, 29 | chapters: { 30 | where: { 31 | isPublished: true, 32 | }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | const courses = purchasedCourses.map( 40 | (purchase) => purchase.course 41 | ) as CourseWithProgressWithCategory[]; 42 | 43 | for (let course of courses) { 44 | const progress = await getProgress(userId, course.id); 45 | course["progress"] = progress; 46 | } 47 | 48 | const completedCourses = courses.filter( 49 | (course) => course.progress === 100 50 | ); 51 | const coursesInProgress = courses.filter( 52 | (course) => (course.progress ?? 0) < 100 53 | ); 54 | 55 | return { 56 | completedCourses, 57 | coursesInProgress, 58 | }; 59 | } catch (error) { 60 | console.log("[GET_DASHBOARD_COURSES]", error); 61 | return { 62 | completedCourses: [], 63 | coursesInProgress: [], 64 | }; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /actions/get-progress.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export const getProgress = async ( 4 | userId: string, 5 | courseId: string 6 | ): Promise => { 7 | try { 8 | const publishedChapters = await db.chapter.findMany({ 9 | where: { 10 | courseId: courseId, 11 | isPublished: true, 12 | }, 13 | select: { 14 | id: true, 15 | }, 16 | }); 17 | 18 | const publishedChapterIds = publishedChapters.map((chapter) => chapter.id); 19 | const validCompletedChapters = await db.userProgress.count({ 20 | where: { 21 | userId: userId, 22 | chapterId: { 23 | in: publishedChapterIds, 24 | }, 25 | isCompleted: true, 26 | }, 27 | }); 28 | const progressPercentage = 29 | (validCompletedChapters / publishedChapterIds.length) * 100; 30 | 31 | return progressPercentage; 32 | } catch (error) { 33 | console.log("GET_PROGRESS", error); 34 | return 0; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /actions/getAnalytics.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { Course, Purchase } from "@prisma/client"; 3 | 4 | type PurchaseWithCourse = Purchase & { 5 | course: Course; 6 | }; 7 | 8 | const groupByCourse = (purchases: PurchaseWithCourse[]) => { 9 | const grouped: { [courseTitle: string]: number } = {}; 10 | 11 | purchases.forEach((purchase) => { 12 | const courseTitle = purchase.course.title; 13 | if (!grouped[courseTitle]) { 14 | grouped[courseTitle] = 0; 15 | } 16 | grouped[courseTitle] += purchase.course.price!; 17 | }); 18 | 19 | return grouped; 20 | }; 21 | 22 | export const getAnalytics = async (userId: string) => { 23 | try { 24 | const purchases = await db.purchase.findMany({ 25 | where: { 26 | userId: { 27 | equals: userId, 28 | mode: 'insensitive', 29 | }, 30 | }, 31 | include: { 32 | course: true, 33 | }, 34 | }); 35 | 36 | const groupedEarnings = groupByCourse(purchases); 37 | const data = Object.entries(groupedEarnings).map( 38 | ([courseTitle, total]) => ({ 39 | name: courseTitle, 40 | total: total, 41 | }) 42 | ); 43 | 44 | const totalRevenue = data.reduce((acc, curr) => acc + curr.total, 0); 45 | const totalSales = purchases.length; 46 | 47 | return { 48 | data, 49 | totalRevenue, 50 | totalSales, 51 | }; 52 | } catch (error) { 53 | console.log("[GET_ANALYTICS]", error); 54 | return { 55 | data: [], 56 | totalRevenue: 0, 57 | totalSales: 0, 58 | }; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 2 | return ( 3 |
{children}
4 | ); 5 | }; 6 | 7 | export default AuthLayout; 8 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/_components/course-mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "lucide-react"; 2 | import { Chapter, Course, UserProgress } from "@prisma/client"; 3 | 4 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 5 | import CourseSidebar from "./course-sidebar"; 6 | 7 | interface CourseMobileSidebarProps { 8 | course: Course & { 9 | chapters: (Chapter & { 10 | userProgress: UserProgress[] | null; 11 | })[]; 12 | }; 13 | progressCount: number; 14 | } 15 | 16 | const CourseMobileSidebar = ({ 17 | course, 18 | progressCount, 19 | }: CourseMobileSidebarProps) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default CourseMobileSidebar; 33 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/_components/course-navbar.tsx: -------------------------------------------------------------------------------- 1 | import NavbarRoutes from "@/components/navbar-routes"; 2 | import { Chapter, Course, UserProgress } from "@prisma/client"; 3 | import CourseMobileSidebar from "./course-mobile-sidebar"; 4 | 5 | interface CourseNavbarProps { 6 | course: Course & { 7 | chapters: (Chapter & { 8 | userProgress: UserProgress[] | null; 9 | })[]; 10 | }; 11 | progressCount: number; 12 | } 13 | 14 | const CourseNavbar = ({ course, progressCount }: CourseNavbarProps) => { 15 | return ( 16 |
17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default CourseNavbar; 24 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/_components/course-sidebar-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { CheckCircle, Lock, PlayCircle } from "lucide-react"; 5 | import { usePathname, useRouter } from "next/navigation"; 6 | 7 | interface CourseSidebarItemProps { 8 | label: string; 9 | id: string; 10 | isCompleted: boolean; 11 | courseId: string; 12 | isLocked: boolean; 13 | } 14 | 15 | const CourseSidebarItem = ({ 16 | label, 17 | id, 18 | isCompleted, 19 | courseId, 20 | isLocked, 21 | }: CourseSidebarItemProps) => { 22 | const pathname = usePathname(); 23 | const router = useRouter(); 24 | 25 | const Icon = isLocked ? Lock : isCompleted ? CheckCircle : PlayCircle; 26 | 27 | const isActive = pathname?.includes(id); 28 | const onClick = () => { 29 | router.push(`/courses/${courseId}/chapters/${id}`); 30 | }; 31 | 32 | return ( 33 | 63 | ); 64 | }; 65 | 66 | export default CourseSidebarItem; 67 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/_components/course-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { Chapter, Course, UserProgress } from "@prisma/client"; 4 | import { redirect } from "next/navigation"; 5 | import CourseSidebarItem from "./course-sidebar-item"; 6 | import { CourseProgress } from "@/components/course-progress"; 7 | 8 | interface CourseSidebarProps { 9 | course: Course & { 10 | chapters: (Chapter & { 11 | userProgress: UserProgress[] | null; 12 | })[]; 13 | }; 14 | progressCount: number; 15 | } 16 | 17 | const CourseSidebar = async ({ course, progressCount }: CourseSidebarProps) => { 18 | const { userId } = auth(); 19 | if (!userId) { 20 | return redirect("/"); 21 | } 22 | 23 | const purchase = await db.purchase.findUnique({ 24 | where: { 25 | userId_courseId: { 26 | userId, 27 | courseId: course.id, 28 | }, 29 | }, 30 | }); 31 | 32 | return ( 33 |
34 |
35 |

{course.title}

36 | {/* Check purchase */} 37 | {purchase && ( 38 |
39 | 40 |
41 | )} 42 |
43 |
44 | {course.chapters.map((chapter) => ( 45 | 53 | ))} 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default CourseSidebar; 60 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/chapters/[chapterId]/_components/course-enroll-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { formatPrice } from "@/lib/format"; 5 | import axios from "axios"; 6 | import { useState } from "react"; 7 | import toast from "react-hot-toast"; 8 | 9 | interface CourseEnrollButtonProps { 10 | price: number; 11 | courseId: string; 12 | } 13 | 14 | export const CourseEnrollButton = ({ 15 | price, 16 | courseId, 17 | }: CourseEnrollButtonProps) => { 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | const onClick = async () => { 21 | try { 22 | setIsLoading(true); 23 | alert("Please do not use India as the billing address"); 24 | const response = await axios.post(`/api/courses/${courseId}/checkout`); 25 | window.location.assign(response.data.url); 26 | } catch (error) { 27 | toast.error("Something went wrong"); 28 | } finally { 29 | setIsLoading(false); 30 | } 31 | }; 32 | 33 | return ( 34 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/chapters/[chapterId]/_components/course-progress-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { CheckCircle, XCircle } from "lucide-react"; 5 | import { useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import toast from "react-hot-toast"; 8 | 9 | import { Button } from "@/components/ui/button"; 10 | import { useConfettiStore } from "@/hooks/use-confetti-store"; 11 | 12 | interface CourseProgressButtonProps { 13 | chapterId: string; 14 | courseId: string; 15 | isCompleted?: boolean; 16 | nextChapterId?: string; 17 | } 18 | 19 | export const CourseProgressButton = ({ 20 | chapterId, 21 | courseId, 22 | isCompleted, 23 | nextChapterId, 24 | }: CourseProgressButtonProps) => { 25 | const router = useRouter(); 26 | const confetti = useConfettiStore(); 27 | const [isLoading, setIsLoading] = useState(false); 28 | 29 | const onClick = async () => { 30 | try { 31 | setIsLoading(true); 32 | 33 | await axios.put( 34 | `/api/courses/${courseId}/chapters/${chapterId}/progress`, 35 | { 36 | isCompleted: !isCompleted, 37 | } 38 | ); 39 | 40 | if (!isCompleted && !nextChapterId) { 41 | confetti.onOpen(); 42 | } 43 | 44 | if (!isCompleted && nextChapterId) { 45 | router.push(`/courses/${courseId}/chapters/${nextChapterId}`); 46 | } 47 | 48 | toast.success("Progress updated"); 49 | router.refresh(); 50 | } catch { 51 | toast.error("Something went wrong"); 52 | } finally { 53 | setIsLoading(false); 54 | } 55 | }; 56 | 57 | const Icon = isCompleted ? XCircle : CheckCircle; 58 | 59 | return ( 60 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/chapters/[chapterId]/_components/video-player.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import MuxPlayer from "@mux/mux-player-react"; 5 | import { useState } from "react"; 6 | import { toast } from "react-hot-toast"; 7 | import { useRouter } from "next/navigation"; 8 | import { Loader2, Lock } from "lucide-react"; 9 | 10 | import { cn } from "@/lib/utils"; 11 | import { useConfettiStore } from "@/hooks/use-confetti-store"; 12 | 13 | interface VideoPlayerProps { 14 | playbackId: string; 15 | courseId: string; 16 | chapterId: string; 17 | nextChapterId?: string; 18 | isLocked: boolean; 19 | completeOnEnd: boolean; 20 | title: string; 21 | } 22 | 23 | export const VideoPlayer = ({ 24 | playbackId, 25 | courseId, 26 | chapterId, 27 | nextChapterId, 28 | isLocked, 29 | completeOnEnd, 30 | title, 31 | }: VideoPlayerProps) => { 32 | const [isReady, setIsReady] = useState(false); 33 | const router = useRouter(); 34 | const confetti = useConfettiStore(); 35 | 36 | const onEnd = async () => { 37 | try { 38 | if (completeOnEnd) { 39 | await axios.put( 40 | `/api/courses/${courseId}/chapters/${chapterId}/progress`, 41 | { 42 | isCompleted: true, 43 | } 44 | ); 45 | 46 | if (!nextChapterId) { 47 | confetti.onOpen(); 48 | } 49 | 50 | toast.success("Progress updated"); 51 | router.refresh(); 52 | 53 | if (nextChapterId) { 54 | router.push(`/courses/${courseId}/chapters/${nextChapterId}`); 55 | } 56 | } 57 | } catch { 58 | toast.error("Something went wrong"); 59 | } 60 | }; 61 | 62 | return ( 63 |
64 | {!isReady && !isLocked && ( 65 |
66 | 67 |
68 | )} 69 | {isLocked && ( 70 |
71 | 72 |

This chapter is locked

73 |
74 | )} 75 | {!isLocked && ( 76 | setIsReady(true)} 80 | onEnded={onEnd} 81 | autoPlay 82 | playbackId={playbackId} 83 | /> 84 | )} 85 |
86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/chapters/[chapterId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | import { File } from "lucide-react"; 4 | 5 | import { getChapter } from "@/actions/get-chapter"; 6 | import Banner from "@/components/banner"; 7 | import { Preview } from "@/components/preview"; 8 | 9 | import { VideoPlayer } from "./_components/video-player"; 10 | import { CourseEnrollButton } from "./_components/course-enroll-button"; 11 | import { Separator } from "@/components/ui/separator"; 12 | import { CourseProgressButton } from "./_components/course-progress-button"; 13 | 14 | const ChapterIdPage = async ({ 15 | params, 16 | }: { 17 | params: { courseId: string; chapterId: string }; 18 | }) => { 19 | const { userId } = auth(); 20 | 21 | if (!userId) { 22 | return redirect("/"); 23 | } 24 | 25 | const { 26 | chapter, 27 | course, 28 | muxData, 29 | attachments, 30 | nextChapter, 31 | userProgress, 32 | purchase, 33 | } = await getChapter({ 34 | userId, 35 | chapterId: params.chapterId, 36 | courseId: params.courseId, 37 | }); 38 | 39 | if (!chapter || !course) { 40 | return redirect("/"); 41 | } 42 | 43 | const isLocked = !chapter.isFree && !purchase; 44 | const completeOnEnd = !!purchase && !userProgress?.isCompleted; 45 | 46 | return ( 47 |
48 | {userProgress?.isCompleted && ( 49 | 50 | )} 51 | {isLocked && ( 52 | 56 | )} 57 |
58 |
59 | 68 |
69 |
70 |
71 |

{chapter.title}

72 | {purchase ? ( 73 | 79 | ) : ( 80 | 84 | )} 85 |
86 | 87 |
88 | 89 |
90 | {!!attachments.length && ( 91 | <> 92 |
93 | {attachments.map((attachment) => ( 94 | 100 | 101 |

{attachment.name}

102 |
103 | ))} 104 |
105 | 106 | )} 107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default ChapterIdPage; 114 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { db } from "@/lib/db"; 5 | import { getProgress } from "@/actions/get-progress"; 6 | 7 | import CourseSidebar from "./_components/course-sidebar"; 8 | import CourseNavbar from "./_components/course-navbar"; 9 | 10 | const CourseLayout = async ({ 11 | children, 12 | params, 13 | }: { 14 | children: React.ReactNode; 15 | params: { courseId: string }; 16 | }) => { 17 | const { userId } = auth(); 18 | 19 | if (!userId) { 20 | return redirect("/"); 21 | } 22 | 23 | const course = await db.course.findUnique({ 24 | where: { 25 | id: params.courseId, 26 | }, 27 | include: { 28 | chapters: { 29 | where: { 30 | isPublished: true, 31 | }, 32 | include: { 33 | userProgress: { 34 | where: { 35 | userId, 36 | }, 37 | }, 38 | }, 39 | orderBy: { 40 | position: "asc", 41 | }, 42 | }, 43 | }, 44 | }); 45 | 46 | if (!course) { 47 | return redirect("/"); 48 | } 49 | 50 | const progressCount = await getProgress(userId, course.id); 51 | 52 | return ( 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
{children}
61 |
62 | ); 63 | }; 64 | 65 | export default CourseLayout; 66 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { redirect } from "next/navigation"; 3 | 4 | const CourseIdPage = async ({ params }: { params: { courseId: string } }) => { 5 | const course = await db.course.findUnique({ 6 | where: { 7 | id: params.courseId, 8 | }, 9 | include: { 10 | chapters: { 11 | where: { isPublished: true }, 12 | orderBy: { 13 | createdAt: "asc", 14 | }, 15 | }, 16 | }, 17 | }); 18 | 19 | if (!course) { 20 | return redirect("/"); 21 | } 22 | 23 | return redirect(`/courses/${course.id}/chapters/${course.chapters[0].id}`); 24 | 25 | return
Watch Course
; 26 | }; 27 | 28 | export default CourseIdPage; 29 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/attachments/[attachmentId]/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { isTeacher } from "@/lib/teacher"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function DELETE( 7 | req: Request, 8 | { params }: { params: { courseId: string; attachmentId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | 13 | if (!userId || !isTeacher(userId)) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | 17 | const courseOwner = await db.course.findUnique({ 18 | where: { 19 | id: params.courseId, 20 | userId: userId, 21 | }, 22 | }); 23 | 24 | if (!courseOwner) { 25 | return new NextResponse("Unauthorized", { status: 401 }); 26 | } 27 | 28 | const attachment = await db.attachment.delete({ 29 | where: { 30 | courseId: params.courseId, 31 | id: params.attachmentId, 32 | }, 33 | }); 34 | 35 | return NextResponse.json(attachment); 36 | } catch (error) { 37 | console.log("ATTACHMENT_ID", error); 38 | return new NextResponse("Internal server error", { status: 500 }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/attachments/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { NextResponse } from "next/server"; 3 | 4 | import { db } from "@/lib/db"; 5 | 6 | export async function POST( 7 | req: Request, 8 | { params }: { params: { courseId: string } } 9 | ) { 10 | try { 11 | const { userId } = auth(); 12 | const { url } = await req.json(); 13 | 14 | if (!userId) { 15 | return new NextResponse("Unauthorized", { status: 401 }); 16 | } 17 | 18 | const courseOwner = await db.course.findUnique({ 19 | where: { 20 | id: params.courseId, 21 | userId: userId, 22 | }, 23 | }); 24 | 25 | if (!courseOwner) { 26 | return new NextResponse("Unauthorized", { status: 401 }); 27 | } 28 | 29 | const attachment = await db.attachment.create({ 30 | data: { 31 | url, 32 | name: url.split("/").pop(), 33 | courseId: params.courseId, 34 | }, 35 | }); 36 | 37 | return NextResponse.json(attachment); 38 | } catch (error) { 39 | console.log("COURSE_ID_ATTACHMENTS", error); 40 | return new NextResponse("Internal Error", { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/chapters/[chapterId]/progress/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PUT( 6 | req: Request, 7 | { params }: { params: { chapterId: string; courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | const { isCompleted } = await req.json(); 12 | if (!userId) { 13 | return new NextResponse("Unauthorized", { status: 401 }); 14 | } 15 | const userProgress = await db.userProgress.upsert({ 16 | where: { 17 | userId_chapterId: { 18 | userId, 19 | chapterId: params.chapterId, 20 | }, 21 | }, 22 | update: { 23 | isCompleted, 24 | }, 25 | create: { 26 | userId, 27 | chapterId: params.chapterId, 28 | isCompleted, 29 | }, 30 | }); 31 | return NextResponse.json(userProgress); 32 | } catch (error) { 33 | console.log("[CHAPTER_ID_PROGRESS_ERROR]", error); 34 | return new NextResponse("Internal server error", { status: 500 }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/chapters/[chapterId]/publish/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { chapterId: string; courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | if (!userId) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const ownCourse = await db.course.findUnique({ 15 | where: { 16 | id: params.courseId, 17 | userId, 18 | }, 19 | }); 20 | if (!ownCourse) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | const chapter = await db.chapter.findUnique({ 24 | where: { 25 | id: params.chapterId, 26 | courseId: params.courseId, 27 | }, 28 | }); 29 | const muxData = await db.muxData.findUnique({ 30 | where: { 31 | chapterId: params.chapterId, 32 | }, 33 | }); 34 | 35 | if ( 36 | !chapter || 37 | !muxData || 38 | !chapter.title || 39 | !chapter.description || 40 | !chapter.videoUrl 41 | ) { 42 | return new NextResponse("Missing required fields", { status: 400 }); 43 | } 44 | const publishedChapter = await db.chapter.update({ 45 | where: { 46 | id: params.chapterId, 47 | courseId: params.courseId, 48 | }, 49 | data: { 50 | isPublished: true, 51 | }, 52 | }); 53 | return NextResponse.json(publishedChapter); 54 | } catch (error) { 55 | console.log("[CHAPTER_PUBLISH]", error); 56 | return new NextResponse("Internal Error", { status: 500 }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/chapters/[chapterId]/unpublish/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { chapterId: string; courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | if (!userId) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const ownCourse = await db.course.findUnique({ 15 | where: { 16 | id: params.courseId, 17 | userId, 18 | }, 19 | }); 20 | if (!ownCourse) { 21 | return new NextResponse("Unauthorized", { status: 401 }); 22 | } 23 | 24 | const unpublishedChapter = await db.chapter.update({ 25 | where: { 26 | id: params.chapterId, 27 | courseId: params.courseId, 28 | }, 29 | data: { 30 | isPublished: false, 31 | }, 32 | }); 33 | 34 | const publishedChapters = await db.chapter.findMany({ 35 | where: { 36 | courseId: params.courseId, 37 | isPublished: true, 38 | }, 39 | }); 40 | 41 | if (!publishedChapters.length) { 42 | await db.course.update({ 43 | where: { 44 | id: params.courseId, 45 | }, 46 | data: { 47 | isPublished: false, 48 | }, 49 | }); 50 | } 51 | 52 | return NextResponse.json(unpublishedChapter); 53 | } catch (error) { 54 | console.log("[CHAPTER_UNPUBLISH]", error); 55 | return new NextResponse("Internal Error", { status: 500 }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/chapters/reorder/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PUT( 6 | req: Request, 7 | { params }: { params: { courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | if (!userId) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | const { list } = await req.json(); 15 | const ownCourse = await db.course.findUnique({ 16 | where: { 17 | id: params.courseId, 18 | userId: userId, 19 | }, 20 | }); 21 | if (!ownCourse) { 22 | return new NextResponse("Unauthorized", { status: 401 }); 23 | } 24 | for (let item of list) { 25 | await db.chapter.update({ 26 | where: { 27 | id: item.id, 28 | }, 29 | data: { position: item.position }, 30 | }); 31 | } 32 | return NextResponse.json("Success", { status: 200 }); 33 | } catch (error) { 34 | console.log("REORDER_ERROR", error); 35 | return new NextResponse("Internal Server Error", { status: 500 }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/chapters/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST( 6 | req: Request, 7 | { params }: { params: { courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | const { title } = await req.json(); 12 | 13 | if (!userId) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | 17 | const courseOwner = await db.course.findUnique({ 18 | where: { 19 | id: params.courseId, 20 | userId: userId, 21 | }, 22 | }); 23 | 24 | if (!courseOwner) { 25 | return new NextResponse("Unauthorized", { status: 401 }); 26 | } 27 | 28 | const lastChapter = await db.chapter.findFirst({ 29 | where: { 30 | courseId: params.courseId, 31 | }, 32 | orderBy: { 33 | position: "desc", 34 | }, 35 | }); 36 | 37 | const newPosition = lastChapter ? lastChapter.position + 1 : 1; 38 | const chapter = await db.chapter.create({ 39 | data: { 40 | title, 41 | courseId: params.courseId, 42 | position: newPosition, 43 | }, 44 | }); 45 | 46 | return NextResponse.json(chapter); 47 | } catch (error) { 48 | console.log("[CHAPTERS_ERROR]", error); 49 | return new NextResponse("Internal server error", { status: 500 }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/checkout/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { db } from "@/lib/db"; 6 | import { stripe } from "@/lib/stripe"; 7 | 8 | export async function POST( 9 | req: Request, 10 | { params }: { params: { courseId: string } } 11 | ) { 12 | try { 13 | const user = await currentUser(); 14 | 15 | if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) { 16 | return new NextResponse("Unauthorized", { status: 401 }); 17 | } 18 | 19 | const course = await db.course.findUnique({ 20 | where: { 21 | id: params.courseId, 22 | isPublished: true, 23 | }, 24 | }); 25 | 26 | const purchase = await db.purchase.findUnique({ 27 | where: { 28 | userId_courseId: { 29 | userId: user.id, 30 | courseId: params.courseId, 31 | }, 32 | }, 33 | }); 34 | 35 | if (purchase) { 36 | return new NextResponse("Already purchased", { status: 400 }); 37 | } 38 | 39 | if (!course) { 40 | return new NextResponse("Course not found", { status: 404 }); 41 | } 42 | 43 | const line_items: Stripe.Checkout.SessionCreateParams.LineItem[] = [ 44 | { 45 | quantity: 1, 46 | price_data: { 47 | currency: "USD", 48 | product_data: { 49 | name: course.title, 50 | description: course.description!, 51 | }, 52 | unit_amount: Math.round(course.price! * 100), 53 | }, 54 | }, 55 | ]; 56 | 57 | let stripeCustomer = await db.stripeCustomer.findUnique({ 58 | where: { 59 | userId: user.id, 60 | }, 61 | select: { 62 | stripeCustomerId: true, 63 | }, 64 | }); 65 | 66 | if (!stripeCustomer) { 67 | const customer = await stripe.customers.create({ 68 | email: user.emailAddresses[0].emailAddress, 69 | }); 70 | 71 | stripeCustomer = await db.stripeCustomer.create({ 72 | data: { 73 | userId: user.id, 74 | stripeCustomerId: customer.id, 75 | }, 76 | }); 77 | } 78 | 79 | const session = await stripe.checkout.sessions.create({ 80 | customer: stripeCustomer.stripeCustomerId, 81 | line_items, 82 | mode: "payment", 83 | success_url: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}?success=1`, 84 | cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}?canceled=1`, 85 | billing_address_collection: 'auto', 86 | metadata: { 87 | courseId: course.id, 88 | userId: user.id, 89 | }, 90 | }); 91 | await db.purchase.create({ 92 | data: { 93 | userId: user.id, 94 | courseId: course.id, 95 | }, 96 | }); 97 | 98 | 99 | return NextResponse.json({ url: session.url }); 100 | } catch (error) { 101 | console.log("[COURSE_ID_CHECKOUT]", error); 102 | return new NextResponse("Internal Error", { status: 500 }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/publish/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | const { courseId } = params; 12 | 13 | if (!userId) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | const course = await db.course.findUnique({ 17 | where: { 18 | id: courseId, 19 | userId, 20 | }, 21 | include: { 22 | chapters: { 23 | include: { 24 | muxData: true, 25 | }, 26 | }, 27 | }, 28 | }); 29 | 30 | if (!course) { 31 | return new NextResponse("Not found", { status: 404 }); 32 | } 33 | 34 | const hasPublishedChapters = course.chapters.some( 35 | (chapter) => chapter.isPublished 36 | ); 37 | if ( 38 | !course.title || 39 | !course.description || 40 | !course.imageUrl || 41 | !course.categoryId || 42 | !hasPublishedChapters 43 | ) { 44 | return new NextResponse("Missing required fields", { status: 400 }); 45 | } 46 | 47 | const publishedCourse = await db.course.update({ 48 | where: { 49 | id: courseId, 50 | userId, 51 | }, 52 | data: { 53 | isPublished: true, 54 | }, 55 | }); 56 | 57 | return NextResponse.json(publishedCourse); 58 | } catch (error) { 59 | console.log("[COURSE_ID_PUBLISH]", error); 60 | return new NextResponse("Internal server error", { status: 500 }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/route.ts: -------------------------------------------------------------------------------- 1 | import Mux from "@mux/mux-node"; 2 | import { db } from "@/lib/db"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | 6 | const { Video } = new Mux( 7 | process.env.MUX_TOKEN_ID!, 8 | process.env.MUX_TOKEN_SECRET! 9 | ); 10 | 11 | export async function DELETE( 12 | req: Request, 13 | { params }: { params: { courseId: string } } 14 | ) { 15 | try { 16 | const { userId } = auth(); 17 | 18 | if (!userId) { 19 | return new NextResponse("Unauthorized", { status: 401 }); 20 | } 21 | const course = await db.course.findUnique({ 22 | where: { 23 | id: params.courseId, 24 | userId, 25 | }, 26 | include: { 27 | chapters: { 28 | include: { 29 | muxData: true, 30 | }, 31 | }, 32 | }, 33 | }); 34 | 35 | if (!course) { 36 | return new NextResponse("Not found", { status: 404 }); 37 | } 38 | 39 | for (const chapter of course.chapters) { 40 | if (chapter.muxData) { 41 | await Video.Assets.del(chapter.muxData.assetId); 42 | } 43 | } 44 | const deletedCourse = await db.course.delete({ 45 | where: { 46 | id: params.courseId, 47 | }, 48 | }); 49 | return NextResponse.json(deletedCourse); 50 | } catch (error) { 51 | console.log("[COURSE_ID_DELETE]", error); 52 | return new NextResponse("Internal server error", { status: 500 }); 53 | } 54 | } 55 | 56 | export async function PATCH( 57 | req: Request, 58 | { params }: { params: { courseId: string } } 59 | ) { 60 | try { 61 | const { userId } = auth(); 62 | const { courseId } = params; 63 | const values = await req.json(); 64 | 65 | if (!userId) { 66 | return new NextResponse("Unauthorized", { status: 401 }); 67 | } 68 | const course = await db.course.update({ 69 | where: { 70 | id: courseId, 71 | userId, 72 | }, 73 | data: { 74 | ...values, 75 | }, 76 | }); 77 | 78 | return NextResponse.json(course); 79 | } catch (error) { 80 | console.log("[COURSE_ID]", error); 81 | return new NextResponse("Internal server error", { status: 500 }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/api/courses/[courseId]/unpublish/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { courseId: string } } 8 | ) { 9 | try { 10 | const { userId } = auth(); 11 | const { courseId } = params; 12 | 13 | if (!userId) { 14 | return new NextResponse("Unauthorized", { status: 401 }); 15 | } 16 | const course = await db.course.findUnique({ 17 | where: { 18 | id: courseId, 19 | userId, 20 | }, 21 | }); 22 | 23 | if (!course) { 24 | return new NextResponse("Not found", { status: 404 }); 25 | } 26 | 27 | const unpublishedCourse = await db.course.update({ 28 | where: { 29 | id: courseId, 30 | userId, 31 | }, 32 | data: { 33 | isPublished: false, 34 | }, 35 | }); 36 | 37 | return NextResponse.json(unpublishedCourse); 38 | } catch (error) { 39 | console.log("[COURSE_ID_UNPUBLISH]", error); 40 | return new NextResponse("Internal server error", { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/api/courses/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { isTeacher } from "@/lib/teacher"; 3 | import { auth } from "@clerk/nextjs"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | const { userId } = auth(); 9 | const { title } = await req.json(); 10 | 11 | if (!userId || !isTeacher(userId)) { 12 | return new NextResponse("Unauthorized", { status: 401 }); 13 | } 14 | 15 | const course = await db.course.create({ 16 | data: { 17 | title, 18 | userId, 19 | }, 20 | }); 21 | 22 | return NextResponse.json(course); 23 | } catch (error) { 24 | console.log("[COURSES]", error); 25 | return new NextResponse("Internal Error", { status: 500 }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 3 | 4 | import { isTeacher } from "@/lib/teacher"; 5 | 6 | const f = createUploadthing(); 7 | 8 | const handleAuth = () => { 9 | const { userId } = auth(); 10 | const isAuthorized = isTeacher(userId); 11 | 12 | if (!userId || !isAuthorized) throw new Error("Unauthorized"); 13 | return { userId }; 14 | }; 15 | 16 | export const ourFileRouter = { 17 | courseImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) 18 | .middleware(() => handleAuth()) 19 | .onUploadComplete(() => {}), 20 | courseAttachment: f(["text", "image", "video", "audio", "pdf"]) 21 | .middleware(() => handleAuth()) 22 | .onUploadComplete(() => {}), 23 | chapterVideo: f({ video: { maxFileCount: 1, maxFileSize: "512GB" } }) 24 | .middleware(() => handleAuth()) 25 | .onUploadComplete(() => {}), 26 | } satisfies FileRouter; 27 | 28 | export type OurFileRouter = typeof ourFileRouter; 29 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createNextRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createNextRouteHandler({ 7 | router: ourFileRouter, 8 | }); 9 | -------------------------------------------------------------------------------- /app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { headers } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | import { stripe } from "@/lib/stripe"; 5 | import { db } from "@/lib/db"; 6 | 7 | export async function POST(req: Request) { 8 | const body = await req.text(); 9 | const signature = headers().get("Stripe-Signature") as string; 10 | 11 | let event: Stripe.Event; 12 | 13 | try { 14 | event = stripe.webhooks.constructEvent( 15 | body, 16 | signature, 17 | process.env.STRIPE_WEBHOOK_SECRET! 18 | ); 19 | } catch (error: any) { 20 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 }); 21 | } 22 | const session = event.data.object as Stripe.Checkout.Session; 23 | const userId = session?.metadata?.userId; 24 | const courseId = session?.metadata?.courseId; 25 | 26 | if (event.type === "checkout.session.completed") { 27 | if (!userId || !courseId) { 28 | return new NextResponse(`Webhook Error: Missing Metadata`, { 29 | status: 400, 30 | }); 31 | } 32 | await db.purchase.create({ 33 | data: { 34 | courseId: courseId, 35 | userId: userId, 36 | }, 37 | }); 38 | } else { 39 | return new NextResponse( 40 | `Webhook Error: Unhandled Event type ${event.type}`, 41 | { status: 200 } 42 | ); 43 | } 44 | return new NextResponse(null, { status: 200 }); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/Banner/Dropdownone.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from "react"; 2 | import { Listbox, Transition } from "@headlessui/react"; 3 | import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; 4 | // import { useGSAP } from "@gsap/react"; 5 | 6 | type Coursetype = { 7 | name: string; 8 | }; 9 | 10 | const course: Coursetype[] = [ 11 | { name: "UX and UI Design" }, 12 | { name: "Front End Development" }, 13 | { name: "Back End Development" }, 14 | { name: "Ethical Hacking" }, 15 | ]; 16 | 17 | const Dropdown = () => { 18 | const [selected, setSelected] = useState(course[0]); 19 | 20 | // useGSAP(() => { 21 | // gsap 22 | // },[]) 23 | 24 | return ( 25 |
26 | 27 |

What do you want to learn?

28 |
29 | 30 | 31 | {selected.name} 32 | 33 | 34 | 39 | 40 | 46 | 47 | {course.map((person, personIdx) => ( 48 | 51 | `relative cursor-default select-none py-2 pl-10 pr-4 ${ 52 | active ? "bg-amber-100 text-amber-900" : "text-gray-900" 53 | }` 54 | } 55 | value={person} 56 | > 57 | {({ selected }) => ( 58 | <> 59 | 64 | {person.name} 65 | 66 | {selected ? ( 67 | 68 | 70 | ) : null} 71 | 72 | )} 73 | 74 | ))} 75 | 76 | 77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export default Dropdown; 84 | -------------------------------------------------------------------------------- /app/components/Banner/Dropdowntwo.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react' 2 | import { Listbox, Transition } from '@headlessui/react' 3 | import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid' 4 | 5 | type Hourtype = { 6 | name: string; 7 | }; 8 | 9 | 10 | const Hour: Hourtype[] = [ 11 | { name: '20hrs in a Month' }, 12 | { name: '30hrs in a Month' }, 13 | { name: '40hrs in a Month' }, 14 | { name: '50hrs in a Month' }, 15 | ] 16 | 17 | const Dropdown = () => { 18 | const [selected, setSelected] = useState(Hour[0]) 19 | 20 | return ( 21 |
22 | 23 |

Hours you going to invest?

24 |
25 | 26 | {selected.name} 27 | 28 | 33 | 34 | 40 | 41 | {Hour.map((person, personIdx) => ( 42 | 45 | `relative cursor-default select-none py-2 pl-10 pr-4 ${ 46 | active ? 'bg-amber-100 text-amber-900' : 'text-gray-900' 47 | }` 48 | } 49 | value={person} 50 | > 51 | {({ selected }) => ( 52 | <> 53 | 58 | {person.name} 59 | 60 | {selected ? ( 61 | 62 | 64 | ) : null} 65 | 66 | )} 67 | 68 | ))} 69 | 70 | 71 |
72 |
73 |
74 | ) 75 | } 76 | 77 | export default Dropdown; 78 | -------------------------------------------------------------------------------- /app/components/Companies/Companies.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { Component } from "react"; 3 | import Slider from "react-slick"; 4 | 5 | // IMAGES DATA FOR CAROUSEL 6 | interface Data { 7 | imgSrc: string; 8 | } 9 | 10 | const data: Data[] = [ 11 | { 12 | imgSrc: "/assets/slickCompany/airbnb.svg" 13 | }, 14 | { 15 | imgSrc: "/assets/slickCompany/hubspot.svg" 16 | }, 17 | { 18 | imgSrc: "/assets/slickCompany/microsoft.svg" 19 | }, 20 | { 21 | imgSrc: "/assets/slickCompany/google.svg" 22 | }, 23 | { 24 | imgSrc: "/assets/slickCompany/walmart.svg" 25 | }, 26 | { 27 | imgSrc: "/assets/slickCompany/fedex.svg" 28 | }, 29 | ] 30 | 31 | 32 | // CAROUSEL SETTINGS 33 | export default class MultipleItems extends Component { 34 | render() { 35 | const settings = { 36 | dots: false, 37 | infinite: true, 38 | slidesToShow: 4, 39 | slidesToScroll: 1, 40 | arrows: false, 41 | autoplay: true, 42 | speed: 2000, 43 | autoplaySpeed: 2000, 44 | cssEase: "linear", 45 | responsive: [ 46 | { 47 | breakpoint: 1024, 48 | settings: { 49 | slidesToShow: 4, 50 | slidesToScroll: 1, 51 | infinite: true, 52 | dots: false 53 | } 54 | }, 55 | { 56 | breakpoint: 700, 57 | settings: { 58 | slidesToShow: 2, 59 | slidesToScroll: 1, 60 | infinite: true, 61 | dots: false 62 | } 63 | }, 64 | { 65 | breakpoint: 500, 66 | settings: { 67 | slidesToShow: 1, 68 | slidesToScroll: 1, 69 | infinite: true, 70 | dots: false 71 | } 72 | } 73 | ] 74 | }; 75 | 76 | return ( 77 | 78 |
79 |
80 |

Trusted by companies of all sizes

81 |
82 | 83 | {data.map((item, i) => 84 |
85 | {item.imgSrc} 86 |
87 | )} 88 |
89 |
90 |
91 |
92 | 93 | ) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/components/Navbar/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { XMarkIcon } from '@heroicons/react/24/outline' 3 | 4 | interface DrawerProps { 5 | children: ReactNode; 6 | isOpen: boolean; 7 | setIsOpen: (isOpen: boolean) => void; 8 | } 9 | 10 | const Drawer = ({ children, isOpen, setIsOpen }: DrawerProps) => { 11 | 12 | return ( 13 |
21 |
27 | 28 |
29 |
Courses-Logo { 34 | setIsOpen(false); 35 | }} 36 | /> { 37 | setIsOpen(false); 38 | }} /> 39 |
40 |
{ 41 | setIsOpen(false); 42 | }}>{children}
43 |
44 |
45 |
{ 48 | setIsOpen(false); 49 | }} 50 | >
51 |
52 | ); 53 | } 54 | 55 | export default Drawer; 56 | -------------------------------------------------------------------------------- /app/components/Navbar/Drawerdata.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | import Contactus from "./Contactus"; 5 | import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | interface NavigationItem { 9 | name: string; 10 | href: string; 11 | current: boolean; 12 | } 13 | 14 | const navigation: NavigationItem[] = [ 15 | { name: "Home", href: "#", current: false }, 16 | { name: "Courses", href: "#courses-section", current: false }, 17 | { name: "Mentors", href: "#mentors-section", current: false }, 18 | { name: "Testimonial", href: "#testimonial-section", current: false }, 19 | { name: "Join", href: "#join-section", current: false }, 20 | ]; 21 | 22 | function classNames(...classes: string[]) { 23 | return classes.filter(Boolean).join(" "); 24 | } 25 | 26 | const Data = () => { 27 | const router = useRouter(); 28 | return ( 29 |
30 |
31 |
32 |
33 | {navigation.map((item) => ( 34 | 45 | {item.name} 46 | 47 | ))} 48 | 49 |
50 | 51 | 58 | 65 | 66 | 67 |
68 |
69 |
70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Data; 77 | -------------------------------------------------------------------------------- /app/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Navbar from "./Navbar"; 4 | import React, { useEffect } from "react"; 5 | 6 | const Navbarin: React.FC = () => { 7 | useEffect(() => { 8 | const debounce = (fn: Function) => { 9 | let frame: number; 10 | 11 | return (...params: unknown[]) => { 12 | // 13 | if (frame) { 14 | cancelAnimationFrame(frame); 15 | } 16 | 17 | frame = requestAnimationFrame(() => { 18 | fn(...params); 19 | }); 20 | }; 21 | }; 22 | 23 | const storeScroll = () => { 24 | document.documentElement.dataset.scroll = window.scrollY.toString(); 25 | }; 26 | 27 | document.addEventListener("scroll", debounce(storeScroll), { 28 | passive: true, 29 | }); 30 | 31 | storeScroll(); 32 | }, []); 33 | return ( 34 | <> 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default Navbarin; 41 | -------------------------------------------------------------------------------- /app/components/Newsletter/Newsletter.tsx: -------------------------------------------------------------------------------- 1 | import { useGSAP } from "@gsap/react"; 2 | import gsap from "gsap"; 3 | import { ScrollTrigger } from "gsap/ScrollTrigger"; 4 | import React, { useRef } from "react"; 5 | 6 | const Newsletter = () => { 7 | const container = useRef(null); 8 | useGSAP(() => { 9 | const containerTl = gsap.timeline({ paused: true }); 10 | const texts = container.current?.querySelector(".text-container"); 11 | const Image = container.current?.querySelector("img"); 12 | if (texts && Image) { 13 | containerTl 14 | .from(texts, { y: "10vw", opacity: 0, ease: "power3.inOut" }, 0) 15 | .from(Image, { y: "-10vw", opacity: 0, ease: "power3.inOut" }, 0); 16 | } 17 | 18 | const trigger = ScrollTrigger.create({ 19 | trigger: container.current, 20 | start: "top bottom", 21 | end: "bottom top", 22 | toggleActions: "play none none reverse", 23 | onEnter: () => containerTl.play(), 24 | }); 25 | 26 | return () => { 27 | trigger.kill(); 28 | }; 29 | }); 30 | 31 | return ( 32 |
33 |
38 |
39 |
40 |

Join Our Newsletter

41 |

42 | Subscribe our newsletter for discounts, promo and many more. 43 |

44 |
45 | 52 | 58 |
59 |
60 | 61 |
62 |
63 | bgimg 64 |
65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Newsletter; 73 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/(root)/_components/info-card.tsx: -------------------------------------------------------------------------------- 1 | import { IconBadge } from "@/components/icon-badge"; 2 | import { LucideIcon } from "lucide-react"; 3 | 4 | interface InfoCardProps { 5 | numberOfItems: number; 6 | variant?: "default" | "success"; 7 | label: string; 8 | icon: LucideIcon; 9 | } 10 | 11 | const InfoCard = ({ 12 | variant, 13 | icon: Icon, 14 | numberOfItems, 15 | label, 16 | }: InfoCardProps) => { 17 | return ( 18 |
19 | 20 |
21 |

{label}

22 |

23 | {numberOfItems} {numberOfItems === 1 ? "Course" : "Courses"} 24 |

25 |
26 |
27 | ); 28 | }; 29 | 30 | export default InfoCard; 31 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import { getDashboardCourses } from "@/actions/get-dashboard-courses"; 2 | import CoursesList from "@/components/courses-list"; 3 | import { UserButton, auth } from "@clerk/nextjs"; 4 | import { CheckCircle, Clock } from "lucide-react"; 5 | import { redirect } from "next/navigation"; 6 | import InfoCard from "./_components/info-card"; 7 | 8 | export default async function Dashboard() { 9 | const { userId } = auth(); 10 | if (!userId) { 11 | return redirect("/"); 12 | } 13 | 14 | const { completedCourses, coursesInProgress } = await getDashboardCourses( 15 | userId 16 | ); 17 | 18 | return ( 19 |
20 |
21 | 26 | 27 | 33 |
34 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/search/_components/Categories.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Category } from "@prisma/client"; 4 | import { 5 | FcEngineering, 6 | FcFilmReel, 7 | FcMultipleDevices, 8 | FcMusic, 9 | FcOldTimeCamera, 10 | FcSalesPerformance, 11 | FcSportsMode, 12 | } from "react-icons/fc"; 13 | 14 | import { IconType } from "react-icons"; 15 | import CategoryItem from "./Category-items"; 16 | 17 | interface CategoriesProps { 18 | items: Category[]; 19 | } 20 | 21 | const iconMap: Record = { 22 | Music: FcMusic, 23 | Photography: FcOldTimeCamera, 24 | Fitness: FcSportsMode, 25 | Accounting: FcSalesPerformance, 26 | "Computer Science": FcMultipleDevices, 27 | Filming: FcFilmReel, 28 | Engineering: FcEngineering, 29 | }; 30 | 31 | const Categories = ({ items }: CategoriesProps) => { 32 | return ( 33 |
34 | {items.map((item) => ( 35 | 41 | ))} 42 |
43 | ); 44 | }; 45 | 46 | export default Categories; 47 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/search/_components/Category-items.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | import { IconType } from "react-icons"; 6 | import qs from "query-string"; 7 | 8 | interface CategoryItemProps { 9 | label: string; 10 | value?: string; 11 | icon?: IconType; 12 | } 13 | 14 | const CategoryItem = ({ label, value, icon: Icon }: CategoryItemProps) => { 15 | const pathname = usePathname(); 16 | const router = useRouter(); 17 | const searchParams = useSearchParams(); 18 | const currentCategoryId = searchParams.get("categoryId"); 19 | const currentTitle = searchParams.get("title"); 20 | 21 | const isSelected = currentCategoryId === value; 22 | 23 | const onClick = () => { 24 | const url = qs.stringifyUrl( 25 | { 26 | url: pathname, 27 | query: { 28 | title: currentTitle, 29 | categoryId: isSelected ? null : value, 30 | }, 31 | }, 32 | { skipNull: true, skipEmptyString: true } 33 | ); 34 | 35 | router.push(url); 36 | }; 37 | return ( 38 | 50 | ); 51 | }; 52 | 53 | export default CategoryItem; 54 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import Categories from "./_components/Categories"; 3 | import SearchInput from "@/components/search-input"; 4 | import { getCourses } from "@/actions/get-courses"; 5 | import { auth } from "@clerk/nextjs"; 6 | import { redirect } from "next/navigation"; 7 | import CoursesList from "@/components/courses-list"; 8 | 9 | interface SearchPageProps { 10 | searchParams: { 11 | title: string; 12 | categoryId: string; 13 | }; 14 | } 15 | 16 | const SearchPage = async ({ searchParams }: SearchPageProps) => { 17 | const { userId } = auth(); 18 | 19 | if (!userId) { 20 | return redirect("/"); 21 | } 22 | 23 | const categories = await db.category.findMany({ 24 | orderBy: { 25 | name: "asc", 26 | }, 27 | }); 28 | 29 | const courses = await getCourses({ 30 | userId, 31 | ...searchParams, 32 | }); 33 | 34 | return ( 35 | <> 36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default SearchPage; 48 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/analytics/_components/chart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card } from "@/components/ui/card"; 4 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; 5 | 6 | interface ChartProps { 7 | data: { 8 | name: string; 9 | total: number; 10 | }[]; 11 | } 12 | 13 | export const Chart = ({ data }: ChartProps) => { 14 | return ( 15 | 16 | 17 | 18 | 25 | `$${value}`} 31 | /> 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/analytics/_components/data-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 2 | import { formatPrice } from "@/lib/format"; 3 | 4 | interface DataCardProps { 5 | value: number; 6 | label: string; 7 | shouldFormat?: boolean; 8 | } 9 | 10 | export const DataCard = ({ value, label, shouldFormat }: DataCardProps) => { 11 | return ( 12 | 13 | 14 | {label} 15 | 16 | 17 |
18 | {shouldFormat ? formatPrice(value) : value} 19 |
20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/analytics/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAnalytics } from "@/actions/getAnalytics"; 2 | import { auth } from "@clerk/nextjs"; 3 | import { redirect } from "next/navigation"; 4 | import { DataCard } from "./_components/data-card"; 5 | import { Chart } from "./_components/chart"; 6 | 7 | const AnalyticsPage = async () => { 8 | const { userId } = auth(); 9 | if (!userId) { 10 | return redirect("/"); 11 | } 12 | 13 | const { data, totalRevenue, totalSales } = await getAnalytics(userId); 14 | 15 | return ( 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | ); 24 | }; 25 | 26 | export default AnalyticsPage; 27 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/courses/[courseId]/_components/actions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { Trash } from "lucide-react"; 5 | import { useState } from "react"; 6 | import toast from "react-hot-toast"; 7 | import { useRouter } from "next/navigation"; 8 | 9 | import { Button } from "@/components/ui/button"; 10 | import { ConfirmModal } from "@/components/modals/confirm-modal"; 11 | import { useConfettiStore } from "@/hooks/use-confetti-store"; 12 | 13 | interface ActionsProps { 14 | disabled: boolean; 15 | courseId: string; 16 | isPublished: boolean; 17 | } 18 | 19 | export const Actions = ({ disabled, courseId, isPublished }: ActionsProps) => { 20 | const router = useRouter(); 21 | const confetti = useConfettiStore(); 22 | const [isLoading, setIsLoading] = useState(false); 23 | 24 | const onClick = async () => { 25 | try { 26 | setIsLoading(true); 27 | 28 | if (isPublished) { 29 | await axios.patch(`/api/courses/${courseId}/unpublish`); 30 | toast.success("Course unpublished"); 31 | } else { 32 | await axios.patch(`/api/courses/${courseId}/publish`); 33 | toast.success("Course published"); 34 | confetti.onOpen(); 35 | } 36 | 37 | router.refresh(); 38 | } catch { 39 | toast.error("Something went wrong"); 40 | } finally { 41 | setIsLoading(false); 42 | } 43 | }; 44 | 45 | const onDelete = async () => { 46 | try { 47 | setIsLoading(true); 48 | 49 | await axios.delete(`/api/courses/${courseId}`); 50 | 51 | toast.success("Course deleted"); 52 | router.refresh(); 53 | router.push(`/dashboard/teacher/courses`); 54 | } catch { 55 | toast.error("Something went wrong"); 56 | } finally { 57 | setIsLoading(false); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 | 71 | 72 | 75 | 76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/courses/[courseId]/_components/category-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useForm } from "react-hook-form"; 7 | 8 | import { 9 | Form, 10 | FormControl, 11 | FormField, 12 | FormItem, 13 | FormMessage, 14 | } from "@/components/ui/form"; 15 | import { Button } from "@/components/ui/button"; 16 | import { Pencil } from "lucide-react"; 17 | import { useState } from "react"; 18 | import toast from "react-hot-toast"; 19 | import { useRouter } from "next/navigation"; 20 | import { cn } from "@/lib/utils"; 21 | import { Course } from "@prisma/client"; 22 | import { Combobox } from "@/components/ui/combobox"; 23 | 24 | interface CategoryFormProps { 25 | initialData: Course; 26 | courseId: string; 27 | options: { label: string; value: string }[]; 28 | } 29 | 30 | const formSchema = z.object({ 31 | categoryId: z.string().min(1), 32 | }); 33 | 34 | export const CategoryForm = ({ 35 | initialData, 36 | courseId, 37 | options, 38 | }: CategoryFormProps) => { 39 | const router = useRouter(); 40 | const [isEditing, setIsEditing] = useState(false); 41 | 42 | const toggleEdit = () => setIsEditing((current) => !current); 43 | 44 | const form = useForm>({ 45 | resolver: zodResolver(formSchema), 46 | defaultValues: { 47 | categoryId: initialData?.categoryId || "", 48 | }, 49 | }); 50 | 51 | const { isSubmitting, isValid } = form.formState; 52 | 53 | const onSubmit = async (values: z.infer) => { 54 | try { 55 | await axios.patch(`/api/courses/${courseId}`, values); 56 | toast.success("Course updated"); 57 | toggleEdit(); 58 | router.refresh(); 59 | } catch (error) { 60 | toast.error("Something went wrong"); 61 | } 62 | }; 63 | 64 | const selectedOptions = options.find( 65 | (option) => option.value === initialData.categoryId 66 | ); 67 | 68 | return ( 69 |
70 |
71 | Course Category 72 | 82 |
83 | {!isEditing && ( 84 |

90 | {selectedOptions?.label || "No category"} 91 |

92 | )} 93 | {isEditing && ( 94 |
95 | 99 | ( 103 | 104 | 105 | 106 | 107 | 108 | 109 | )} 110 | /> 111 |
112 | 115 |
116 | 117 | 118 | )} 119 |
120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /app/dashboard/(routes)/teacher/courses/[courseId]/_components/description-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import axios from "axios"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useForm } from "react-hook-form"; 7 | 8 | import { 9 | Form, 10 | FormControl, 11 | FormField, 12 | FormItem, 13 | FormMessage, 14 | } from "@/components/ui/form"; 15 | import { Button } from "@/components/ui/button"; 16 | import { Pencil } from "lucide-react"; 17 | import { useState } from "react"; 18 | import toast from "react-hot-toast"; 19 | import { useRouter } from "next/navigation"; 20 | import { cn } from "@/lib/utils"; 21 | import { Textarea } from "@/components/ui/textarea"; 22 | import { Course } from "@prisma/client"; 23 | 24 | interface DescriptionFormProps { 25 | initialData: Course; 26 | courseId: string; 27 | } 28 | 29 | const formSchema = z.object({ 30 | description: z.string().min(1, { 31 | message: "Description is required", 32 | }), 33 | }); 34 | 35 | export const DescriptionForm = ({ 36 | initialData, 37 | courseId, 38 | }: DescriptionFormProps) => { 39 | const router = useRouter(); 40 | const [isEditing, setIsEditing] = useState(false); 41 | 42 | const toggleEdit = () => setIsEditing((current) => !current); 43 | 44 | const form = useForm>({ 45 | resolver: zodResolver(formSchema), 46 | defaultValues: { 47 | description: initialData?.description || "", 48 | }, 49 | }); 50 | 51 | const { isSubmitting, isValid } = form.formState; 52 | 53 | const onSubmit = async (values: z.infer) => { 54 | try { 55 | await axios.patch(`/api/courses/${courseId}`, values); 56 | toast.success("Course updated"); 57 | toggleEdit(); 58 | router.refresh(); 59 | } catch (error) { 60 | toast.error("Something went wrong"); 61 | } 62 | }; 63 | 64 | return ( 65 |
66 |
67 | Course Description 68 | 78 |
79 | {!isEditing && ( 80 |

86 | {initialData.description || "No description"} 87 |

88 | )} 89 | {isEditing && ( 90 |
91 | 95 | ( 99 | 100 | 101 |