├── .gitignore ├── client ├── .env.example ├── .eslintrc.json ├── README.md ├── components.json ├── next-env.d.ts ├── next.config.js ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── file.svg │ ├── globe.svg │ ├── hero1.jpg │ ├── hero2.jpg │ ├── hero3.jpg │ ├── logo.svg │ ├── next.svg │ ├── placeholder.png │ ├── vercel.svg │ └── window.svg ├── src │ ├── app │ │ ├── (auth) │ │ │ ├── layout.tsx │ │ │ ├── signin │ │ │ │ └── [[...signin]] │ │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── [[...signup]] │ │ │ │ └── page.tsx │ │ ├── (dashboard) │ │ │ ├── layout.tsx │ │ │ ├── teacher │ │ │ │ ├── billing │ │ │ │ │ └── page.tsx │ │ │ │ ├── courses │ │ │ │ │ ├── [id] │ │ │ │ │ │ ├── ChapterModal.tsx │ │ │ │ │ │ ├── Droppable.tsx │ │ │ │ │ │ ├── SectionModal.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── profile │ │ │ │ │ └── [[...profile]] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── settings │ │ │ │ │ └── page.tsx │ │ │ └── user │ │ │ │ ├── billing │ │ │ │ └── page.tsx │ │ │ │ ├── courses │ │ │ │ ├── [courseId] │ │ │ │ │ ├── ChaptersSidebar.tsx │ │ │ │ │ └── chapters │ │ │ │ │ │ └── [chapterId] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── profile │ │ │ │ └── [[...profile]] │ │ │ │ │ └── page.tsx │ │ │ │ └── settings │ │ │ │ └── page.tsx │ │ ├── (nondashboard) │ │ │ ├── checkout │ │ │ │ ├── completion │ │ │ │ │ └── index.tsx │ │ │ │ ├── details │ │ │ │ │ └── index.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── payment │ │ │ │ │ ├── StripeProvider.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── landing │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── search │ │ │ │ ├── SelectedCourse.tsx │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── components │ │ ├── AccordionSections.tsx │ │ ├── AppSidebar.tsx │ │ ├── CourseCard.tsx │ │ ├── CourseCardSearch.tsx │ │ ├── CoursePreview.tsx │ │ ├── CustomFormField.tsx │ │ ├── CustomModal.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Loading.tsx │ │ ├── Navbar.tsx │ │ ├── NonDashboardNavbar.tsx │ │ ├── SharedNotificationSettings.tsx │ │ ├── SignIn.tsx │ │ ├── SignUp.tsx │ │ ├── TeacherCourseCard.tsx │ │ ├── Toolbar.tsx │ │ ├── WizardStepper.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── avatar.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dialog.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ ├── hooks │ │ ├── use-mobile.tsx │ │ ├── useCarousel.ts │ │ ├── useCheckoutNavigation.ts │ │ ├── useCourseProgressData.ts │ │ └── useCurrentCourse.ts │ ├── lib │ │ ├── schemas.ts │ │ └── utils.ts │ ├── middleware.ts │ ├── state │ │ ├── api.ts │ │ ├── index.ts │ │ └── redux.tsx │ └── types │ │ └── index.d.ts ├── tailwind.config.ts └── tsconfig.json └── server ├── .dockerignore ├── .env.example ├── Dockerfile ├── package-lock.json ├── package.json ├── src ├── controllers │ ├── courseController.ts │ ├── transactionController.ts │ ├── userClerkController.ts │ └── userCourseProgressController.ts ├── index.ts ├── models │ ├── courseModel.ts │ ├── transactionModel.ts │ └── userCourseProgressModel.ts ├── routes │ ├── courseRoutes.ts │ ├── transactionRoutes.ts │ ├── userClerkRoutes.ts │ └── userCourseProgressRoutes.ts ├── seed │ ├── data │ │ ├── courses.json │ │ ├── transactions.json │ │ └── userCourseProgress.json │ └── seedDynamodb.ts └── utils │ └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ 3 | 4 | .env 5 | client/.env.local 6 | client/.env.development 7 | client/.env.production 8 | server/.env 9 | 10 | client/.next/ 11 | 12 | .DS_Store 13 | Thumbs.db 14 | 15 | .vscode/ 16 | 17 | server/shared-local-instance.db 18 | server/dynamodb-local-metadata.json 19 | shared-local-instance.db -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8001 2 | NEXT_PUBLIC_LOCAL_URL=http://localhost:3000 3 | 4 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY=YOUR-STRIPE-PUBLIC-KEY 5 | 6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR-CLERK-PUBLISHABLE-KEY 7 | CLERK_SECRET_KEY=YOUR-CLERK-SECRET-KEY 8 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off", 5 | "@typescript-eslint/no-unused-vars": "off", 6 | "@typescript-eslint/no-unused-expressions": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "images.pexels.com", 8 | port: "", 9 | pathname: "/**", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /client/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-management", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/clerk-js": "^5.33.0", 13 | "@clerk/nextjs": "^6.3.1", 14 | "@clerk/themes": "^2.1.43", 15 | "@hello-pangea/dnd": "^17.0.0", 16 | "@hookform/resolvers": "^3.9.1", 17 | "@radix-ui/react-accordion": "^1.2.1", 18 | "@radix-ui/react-avatar": "^1.1.1", 19 | "@radix-ui/react-dialog": "^1.1.2", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-navigation-menu": "^1.2.1", 22 | "@radix-ui/react-popover": "^1.1.2", 23 | "@radix-ui/react-progress": "^1.1.0", 24 | "@radix-ui/react-select": "^2.1.2", 25 | "@radix-ui/react-separator": "^1.1.0", 26 | "@radix-ui/react-slot": "^1.1.0", 27 | "@radix-ui/react-switch": "^1.1.1", 28 | "@radix-ui/react-tabs": "^1.1.1", 29 | "@radix-ui/react-toggle": "^1.1.0", 30 | "@radix-ui/react-tooltip": "^1.1.3", 31 | "@reduxjs/toolkit": "^2.3.0", 32 | "@stripe/react-stripe-js": "^2.9.0", 33 | "@stripe/stripe-js": "^4.10.0", 34 | "class-variance-authority": "^0.7.0", 35 | "clsx": "^2.1.1", 36 | "date-fns": "^4.1.0", 37 | "dotenv": "^16.4.5", 38 | "filepond": "^4.32.1", 39 | "filepond-plugin-image-exif-orientation": "^1.0.11", 40 | "filepond-plugin-image-preview": "^4.6.12", 41 | "framer-motion": "^11.11.11", 42 | "lucide-react": "^0.456.0", 43 | "next": "15.0.3", 44 | "next-themes": "^0.4.3", 45 | "react": "19.0.0-rc-66855b96-20241106", 46 | "react-dom": "19.0.0-rc-66855b96-20241106", 47 | "react-filepond": "^7.1.2", 48 | "react-hook-form": "^7.53.2", 49 | "react-player": "^2.16.0", 50 | "react-redux": "^9.1.2", 51 | "sonner": "^1.7.0", 52 | "tailwind-merge": "^2.5.4", 53 | "tailwindcss-animate": "^1.0.7", 54 | "uuid": "^11.0.3", 55 | "zod": "^3.23.8" 56 | }, 57 | "devDependencies": { 58 | "@types/node": "^20.17.6", 59 | "@types/react": "^18", 60 | "@types/react-dom": "^18", 61 | "@types/uuid": "^10.0.0", 62 | "eslint": "^8", 63 | "eslint-config-next": "15.0.3", 64 | "postcss": "^8", 65 | "prettier-plugin-tailwindcss": "^0.6.8", 66 | "tailwindcss": "^3.4.1", 67 | "typescript": "^5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /client/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/hero1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/learning-management/4cfd99707393daa04f778e58b059013d45bea93a/client/public/hero1.jpg -------------------------------------------------------------------------------- /client/public/hero2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/learning-management/4cfd99707393daa04f778e58b059013d45bea93a/client/public/hero2.jpg -------------------------------------------------------------------------------- /client/public/hero3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/learning-management/4cfd99707393daa04f778e58b059013d45bea93a/client/public/hero3.jpg -------------------------------------------------------------------------------- /client/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/learning-management/4cfd99707393daa04f778e58b059013d45bea93a/client/public/placeholder.png -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Layout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 |
{children}
7 |
8 | ); 9 | }; 10 | 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /client/src/app/(auth)/signin/[[...signin]]/page.tsx: -------------------------------------------------------------------------------- 1 | import SignInComponent from "@/components/SignIn"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/(auth)/signup/[[...signup]]/page.tsx: -------------------------------------------------------------------------------- 1 | import SignUpComponent from "@/components/SignUp"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import AppSidebar from "@/components/AppSidebar"; 3 | import Loading from "@/components/Loading"; 4 | import Navbar from "@/components/Navbar"; 5 | import { SidebarProvider } from "@/components/ui/sidebar"; 6 | import { cn } from "@/lib/utils"; 7 | import { useUser } from "@clerk/nextjs"; 8 | import { usePathname } from "next/navigation"; 9 | import { useEffect, useState } from "react"; 10 | import ChaptersSidebar from "./user/courses/[courseId]/ChaptersSidebar"; 11 | 12 | export default function DashboardLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | const pathname = usePathname(); 18 | const [courseId, setCourseId] = useState(null); 19 | const { user, isLoaded } = useUser(); 20 | const isCoursePage = /^\/user\/courses\/[^\/]+(?:\/chapters\/[^\/]+)?$/.test( 21 | pathname 22 | ); 23 | 24 | useEffect(() => { 25 | if (isCoursePage) { 26 | const match = pathname.match(/\/user\/courses\/([^\/]+)/); 27 | setCourseId(match ? match[1] : null); 28 | } else { 29 | setCourseId(null); 30 | } 31 | }, [isCoursePage, pathname]); 32 | 33 | if (!isLoaded) return ; 34 | if (!user) return
Please sign in to access this page.
; 35 | 36 | return ( 37 | 38 |
39 | 40 |
41 | {courseId && } 42 |
49 | 50 |
{children}
51 |
52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/billing/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Loading from "@/components/Loading"; 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table"; 19 | import { formatPrice } from "@/lib/utils"; 20 | import { useGetTransactionsQuery } from "@/state/api"; 21 | import { useUser } from "@clerk/nextjs"; 22 | import React, { useState } from "react"; 23 | 24 | const TeacherBilling = () => { 25 | const [paymentType, setPaymentType] = useState("all"); 26 | const { user, isLoaded } = useUser(); 27 | const { data: transactions, isLoading: isLoadingTransactions } = 28 | useGetTransactionsQuery(user?.id || "", { 29 | skip: !isLoaded || !user, 30 | }); 31 | 32 | const filteredData = 33 | transactions?.filter((transaction) => { 34 | const matchesTypes = 35 | paymentType === "all" || transaction.paymentProvider === paymentType; 36 | return matchesTypes; 37 | }) || []; 38 | 39 | if (!isLoaded) return ; 40 | if (!user) return
Please sign in to view your billing information.
; 41 | 42 | return ( 43 |
44 |
45 |

Payment History

46 |
47 | 64 |
65 | 66 |
67 | {isLoadingTransactions ? ( 68 | 69 | ) : ( 70 | 71 | 72 | 73 | Date 74 | Amount 75 | 76 | Payment Method 77 | 78 | 79 | 80 | 81 | {filteredData.length > 0 ? ( 82 | filteredData.map((transaction) => ( 83 | 87 | 88 | {new Date(transaction.dateTime).toLocaleDateString()} 89 | 90 | 91 | {formatPrice(transaction.amount)} 92 | 93 | 94 | {transaction.paymentProvider} 95 | 96 | 97 | )) 98 | ) : ( 99 | 100 | 104 | No transactions to display 105 | 106 | 107 | )} 108 | 109 |
110 | )} 111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default TeacherBilling; 118 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/courses/[id]/ChapterModal.tsx: -------------------------------------------------------------------------------- 1 | import { CustomFormField } from "@/components/CustomFormField"; 2 | import CustomModal from "@/components/CustomModal"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | import { ChapterFormData, chapterSchema } from "@/lib/schemas"; 14 | import { addChapter, closeChapterModal, editChapter } from "@/state"; 15 | import { useAppDispatch, useAppSelector } from "@/state/redux"; 16 | import { zodResolver } from "@hookform/resolvers/zod"; 17 | import { X } from "lucide-react"; 18 | import React, { useEffect } from "react"; 19 | import { useForm } from "react-hook-form"; 20 | import { toast } from "sonner"; 21 | import { v4 as uuidv4 } from "uuid"; 22 | 23 | const ChapterModal = () => { 24 | const dispatch = useAppDispatch(); 25 | const { 26 | isChapterModalOpen, 27 | selectedSectionIndex, 28 | selectedChapterIndex, 29 | sections, 30 | } = useAppSelector((state) => state.global.courseEditor); 31 | 32 | const chapter: Chapter | undefined = 33 | selectedSectionIndex !== null && selectedChapterIndex !== null 34 | ? sections[selectedSectionIndex].chapters[selectedChapterIndex] 35 | : undefined; 36 | 37 | const methods = useForm({ 38 | resolver: zodResolver(chapterSchema), 39 | defaultValues: { 40 | title: "", 41 | content: "", 42 | video: "", 43 | }, 44 | }); 45 | 46 | useEffect(() => { 47 | if (chapter) { 48 | methods.reset({ 49 | title: chapter.title, 50 | content: chapter.content, 51 | video: chapter.video || "", 52 | }); 53 | } else { 54 | methods.reset({ 55 | title: "", 56 | content: "", 57 | video: "", 58 | }); 59 | } 60 | }, [chapter, methods]); 61 | 62 | const onClose = () => { 63 | dispatch(closeChapterModal()); 64 | }; 65 | 66 | const onSubmit = (data: ChapterFormData) => { 67 | if (selectedSectionIndex === null) return; 68 | 69 | const newChapter: Chapter = { 70 | chapterId: chapter?.chapterId || uuidv4(), 71 | title: data.title, 72 | content: data.content, 73 | type: data.video ? "Video" : "Text", 74 | video: data.video, 75 | }; 76 | 77 | if (selectedChapterIndex === null) { 78 | dispatch( 79 | addChapter({ 80 | sectionIndex: selectedSectionIndex, 81 | chapter: newChapter, 82 | }) 83 | ); 84 | } else { 85 | dispatch( 86 | editChapter({ 87 | sectionIndex: selectedSectionIndex, 88 | chapterIndex: selectedChapterIndex, 89 | chapter: newChapter, 90 | }) 91 | ); 92 | } 93 | 94 | toast.success( 95 | `Chapter added/updated successfully but you need to save the course to apply the changes` 96 | ); 97 | onClose(); 98 | }; 99 | 100 | return ( 101 | 102 |
103 |
104 |

Add/Edit Chapter

105 | 108 |
109 | 110 |
111 | 115 | 120 | 121 | 127 | 128 | ( 132 | 133 | 134 | Chapter Video 135 | 136 | 137 |
138 | { 142 | const file = e.target.files?.[0]; 143 | if (file) { 144 | onChange(file); 145 | } 146 | }} 147 | className="border-none bg-customgreys-darkGrey py-2 cursor-pointer" 148 | /> 149 | {typeof value === "string" && value && ( 150 |
151 | Current video: {value.split("/").pop()} 152 |
153 | )} 154 | {value instanceof File && ( 155 |
156 | Selected file: {value.name} 157 |
158 | )} 159 |
160 |
161 | 162 |
163 | )} 164 | /> 165 | 166 |
167 | 170 | 173 |
174 | 175 | 176 |
177 |
178 | ); 179 | }; 180 | 181 | export default ChapterModal; 182 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/courses/[id]/SectionModal.tsx: -------------------------------------------------------------------------------- 1 | import { CustomFormField } from "@/components/CustomFormField"; 2 | import CustomModal from "@/components/CustomModal"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Form } from "@/components/ui/form"; 5 | import { SectionFormData, sectionSchema } from "@/lib/schemas"; 6 | import { addSection, closeSectionModal, editSection } from "@/state"; 7 | import { useAppDispatch, useAppSelector } from "@/state/redux"; 8 | import { zodResolver } from "@hookform/resolvers/zod"; 9 | import { X } from "lucide-react"; 10 | import React, { useEffect } from "react"; 11 | import { useForm } from "react-hook-form"; 12 | import { toast } from "sonner"; 13 | import { v4 as uuidv4 } from "uuid"; 14 | 15 | const SectionModal = () => { 16 | const dispatch = useAppDispatch(); 17 | const { isSectionModalOpen, selectedSectionIndex, sections } = useAppSelector( 18 | (state) => state.global.courseEditor 19 | ); 20 | 21 | const section = 22 | selectedSectionIndex !== null ? sections[selectedSectionIndex] : null; 23 | 24 | const methods = useForm({ 25 | resolver: zodResolver(sectionSchema), 26 | defaultValues: { 27 | title: "", 28 | description: "", 29 | }, 30 | }); 31 | 32 | useEffect(() => { 33 | if (section) { 34 | methods.reset({ 35 | title: section.sectionTitle, 36 | description: section.sectionDescription, 37 | }); 38 | } else { 39 | methods.reset({ 40 | title: "", 41 | description: "", 42 | }); 43 | } 44 | }, [section, methods]); 45 | 46 | const onClose = () => { 47 | dispatch(closeSectionModal()); 48 | }; 49 | 50 | const onSubmit = (data: SectionFormData) => { 51 | const newSection: Section = { 52 | sectionId: section?.sectionId || uuidv4(), 53 | sectionTitle: data.title, 54 | sectionDescription: data.description, 55 | chapters: section?.chapters || [], 56 | }; 57 | 58 | if (selectedSectionIndex === null) { 59 | dispatch(addSection(newSection)); 60 | } else { 61 | dispatch( 62 | editSection({ 63 | index: selectedSectionIndex, 64 | section: newSection, 65 | }) 66 | ); 67 | } 68 | 69 | toast.success( 70 | `Section added/updated successfully but you need to save the course to apply the changes` 71 | ); 72 | onClose(); 73 | }; 74 | 75 | return ( 76 | 77 |
78 |
79 |

Add/Edit Section

80 | 83 |
84 | 85 |
86 | 90 | 95 | 96 | 102 | 103 |
104 | 107 | 110 |
111 | 112 | 113 |
114 |
115 | ); 116 | }; 117 | 118 | export default SectionModal; 119 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/courses/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Header from "@/components/Header"; 4 | import Loading from "@/components/Loading"; 5 | import TeacherCourseCard from "@/components/TeacherCourseCard"; 6 | import Toolbar from "@/components/Toolbar"; 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | useCreateCourseMutation, 10 | useDeleteCourseMutation, 11 | useGetCoursesQuery, 12 | } from "@/state/api"; 13 | import { useUser } from "@clerk/nextjs"; 14 | import { useRouter } from "next/navigation"; 15 | import React, { useMemo, useState } from "react"; 16 | 17 | const Courses = () => { 18 | const router = useRouter(); 19 | const { user } = useUser(); 20 | const { 21 | data: courses, 22 | isLoading, 23 | isError, 24 | } = useGetCoursesQuery({ category: "all" }); 25 | 26 | const [createCourse] = useCreateCourseMutation(); 27 | const [deleteCourse] = useDeleteCourseMutation(); 28 | 29 | const [searchTerm, setSearchTerm] = useState(""); 30 | const [selectedCategory, setSelectedCategory] = useState("all"); 31 | 32 | const filteredCourses = useMemo(() => { 33 | if (!courses) return []; 34 | 35 | return courses.filter((course) => { 36 | const matchesSearch = course.title 37 | .toLowerCase() 38 | .includes(searchTerm.toLowerCase()); 39 | const matchesCategory = 40 | selectedCategory === "all" || course.category === selectedCategory; 41 | return matchesSearch && matchesCategory; 42 | }); 43 | }, [courses, searchTerm, selectedCategory]); 44 | 45 | const handleEdit = (course: Course) => { 46 | router.push(`/teacher/courses/${course.courseId}`, { 47 | scroll: false, 48 | }); 49 | }; 50 | 51 | const handleDelete = async (course: Course) => { 52 | if (window.confirm("Are you sure you want to delete this course?")) { 53 | await deleteCourse(course.courseId).unwrap(); 54 | } 55 | }; 56 | 57 | const handleCreateCourse = async () => { 58 | if (!user) return; 59 | 60 | const result = await createCourse({ 61 | teacherId: user.id, 62 | teacherName: user.fullName || "Unknown Teacher", 63 | }).unwrap(); 64 | router.push(`/teacher/courses/${result.courseId}`, { 65 | scroll: false, 66 | }); 67 | }; 68 | 69 | if (isLoading) return ; 70 | if (isError || !courses) return
Error loading courses.
; 71 | 72 | return ( 73 |
74 |
82 | Create Course 83 | 84 | } 85 | /> 86 | 90 |
91 | {filteredCourses.map((course) => ( 92 | 99 | ))} 100 |
101 |
102 | ); 103 | }; 104 | 105 | export default Courses; 106 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/profile/[[...profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/Header"; 2 | import { UserProfile } from "@clerk/nextjs"; 3 | import { dark } from "@clerk/themes"; 4 | import React from "react"; 5 | 6 | const TeacherProfilePage = () => { 7 | return ( 8 | <> 9 |
10 | div:nth-child(1)": { 19 | background: "none", 20 | }, 21 | }, 22 | }, 23 | }} 24 | /> 25 | 26 | ); 27 | }; 28 | 29 | export default TeacherProfilePage; 30 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/teacher/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import SharedNotificationSettings from "@/components/SharedNotificationSettings"; 2 | import React from "react"; 3 | 4 | const TeacherSettings = () => { 5 | return ( 6 |
7 | 11 |
12 | ); 13 | }; 14 | 15 | export default TeacherSettings; 16 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/user/billing/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Loading from "@/components/Loading"; 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table"; 19 | import { formatPrice } from "@/lib/utils"; 20 | import { useGetTransactionsQuery } from "@/state/api"; 21 | import { useUser } from "@clerk/nextjs"; 22 | import React, { useState } from "react"; 23 | 24 | const UserBilling = () => { 25 | const [paymentType, setPaymentType] = useState("all"); 26 | const { user, isLoaded } = useUser(); 27 | const { data: transactions, isLoading: isLoadingTransactions } = 28 | useGetTransactionsQuery(user?.id || "", { 29 | skip: !isLoaded || !user, 30 | }); 31 | 32 | const filteredData = 33 | transactions?.filter((transaction) => { 34 | const matchesTypes = 35 | paymentType === "all" || transaction.paymentProvider === paymentType; 36 | return matchesTypes; 37 | }) || []; 38 | 39 | if (!isLoaded) return ; 40 | if (!user) return
Please sign in to view your billing information.
; 41 | 42 | return ( 43 |
44 |
45 |

Payment History

46 |
47 | 64 |
65 | 66 |
67 | {isLoadingTransactions ? ( 68 | 69 | ) : ( 70 | 71 | 72 | 73 | Date 74 | Amount 75 | 76 | Payment Method 77 | 78 | 79 | 80 | 81 | {filteredData.length > 0 ? ( 82 | filteredData.map((transaction) => ( 83 | 87 | 88 | {new Date(transaction.dateTime).toLocaleDateString()} 89 | 90 | 91 | {formatPrice(transaction.amount)} 92 | 93 | 94 | {transaction.paymentProvider} 95 | 96 | 97 | )) 98 | ) : ( 99 | 100 | 104 | No transactions to display 105 | 106 | 107 | )} 108 | 109 |
110 | )} 111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default UserBilling; 118 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/user/courses/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Toolbar from "@/components/Toolbar"; 4 | import CourseCard from "@/components/CourseCard"; 5 | import { useGetUserEnrolledCoursesQuery } from "@/state/api"; 6 | import { useRouter } from "next/navigation"; 7 | import Header from "@/components/Header"; 8 | import { useUser } from "@clerk/nextjs"; 9 | import { useState, useMemo } from "react"; 10 | import Loading from "@/components/Loading"; 11 | 12 | const Courses = () => { 13 | const router = useRouter(); 14 | const { user, isLoaded } = useUser(); 15 | const [searchTerm, setSearchTerm] = useState(""); 16 | const [selectedCategory, setSelectedCategory] = useState("all"); 17 | 18 | const { 19 | data: courses, 20 | isLoading, 21 | isError, 22 | } = useGetUserEnrolledCoursesQuery(user?.id ?? "", { 23 | skip: !isLoaded || !user, 24 | }); 25 | 26 | const filteredCourses = useMemo(() => { 27 | if (!courses) return []; 28 | 29 | return courses.filter((course) => { 30 | const matchesSearch = course.title 31 | .toLowerCase() 32 | .includes(searchTerm.toLowerCase()); 33 | const matchesCategory = 34 | selectedCategory === "all" || course.category === selectedCategory; 35 | return matchesSearch && matchesCategory; 36 | }); 37 | }, [courses, searchTerm, selectedCategory]); 38 | 39 | const handleGoToCourse = (course: Course) => { 40 | if ( 41 | course.sections && 42 | course.sections.length > 0 && 43 | course.sections[0].chapters.length > 0 44 | ) { 45 | const firstChapter = course.sections[0].chapters[0]; 46 | router.push( 47 | `/user/courses/${course.courseId}/chapters/${firstChapter.chapterId}`, 48 | { 49 | scroll: false, 50 | } 51 | ); 52 | } else { 53 | router.push(`/user/courses/${course.courseId}`, { 54 | scroll: false, 55 | }); 56 | } 57 | }; 58 | 59 | if (!isLoaded || isLoading) return ; 60 | if (!user) return
Please sign in to view your courses.
; 61 | if (isError || !courses || courses.length === 0) 62 | return
You are not enrolled in any courses yet.
; 63 | 64 | return ( 65 |
66 |
67 | 71 |
72 | {filteredCourses.map((course) => ( 73 | 78 | ))} 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Courses; 85 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/user/profile/[[...profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/Header"; 2 | import { UserProfile } from "@clerk/nextjs"; 3 | import { dark } from "@clerk/themes"; 4 | import React from "react"; 5 | 6 | const UserProfilePage = () => { 7 | return ( 8 | <> 9 |
10 | div:nth-child(1)": { 19 | background: "none", 20 | }, 21 | }, 22 | }, 23 | }} 24 | /> 25 | 26 | ); 27 | }; 28 | 29 | export default UserProfilePage; 30 | -------------------------------------------------------------------------------- /client/src/app/(dashboard)/user/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import SharedNotificationSettings from "@/components/SharedNotificationSettings"; 2 | import React from "react"; 3 | 4 | const UserSettings = () => { 5 | return ( 6 |
7 | 11 |
12 | ); 13 | }; 14 | 15 | export default UserSettings; 16 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/checkout/completion/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Check } from "lucide-react"; 5 | import Link from "next/link"; 6 | import React from "react"; 7 | 8 | const CompletionPage = () => { 9 | return ( 10 |
11 |
12 |
13 | 14 |
15 |

COMPLETED

16 |

17 | 🎉 You have made a course purchase successfully! 🎉 18 |

19 |
20 |
21 |

22 | Need help? Contact our{" "} 23 | 26 | . 27 |

28 |
29 |
30 | 31 | Go to Courses 32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default CompletionPage; 39 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/checkout/details/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import CoursePreview from "@/components/CoursePreview"; 4 | import { CustomFormField } from "@/components/CustomFormField"; 5 | import Loading from "@/components/Loading"; 6 | import { Button } from "@/components/ui/button"; 7 | import { useCurrentCourse } from "@/hooks/useCurrentCourse"; 8 | import { GuestFormData, guestSchema } from "@/lib/schemas"; 9 | import { zodResolver } from "@hookform/resolvers/zod"; 10 | import { useSearchParams } from "next/navigation"; 11 | import React from "react"; 12 | import { Form } from "@/components/ui/form"; 13 | import { useForm } from "react-hook-form"; 14 | import SignUpComponent from "@/components/SignUp"; 15 | import SignInComponent from "@/components/SignIn"; 16 | 17 | const CheckoutDetailsPage = () => { 18 | const { course: selectedCourse, isLoading, isError } = useCurrentCourse(); 19 | const searchParams = useSearchParams(); 20 | const showSignUp = searchParams.get("showSignUp") === "true"; 21 | 22 | const methods = useForm({ 23 | resolver: zodResolver(guestSchema), 24 | defaultValues: { 25 | email: "", 26 | }, 27 | }); 28 | 29 | if (isLoading) return ; 30 | if (isError) return
Failed to fetch course data
; 31 | if (!selectedCourse) return
Course not found
; 32 | 33 | return ( 34 |
35 |
36 |
37 | 38 |
39 | 40 | {/* STRETCH FEATURE */} 41 |
42 |
43 |

Guest Checkout

44 |

45 | Enter email to receive course access details and order 46 | confirmation. You can create an account after purchase. 47 |

48 |
49 | { 51 | console.log(data); 52 | })} 53 | className="checkout-details__form" 54 | > 55 | 63 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | Or 73 |
74 |
75 | 76 |
77 | {showSignUp ? : } 78 |
79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | export default CheckoutDetailsPage; 86 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Loading from "@/components/Loading"; 4 | import WizardStepper from "@/components/WizardStepper"; 5 | import { useCheckoutNavigation } from "@/hooks/useCheckoutNavigation"; 6 | import { useUser } from "@clerk/nextjs"; 7 | import React from "react"; 8 | import CheckoutDetailsPage from "./details"; 9 | import PaymentPage from "./payment"; 10 | import CompletionPage from "./completion"; 11 | 12 | const CheckoutWizard = () => { 13 | const { isLoaded } = useUser(); 14 | const { checkoutStep } = useCheckoutNavigation(); 15 | 16 | if (!isLoaded) return ; 17 | 18 | const renderStep = () => { 19 | switch (checkoutStep) { 20 | case 1: 21 | return ; 22 | case 2: 23 | return ; 24 | case 3: 25 | return ; 26 | default: 27 | return ; 28 | } 29 | }; 30 | 31 | return ( 32 |
33 | 34 |
{renderStep()}
35 |
36 | ); 37 | }; 38 | 39 | export default CheckoutWizard; 40 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/checkout/payment/StripeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Elements } from "@stripe/react-stripe-js"; 3 | import { 4 | Appearance, 5 | loadStripe, 6 | StripeElementsOptions, 7 | } from "@stripe/stripe-js"; 8 | import { useCreateStripePaymentIntentMutation } from "@/state/api"; 9 | import { useCurrentCourse } from "@/hooks/useCurrentCourse"; 10 | import Loading from "@/components/Loading"; 11 | 12 | if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) { 13 | throw new Error("NEXT_PUBLIC_STRIPE_PUBLIC_KEY is not set"); 14 | } 15 | 16 | const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); 17 | 18 | const appearance: Appearance = { 19 | theme: "stripe", 20 | variables: { 21 | colorPrimary: "#0570de", 22 | colorBackground: "#18181b", 23 | colorText: "#d2d2d2", 24 | colorDanger: "#df1b41", 25 | colorTextPlaceholder: "#6e6e6e", 26 | fontFamily: "Inter, system-ui, sans-serif", 27 | spacingUnit: "3px", 28 | borderRadius: "10px", 29 | fontSizeBase: "14px", 30 | }, 31 | }; 32 | 33 | const StripeProvider = ({ children }: { children: React.ReactNode }) => { 34 | const [clientSecret, setClientSecret] = useState(""); 35 | const [createStripePaymentIntent] = useCreateStripePaymentIntentMutation(); 36 | const { course } = useCurrentCourse(); 37 | 38 | useEffect(() => { 39 | if (!course) return; 40 | const fetchPaymentIntent = async () => { 41 | const result = await createStripePaymentIntent({ 42 | amount: course?.price ?? 9999999999999, 43 | }).unwrap(); 44 | 45 | setClientSecret(result.clientSecret); 46 | }; 47 | 48 | fetchPaymentIntent(); 49 | }, [createStripePaymentIntent, course?.price, course]); 50 | 51 | const options: StripeElementsOptions = { 52 | clientSecret, 53 | appearance, 54 | }; 55 | 56 | if (!clientSecret) return ; 57 | 58 | return ( 59 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | export default StripeProvider; 66 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/checkout/payment/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StripeProvider from "./StripeProvider"; 3 | import { 4 | PaymentElement, 5 | useElements, 6 | useStripe, 7 | } from "@stripe/react-stripe-js"; 8 | import { useCheckoutNavigation } from "@/hooks/useCheckoutNavigation"; 9 | import { useCurrentCourse } from "@/hooks/useCurrentCourse"; 10 | import { useClerk, useUser } from "@clerk/nextjs"; 11 | import CoursePreview from "@/components/CoursePreview"; 12 | import { CreditCard } from "lucide-react"; 13 | import { Button } from "@/components/ui/button"; 14 | import { useCreateTransactionMutation } from "@/state/api"; 15 | import { toast } from "sonner"; 16 | 17 | const PaymentPageContent = () => { 18 | const stripe = useStripe(); 19 | const elements = useElements(); 20 | const [createTransaction] = useCreateTransactionMutation(); 21 | const { navigateToStep } = useCheckoutNavigation(); 22 | const { course, courseId } = useCurrentCourse(); 23 | const { user } = useUser(); 24 | const { signOut } = useClerk(); 25 | 26 | const handleSubmit = async (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | 29 | if (!stripe || !elements) { 30 | toast.error("Stripe service is not available"); 31 | return; 32 | } 33 | 34 | const baseUrl = process.env.NEXT_PUBLIC_LOCAL_URL 35 | ? `http://${process.env.NEXT_PUBLIC_LOCAL_URL}` 36 | : process.env.NEXT_PUBLIC_VERCEL_URL 37 | ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` 38 | : undefined; 39 | 40 | const result = await stripe.confirmPayment({ 41 | elements, 42 | confirmParams: { 43 | return_url: `${baseUrl}/checkout?step=3&id=${courseId}`, 44 | }, 45 | redirect: "if_required", 46 | }); 47 | 48 | if (result.paymentIntent?.status === "succeeded") { 49 | const transactionData: Partial = { 50 | transactionId: result.paymentIntent.id, 51 | userId: user?.id, 52 | courseId: courseId, 53 | paymentProvider: "stripe", 54 | amount: course?.price || 0, 55 | }; 56 | 57 | await createTransaction(transactionData), navigateToStep(3); 58 | } 59 | }; 60 | 61 | const handleSignOutAndNavigate = async () => { 62 | await signOut(); 63 | navigateToStep(1); 64 | }; 65 | 66 | if (!course) return null; 67 | 68 | return ( 69 |
70 |
71 | {/* Order Summary */} 72 |
73 | 74 |
75 | 76 | {/* Pyament Form */} 77 |
78 |
83 |
84 |

Checkout

85 |

86 | Fill out the payment details below to complete your purchase. 87 |

88 | 89 |
90 |

Payment Method

91 | 92 |
93 |
94 | 95 | Credit/Debit Card 96 |
97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | {/* Navigation Buttons */} 108 |
109 | 117 | 118 | 126 |
127 |
128 | ); 129 | }; 130 | 131 | const PaymentPage = () => ( 132 | 133 | 134 | 135 | ); 136 | 137 | export default PaymentPage; 138 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/landing/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | import { useCarousel } from "@/hooks/useCarousel"; 8 | import { Skeleton } from "@/components/ui/skeleton"; 9 | import { useGetCoursesQuery } from "@/state/api"; 10 | import { useRouter } from "next/navigation"; 11 | import CourseCardSearch from "@/components/CourseCardSearch"; 12 | import { useUser } from "@clerk/nextjs"; 13 | 14 | const LoadingSkeleton = () => { 15 | return ( 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 |
32 | {[1, 2, 3, 4, 5].map((_, index) => ( 33 | 34 | ))} 35 |
36 | 37 |
38 | {[1, 2, 3, 4].map((_, index) => ( 39 | 40 | ))} 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | const Landing = () => { 48 | const router = useRouter(); 49 | const currentImage = useCarousel({ totalImages: 3 }); 50 | const { data: courses, isLoading, isError } = useGetCoursesQuery({}); 51 | 52 | const handleCourseClick = (courseId: string) => { 53 | router.push(`/search?id=${courseId}`, { 54 | scroll: false, 55 | }); 56 | }; 57 | 58 | if (isLoading) return ; 59 | 60 | return ( 61 | 67 | 73 |
74 |

Courses

75 |

76 | This is the list of the courses you can enroll in. 77 |
78 | Courses when you need them and want them. 79 |

80 |
81 | 82 |
Search for Courses
83 | 84 |
85 |
86 |
87 | {["/hero1.jpg", "/hero2.jpg", "/hero3.jpg"].map((src, index) => ( 88 | {`Hero 99 | ))} 100 |
101 |
102 | 109 |

Featured Courses

110 |

111 | From beginner to advanced, in all industries, we have the right 112 | courses just for you and preparing your entire journey for learning 113 | and making the most. 114 |

115 | 116 |
117 | {[ 118 | "web development", 119 | "enterprise IT", 120 | "react nextjs", 121 | "javascript", 122 | "backend development", 123 | ].map((tag, index) => ( 124 | 125 | {tag} 126 | 127 | ))} 128 |
129 | 130 |
131 | {courses && 132 | courses.slice(0, 4).map((course, index) => ( 133 | 140 | handleCourseClick(course.courseId)} 143 | /> 144 | 145 | ))} 146 |
147 |
148 |
149 | ); 150 | }; 151 | 152 | export default Landing; 153 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import NonDashboardNavbar from "@/components/NonDashboardNavbar"; 2 | import Footer from "@/components/Footer"; 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 |
7 | 8 |
{children}
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/SelectedCourse.tsx: -------------------------------------------------------------------------------- 1 | import AccordionSections from "@/components/AccordionSections"; 2 | import { Button } from "@/components/ui/button"; 3 | import { formatPrice } from "@/lib/utils"; 4 | import React from "react"; 5 | 6 | const SelectedCourse = ({ course, handleEnrollNow }: SelectedCourseProps) => { 7 | return ( 8 |
9 |
10 |

{course.title}

11 |

12 | By {course.teacherName} |{" "} 13 | 14 | {course?.enrollments?.length} 15 | 16 |

17 |
18 | 19 |
20 |

{course.description}

21 | 22 |
23 |

Course Content

24 | 25 |
26 | 27 |
28 | 29 | {formatPrice(course.price)} 30 | 31 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default SelectedCourse; 44 | -------------------------------------------------------------------------------- /client/src/app/(nondashboard)/search/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Loading from "@/components/Loading"; 4 | import { useGetCoursesQuery } from "@/state/api"; 5 | import { useRouter, useSearchParams } from "next/navigation"; 6 | import React, { useEffect, useState } from "react"; 7 | import { motion } from "framer-motion"; 8 | import CourseCardSearch from "@/components/CourseCardSearch"; 9 | import SelectedCourse from "./SelectedCourse"; 10 | 11 | const Search = () => { 12 | const searchParams = useSearchParams(); 13 | const id = searchParams.get("id"); 14 | const { data: courses, isLoading, isError } = useGetCoursesQuery({}); 15 | const [selectedCourse, setSelectedCourse] = useState(null); 16 | const router = useRouter(); 17 | 18 | useEffect(() => { 19 | if (courses) { 20 | if (id) { 21 | const course = courses.find((c) => c.courseId === id); 22 | setSelectedCourse(course || courses[0]); 23 | } else { 24 | setSelectedCourse(courses[0]); 25 | } 26 | } 27 | }, [courses, id]); 28 | 29 | if (isLoading) return ; 30 | if (isError || !courses) return
Failed to fetch courses
; 31 | 32 | const handleCourseSelect = (course: Course) => { 33 | setSelectedCourse(course); 34 | router.push(`/search?id=${course.courseId}`, { 35 | scroll: false, 36 | }); 37 | }; 38 | 39 | const handleEnrollNow = (courseId: string) => { 40 | router.push(`/checkout?step=1&id=${courseId}&showSignUp=false`, { 41 | scroll: false, 42 | }); 43 | }; 44 | 45 | return ( 46 | 52 |

List of available courses

53 |

{courses.length} courses avaiable

54 |
55 | 61 | {courses.map((course) => ( 62 | handleCourseSelect(course)} 67 | /> 68 | ))} 69 | 70 | 71 | {selectedCourse && ( 72 | 78 | 82 | 83 | )} 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Search; 90 | -------------------------------------------------------------------------------- /client/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ed-roh/learning-management/4cfd99707393daa04f778e58b059013d45bea93a/client/src/app/favicon.ico -------------------------------------------------------------------------------- /client/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { DM_Sans } from "next/font/google"; 3 | import "./globals.css"; 4 | import Providers from "./providers"; 5 | import { ClerkProvider } from "@clerk/nextjs"; 6 | import { Toaster } from "sonner"; 7 | import { Suspense } from "react"; 8 | 9 | const dmSans = DM_Sans({ 10 | subsets: ["latin"], 11 | display: "swap", 12 | variable: "--font-dm-sans", 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 |
{children}
32 |
33 | 34 |
35 | 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /client/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import NonDashboardNavbar from "@/components/NonDashboardNavbar"; 2 | import Landing from "@/app/(nondashboard)/landing/page"; 3 | import Footer from "@/components/Footer"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import StoreProvider from "@/state/redux"; 5 | 6 | const Providers = ({ children }: { children: React.ReactNode }) => { 7 | return {children}; 8 | }; 9 | 10 | export default Providers; 11 | -------------------------------------------------------------------------------- /client/src/components/AccordionSections.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Accordion, 4 | AccordionContent, 5 | AccordionItem, 6 | AccordionTrigger, 7 | } from "@/components/ui/accordion"; 8 | import { FileText } from "lucide-react"; 9 | 10 | const AccordionSections = ({ sections }: AccordionSectionsProps) => { 11 | return ( 12 | 13 | {sections.map((section) => ( 14 | 19 | 20 |
{section.sectionTitle}
21 |
22 | 23 |
    24 | {section.chapters.map((chapter) => ( 25 |
  • 29 | 30 | {chapter.title} 31 |
  • 32 | ))} 33 |
34 |
35 |
36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | export default AccordionSections; 42 | -------------------------------------------------------------------------------- /client/src/components/AppSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useClerk, useUser } from "@clerk/nextjs"; 2 | import { usePathname } from "next/navigation"; 3 | import React from "react"; 4 | import { 5 | Sidebar, 6 | SidebarContent, 7 | SidebarFooter, 8 | SidebarHeader, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | useSidebar, 13 | } from "@/components/ui/sidebar"; 14 | import { 15 | BookOpen, 16 | Briefcase, 17 | DollarSign, 18 | LogOut, 19 | PanelLeft, 20 | Settings, 21 | User, 22 | } from "lucide-react"; 23 | import Loading from "./Loading"; 24 | import Image from "next/image"; 25 | import { cn } from "@/lib/utils"; 26 | import Link from "next/link"; 27 | 28 | const AppSidebar = () => { 29 | const { user, isLoaded } = useUser(); 30 | const { signOut } = useClerk(); 31 | const pathname = usePathname(); 32 | const { toggleSidebar } = useSidebar(); 33 | 34 | const navLinks = { 35 | student: [ 36 | { icon: BookOpen, label: "Courses", href: "/user/courses" }, 37 | { icon: Briefcase, label: "Billing", href: "/user/billing" }, 38 | { icon: User, label: "Profile", href: "/user/profile" }, 39 | { icon: Settings, label: "Settings", href: "/user/settings" }, 40 | ], 41 | teacher: [ 42 | { icon: BookOpen, label: "Courses", href: "/teacher/courses" }, 43 | { icon: DollarSign, label: "Billing", href: "/teacher/billing" }, 44 | { icon: User, label: "Profile", href: "/teacher/profile" }, 45 | { icon: Settings, label: "Settings", href: "/teacher/settings" }, 46 | ], 47 | }; 48 | 49 | if (!isLoaded) return ; 50 | if (!user) return
User not found
; 51 | 52 | const userType = 53 | (user.publicMetadata.userType as "student" | "teacher") || "student"; 54 | const currentNavLinks = navLinks[userType]; 55 | 56 | return ( 57 | 62 | 63 | 64 | 65 | toggleSidebar()} 68 | className="group hover:bg-customgreys-secondarybg" 69 | > 70 |
71 |
72 | logo 79 |

EDROH

80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 | {currentNavLinks.map((link) => { 90 | const isActive = pathname.startsWith(link.href); 91 | return ( 92 | 99 | 107 | 112 | 115 | 121 | {link.label} 122 | 123 | 124 | 125 | {isActive &&
} 126 | 127 | ); 128 | })} 129 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 146 | 147 | ); 148 | }; 149 | 150 | export default AppSidebar; 151 | -------------------------------------------------------------------------------- /client/src/components/CourseCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardContent, 5 | CardTitle, 6 | CardFooter, 7 | } from "@/components/ui/card"; 8 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; 9 | import Image from "next/image"; 10 | import { formatPrice } from "@/lib/utils"; 11 | 12 | const CourseCard = ({ course, onGoToCourse }: CourseCardProps) => { 13 | return ( 14 | onGoToCourse(course)}> 15 | 16 | {course.title} 24 | 25 | 26 | 27 | {course.title}: {course.description} 28 | 29 | 30 |
31 | 32 | 33 | 34 | {course.teacherName[0]} 35 | 36 | 37 | 38 |

39 | {course.teacherName} 40 |

41 |
42 | 43 | 44 |
{course.category}
45 | 46 | {formatPrice(course.price)} 47 | 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default CourseCard; 55 | -------------------------------------------------------------------------------- /client/src/components/CourseCardSearch.tsx: -------------------------------------------------------------------------------- 1 | import { formatPrice } from "@/lib/utils"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | 5 | const CourseCardSearch = ({ 6 | course, 7 | isSelected, 8 | onClick, 9 | }: SearchCourseCardProps) => { 10 | return ( 11 |
19 |
20 | {course.title} 28 |
29 |
30 |
31 |

{course.title}

32 |

33 | {course.description} 34 |

35 |
36 |
37 |

By {course.teacherName}

38 |
39 | 40 | {formatPrice(course.price)} 41 | 42 | 43 | {course.enrollments?.length} Enrolled 44 | 45 |
46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default CourseCardSearch; 53 | -------------------------------------------------------------------------------- /client/src/components/CoursePreview.tsx: -------------------------------------------------------------------------------- 1 | import { formatPrice } from "@/lib/utils"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | import AccordionSections from "./AccordionSections"; 5 | 6 | const CoursePreview = ({ course }: CoursePreviewProps) => { 7 | const price = formatPrice(course.price); 8 | return ( 9 |
10 |
11 |
12 | Course Preview 19 |
20 |
21 |

{course.title}

22 |

by {course.teacherName}

23 |

24 | {course.description} 25 |

26 |
27 | 28 |
29 |

30 | Course Content 31 |

32 | 33 |
34 |
35 | 36 |
37 |

Price Details (1 item)

38 |
39 | 1x {course.title} 40 | {price} 41 |
42 |
43 | Total Amount 44 | {price} 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default CoursePreview; 52 | -------------------------------------------------------------------------------- /client/src/components/CustomModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CustomModal = ({ isOpen, onClose, children }: CustomFixedModalProps) => { 4 | if (!isOpen) return null; 5 | 6 | return ( 7 | <> 8 |
9 |
10 |
{children}
11 |
12 | 13 | ); 14 | }; 15 | 16 | export default CustomModal; 17 | -------------------------------------------------------------------------------- /client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |

© 2024 EDROH. All Rights Reserved.

8 |
9 | {["About", "Privacy Policy", "Licensing", "Contact"].map((item) => ( 10 | 16 | {item} 17 | 18 | ))} 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Footer; 25 | -------------------------------------------------------------------------------- /client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = ({ title, subtitle, rightElement }: HeaderProps) => { 4 | return ( 5 |
6 |
7 |

{title}

8 |

{subtitle}

9 |
10 | {rightElement &&
{rightElement}
} 11 |
12 | ); 13 | }; 14 | 15 | export default Header; 16 | -------------------------------------------------------------------------------- /client/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | import React from "react"; 3 | 4 | const Loading = () => { 5 | return ( 6 |
7 | 8 | Loading... 9 |
10 | ); 11 | }; 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /client/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignedIn, SignedOut, UserButton, useUser } from "@clerk/nextjs"; 4 | import { dark } from "@clerk/themes"; 5 | import { Bell, BookOpen } from "lucide-react"; 6 | import Link from "next/link"; 7 | import React, { useState } from "react"; 8 | import { SidebarTrigger } from "@/components/ui/sidebar"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | const Navbar = ({ isCoursePage }: { isCoursePage: boolean }) => { 12 | const { user } = useUser(); 13 | const userRole = user?.publicMetadata?.userType as "student" | "teacher"; 14 | 15 | return ( 16 | 63 | ); 64 | }; 65 | 66 | export default Navbar; 67 | -------------------------------------------------------------------------------- /client/src/components/NonDashboardNavbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignedIn, SignedOut, UserButton, useUser } from "@clerk/nextjs"; 4 | import { dark } from "@clerk/themes"; 5 | import { Bell, BookOpen } from "lucide-react"; 6 | import Link from "next/link"; 7 | import React from "react"; 8 | 9 | const NonDashboardNavbar = () => { 10 | const { user } = useUser(); 11 | const userRole = user?.publicMetadata?.userType as "student" | "teacher"; 12 | 13 | return ( 14 | 78 | ); 79 | }; 80 | 81 | export default NonDashboardNavbar; 82 | -------------------------------------------------------------------------------- /client/src/components/SharedNotificationSettings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | NotificationSettingsFormData, 5 | notificationSettingsSchema, 6 | } from "@/lib/schemas"; 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import { useUpdateUserMutation } from "@/state/api"; 9 | import { useUser } from "@clerk/nextjs"; 10 | import React from "react"; 11 | import { useForm } from "react-hook-form"; 12 | import Header from "./Header"; 13 | import { Form } from "@/components/ui/form"; 14 | import { CustomFormField } from "./CustomFormField"; 15 | import { Button } from "@/components/ui/button"; 16 | 17 | const SharedNotificationSettings = ({ 18 | title = "Notification Settings", 19 | subtitle = "Manage your notification settings", 20 | }: SharedNotificationSettingsProps) => { 21 | const { user } = useUser(); 22 | const [updateUser] = useUpdateUserMutation(); 23 | 24 | const currentSettings = 25 | (user?.publicMetadata as { settings?: UserSettings })?.settings || {}; 26 | 27 | const methods = useForm({ 28 | resolver: zodResolver(notificationSettingsSchema), 29 | defaultValues: { 30 | courseNotifications: currentSettings.courseNotifications || false, 31 | emailAlerts: currentSettings.emailAlerts || false, 32 | smsAlerts: currentSettings.smsAlerts || false, 33 | notificationFrequency: currentSettings.notificationFrequency || "daily", 34 | }, 35 | }); 36 | 37 | const onSubmit = async (data: NotificationSettingsFormData) => { 38 | if (!user) return; 39 | 40 | const updatedUser = { 41 | userId: user.id, 42 | publicMetadata: { 43 | ...user.publicMetadata, 44 | settings: { 45 | ...currentSettings, 46 | ...data, 47 | }, 48 | }, 49 | }; 50 | 51 | try { 52 | await updateUser(updatedUser); 53 | } catch (error) { 54 | console.error("Failed to update user settings: ", error); 55 | } 56 | }; 57 | 58 | if (!user) return
Please sign in to manage your settings.
; 59 | 60 | return ( 61 |
62 |
63 |
64 | 68 |
69 | 74 | 79 | 84 | 85 | 95 |
96 | 97 | 100 |
101 | 102 |
103 | ); 104 | }; 105 | 106 | export default SharedNotificationSettings; 107 | -------------------------------------------------------------------------------- /client/src/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignIn, useUser } from "@clerk/nextjs"; 4 | import React from "react"; 5 | import { dark } from "@clerk/themes"; 6 | import { useSearchParams } from "next/navigation"; 7 | 8 | const SignInComponent = () => { 9 | const { user } = useUser(); 10 | const searchParams = useSearchParams(); 11 | const isCheckoutPage = searchParams.get("showSignUp") !== null; 12 | const courseId = searchParams.get("id"); 13 | 14 | const signUpUrl = isCheckoutPage 15 | ? `/checkout?step=1&id=${courseId}&showSignUp=true` 16 | : "/signup"; 17 | 18 | const getRedirectUrl = () => { 19 | if (isCheckoutPage) { 20 | return `/checkout?step=2&id=${courseId}&showSignUp=true`; 21 | } 22 | 23 | const userType = user?.publicMetadata?.userType as string; 24 | if (userType === "teacher") { 25 | return "/teacher/courses"; 26 | } 27 | return "/user/courses"; 28 | }; 29 | 30 | return ( 31 | div > div:nth-child(1)": { 42 | background: "#25262F", 43 | }, 44 | }, 45 | formFieldLabel: "text-white-50 font-normal", 46 | formButtonPrimary: 47 | "bg-primary-700 text-white-100 hover:bg-primary-600 !shadow-none", 48 | formFieldInput: "bg-customgreys-primarybg text-white-50 !shadow-none", 49 | footerActionLink: "text-primary-750 hover:text-primary-600", 50 | }, 51 | }} 52 | signUpUrl={signUpUrl} 53 | forceRedirectUrl={getRedirectUrl()} 54 | routing="hash" 55 | afterSignOutUrl="/" 56 | /> 57 | ); 58 | }; 59 | 60 | export default SignInComponent; 61 | -------------------------------------------------------------------------------- /client/src/components/SignUp.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignUp, useUser } from "@clerk/nextjs"; 4 | import React from "react"; 5 | import { dark } from "@clerk/themes"; 6 | import { useSearchParams } from "next/navigation"; 7 | 8 | const SignUpComponent = () => { 9 | const { user } = useUser(); 10 | const searchParams = useSearchParams(); 11 | const isCheckoutPage = searchParams.get("showSignUp") !== null; 12 | const courseId = searchParams.get("id"); 13 | 14 | const signInUrl = isCheckoutPage 15 | ? `/checkout?step=1&id=${courseId}&showSignUp=false` 16 | : "/signin"; 17 | 18 | const getRedirectUrl = () => { 19 | if (isCheckoutPage) { 20 | return `/checkout?step=2&id=${courseId}&showSignUp=false`; 21 | } 22 | 23 | const userType = user?.publicMetadata?.userType as string; 24 | if (userType === "teacher") { 25 | return "/teacher/courses"; 26 | } 27 | return "/user/courses"; 28 | }; 29 | 30 | return ( 31 | div > div:nth-child(1)": { 42 | background: "#25262F", 43 | }, 44 | }, 45 | formFieldLabel: "text-white-50 font-normal", 46 | formButtonPrimary: 47 | "bg-primary-700 text-white-100 hover:bg-primary-600 !shadow-none", 48 | formFieldInput: "bg-customgreys-primarybg text-white-50 !shadow-none", 49 | footerActionLink: "text-primary-750 hover:text-primary-600", 50 | }, 51 | }} 52 | signInUrl={signInUrl} 53 | forceRedirectUrl={getRedirectUrl()} 54 | routing="hash" 55 | afterSignOutUrl="/" 56 | /> 57 | ); 58 | }; 59 | 60 | export default SignUpComponent; 61 | -------------------------------------------------------------------------------- /client/src/components/TeacherCourseCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardHeader, 7 | CardTitle, 8 | } from "@/components/ui/card"; 9 | import Image from "next/image"; 10 | import { cn } from "@/lib/utils"; 11 | import { Button } from "./ui/button"; 12 | import { Pencil, Trash2 } from "lucide-react"; 13 | 14 | const TeacherCourseCard = ({ 15 | course, 16 | onEdit, 17 | onDelete, 18 | isOwner, 19 | }: TeacherCourseCardProps) => { 20 | return ( 21 | 22 | 23 | {course.title} 31 | 32 | 33 | 34 |
35 | 36 | {course.title} 37 | 38 | 39 | 40 | {course.category} 41 | 42 | 43 |

44 | Status:{" "} 45 | 53 | {course.status} 54 | 55 |

56 | {course.enrollments && ( 57 |

58 | 59 | {course.enrollments.length} 60 | {" "} 61 | Student{course.enrollments.length > 1 ? "s" : ""} Enrolled 62 |

63 | )} 64 |
65 | 66 |
67 | {isOwner ? ( 68 | <> 69 |
70 | 77 |
78 |
79 | 86 |
87 | 88 | ) : ( 89 |

View Only

90 | )} 91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | export default TeacherCourseCard; 98 | -------------------------------------------------------------------------------- /client/src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from "@/components/ui/select"; 9 | import { courseCategories } from "@/lib/utils"; 10 | 11 | const Toolbar = ({ onSearch, onCategoryChange }: ToolbarProps) => { 12 | const [searchTerm, setSearchTerm] = useState(""); 13 | 14 | const handleSearch = (value: string) => { 15 | setSearchTerm(value); 16 | onSearch(value); 17 | }; 18 | 19 | return ( 20 |
21 | handleSearch(e.target.value)} 25 | placeholder="Search courses" 26 | className="toolbar__search" 27 | /> 28 | 47 |
48 | ); 49 | }; 50 | 51 | export default Toolbar; 52 | -------------------------------------------------------------------------------- /client/src/components/WizardStepper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Check } from "lucide-react"; 3 | import React from "react"; 4 | 5 | const WizardStepper = ({ currentStep }: WizardStepperProps) => { 6 | return ( 7 |
8 |
9 | {[1, 2, 3].map((step, index) => ( 10 | 11 |
12 |
step || (currentStep === 3 && step === 3), 16 | "wizard-stepper__circle--current": 17 | currentStep === step && step !== 3, 18 | "wizard-stepper__circle--upcoming": currentStep < step, 19 | })} 20 | > 21 | {currentStep > step || (currentStep === 3 && step === 3) ? ( 22 | 23 | ) : ( 24 | {step} 25 | )} 26 |
27 |

= step, 30 | "wizard-stepper__text--inactive": currentStep < step, 31 | })} 32 | > 33 | {step === 1 && "Details"} 34 | {step === 2 && "Payment"} 35 | {step === 3 && "Completion"} 36 |

37 |
38 | {index < 2 && ( 39 |
step, 42 | "wizard-stepper__line--incomplete": currentStep <= step, 43 | })} 44 | /> 45 | )} 46 | 47 | ))} 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default WizardStepper; 54 | -------------------------------------------------------------------------------- /client/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /client/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /client/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /client/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /client/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /client/src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |