├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── emails ├── CourseCompletionEmail.tsx ├── CourseEnrollmentEmail.tsx └── WelcomeToLMS.tsx ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240707222251_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── banner.png ├── certificate-background.png ├── footer.png ├── icon-author.png ├── icon-certificate.png ├── icon-chapters.png ├── icon-community.png ├── icon-course.png ├── icon-progress.png ├── icon-support.png ├── image.png ├── next.svg ├── signature.jpg ├── user.png └── vercel.svg ├── scripts └── seed.js ├── src ├── app │ ├── (common) │ │ ├── (teacher) │ │ │ ├── course-studio │ │ │ │ ├── [courseId] │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── ActionsField.tsx │ │ │ │ │ │ └── StudentsColumn.tsx │ │ │ │ │ ├── actions.ts │ │ │ │ │ └── users │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── ActionsField.tsx │ │ │ │ │ ├── Column.tsx │ │ │ │ │ ├── DataTable.tsx │ │ │ │ │ └── DataTableForStudents.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── create │ │ │ │ ├── course │ │ │ │ └── [courseId] │ │ │ │ │ ├── _components │ │ │ │ │ ├── CategoryField.tsx │ │ │ │ │ ├── ChaptersArea.tsx │ │ │ │ │ ├── ChaptersField.tsx │ │ │ │ │ ├── CourseFreeField.tsx │ │ │ │ │ ├── DescriptionField.tsx │ │ │ │ │ ├── DurationField.tsx │ │ │ │ │ ├── PublishField.tsx │ │ │ │ │ ├── ThumbnailField.tsx │ │ │ │ │ └── TitleField.tsx │ │ │ │ │ ├── chapter │ │ │ │ │ └── [chapterId] │ │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── AttachmentsField.tsx │ │ │ │ │ │ ├── BackField.tsx │ │ │ │ │ │ ├── ChapterAccessField.tsx │ │ │ │ │ │ ├── ContentField.tsx │ │ │ │ │ │ ├── PublishField.tsx │ │ │ │ │ │ ├── TitleField.tsx │ │ │ │ │ │ └── VideoField.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── certificate │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ └── _components │ │ │ │ └── CertificateGenerator.tsx │ │ ├── certificates │ │ │ ├── _components │ │ │ │ └── Certificates.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── checkout │ │ │ ├── _components │ │ │ │ ├── FormForCheckOut.tsx │ │ │ │ └── RightPart.tsx │ │ │ ├── actions.ts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── courses │ │ │ ├── _components │ │ │ │ ├── Categories.tsx │ │ │ │ ├── CourseCard.tsx │ │ │ │ ├── Courses.tsx │ │ │ │ └── SearchInput.tsx │ │ │ ├── actions.ts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── _components │ │ │ │ └── CourseCard.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── preview │ │ │ ├── _components │ │ │ │ └── CoursePreviewPage.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── profile │ │ │ ├── [profileId] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── settings │ │ │ ├── _components │ │ │ └── SettingsField.tsx │ │ │ ├── actions.ts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── razorpay │ │ │ └── route.ts │ │ ├── student │ │ │ ├── request │ │ │ │ └── route.ts │ │ │ └── update │ │ │ │ ├── chapter │ │ │ │ └── [chapterId] │ │ │ │ │ └── route.ts │ │ │ │ └── settings │ │ │ │ └── route.ts │ │ ├── teacher │ │ │ ├── add-to-course │ │ │ │ └── route.ts │ │ │ ├── add-users │ │ │ │ └── route.ts │ │ │ ├── create │ │ │ │ └── route.ts │ │ │ ├── queries │ │ │ │ └── [queryId] │ │ │ │ │ └── route.ts │ │ │ └── update │ │ │ │ └── [courseId] │ │ │ │ ├── chapter │ │ │ │ ├── [chapterId] │ │ │ │ │ ├── checking │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── users │ │ │ │ └── route.ts │ │ ├── uploadthing │ │ │ ├── core.ts │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── auth │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── course │ │ ├── [courseId] │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── _components │ │ │ ├── ChapterBanner.tsx │ │ │ ├── ChapterButton.tsx │ │ │ ├── LeftPart.tsx │ │ │ ├── MobileLeftPart.tsx │ │ │ ├── PreviewForChapter.tsx │ │ │ ├── RightPart.tsx │ │ │ └── VideoPlayer.tsx │ │ └── action.ts │ ├── custom.css │ ├── demo.js │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── not-found.tsx │ ├── onboarding │ │ ├── _components │ │ │ └── OnboardingForm.tsx │ │ ├── actions.ts │ │ ├── loading.tsx │ │ └── page.tsx │ └── page.tsx ├── components │ ├── Banner.tsx │ ├── CreateCourseForm.tsx │ ├── Editor.tsx │ ├── Fileupload.tsx │ ├── GridPattern.tsx │ ├── GroupButtons.tsx │ ├── Loading.tsx │ ├── MobileSideBar.tsx │ ├── Navbar.tsx │ ├── NewCourseButton.tsx │ ├── Preview.tsx │ ├── Sidebar.tsx │ ├── SidebarItem.tsx │ ├── SidebarWraper.tsx │ ├── Sliders.tsx │ ├── ThemeSwitch.tsx │ ├── magicui │ │ ├── animated-grid-pattern.tsx │ │ ├── animated-shiny-text.tsx │ │ ├── cool-mode.tsx │ │ ├── gradual-spacing.tsx │ │ ├── letter-pullup.tsx │ │ ├── magic-card.tsx │ │ ├── marquee.tsx │ │ ├── number-ticker.tsx │ │ └── sparkles-text.tsx │ ├── starter │ │ └── Navbar.tsx │ └── ui │ │ ├── badge.tsx │ │ ├── bento-grid.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── flip-words.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── lamp.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── text-generate-effect.tsx │ │ └── textarea.tsx ├── lib │ ├── db.ts │ ├── uploadthing.ts │ └── utils.ts ├── middleware.ts ├── providers │ ├── context-provider.tsx │ ├── theme-provider.tsx │ └── toast-provider.tsx ├── schema │ └── zod-schemes.ts ├── styles │ └── canvas.css ├── templates │ ├── CourseEnrollmentEmail.tsx │ └── WelcomeToLMS.tsx └── types │ └── general-types.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= 3 | 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/auth/sign-in 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/auth/sign-up 6 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/courses 7 | PRIVATE_ID= 8 | 9 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/lms?schema=public 10 | 11 | 12 | UPLOADTHING_SECRET= 13 | UPLOADTHING_APP_ID= 14 | 15 | MAIL_USER= 16 | MAIL_PASS= 17 | 18 | RAZORPAY_KEY= 19 | RAZORPAY_SECRET= 20 | 21 | BASE_URL=http://localhost:3000 22 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "magicui": "@/components/magicui" 18 | } 19 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | hostname: "utfs.io", 7 | }, 8 | { 9 | hostname: "img.clerk.com", 10 | }, 11 | { 12 | hostname: "localhost", 13 | }, 14 | { 15 | hostname: "assets.aceternity.com", 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lms", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "email": "email dev", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "postinstall": "prisma generate" 12 | }, 13 | "dependencies": { 14 | "@ckeditor/ckeditor5-react": "^8.0.0", 15 | "@clerk/nextjs": "^5.1.6", 16 | "@clerk/themes": "^2.1.10", 17 | "@hello-pangea/dnd": "^16.6.0", 18 | "@hookform/resolvers": "^3.6.0", 19 | "@mux/mux-node": "^8.8.0", 20 | "@mux/mux-player-react": "^2.7.0", 21 | "@prisma/client": "5.16.1", 22 | "@radix-ui/react-checkbox": "^1.1.1", 23 | "@radix-ui/react-dialog": "^1.1.1", 24 | "@radix-ui/react-dropdown-menu": "^2.1.1", 25 | "@radix-ui/react-icons": "^1.3.0", 26 | "@radix-ui/react-label": "^2.1.0", 27 | "@radix-ui/react-popover": "^1.1.1", 28 | "@radix-ui/react-progress": "^1.1.0", 29 | "@radix-ui/react-radio-group": "^1.2.0", 30 | "@radix-ui/react-select": "^2.1.1", 31 | "@radix-ui/react-slot": "^1.1.0", 32 | "@radix-ui/react-tabs": "^1.1.0", 33 | "@react-email/components": "^0.0.20", 34 | "@react-email/render": "0.0.16", 35 | "@react-pdf/renderer": "^3.4.4", 36 | "@tabler/icons-react": "^3.11.0", 37 | "@tanstack/react-table": "^8.19.2", 38 | "@types/jspdf": "^2.0.0", 39 | "@uploadthing/react": "^6.7.2", 40 | "axios": "^1.7.2", 41 | "canvas-confetti": "^1.9.3", 42 | "ckeditor5": "^42.0.1", 43 | "ckeditor5-premium-features": "^42.0.1", 44 | "class-variance-authority": "^0.7.0", 45 | "clsx": "^2.1.1", 46 | "cmdk": "^1.0.0", 47 | "date-fns": "^3.6.0", 48 | "framer-motion": "^11.2.13", 49 | "generate-password-ts": "^1.6.5", 50 | "highlight.js": "^11.10.0", 51 | "html2canvas": "^1.4.1", 52 | "jspdf": "^2.5.1", 53 | "lodash": "^4.17.21", 54 | "lucide-react": "^0.399.0", 55 | "next": "14.2.4", 56 | "next-themes": "^0.3.0", 57 | "next-video": "^1.1.1", 58 | "nextjs-toploader": "^1.6.12", 59 | "nodemailer": "^6.9.14", 60 | "quill": "^2.0.2", 61 | "razorpay": "^2.9.4", 62 | "react": "^18", 63 | "react-dom": "^18", 64 | "react-email": "2.1.5", 65 | "react-hook-form": "^7.52.0", 66 | "react-icons": "^5.2.1", 67 | "react-quill": "^2.0.0", 68 | "react-quilljs": "^2.0.3", 69 | "react-to-print": "^2.15.1", 70 | "resend": "^3.4.0", 71 | "sonner": "^1.5.0", 72 | "svix": "^1.25.0", 73 | "tailwind-merge": "^2.3.0", 74 | "tailwindcss-animate": "^1.0.7", 75 | "uploadthing": "^6.13.2", 76 | "video-react": "^0.16.0", 77 | "zod": "^3.23.8" 78 | }, 79 | "devDependencies": { 80 | "@types/canvas-confetti": "^1.6.4", 81 | "@types/lodash": "^4.17.7", 82 | "@types/node": "^20", 83 | "@types/nodemailer": "^6.4.15", 84 | "@types/react": "^18", 85 | "@types/react-dom": "^18", 86 | "@types/video-react": "^0.15.8", 87 | "eslint": "^8", 88 | "eslint-config-next": "14.2.4", 89 | "postcss": "^8", 90 | "prisma": "^5.16.1", 91 | "tailwindcss": "^3.4.1", 92 | "typescript": "^5" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/banner.png -------------------------------------------------------------------------------- /public/certificate-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/certificate-background.png -------------------------------------------------------------------------------- /public/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/footer.png -------------------------------------------------------------------------------- /public/icon-author.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-author.png -------------------------------------------------------------------------------- /public/icon-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-certificate.png -------------------------------------------------------------------------------- /public/icon-chapters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-chapters.png -------------------------------------------------------------------------------- /public/icon-community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-community.png -------------------------------------------------------------------------------- /public/icon-course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-course.png -------------------------------------------------------------------------------- /public/icon-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-progress.png -------------------------------------------------------------------------------- /public/icon-support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/icon-support.png -------------------------------------------------------------------------------- /public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/image.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/signature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/signature.jpg -------------------------------------------------------------------------------- /public/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/public/user.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/seed.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require("@prisma/client"); 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | async function CategoriesGenerator() { 6 | const categories = [ 7 | { title: "Web Development" }, 8 | { title: "Mobile App Development" }, 9 | { title: "Data Science" }, 10 | { title: "Machine Learning" }, 11 | { title: "Artificial Intelligence" }, 12 | { title: "Cloud Computing" }, 13 | { title: "Cybersecurity" }, 14 | { title: "DevOps" }, 15 | { title: "Blockchain" }, 16 | { title: "Digital Marketing" }, 17 | { title: "Graphic Design" }, 18 | { title: "UX/UI Design" }, 19 | { title: "Business Analytics" }, 20 | { title: "Project Management" }, 21 | { title: "Photography" }, 22 | { title: "Music Production" }, 23 | { title: "Language Learning" }, 24 | { title: "Personal Development" }, 25 | { title: "Fitness and Health" }, 26 | { title: "Cooking and Nutrition" }, 27 | ]; 28 | 29 | for (const category of categories) { 30 | await prisma.category.create({ 31 | data: category, 32 | }); 33 | } 34 | 35 | console.log("Categories seeded successfully"); 36 | } 37 | 38 | async function GoalsGenerator() { 39 | const goals = [ 40 | "Career Advancement", 41 | "Personal Interest", 42 | "Academic Requirements", 43 | "Skill Development", 44 | "Certification", 45 | "Starting a Business", 46 | ]; 47 | 48 | for (const goal of goals) { 49 | await prisma.goal.create({ 50 | data: { 51 | title: goal, 52 | }, 53 | }); 54 | } 55 | 56 | console.log("goals seeded successfully"); 57 | } 58 | 59 | GoalsGenerator() 60 | .catch((e) => { 61 | console.error(e); 62 | process.exit(1); 63 | }) 64 | .finally(async () => { 65 | await prisma.$disconnect(); 66 | }); 67 | 68 | CategoriesGenerator() 69 | .catch((e) => { 70 | console.error(e); 71 | process.exit(1); 72 | }) 73 | .finally(async () => { 74 | await prisma.$disconnect(); 75 | }); 76 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/[courseId]/_components/ActionsField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import { updateTheField } from "@/lib/utils"; 10 | import { MoreHorizontal, Trash2 } from "lucide-react"; 11 | import { useRouter } from "next/navigation"; 12 | import { StudentsForCourseType } from "../actions"; 13 | 14 | interface ActionsFieldProps { 15 | student: StudentsForCourseType; 16 | } 17 | const ActionsField = ({ student }: ActionsFieldProps) => { 18 | const router = useRouter(); 19 | 20 | const removeUserFromCourse = async () => { 21 | await updateTheField( 22 | { 23 | id: student.userId, 24 | }, 25 | `/api/teacher/update/${student.courseId}/users`, 26 | "PUT" 27 | ); 28 | router.refresh(); 29 | }; 30 | 31 | return ( 32 | 35 | ); 36 | }; 37 | 38 | export default ActionsField; 39 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/[courseId]/_components/StudentsColumn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { StudentsForCourseType } from "../actions"; 5 | import ActionsField from "./ActionsField"; 6 | import { ArrowUpDown } from "lucide-react"; 7 | 8 | export const StudentsColumn: ColumnDef[] = [ 9 | { 10 | accessorKey: "name", 11 | header: ({ column }) => { 12 | return ( 13 |
column.toggleSorting(column.getIsSorted() === "asc")} 16 | > 17 | Name 18 | 19 |
20 | ); 21 | }, 22 | }, 23 | { 24 | accessorKey: "email", 25 | header: ({ column }) => { 26 | return ( 27 |
column.toggleSorting(column.getIsSorted() === "asc")} 30 | > 31 | Email 32 | 33 |
34 | ); 35 | }, 36 | }, 37 | { 38 | accessorKey: "chapters", 39 | header: ({ column }) => { 40 | return ( 41 |
column.toggleSorting(column.getIsSorted() === "asc")} 44 | > 45 | Chapters 46 | 47 |
48 | ); 49 | }, 50 | }, 51 | { 52 | accessorKey: "progress", 53 | header: ({ column }) => { 54 | return ( 55 |
column.toggleSorting(column.getIsSorted() === "asc")} 58 | > 59 | Progress 60 | 61 |
62 | ); 63 | }, 64 | }, 65 | { 66 | accessorKey: "accessDate", 67 | header: ({ column }) => { 68 | return ( 69 |
column.toggleSorting(column.getIsSorted() === "asc")} 72 | > 73 | Access Date 74 | 75 |
76 | ); 77 | }, 78 | cell: ({ row }) => { 79 | return new Date(row.original.accessDate).toLocaleDateString(); 80 | }, 81 | }, 82 | // { 83 | // id: "actions", 84 | // cell: ({ row: { original } }) => { 85 | // return ; 86 | // }, 87 | // }, 88 | ]; 89 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/[courseId]/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { prisma } from "@/lib/db"; 4 | import { auth } from "@clerk/nextjs/server"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export const getStudentsForCourse = async (courseId: string) => { 8 | const { userId } = auth(); 9 | if (!userId) { 10 | redirect("/"); 11 | } 12 | const user = await prisma.user.findUnique({ 13 | where: { 14 | authId: userId, 15 | }, 16 | }); 17 | 18 | if (!user) { 19 | redirect("/"); 20 | } 21 | 22 | const course = await prisma.course.findUnique({ 23 | where: { 24 | id: courseId, 25 | userId: user.id, 26 | }, 27 | include: { 28 | _count: { 29 | select: { 30 | chapters: true, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | if (!course) { 37 | redirect("/"); 38 | } 39 | 40 | const data = await prisma.access.findMany({ 41 | where: { 42 | courseId: courseId, 43 | }, 44 | include: { 45 | user: { 46 | select: { 47 | name: true, 48 | _count: { 49 | select: { 50 | progress: { 51 | where: { 52 | status: "COMPLETED", 53 | }, 54 | }, 55 | }, 56 | }, 57 | email: true, 58 | }, 59 | }, 60 | }, 61 | orderBy: { 62 | createdAt: "desc", 63 | }, 64 | }); 65 | const filteredData = data.map((access) => { 66 | return { 67 | name: access.user.name || "No Name", 68 | email: access.user.email, 69 | chapters: course._count.chapters, 70 | progress: access.user._count.progress, 71 | accessDate: access.createdAt, 72 | courseId: courseId, 73 | userId: access.userId, 74 | }; 75 | }); 76 | 77 | return filteredData; 78 | }; 79 | 80 | export type StudentsForCourseType = Awaited< 81 | ReturnType 82 | >[0]; 83 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/[courseId]/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/db"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import { redirect } from "next/navigation"; 4 | import React from "react"; 5 | import { DataTable } from "../../_components/DataTable"; 6 | import { StudentsColumn } from "../_components/StudentsColumn"; 7 | import { getStudentsForCourse } from "../actions"; 8 | import { DataTableForStudents } from "../../_components/DataTableForStudents"; 9 | 10 | const page = async ({ params }: { params: { courseId: string } }) => { 11 | if (!params.courseId) 12 | return
Invalid Course Id
; 13 | const { userId } = auth(); 14 | if (!userId) redirect("/"); 15 | const user = await prisma.user.findUnique({ 16 | where: { 17 | authId: userId, 18 | }, 19 | }); 20 | if (!user) redirect("/not-allowed"); 21 | if (user.role !== "TEACHER") redirect("/not-authenticated"); 22 | const courseId = params.courseId; 23 | const course = await prisma.course.findUnique({ 24 | where: { 25 | id: courseId, 26 | userId: user.id, 27 | }, 28 | }); 29 | if (!course) 30 | return
Course not found
; 31 | const data = await getStudentsForCourse(courseId); 32 | return ( 33 |
34 |
35 |
36 |

37 | Students in {`${course.title}`} 38 |

39 |

40 | (Students who are currently following this course) 41 |

42 |
43 |
44 | 45 |
46 | ); 47 | }; 48 | 49 | export default page; 50 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/_components/ActionsField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import { updateTheField } from "@/lib/utils"; 10 | import { MoreHorizontal } from "lucide-react"; 11 | import { useRouter } from "next/navigation"; 12 | import { TeachersPublishedCoursesType } from "../actions"; 13 | import { toast } from "sonner"; 14 | 15 | interface ActionsFieldProps { 16 | chapter: TeachersPublishedCoursesType; 17 | } 18 | const ActionsField = ({ chapter }: ActionsFieldProps) => { 19 | const router = useRouter(); 20 | 21 | const pushToEditScreen = (courseId: string) => { 22 | toast.loading("loading course...", { 23 | id: "loading-course", 24 | }); 25 | router.push(`/create/course/${courseId}`); 26 | toast.success("Course loaded", { 27 | id: "loading-course", 28 | }); 29 | }; 30 | 31 | const pushToUsersScreen = (courseId: string) => { 32 | toast.loading("loading users...", { 33 | id: "loading-users", 34 | }); 35 | router.push(`course-studio/${courseId}/users`); 36 | toast.success("Users loaded", { 37 | id: "loading-users", 38 | }); 39 | }; 40 | 41 | const deleteCourse = async (courseId: string) => { 42 | await updateTheField({}, `/api/teacher/update/${courseId}`, "DELETE"); 43 | router.refresh(); 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 53 | 54 | 55 | pushToEditScreen(chapter.id)}> 56 | Edit 57 | 58 | pushToUsersScreen(chapter.id)}> 59 | Users 60 | 61 | deleteCourse(chapter.id)}> 62 | Delete 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default ActionsField; 70 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/_components/Column.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table"; 4 | import { TeachersPublishedCoursesType } from "../actions"; 5 | import ActionsField from "./ActionsField"; 6 | import { ArrowUpDown } from "lucide-react"; 7 | import { Button } from "@/components/ui/button"; 8 | export const columns: ColumnDef[] = [ 9 | { 10 | accessorKey: "title", 11 | header: ({ column }) => { 12 | return ( 13 |
column.toggleSorting(column.getIsSorted() === "asc")} 16 | > 17 | Title 18 | 19 |
20 | ); 21 | }, 22 | }, 23 | { 24 | accessorKey: "price", 25 | header: ({ column }) => { 26 | return ( 27 |
column.toggleSorting(column.getIsSorted() === "asc")} 30 | > 31 | Price 32 | 33 |
34 | ); 35 | }, 36 | }, 37 | { 38 | accessorKey: "category", 39 | header: "Category", 40 | cell: ({ row: { original } }) => { 41 | return original.category?.title || "No Category"; 42 | }, 43 | }, 44 | { 45 | accessorKey: "isPublished", 46 | header: "Published", 47 | cell: ({ row: { original } }) => { 48 | return original.isPublished ? "Published" : "UnPublished"; 49 | }, 50 | }, 51 | { 52 | accessorKey: "isFree", 53 | header: "Free", 54 | cell: ({ row: { original } }) => { 55 | return original.isFree ? "Free" : "Paid"; 56 | }, 57 | }, 58 | { 59 | accessorKey: "users", 60 | header: ({ column }) => { 61 | return ( 62 |
column.toggleSorting(column.getIsSorted() === "asc")} 65 | > 66 | Users 67 | 68 |
69 | ); 70 | }, 71 | }, 72 | { 73 | accessorKey: "chapters", 74 | header: ({ column }) => { 75 | return ( 76 |
column.toggleSorting(column.getIsSorted() === "asc")} 79 | > 80 | Chapters 81 | 82 |
83 | ); 84 | }, 85 | }, 86 | { 87 | id: "actions", 88 | cell: ({ row: { original } }) => { 89 | return ; 90 | }, 91 | }, 92 | ]; 93 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { prisma } from "@/lib/db"; 4 | import { auth } from "@clerk/nextjs/server"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export const getTeacherPublichedCourses = async () => { 8 | const { userId } = auth(); 9 | if (!userId) { 10 | redirect("/"); 11 | } 12 | const user = await prisma.user.findUnique({ 13 | where: { 14 | authId: userId, 15 | }, 16 | }); 17 | 18 | if (!user) { 19 | redirect("/"); 20 | } 21 | 22 | const courses = await prisma.course.findMany({ 23 | where: { 24 | userId: user.id, 25 | }, 26 | select: { 27 | _count: { 28 | select: { 29 | accesses: true, 30 | chapters: true, 31 | }, 32 | }, 33 | id: true, 34 | title: true, 35 | price: true, 36 | isPublished: true, 37 | category: true, 38 | isFree: true, 39 | }, 40 | orderBy: { 41 | createdAt: "desc", 42 | }, 43 | }); 44 | 45 | const filteredData = courses.map((course) => { 46 | return { 47 | ...course, 48 | users: course._count.accesses, 49 | chapters: course._count.chapters, 50 | }; 51 | }); 52 | return filteredData; 53 | }; 54 | 55 | export type TeachersPublishedCoursesType = Omit< 56 | Awaited>[0], 57 | "_count" 58 | >; 59 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import React from "react"; 3 | 4 | const loading = () => { 5 | return ( 6 |
7 |
8 |
9 |

Dashboard

10 |

11 | (Manage your courses here) 12 |

13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 |
21 | {[1, 2, 3, 4].map((access) => ( 22 | 23 | ))} 24 |
25 |
26 | 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default loading; 34 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/course-studio/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewCourseButton } from "@/components/NewCourseButton"; 2 | import React from "react"; 3 | import { DataTable } from "./_components/DataTable"; 4 | import { columns } from "./_components/Column"; 5 | import { getTeacherPublichedCourses } from "./actions"; 6 | import { auth } from "@clerk/nextjs/server"; 7 | import { redirect } from "next/navigation"; 8 | import { prisma } from "@/lib/db"; 9 | import Loading from "@/components/Loading"; 10 | import { Skeleton } from "@/components/ui/skeleton"; 11 | 12 | const page = async () => { 13 | const { userId } = auth(); 14 | if (!userId) { 15 | redirect("/"); 16 | } 17 | const user = await prisma.user.findUnique({ 18 | where: { 19 | authId: userId, 20 | }, 21 | }); 22 | if (!user) { 23 | redirect("/not-allowed"); 24 | } 25 | 26 | if (user.role !== "TEACHER") { 27 | redirect("/not-authenticated"); 28 | } 29 | 30 | const data = await getTeacherPublichedCourses(); 31 | 32 | if (!data) { 33 | return ( 34 |
35 |
36 |
37 |

Dashboard

38 |

39 | (Manage your courses here) 40 |

41 |
42 |
43 | 44 |
45 |
46 | 47 | 48 |
49 | {[1, 2, 3, 4].map((access) => ( 50 | 51 | ))} 52 |
53 |
54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | return ( 61 |
62 |
63 |
64 |

Dashboard

65 |

66 | (Manage your courses here) 67 |

68 |
69 | 70 |
71 | 72 |
73 | ); 74 | }; 75 | 76 | export default page; 77 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/create/course/[courseId]/_components/CourseFreeField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | import { Checkbox } from "@/components/ui/checkbox"; 6 | 7 | import { useRouter } from "next/navigation"; 8 | import { currencyFormater, updateTheField } from "@/lib/utils"; 9 | import { toast } from "sonner"; 10 | import { Input } from "@/components/ui/input"; 11 | 12 | interface CourseFreeFieldProps { 13 | isFree: boolean; 14 | price: number; 15 | courseId: string; 16 | } 17 | 18 | const CourseFreeField = ({ courseId, isFree, price }: CourseFreeFieldProps) => { 19 | const router = useRouter(); 20 | 21 | async function onSubmit() { 22 | if ((!selected && cost > 0) || selected) { 23 | await updateTheField( 24 | { isFree: selected, price: cost }, 25 | `/api/teacher/update/${courseId}`, 26 | "PUT" 27 | ); 28 | setEdit((prev) => !prev); 29 | router.refresh(); 30 | } else { 31 | toast.error("Price should be greater than 0", { 32 | id: "course-update", 33 | }); 34 | } 35 | } 36 | const [edit, setEdit] = React.useState(false); 37 | const [selected, setSelected] = React.useState(isFree); 38 | const [cost, setCost] = React.useState(price); 39 | return ( 40 |
41 |
42 |

43 | Course price * 44 |

45 | 48 |
49 | 50 | {edit ? ( 51 |
52 |
setSelected((d) => !d)} 55 | > 56 | setSelected((d) => !d)} 59 | /> 60 | free 61 |
62 | {!selected && ( 63 | setCost(Number(e.target.value))} 67 | placeholder="Enter the price of the course" 68 | /> 69 | )} 70 |
71 | 74 |
75 | {/*
76 |

77 | Note: You can't change this option once you make the course 78 | published 79 |

80 |
*/} 81 |
82 | ) : ( 83 |

84 | {selected 85 | ? "Course is free for all" 86 | : cost <= 0 87 | ? "Please set the price for the course" 88 | : `This course costs Rs. ${currencyFormater(cost)}`} 89 |

90 | )} 91 |
92 | ); 93 | }; 94 | 95 | export default CourseFreeField; 96 | -------------------------------------------------------------------------------- /src/app/(common)/(teacher)/create/course/[courseId]/_components/DescriptionField.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { z } from "zod"; 6 | import { descriptionSchema, titleSchema } from "@/schema/zod-schemes"; 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | Form, 10 | FormControl, 11 | FormDescription, 12 | FormField, 13 | FormItem, 14 | FormLabel, 15 | FormMessage, 16 | } from "@/components/ui/form"; 17 | import { updateTheField } from "@/lib/utils"; 18 | import { useRouter } from "next/navigation"; 19 | import { Textarea } from "@/components/ui/textarea"; 20 | 21 | interface DescriptionFieldProps { 22 | description: string; 23 | courseId: string; 24 | } 25 | 26 | const DescriptionField = ({ courseId, description }: DescriptionFieldProps) => { 27 | const router = useRouter(); 28 | const form = useForm>({ 29 | resolver: zodResolver(descriptionSchema), 30 | defaultValues: { 31 | description: description ? description : "", 32 | }, 33 | }); 34 | const { 35 | formState: { isSubmitting, isValid }, 36 | } = form; 37 | 38 | async function onSubmit(values: z.infer) { 39 | await updateTheField(values, `/api/teacher/update/${courseId}`); 40 | setEdit((prev) => !prev); 41 | router.refresh(); 42 | } 43 | const [edit, setEdit] = React.useState(false); 44 | return ( 45 |
46 |
47 |

48 | Description * 49 |

50 | 53 |
54 | 55 | {edit ? ( 56 |
57 | 58 | ( 62 | 63 | 64 |