├── .eslintrc.json ├── public ├── favicon.ico ├── img │ ├── solid.jpg │ ├── background.jpg │ └── background_flip.png ├── fonts │ └── font.woff2 ├── vercel.svg ├── logo.svg ├── google.svg └── next.svg ├── postcss.config.js ├── components ├── providers │ ├── session-provider.tsx │ ├── toaster-provider.tsx │ ├── theme-provider.tsx │ └── confetti-provider.tsx ├── preview.tsx ├── editor.tsx ├── file-upload.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── tag-list.tsx │ ├── checkbox.tsx │ ├── tag-popover.tsx │ ├── badge.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── avatar.tsx │ ├── auto-complete.tsx │ ├── button.tsx │ ├── card.tsx │ ├── combobox.tsx │ └── table.tsx ├── select.tsx ├── course-progress.tsx ├── banner.tsx ├── modals │ └── confirm-modal.tsx ├── courses-list.tsx ├── mode-toggle.tsx ├── search-input.tsx ├── icon-badge.tsx ├── navbar-routes.tsx └── course-card.tsx ├── prisma └── migrations │ └── migration_lock.toml ├── app ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── register │ │ │ └── route.ts │ ├── uploadthing │ │ ├── route.ts │ │ └── core.ts │ ├── courses │ │ ├── [courseId] │ │ │ ├── enroll │ │ │ │ └── route.ts │ │ │ ├── chapters │ │ │ │ ├── [chapterId] │ │ │ │ │ ├── progress │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── unpublish │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── publish │ │ │ │ │ │ └── route.ts │ │ │ │ ├── reorder │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── unpublish │ │ │ │ └── route.ts │ │ │ ├── comment │ │ │ │ ├── like │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── attachments │ │ │ │ ├── [attachmentId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── publish │ │ │ │ └── route.ts │ │ │ ├── categories │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ ├── users │ │ └── change │ │ │ ├── image │ │ │ └── route.ts │ │ │ ├── password │ │ │ └── route.ts │ │ │ └── role │ │ │ └── route.ts │ └── enroll │ │ └── [enrollId] │ │ └── route.ts ├── auth │ ├── _components │ │ ├── logo.tsx │ │ └── navbar.tsx │ ├── register │ │ └── page.tsx │ └── signin │ │ └── page.tsx ├── (dashboard) │ ├── _components │ │ ├── navbar.tsx │ │ ├── logo.tsx │ │ ├── sidebar.tsx │ │ ├── mobile-sidebar.tsx │ │ ├── sidebar-routes.tsx │ │ └── sidebar-item.tsx │ ├── (routes) │ │ ├── search │ │ │ ├── overview │ │ │ │ └── [courseId] │ │ │ │ │ ├── _components │ │ │ │ │ ├── thumbnail.tsx │ │ │ │ │ ├── forum-button.tsx │ │ │ │ │ ├── view.tsx │ │ │ │ │ ├── coures-description.tsx │ │ │ │ │ └── enrollment.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── _components │ │ │ │ ├── categories.tsx │ │ │ │ └── category-item.tsx │ │ │ └── page.tsx │ │ ├── teacher │ │ │ ├── layout.tsx │ │ │ ├── analytics │ │ │ │ ├── _components │ │ │ │ │ ├── data-card.tsx │ │ │ │ │ └── chart.tsx │ │ │ │ └── page.tsx │ │ │ ├── courses │ │ │ │ ├── page.tsx │ │ │ │ ├── [courseId] │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── actions.tsx │ │ │ │ │ │ ├── image-form.tsx │ │ │ │ │ │ └── title-form.tsx │ │ │ │ │ └── chapters │ │ │ │ │ │ └── [chapterId] │ │ │ │ │ │ └── _components │ │ │ │ │ │ ├── chapter-actions.tsx │ │ │ │ │ │ ├── chapter-title-form.tsx │ │ │ │ │ │ └── chapter-video-form.tsx │ │ │ │ └── _components │ │ │ │ │ └── columns.tsx │ │ │ └── enrollment │ │ │ │ └── page.tsx │ │ ├── admin │ │ │ ├── layout.tsx │ │ │ └── users │ │ │ │ └── page.tsx │ │ └── (root) │ │ │ ├── _components │ │ │ └── info-card.tsx │ │ │ └── page.tsx │ └── layout.tsx ├── profile │ ├── layout.tsx │ ├── page.tsx │ └── _components │ │ └── navbar.tsx ├── (course) │ └── courses │ │ └── [courseId] │ │ ├── forum │ │ ├── _components │ │ │ ├── comment-list.tsx │ │ │ ├── forum.tsx │ │ │ ├── icon-btn.tsx │ │ │ ├── _contexts │ │ │ │ └── forum-context.tsx │ │ │ └── comment-form.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── _components │ │ ├── course-navbar.tsx │ │ ├── course-mobile-sidebar.tsx │ │ ├── course-sidebar.tsx │ │ └── course-sidebar-item.tsx │ │ └── chapters │ │ └── [chapterId] │ │ ├── _components │ │ ├── course-enroll-button.tsx │ │ ├── course-progress-button.tsx │ │ └── video-player.tsx │ │ └── layout.tsx ├── denied │ └── page.tsx ├── layout.tsx └── globals.css ├── lib ├── uploadthing.ts ├── db.ts ├── admin.ts ├── teacher.ts ├── constant.ts ├── utils.ts └── auth.ts ├── .env.example ├── hooks ├── use-confetti-store.ts └── use-debounce.ts ├── next.config.js ├── actions ├── get-categories.ts ├── get-current-course.ts ├── get-enroll.ts ├── get-all-users.tsx ├── get-courses-created.ts ├── get-current-chapter.ts ├── get-redirect-course.ts ├── get-course-with-progress.ts ├── get-creating-course.ts ├── get-progress.tsx ├── get-overview-course.ts ├── get-waitlist.ts ├── get-comment.tsx ├── get-analytic.ts ├── get-dashboard-courses.ts └── get-chapter.ts ├── components.json ├── next-auth.d.ts ├── .gitignore ├── scripts └── seed.ts ├── tsconfig.json ├── LICENSE ├── middleware.ts ├── README.md ├── package.json └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgquannn/CourseX/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/solid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgquannn/CourseX/HEAD/public/img/solid.jpg -------------------------------------------------------------------------------- /public/fonts/font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgquannn/CourseX/HEAD/public/fonts/font.woff2 -------------------------------------------------------------------------------- /public/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgquannn/CourseX/HEAD/public/img/background.jpg -------------------------------------------------------------------------------- /public/img/background_flip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hgquannn/CourseX/HEAD/public/img/background_flip.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /components/providers/session-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import {SessionProvider} from "next-auth/react"; 3 | export default SessionProvider; -------------------------------------------------------------------------------- /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 = "mysql" -------------------------------------------------------------------------------- /components/providers/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster } from "react-hot-toast"; 4 | 5 | export const ToasterProvider = () => { 6 | return ; 7 | }; -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { options } from "@/lib/auth"; 3 | 4 | const handler = NextAuth(options); 5 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateComponents } from "@uploadthing/react"; 2 | 3 | import type { OurFileRouter } from "@/app/api/uploadthing/core"; 4 | 5 | export const { UploadButton, UploadDropzone, Uploader } = 6 | generateComponents(); -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createNextRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createNextRouteHandler({ 7 | router: ourFileRouter, 8 | }); 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | 3 | UPLOADTHING_SECRET= 4 | UPLOADTHING_APP_ID= 5 | MUX_TOKEN_ID= 6 | MUX_TOKEN_SECRET= 7 | 8 | NEXTAUTH_URL="http://localhost:3000" 9 | NEXTAUTH_SECRET="" 10 | 11 | GITHUB_ID="" 12 | GITHUB_SECRET="" 13 | 14 | GOOGLE_ID="" 15 | GOOGLE_SECRET="" -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined 5 | } 6 | 7 | export const db = globalThis.prisma || new PrismaClient() 8 | 9 | if (process.env.NODE_ENV !== 'production') globalThis.prisma = db 10 | 11 | -------------------------------------------------------------------------------- /hooks/use-confetti-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type ConfettiStore = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useConfettiStore = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: [ 5 | "utfs.io", 6 | "avatars.githubusercontent.com" 7 | ] 8 | }, 9 | reactStrictMode: true, 10 | eslint: { 11 | ignoreDuringBuilds: true, 12 | }, 13 | } 14 | 15 | module.exports = nextConfig 16 | -------------------------------------------------------------------------------- /app/auth/_components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | export const Logo = () => { 5 | return ( 6 | logo 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /app/(dashboard)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { NavbarRoutes } from "@/components/navbar-routes" 2 | import { MobileSidebar } from "./mobile-sidebar" 3 | 4 | export const Navbar = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /actions/get-categories.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export default async function getAllCategories() { 4 | try { 5 | const categories = await db.category.findMany({ 6 | orderBy: { 7 | name: "asc", 8 | }, 9 | }); 10 | 11 | return categories; 12 | } catch (error) { 13 | console.error("[GET_CATEGORIES]", error); 14 | return []; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /actions/get-current-course.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export async function getCurrentCourse(courseId: string) { 4 | try { 5 | const course = await db.course.findUnique({ 6 | where: { 7 | id: courseId, 8 | }, 9 | }); 10 | 11 | return course; 12 | 13 | } catch (error) { 14 | console.error("[GET_CURRENT_COURSE]", error); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /actions/get-enroll.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export const getEnroll = async (courseId: string, userId: string) => { 4 | try { 5 | const enroll = await db.enroll.findFirst({ 6 | where: { 7 | courseId: courseId, 8 | userId: userId, 9 | }, 10 | }); 11 | 12 | return enroll; 13 | } catch (error) { 14 | console.error("[GET_ENROLL]", error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /actions/get-all-users.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { User } from "@prisma/client"; 3 | 4 | 5 | export const getAllUsers = async (): Promise => { 6 | try { 7 | const users = await db.user.findMany({ 8 | where: {}, 9 | distinct: ["id"], 10 | }); 11 | 12 | return users; 13 | } catch (error) { 14 | console.error("[GET_ALL_USERS]", error); 15 | return []; 16 | } 17 | } -------------------------------------------------------------------------------- /app/profile/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "./_components/navbar"; 2 | 3 | const ProfileLayout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 |
7 | 8 |
9 |
{children}
10 |
11 | ); 12 | }; 13 | 14 | export default ProfileLayout; 15 | -------------------------------------------------------------------------------- /app/(dashboard)/_components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | export const Logo = () => { 5 | const logoSrc = "/logo.svg"; 6 | 7 | return ( 8 | 9 | logo 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /actions/get-courses-created.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export default async function getCoursesCreated(userId: string) { 4 | try { 5 | const courses = await db.course.findMany({ 6 | where: { 7 | userId, 8 | }, 9 | orderBy: { 10 | createdAt: "desc", 11 | }, 12 | }); 13 | return courses; 14 | } catch (error) { 15 | console.error("[GET_COURSES_CREATED]", error); 16 | return []; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value:T, delay?:number):T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay || 500); 10 | 11 | return () => { 12 | clearTimeout(timer); 13 | } 14 | }, [value, delay]); 15 | return debouncedValue; 16 | } -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/search/overview/[courseId]/_components/thumbnail.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | interface ThumbnailProps { 6 | imgUrl: string; 7 | } 8 | 9 | export const Thumbnail = ({ 10 | imgUrl 11 | }: ThumbnailProps) => { 12 | 13 | return ( 14 |
15 | alt 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/teacher/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getSession } from "@/lib/auth"; 3 | import { isTeacherSession } from "@/lib/teacher"; 4 | 5 | const TeacherLayout = async ({ 6 | children 7 | }: { 8 | children: React.ReactNode; 9 | }) => { 10 | const session = await getSession(); 11 | 12 | if (!isTeacherSession(session?.user.role)) { 13 | return redirect("/"); 14 | } 15 | return <>{children} 16 | } 17 | 18 | export default TeacherLayout; -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getSession } from "@/lib/auth"; 3 | import { useSession } from "next-auth/react"; 4 | import { isAdminSession } from "@/lib/admin"; 5 | 6 | const AdminLayout = async ({ children }: { children: React.ReactNode }) => { 7 | const session = await getSession(); 8 | 9 | if (!isAdminSession(session?.user.role)) { 10 | return redirect("/"); 11 | } 12 | 13 | return <>{children}; 14 | }; 15 | 16 | export default AdminLayout; 17 | -------------------------------------------------------------------------------- /actions/get-current-chapter.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export async function getCurrentChapter( 4 | courseId: string, 5 | chapterId: string, 6 | ) { 7 | try { 8 | const chapter = await db.chapter.findUnique({ 9 | where: { 10 | id: chapterId, 11 | courseId: courseId, 12 | }, 13 | include: { 14 | muxData: true, 15 | } 16 | }); 17 | 18 | return chapter; 19 | } catch (error) { 20 | console.error("[GET_CURRENT_CHAPTER]", error); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getSession } from "@/lib/auth"; 3 | import { Profile } from "./_components/profile"; 4 | import toast from "react-hot-toast"; 5 | import { db } from "@/lib/db"; 6 | 7 | export default async function ProfilePage() { 8 | const session = await getSession(); 9 | 10 | if (!session) { 11 | redirect("/"); 12 | } 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/preview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { useMemo } from "react"; 5 | import "react-quill/dist/quill.bubble.css"; 6 | 7 | interface PreviewProps { 8 | value: string; 9 | } 10 | 11 | export const Preview = ({ 12 | value, 13 | }: PreviewProps) => { 14 | const ReactQuill = useMemo(() => dynamic(() => import("react-quill"), { ssr: false }), []) 15 | 16 | return ( 17 | 22 | ); 23 | }; -------------------------------------------------------------------------------- /app/(dashboard)/_components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Logo } from "./logo" 4 | import { SidebarRoutes } from "./sidebar-routes" 5 | 6 | export const Sidebar = () => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession, DefaultUser } from "next-auth" 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user: { 9 | role: string | undefined | null 10 | uid: string 11 | } & DefaultSession["user"] 12 | } 13 | } 14 | 15 | declare module "next-auth/jwt" { 16 | interface JWT { 17 | id: string; 18 | role: string | undefined | null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .nvmrc 38 | -------------------------------------------------------------------------------- /app/(dashboard)/_components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "lucide-react"; 2 | 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetTrigger 7 | } from "@/components/ui/sheet"; 8 | import { Sidebar } from "./sidebar"; 9 | 10 | export const MobileSidebar = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /components/providers/confetti-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ReactConfetti from "react-confetti"; 4 | 5 | import { useConfettiStore } from "@/hooks/use-confetti-store"; 6 | 7 | export const ConfettiProvider = () => { 8 | const confetti = useConfettiStore(); 9 | 10 | if (!confetti.isOpen) return null; 11 | 12 | return ( 13 | { 19 | confetti.onClose(); 20 | }} 21 | /> 22 | ) 23 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/forum/_components/comment-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { CommentCard } from "./comment-card"; 3 | import { CommentItem } from "@/lib/constant"; 4 | 5 | export interface CommentListProps { 6 | items: CommentItem[]; 7 | } 8 | 9 | export default function CommentList({ 10 | items, 11 | }: CommentListProps) { 12 | return ( 13 |
14 | {items.slice().reverse().map((item) => ( 15 |
16 | 18 | 19 |
20 | ))} 21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /actions/get-redirect-course.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export default async function getRedirectCourse(courseId: string) { 4 | try { 5 | const course = await db.course.findUnique({ 6 | where: { 7 | id: courseId, 8 | }, 9 | include: { 10 | chapters: { 11 | where: { 12 | isPublished: true, 13 | }, 14 | orderBy: { 15 | position: "asc", 16 | }, 17 | }, 18 | }, 19 | }); 20 | 21 | return course; 22 | } catch (error) { 23 | console.error("[GET_REDIRECT_COURSE]", error); 24 | throw error; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/profile/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Logo } from "../../(dashboard)/_components/logo"; 3 | import { ModeToggle } from "@/components/mode-toggle"; 4 | import { UserButton } from "@/components/user-button"; 5 | 6 | export const Navbar = () => { 7 | return ( 8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "./_components/navbar"; 2 | import { Sidebar } from "./_components/sidebar"; 3 | 4 | const DashboardLayout = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 |
{children}
15 |
16 | ); 17 | }; 18 | 19 | export default DashboardLayout; 20 | -------------------------------------------------------------------------------- /components/editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { useMemo } from "react"; 5 | import "react-quill/dist/quill.snow.css"; 6 | 7 | interface EditorProps { 8 | onChange: (value: string) => void; 9 | value: string; 10 | } 11 | 12 | export const Editor = ({ 13 | onChange, 14 | value, 15 | }: EditorProps) => { 16 | const ReactQuill = useMemo(() => dynamic(() => import("react-quill"), { ssr: false }), []) 17 | 18 | return ( 19 |
20 | 25 |
26 | ); 27 | }; -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/page.tsx: -------------------------------------------------------------------------------- 1 | import getRedirectCourse from "@/actions/get-redirect-course"; 2 | import { db } from "@/lib/db"; 3 | import { Metadata } from "next"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export const metadata: Metadata = { 7 | title: "CourseX", 8 | }; 9 | 10 | const CourseIdPage = async ({ 11 | params 12 | }: { 13 | params: { courseId: string; } 14 | }) => { 15 | const course = await getRedirectCourse(params.courseId); 16 | 17 | if (!course) { 18 | return redirect("/"); 19 | } 20 | 21 | return redirect(`/courses/${course.id}/chapters/${course.chapters[0].id}`); 22 | } 23 | 24 | export default CourseIdPage; -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/search/_components/categories.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Category } from "@prisma/client"; 4 | import { CategoryItem } from "./category-item"; 5 | 6 | 7 | interface CategoriesProps{ 8 | items: Category[]; 9 | } 10 | 11 | export const Categories = ({ 12 | items, 13 | }: CategoriesProps) => { 14 | return ( 15 |
16 | {items.map((item) => ( 17 | 22 | ))} 23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/forum/_components/forum.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useForum } from "./_contexts/forum-context"; 3 | import CommentList from "./comment-list"; 4 | import CommentForm from "./comment-form"; 5 | 6 | export default function Forum() { 7 | const forumContext = useForum(); 8 | var rootComments = forumContext.rootComments; 9 | return ( 10 |
11 | 12 | {rootComments != null && rootComments.length > 0 && ( 13 | <> 14 | 15 | 16 | )} 17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /app/denied/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button } from '@/components/ui/button'; 3 | import Link from 'next/link'; 4 | import React from 'react' 5 | 6 | const Denied = () => { 7 | return ( 8 |
9 |

403

10 |
11 |

You dont have permission to access this page

12 |
13 | 16 |
17 | ) 18 | } 19 | 20 | export default Denied; -------------------------------------------------------------------------------- /lib/admin.ts: -------------------------------------------------------------------------------- 1 | import { ROLES, PRIVILEGES } from "./constant"; 2 | import { db } from "./db"; 3 | 4 | 5 | export const isAdminDB = async (userId?: string | undefined) => { 6 | if (userId) { 7 | const existUser = await db.user.findUnique({ 8 | where:{ 9 | id: userId, 10 | }, 11 | }); 12 | const role = existUser?.role; 13 | if (role) { 14 | return role[PRIVILEGES["ADMIN"]] === String(ROLES["ADMIN"]); 15 | } 16 | } 17 | return false; 18 | }; 19 | export const isAdminSession = (sessionRole: string | null | undefined): boolean => { 20 | if (sessionRole) { 21 | return sessionRole[PRIVILEGES["ADMIN"]] === String(ROLES["ADMIN"]); 22 | } 23 | return false; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /scripts/seed.ts: -------------------------------------------------------------------------------- 1 | // import { Database } from "lucide-react"; 2 | 3 | const { PrismaClient } = require("@prisma/client"); 4 | 5 | const database = new PrismaClient(); 6 | 7 | async function main() { 8 | try { 9 | await database.category.createMany({ 10 | data: [ 11 | { name: "Computer Science" }, 12 | { name: "Music" }, 13 | { name: "Maths" }, 14 | { name: "Travel" }, 15 | { name: "Marketing" }, 16 | { name: "Engineering" }, 17 | ], 18 | }); 19 | 20 | console.log("Success"); 21 | 22 | } catch (error) { 23 | console.log("Error while seeding the database categories", error); 24 | } finally { 25 | await database.$disconnect(); 26 | } 27 | } 28 | 29 | main(); -------------------------------------------------------------------------------- /lib/teacher.ts: -------------------------------------------------------------------------------- 1 | import { PRIVILEGES, ROLES } from "./constant"; 2 | import { db } from "./db"; 3 | 4 | export const isTeacherDB = async (userId?: string | undefined) => { 5 | if (userId) { 6 | const existUser = await db.user.findUnique({ 7 | where:{ 8 | id: userId, 9 | }, 10 | }); 11 | const role = existUser?.role; 12 | if (role) { 13 | return role[PRIVILEGES["OTHERS"]] === String(ROLES["TEACHER"]); 14 | } 15 | } 16 | return false; 17 | }; 18 | 19 | export const isTeacherSession = (sessionRole: string | null | undefined): boolean => { 20 | if (sessionRole) { 21 | return sessionRole[PRIVILEGES["OTHERS"]] === String(ROLES["TEACHER"]); 22 | } 23 | return false; 24 | }; -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { db } from "@/lib/db"; 3 | import { getSession } from "@/lib/auth"; 4 | import { DataTable } from "./_components/data-table"; 5 | import { columns } from "./_components/columns"; 6 | import { Metadata } from "next"; 7 | import { getAllUsers } from "@/actions/get-all-users"; 8 | 9 | export const metadata: Metadata = { 10 | title: "User Authorization | CourseX", 11 | }; 12 | 13 | const UserPage = async () => { 14 | const session = await getSession(); 15 | if (!session) { 16 | return redirect("/auth/signin"); 17 | } 18 | const users = await getAllUsers(); 19 | return ( 20 |
21 | 22 |
23 | ); 24 | }; 25 | 26 | export default UserPage; 27 | -------------------------------------------------------------------------------- /actions/get-course-with-progress.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export async function getCourseWithProgress(courseId: string, userId: string) { 4 | try { 5 | const course = await db.course.findUnique({ 6 | where: { 7 | id: courseId, 8 | }, 9 | include: { 10 | chapters: { 11 | where: { 12 | isPublished: true, 13 | }, 14 | include: { 15 | userProgress: { 16 | where: { 17 | userId, 18 | }, 19 | }, 20 | }, 21 | orderBy: { 22 | position: "asc", 23 | }, 24 | }, 25 | }, 26 | }); 27 | 28 | return course; 29 | 30 | } catch (error) { 31 | console.error("[GET_COURSE_WITH_PROGRESS]", error); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/(dashboard)/(routes)/teacher/analytics/_components/data-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 2 | import { format } from "path"; 3 | 4 | interface DataCardProps { 5 | value: number; 6 | label: string; 7 | shouldFormat?: boolean; 8 | } 9 | 10 | export const DataCard = ({ 11 | value, 12 | label, 13 | shouldFormat, 14 | }: DataCardProps) => { 15 | return ( 16 | 17 | 18 | 19 | {label} 20 | 21 | 22 | 23 |
24 | {value} 25 |
26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /components/file-upload.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import toast from "react-hot-toast"; 3 | 4 | import { UploadDropzone } from "@/lib/uploadthing" 5 | import { ourFileRouter } from "@/app/api/uploadthing/core" 6 | 7 | interface FileUploadProps { 8 | onChange: (url?: string, filename?:string) => void; 9 | endpoint: keyof typeof ourFileRouter; 10 | }; 11 | 12 | export const FileUpload = ({ 13 | onChange, 14 | endpoint 15 | }: FileUploadProps) => { 16 | return ( 17 | { 20 | onChange(res?.[0].url, res?.[0].fileName); 21 | }} 22 | onUploadError={(error: Error) => { 23 | toast.error(`${error?.message}`); 24 | }} 25 | /> 26 | ) 27 | } -------------------------------------------------------------------------------- /actions/get-creating-course.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export default async function getCreatingCourse( 4 | courseId: string, 5 | userId: string 6 | ) { 7 | try { 8 | const course = await db.course.findUnique({ 9 | where: { 10 | id: courseId, 11 | userId, 12 | }, 13 | include: { 14 | chapters: { 15 | orderBy: { 16 | position: "asc", 17 | }, 18 | }, 19 | attachments: { 20 | orderBy: { 21 | createdAt: "desc", 22 | }, 23 | }, 24 | categories: { 25 | include: { 26 | category: {}, 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | return course; 33 | } catch (error) { 34 | console.error("[GET_CREATING_COURSE]", error); 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /app/(course)/courses/[courseId]/forum/_components/icon-btn.tsx: -------------------------------------------------------------------------------- 1 | import { Heart, LucideProps } from "lucide-react"; 2 | import React from "react"; 3 | 4 | export interface IconProps { 5 | Icon: React.FC; 6 | isActive: boolean; 7 | isHidden: boolean; 8 | children: React.ReactNode | null; 9 | onClick: () => void; 10 | width: number; 11 | isFill: boolean; 12 | } 13 | 14 | export function IconBtn({ 15 | Icon, 16 | isActive, 17 | children, 18 | onClick, 19 | isHidden, 20 | ...props 21 | }: IconProps) { 22 | return ( 23 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |