Repository: phuc-mai/academy Branch: master Commit: d3b5fe3aff7b Files: 96 Total size: 152.7 KB Directory structure: gitextract_258s52nm/ ├── .eslintrc.json ├── .gitignore ├── README.md ├── app/ │ ├── (auth)/ │ │ ├── layout.tsx │ │ ├── sign-in/ │ │ │ └── [[...sign-in]]/ │ │ │ └── page.tsx │ │ └── sign-up/ │ │ └── [[...sign-up]]/ │ │ └── page.tsx │ ├── (course)/ │ │ └── courses/ │ │ └── [courseId]/ │ │ ├── layout.tsx │ │ ├── overview/ │ │ │ └── page.tsx │ │ └── sections/ │ │ └── [sectionId]/ │ │ └── page.tsx │ ├── (home)/ │ │ ├── categories/ │ │ │ └── [categoryId]/ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── learning/ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── search/ │ │ └── page.tsx │ ├── (instructor)/ │ │ ├── instructor/ │ │ │ ├── courses/ │ │ │ │ ├── [courseId]/ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── sections/ │ │ │ │ │ ├── [sectionId]/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── create-course/ │ │ │ │ └── page.tsx │ │ │ └── performance/ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── actions/ │ │ ├── getCourses.tsx │ │ └── getPerformance.ts │ ├── api/ │ │ ├── courses/ │ │ │ ├── [courseId]/ │ │ │ │ ├── checkout/ │ │ │ │ │ └── route.ts │ │ │ │ ├── publish/ │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ ├── sections/ │ │ │ │ │ ├── [sectionId]/ │ │ │ │ │ │ ├── progress/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── publish/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── resources/ │ │ │ │ │ │ │ ├── [resourceId]/ │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ └── unpublish/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── reorder/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── unpublish/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── uploadthing/ │ │ │ ├── core.ts │ │ │ └── route.ts │ │ └── webhook/ │ │ └── route.ts │ ├── globals.css │ └── layout.tsx ├── components/ │ ├── courses/ │ │ ├── Columns.tsx │ │ ├── CourseCard.tsx │ │ ├── CreateCourseForm.tsx │ │ └── EditCourseForm.tsx │ ├── custom/ │ │ ├── AlertBanner.tsx │ │ ├── Categories.tsx │ │ ├── ComboBox.tsx │ │ ├── DataTable.tsx │ │ ├── Delete.tsx │ │ ├── FileUpload.tsx │ │ ├── PublishButton.tsx │ │ ├── ReadText.tsx │ │ └── RichEditor.tsx │ ├── layout/ │ │ ├── CourseSideBar.tsx │ │ ├── SectionMenu.tsx │ │ ├── Sidebar.tsx │ │ └── Topbar.tsx │ ├── performance/ │ │ ├── Chart.tsx │ │ └── DataCard.tsx │ ├── providers/ │ │ └── ToasterProvider.tsx │ ├── sections/ │ │ ├── CreateSectionForm.tsx │ │ ├── EditSectionForm.tsx │ │ ├── ProgressButton.tsx │ │ ├── ResourceForm.tsx │ │ ├── SectionList.tsx │ │ └── SectionsDetails.tsx │ └── ui/ │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── sheet.tsx │ ├── switch.tsx │ └── table.tsx ├── components.json ├── lib/ │ ├── db.ts │ ├── formatPrice.ts │ ├── stripe.ts │ ├── uploadthing.ts │ └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── prisma/ │ └── schema.prisma ├── scripts/ │ └── seed.ts ├── tailwind.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel 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. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: app/(auth)/layout.tsx ================================================ const AuthLayout = ({ children }: { children: React.ReactNode }) => { return (
{children}
) } export default AuthLayout ================================================ FILE: app/(auth)/sign-in/[[...sign-in]]/page.tsx ================================================ import { SignIn } from "@clerk/nextjs"; export default function Page() { return ; } ================================================ FILE: app/(auth)/sign-up/[[...sign-up]]/page.tsx ================================================ import { SignUp } from "@clerk/nextjs"; export default function Page() { return ; } ================================================ FILE: app/(course)/courses/[courseId]/layout.tsx ================================================ import CourseSideBar from "@/components/layout/CourseSideBar"; import Topbar from "@/components/layout/Topbar"; import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; const CourseDetailsLayout = async ({ children, params, }: { children: React.ReactNode; params: { courseId: string }; }) => { const { userId } = auth(); if (!userId) { return redirect("/sign-in"); } const course = await db.course.findUnique({ where: { id: params.courseId, }, include: { sections: { where: { isPublished: true, }, orderBy: { position: "asc", }, }, }, }); if (!course) { return redirect("/"); } return (
{children}
); }; export default CourseDetailsLayout; ================================================ FILE: app/(course)/courses/[courseId]/overview/page.tsx ================================================ import { clerkClient } from "@clerk/nextjs/server"; import Image from "next/image"; import { redirect } from "next/navigation"; import { db } from "@/lib/db"; import ReadText from "@/components/custom/ReadText"; import SectionMenu from "@/components/layout/SectionMenu"; const CourseOverview = async ({ params }: { params: { courseId: string } }) => { const course = await db.course.findUnique({ where: { id: params.courseId, isPublished: true, }, include: { sections: { where: { isPublished: true, }, }, }, }); if (!course) { return redirect("/"); } const instructor = await clerkClient.users.getUser(course.instructorId); let level; if (course.levelId) { level = await db.level.findUnique({ where: { id: course.levelId, }, }); } return (

{course.title}

{course.subtitle}

{instructor.fullName

Instructor:

{instructor.fullName}

Price:

${course.price}

Level:

{level?.name}

Description:

); }; export default CourseOverview; ================================================ FILE: app/(course)/courses/[courseId]/sections/[sectionId]/page.tsx ================================================ import SectionsDetails from "@/components/sections/SectionsDetails"; import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { Resource } from "@prisma/client"; import { redirect } from "next/navigation"; const SectionDetailsPage = async ({ params, }: { params: { courseId: string; sectionId: string }; }) => { const { courseId, sectionId } = params; const { userId } = auth(); if (!userId) { return redirect("/sign-in"); } const course = await db.course.findUnique({ where: { id: courseId, isPublished: true, }, include: { sections: { where: { isPublished: true, }, }, }, }); if (!course) { return redirect("/"); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, isPublished: true, }, }); if (!section) { return redirect(`/courses/${courseId}/overview`); } const purchase = await db.purchase.findUnique({ where: { customerId_courseId: { customerId: userId, courseId, }, }, }); let muxData = null; let resources: Resource[] = []; if (section.isFree || purchase) { muxData = await db.muxData.findUnique({ where: { sectionId, }, }); } if (purchase) { resources = await db.resource.findMany({ where: { sectionId, }, }); } const progress = await db.progress.findUnique({ where: { studentId_sectionId: { studentId: userId, sectionId, }, }, }); return ( ); }; export default SectionDetailsPage; ================================================ FILE: app/(home)/categories/[categoryId]/page.tsx ================================================ import getCoursesByCategory from "@/app/actions/getCourses"; import CourseCard from "@/components/courses/CourseCard"; import Categories from "@/components/custom/Categories"; import { db } from "@/lib/db"; const CoursesByCategory = async ({ params, }: { params: { categoryId: string }; }) => { const categories = await db.category.findMany({ orderBy: { name: "asc", }, }); const courses = await getCoursesByCategory(params.categoryId); return (
{courses.map((course) => ( ))}
); }; export default CoursesByCategory; ================================================ FILE: app/(home)/layout.tsx ================================================ import Topbar from "@/components/layout/Topbar" const HomeLayout = ({ children }: { children: React.ReactNode }) => { return ( <> {children} ) } export default HomeLayout ================================================ FILE: app/(home)/learning/page.tsx ================================================ import CourseCard from "@/components/courses/CourseCard" import { db } from "@/lib/db" import { auth } from "@clerk/nextjs/server" import { redirect } from "next/navigation" const LearningPage = async () => { const { userId } = auth() if (!userId) { return redirect('/sign-in') } const purchasedCourses = await db.purchase.findMany({ where: { customerId: userId, }, select: { course: { include: { category: true, subCategory: true, sections: { where: { isPublished: true, }, } } } } }) return (

Your courses

{purchasedCourses.map((purchase) => ( ))}
) } export default LearningPage ================================================ FILE: app/(home)/page.tsx ================================================ import { db } from "@/lib/db"; import getCoursesByCategory from "../actions/getCourses"; import Categories from "@/components/custom/Categories"; import CourseCard from "@/components/courses/CourseCard"; export default async function Home() { const categories = await db.category.findMany({ orderBy: { name: "asc", }, include: { subCategories: { orderBy: { name: "asc", }, }, }, }); const courses = await getCoursesByCategory(null); return (
{courses.map((course) => ( ))}
); } ================================================ FILE: app/(home)/search/page.tsx ================================================ import CourseCard from "@/components/courses/CourseCard"; import { db } from "@/lib/db" const SearchPage = async ({ searchParams }: { searchParams: { query: string }}) => { const queryText = searchParams.query || '' const courses = await db.course.findMany({ where: { isPublished: true, OR: [ { title: { contains: queryText } }, { category: { name: { contains: queryText } }}, { subCategory: { name: { contains: queryText } }} ] }, include: { category: true, subCategory: true, level: true, sections: { where: { isPublished: true, } } }, orderBy: { createdAt: 'desc' } }); return (

Recommended courses for {queryText}

{courses.map((course) => ( ))}
) } export default SearchPage ================================================ FILE: app/(instructor)/instructor/courses/[courseId]/basic/page.tsx ================================================ import EditCourseForm from "@/components/courses/EditCourseForm"; import AlertBanner from "@/components/custom/AlertBanner"; import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; const CourseBasics = async ({ params }: { params: { courseId: string } }) => { const { userId } = auth(); if (!userId) { return redirect("/sign-in"); } const course = await db.course.findUnique({ where: { id: params.courseId, instructorId: userId, }, include: { sections: true, }, }); if (!course) { return redirect("/instructor/courses"); } const categories = await db.category.findMany({ orderBy: { name: "asc", }, include: { subCategories: true, }, }); const levels = await db.level.findMany(); const requiredFields = [ course.title, course.description, course.categoryId, course.subCategoryId, course.levelId, course.imageUrl, course.price, course.sections.some((section) => section.isPublished), ]; const requiredFieldsCount = requiredFields.length; const missingFields = requiredFields.filter((field) => !Boolean(field)); const missingFieldsCount = missingFields.length; const isCompleted = requiredFields.every(Boolean); return (
({ label: category.name, value: category.id, subCategories: category.subCategories.map((subcategory) => ({ label: subcategory.name, value: subcategory.id, })), }))} levels={levels.map((level) => ({ label: level.name, value: level.id, }))} isCompleted={isCompleted} />
); }; export default CourseBasics; ================================================ FILE: app/(instructor)/instructor/courses/[courseId]/sections/[sectionId]/page.tsx ================================================ import AlertBanner from "@/components/custom/AlertBanner"; import EditSectionForm from "@/components/sections/EditSectionForm"; import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; const SectionDetailsPage = async ({ params, }: { params: { courseId: string; sectionId: string }; }) => { const { userId } = auth(); if (!userId) { return redirect("/sign-in"); } const course = await db.course.findUnique({ where: { id: params.courseId, instructorId: userId, }, }); if (!course) { return redirect("/instructor/courses"); } const section = await db.section.findUnique({ where: { id: params.sectionId, courseId: params.courseId, }, include: { muxData: true, resources: true, }, }); if (!section) { return redirect(`/instructor/courses/${params.courseId}/sections`); } const requiredFields = [section.title, section.description, section.videoUrl]; const requiredFieldsCount = requiredFields.length; const missingFields = requiredFields.filter((field) => !Boolean(field)); // Return falsy values: undefined, null, 0, false, NaN, '' const missingFieldsCount = missingFields.length; const isCompleted = requiredFields.every(Boolean); return (
); }; export default SectionDetailsPage; ================================================ FILE: app/(instructor)/instructor/courses/[courseId]/sections/page.tsx ================================================ import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; import CreateSectionForm from "@/components/sections/CreateSectionForm"; import { db } from "@/lib/db"; const CourseCurriculumPage = async ({ params }: { params: { courseId: string }}) => { const { userId } = auth() if (!userId) { return redirect("/sign-in") } const course = await db.course.findUnique({ where: { id: params.courseId, instructorId: userId, }, include: { sections: { orderBy: { position: "asc", }, }, }, }); if (!course) { return redirect("/instructor/courses") } return ( ); } export default CourseCurriculumPage; ================================================ FILE: app/(instructor)/instructor/courses/page.tsx ================================================ import { auth } from "@clerk/nextjs/server"; import Link from "next/link"; import { redirect } from "next/navigation"; import { Button } from "@/components/ui/button"; import { db } from "@/lib/db"; import { DataTable } from "@/components/custom/DataTable"; import { columns } from "@/components/courses/Columns"; const CoursesPage = async () => { const { userId } = auth(); if (!userId) { return redirect("/sign-in"); } const courses = await db.course.findMany({ where: { instructorId: userId, }, orderBy: { createdAt: "desc", }, }); return (
); }; export default CoursesPage; ================================================ FILE: app/(instructor)/instructor/create-course/page.tsx ================================================ import CreateCourseForm from "@/components/courses/CreateCourseForm" import { db } from "@/lib/db" const CreateCoursePage = async () => { const categories = await db.category.findMany({ orderBy: { name: "asc" }, include: { subCategories: true } }) return (
({ label: category.name, value: category.id, subCategories: category.subCategories.map((subcategory) => ({ label: subcategory.name, value: subcategory.id })) }))} />
) } export default CreateCoursePage ================================================ FILE: app/(instructor)/instructor/performance/page.tsx ================================================ import { getPerformance } from "@/app/actions/getPerformance" import Chart from "@/components/performance/Chart" import DataCard from "@/components/performance/DataCard" import { auth } from "@clerk/nextjs/server" import { redirect } from "next/navigation" const PerformancePage = async () => { const { userId } = auth() if (!userId) { return redirect("/sign-in") } const { data, totalRevenue, totalSales } = await getPerformance(userId) return (
) } export default PerformancePage ================================================ FILE: app/(instructor)/layout.tsx ================================================ import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; import Topbar from "@/components/layout/Topbar"; import Sidebar from "@/components/layout/Sidebar"; const InstructorLayout = ({ children }: { children: React.ReactNode }) => { const { userId } = auth() if (!userId) { return redirect("/sign-in") } return (
{children}
); }; export default InstructorLayout; ================================================ FILE: app/actions/getCourses.tsx ================================================ import { db } from "@/lib/db" import { Course } from "@prisma/client" const getCoursesByCategory = async (categoryId: string | null): Promise => { const whereClause: any = { ...(categoryId ? { categoryId, isPublished: true } : { isPublished: true }), } const courses = await db.course.findMany({ where: whereClause, include: { category: true, subCategory: true, level: true, sections: { where: { isPublished: true, } }, }, orderBy: { createdAt: "desc", }, }) return courses } export default getCoursesByCategory ================================================ FILE: app/actions/getPerformance.ts ================================================ import { db } from "@/lib/db"; import { Course, Purchase } from "@prisma/client"; type PurchaseWithCourse = Purchase & { course: Course }; const groupByCourse = (purchases: PurchaseWithCourse[]) => { const grouped: { [courseTitle: string]: { total: number; count: number } } = {}; purchases.forEach((purchase) => { const courseTitle = purchase.course.title; if (!grouped[courseTitle]) { grouped[courseTitle] = { total: 0, count: 0 }; } grouped[courseTitle].total += purchase.course.price!; grouped[courseTitle].count += 1; }); return grouped; }; export const getPerformance = async (userId: string) => { try { const purchases = await db.purchase.findMany({ where: { course: { instructorId: userId } }, include: { course: true }, }); const groupedEarnings = groupByCourse(purchases); const data = Object.entries(groupedEarnings).map( ([courseTitle, { total, count }]) => ({ name: courseTitle, total, count, }) ); const totalRevenue = data.reduce((acc, current) => acc + current.total, 0); const totalSales = purchases.length return { data, totalRevenue, totalSales, }; } catch (err) { console.log("[getPerformance]", err); return { data: [], totalRevenue: 0, totalSales: 0, }; } }; ================================================ FILE: app/api/courses/[courseId]/checkout/route.ts ================================================ import Stripe from "stripe"; import { NextRequest, NextResponse } from "next/server"; import { currentUser } from "@clerk/nextjs/server"; import { db } from "@/lib/db"; import { stripe } from "@/lib/stripe"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const user = await currentUser(); if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) { return new NextResponse("Unauthorized", { status: 401 }); } const course = await db.course.findUnique({ where: { id: params.courseId, isPublished: true }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const purchase = await db.purchase.findUnique({ where: { customerId_courseId: { customerId: user.id, courseId: course.id }, }, }); if (purchase) { return new NextResponse("Course Already Purchased", { status: 400 }); } const line_items: Stripe.Checkout.SessionCreateParams.LineItem[] = [ { quantity: 1, price_data: { currency: "cad", product_data: { name: course.title, }, unit_amount: Math.round(course.price! * 100), }, } ] let stripeCustomer = await db.stripeCustomer.findUnique({ where: { customerId: user.id }, select: { stripeCustomerId: true }, }); if (!stripeCustomer) { const customer = await stripe.customers.create({ email: user.emailAddresses[0].emailAddress, }); stripeCustomer = await db.stripeCustomer.create({ data: { customerId: user.id, stripeCustomerId: customer.id, }, }); } const session = await stripe.checkout.sessions.create({ customer: stripeCustomer.stripeCustomerId, payment_method_types: ["card"], line_items, mode: "payment", success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/courses/${course.id}/overview?success=true`, cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/courses/${course.id}/overview?canceled=true`, metadata: { courseId: course.id, customerId: user.id, } }); return NextResponse.json({ url: session.url }) } catch (err) { console.log("[courseId_checkout_POST]", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/publish/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); const { courseId } = params; if (!userId) { return new Response("Unauthorized", { status: 401 }); } const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId }, include: { sections: { include: { muxData: true, }, }, }, }); if (!course) { return new Response("Course not found", { status: 404 }); } const isPublishedSections = course.sections.some( (section) => section.isPublished ); if ( !course.title || !course.description || !course.categoryId || !course.subCategoryId || !course.levelId || !course.imageUrl || !course.price || !isPublishedSections ) { return new NextResponse("Missing required fields", { status: 400 }); } const pusblishedCourse = await db.course.update({ where: { id: courseId, instructorId: userId }, data: { isPublished: true }, }); return NextResponse.json(pusblishedCourse, { status: 200 }); } catch (err) { console.log("[courseId_publish_POST]", err); return new Response("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; import Mux from "@mux/mux-node"; const { video } = new Mux({ tokenId: process.env.MUX_TOKEN_ID, tokenSecret: process.env.MUX_TOKEN_SECRET, }); export const PATCH = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); const { courseId } = params; const values = await req.json(); if (!userId) { return new Response("Unauthorized", { status: 401 }); } const course = await db.course.update({ where: { id: courseId, instructorId: userId }, data: { ...values }, }); return NextResponse.json(course, { status: 200 }); } catch (err) { console.error(["courseId_PATCH", err]); return new Response("Internal Server Error", { status: 500 }); } }; export const DELETE = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); const { courseId } = params; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId}, include: { sections: { include: { muxData: true, } } } }); if (!course) { return new NextResponse("Course not found", { status: 404 }); } for (const section of course.sections) { if (section.muxData?.assetId) { await video.assets.delete(section.muxData.assetId); } } await db.course.delete({ where: { id: courseId, instructorId: userId }, }); return new NextResponse("Course Deleted", { status: 200 }); } catch (err) { console.error(["courseId_DELETE", err]); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/progress/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth(); const { isCompleted } = await req.json(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, }, }); if (!section) { return new NextResponse("Section Not Found", { status: 404 }); } let progress = await db.progress.findUnique({ where: { studentId_sectionId: { studentId: userId, sectionId, }, }, }); if (progress) { progress = await db.progress.update({ where: { studentId_sectionId: { studentId: userId, sectionId, }, }, data: { isCompleted, }, }); } else { progress = await db.progress.create({ data: { studentId: userId, sectionId, isCompleted, }, }); } return NextResponse.json(progress, { status: 200 }); } catch (err) { console.log("[sectionId_progress_POST]", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/publish/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, }, }); const muxData = await db.muxData.findUnique({ where: { sectionId, }, }); if (!section || !muxData || !section.title || !section.description || !section.videoUrl) { return new NextResponse("Missing required fields", { status: 400 }); } const publishedSection = await db.section.update({ where: { id: sectionId, courseId, }, data: { isPublished: true, }, }); return NextResponse.json(publishedSection, { status: 200 }); } catch (err) { console.log("[section_publish_POST]", err) return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/resources/[resourceId]/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string, resourceId: string } } ) => { try { const { userId } = auth() if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId, resourceId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, }, }); if (!section) { return new NextResponse("Section Not Found", { status: 404 }); } await db.resource.delete({ where: { id: resourceId, sectionId, }, }); return NextResponse.json("Resource deleted", { status: 200 }); } catch (err) { console.log("[resourceId_DELETE", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/resources/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth() if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, }, }); if (!section) { return new NextResponse("Section Not Found", { status: 404 }); } const { name, fileUrl } = await req.json(); const resource = await db.resource.create({ data: { name, fileUrl, sectionId, }, }); return NextResponse.json(resource, { status: 200 }); } catch (err) { console.log("[resources_POST", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; import Mux from "@mux/mux-node"; const { video } = new Mux({ tokenId: process.env.MUX_TOKEN_ID, tokenSecret: process.env.MUX_TOKEN_SECRET, }); export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const values = await req.json(); const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.update({ where: { id: sectionId, courseId, }, data: { ...values, }, }); if (values.videoUrl) { const existingMuxData = await db.muxData.findFirst({ where: { sectionId, }, }); if (existingMuxData) { await video.assets.delete(existingMuxData.assetId); await db.muxData.delete({ where: { id: existingMuxData.id, }, }); } const asset = await video.assets.create({ input: values.videoUrl, playback_policy: ["public"], test: false, }); await db.muxData.create({ data: { assetId: asset.id, playbackId: asset.playback_ids?.[0]?.id, sectionId, }, }); } return NextResponse.json(section, { status: 200 }); } catch (err) { console.log("[sectionId_POST]", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; export const DELETE = async (req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const section = await db.section.findUnique({ where: { id: sectionId, courseId, } }); if (!section) { return new NextResponse("Section Not Found", { status: 404 }); } if (section.videoUrl) { const existingMuxData = await db.muxData.findFirst({ where: { sectionId, }, }); if (existingMuxData) { await video.assets.delete(existingMuxData.assetId); await db.muxData.delete({ where: { id: existingMuxData.id, }, }); } } await db.section.delete({ where: { id: sectionId, courseId, }, }); const publishedSectionsInCourse = await db.section.findMany({ where: { courseId, isPublished: true, }, }); if (!publishedSectionsInCourse.length) { await db.course.update({ where: { id: courseId, }, data: { isPublished: false, }, }); } return new NextResponse("Section Deleted", { status: 200 }); } catch (err) { console.log("[sectionId_DELETE]", err); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/courses/[courseId]/sections/[sectionId]/unpublish/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string; sectionId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { courseId, sectionId } = params; const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const unpublishedSection = await db.section.update({ where: { id: sectionId, courseId, }, data: { isPublished: false, }, }); const publishedSectionsInCourse = await db.section.findMany({ where: { courseId, isPublished: true, }, }); if (publishedSectionsInCourse.length === 0) { await db.course.update({ where: { id: courseId, instructorId: userId, }, data: { isPublished: false, }, }); } return NextResponse.json(unpublishedSection, { status: 200 }); } catch (err) { console.log("[sectionId_unpublish_POST]", err); return new NextResponse("Internal Server Error", { status: 500 }); } } ================================================ FILE: app/api/courses/[courseId]/sections/reorder/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const PUT = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const { list } = await req.json(); const course = await db.course.findUnique({ where: { id: params.courseId, instructorId: userId, }, }); if (!course) { return new NextResponse("Course not found", { status: 404 }); } for (let item of list) { await db.section.update({ where: { id: item.id, }, data: { position: item.position, }, }); } return new NextResponse("Reorder sections successfully", { status: 200 }); } catch (err) { console.log("[reorder_PUT]", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/sections/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); } const course = await db.course.findUnique({ where: { id: params.courseId, instructorId: userId }, }); if (!course) { return new NextResponse("Course Not Found", { status: 404 }); } const lastSection = await db.section.findFirst({ where: { courseId: params.courseId }, orderBy: { position: "desc" }, }); const newPosition = lastSection ? lastSection.position + 1 : 0; const { title } = await req.json(); const newSection = await db.section.create({ data: { title, courseId: params.courseId, position: newPosition, }, }); return NextResponse.json(newSection, { status: 200 }); } catch (err) { console.log("[sections_POST]", err); return new NextResponse("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/[courseId]/unpublish/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async ( req: NextRequest, { params }: { params: { courseId: string } } ) => { try { const { userId } = auth(); const { courseId } = params; if (!userId) { return new Response("Unauthorized", { status: 401 }); } const course = await db.course.findUnique({ where: { id: courseId, instructorId: userId }, }); if (!course) { return new Response("Course not found", { status: 404 }); } const unpusblishedCourse = await db.course.update({ where: { id: courseId, instructorId: userId }, data: { isPublished: false }, }); return NextResponse.json(unpusblishedCourse, { status: 200 }); } catch (err) { console.log("[courseId_unpublish_POST]", err); return new Response("Internal Server Error", { status: 500 }); } }; ================================================ FILE: app/api/courses/route.ts ================================================ import { db } from "@/lib/db"; import { auth } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export const POST = async (req: NextRequest) => { try { const { userId } = auth() if (!userId) { return new NextResponse("Unauthorized", { status: 401 }) } const { title, categoryId, subCategoryId } = await req.json() const newCourse = await db.course.create({ data: { title, categoryId, subCategoryId, instructorId: userId } }) return NextResponse.json(newCourse, {status: 200 }) } catch (err) { console.log("[courses_POST]", err) return new NextResponse("Internal Server Error", { status: 500 }) } } ================================================ FILE: app/api/uploadthing/core.ts ================================================ import { auth } from "@clerk/nextjs/server"; import { createUploadthing, type FileRouter } from "uploadthing/next"; import { UploadThingError } from "uploadthing/server"; const f = createUploadthing(); const handleAuth = () => { const { userId } = auth(); if (!userId) throw new Error("Unauthorized"); return { userId }; }; // FileRouter for your app, can contain multiple FileRoutes export const ourFileRouter = { courseBanner: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) .middleware(handleAuth) .onUploadComplete(() => {}), sectionVideo: f({ video: { maxFileSize: "512GB", maxFileCount: 1 } }) .middleware(handleAuth) .onUploadComplete(() => {}), sectionResource: f(["text", "image", "video", "audio", "pdf"]) .middleware(handleAuth) .onUploadComplete(() => {}), } satisfies FileRouter; export type OurFileRouter = typeof ourFileRouter; ================================================ FILE: app/api/uploadthing/route.ts ================================================ import { createRouteHandler } from "uploadthing/next"; import { ourFileRouter } from "./core"; // Export routes for Next App Router export const { GET, POST } = createRouteHandler({ router: ourFileRouter, }); ================================================ FILE: app/api/webhook/route.ts ================================================ import { db } from "@/lib/db"; import { stripe } from "@/lib/stripe"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import Stripe from "stripe"; export const POST = async (req: NextRequest) => { const rawBody = await req.text(); const signature = headers().get("Stripe-Signature") as string; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err: any) { return new NextResponse(`Webhook error: ${err.message}`, { status: 400 }); } const session = event.data.object as Stripe.Checkout.Session; const customerId = session?.metadata?.customerId; const courseId = session?.metadata?.courseId; if (event.type === "checkout.session.completed") { if (!customerId || !courseId) { return new NextResponse("Missing metadata", { status: 400 }); } await db.purchase.create({ data: { customerId, courseId, }, }); } else { return new NextResponse(`Unhandled event type: ${event.type}`, { status: 400, }); } return new NextResponse("Success", { status: 200 }); }; ================================================ FILE: app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; html, body, :root { height: 100%; } @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } .cl-formButtonPrimary { color: black; background-color: #FDAB04; } .cl-formButtonPrimary:hover { background-color: rgba(253, 171, 4, 0.8); } @import "@uploadthing/react/styles.css"; ================================================ FILE: app/layout.tsx ================================================ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { ClerkProvider } from "@clerk/nextjs"; import "./globals.css"; import ToasterProvider from "@/components/providers/ToasterProvider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Tech Vision Academy", description: "Empowering minds, shaping future", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: components/courses/Columns.tsx ================================================ "use client"; import { Course } from "@prisma/client"; import { ColumnDef } from "@tanstack/react-table"; import { Pencil } from "lucide-react"; import Link from "next/link"; import { ArrowUpDown } from "lucide-react"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; export const columns: ColumnDef[] = [ { accessorKey: "title", // course.title header: ({ column }) => { return ( ); }, }, { accessorKey: "price", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")); const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(price); return
{formatted}
; }, }, { accessorKey: "isPublished", header: "Status", cell: ({ row }) => { const isPublished = row.getValue("isPublished") || false; return ( {isPublished ? "Published" : "Draft"} ); }, }, { id: "actions", cell: ({ row }) => ( Edit ), }, ]; ================================================ FILE: components/courses/CourseCard.tsx ================================================ import { db } from "@/lib/db"; import { clerkClient } from "@clerk/nextjs/server"; import { Course } from "@prisma/client"; import { Gem } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; const CourseCard = async ({ course }: { course: Course }) => { const instructor = await clerkClient.users.getUser(course.instructorId); let level; if (course.levelId) { level = await db.level.findUnique({ where: { id: course.levelId, }, }); } return ( {course.title}

{course.title}

{instructor && (
{

{instructor.fullName}

)} {level && (

{level.name}

)}

$ {course.price}

); }; export default CourseCard; ================================================ FILE: components/courses/CreateCourseForm.tsx ================================================ "use client"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import axios from "axios"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ComboBox } from "@/components/custom/ComboBox"; import { useRouter } from "next/navigation"; import toast from "react-hot-toast"; import { Loader2 } from "lucide-react"; const formSchema = z.object({ title: z.string().min(2, { message: "Title is required and minimum 2 characters", }), categoryId: z.string().min(1, { message: "Category is required", }), subCategoryId: z.string().min(1, { message: "Subcategory is required", }), }); interface CreateCourseFormProps { categories: { label: string; // name of category value: string; // categoryId subCategories: { label: string; value: string }[]; }[]; } const CreateCourseForm = ({ categories }: CreateCourseFormProps) => { const router = useRouter(); // 1. Define your form. const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { title: "", categoryId: "", subCategoryId: "", }, }); const { isValid, isSubmitting } = form.formState; // 2. Define a submit handler. const onSubmit = async (values: z.infer) => { try { const response = await axios.post("/api/courses", values); router.push(`/instructor/courses/${response.data.id}/basic`); toast.success("New Course Created"); } catch (err) { console.log("Failed to create new course", err); toast.error("Something went wrong!"); } }; return (

Let give some basics for your course

It is ok if you cannot think of a good title or correct category now. You can change them later.

( Title )} /> ( Category )} /> ( Subcategory category.value === form.watch("categoryId") )?.subCategories || [] } {...field} /> )} />
); }; export default CreateCourseForm; ================================================ FILE: components/courses/EditCourseForm.tsx ================================================ "use client"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { Course } from "@prisma/client"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import RichEditor from "@/components/custom/RichEditor"; import { ComboBox } from "../custom/ComboBox"; import FileUpload from "../custom/FileUpload"; import Link from "next/link"; import axios from "axios"; import { usePathname, useRouter } from "next/navigation"; import toast from "react-hot-toast"; import { Loader2, Trash } from "lucide-react"; import Delete from "../custom/Delete"; import PublishButton from "../custom/PublishButton"; const formSchema = z.object({ title: z.string().min(2, { message: "Title is required and must be at least 2 characters long", }), subtitle: z.string().optional(), description: z.string().optional(), categoryId: z.string().min(1, { message: "Category is required", }), subCategoryId: z.string().min(1, { message: "Subcategory is required", }), levelId: z.string().optional(), imageUrl: z.string().optional(), price: z.coerce.number().optional(), }); interface EditCourseFormProps { course: Course; categories: { label: string; // name of category value: string; // categoryId subCategories: { label: string; value: string }[]; }[]; levels: { label: string; value: string }[]; isCompleted: boolean; } const EditCourseForm = ({ course, categories, levels, isCompleted, }: EditCourseFormProps) => { const router = useRouter(); const pathname = usePathname(); // 1. Define your form. const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { title: course.title, subtitle: course.subtitle || "", description: course.description || "", categoryId: course.categoryId, subCategoryId: course.subCategoryId, levelId: course.levelId || "", imageUrl: course.imageUrl || "", price: course.price || undefined, }, }); const { isValid, isSubmitting } = form.formState; // 2. Define a submit handler. const onSubmit = async (values: z.infer) => { try { await axios.patch(`/api/courses/${course.id}`, values); toast.success("Course Updated"); router.refresh(); } catch (err) { console.log("Failed to update the course", err); toast.error("Something went wrong!"); } }; const routes = [ { label: "Basic Information", path: `/instructor/courses/${course.id}/basic`, }, { label: "Curriculum", path: `/instructor/courses/${course.id}/sections` }, ]; return ( <>
{routes.map((route) => ( ))}
( Title * )} /> ( Subtitle )} /> ( Description * )} />
( Category * )} /> ( Subcategory * category.value === form.watch("categoryId") )?.subCategories || [] } {...field} /> )} /> ( Level * )} />
( Couse Banner * field.onChange(url)} endpoint="courseBanner" page="Edit Course" /> )} /> ( Price * (USD) )} />
); }; export default EditCourseForm; ================================================ FILE: components/custom/AlertBanner.tsx ================================================ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Rocket, TriangleAlert } from "lucide-react"; interface AlertBannerProps { isCompleted: boolean; requiredFieldsCount: number; missingFieldsCount: number; } const AlertBanner = ({ isCompleted, requiredFieldsCount, missingFieldsCount, }: AlertBannerProps) => { return ( {isCompleted ? ( ) : ( )} {missingFieldsCount} missing field(s) / {requiredFieldsCount} required fields {isCompleted ? "Great job! Ready to publish" : "You can only publish when all the required fields are completed"} ); }; export default AlertBanner; ================================================ FILE: components/custom/Categories.tsx ================================================ "use client" import { Category } from "@prisma/client"; import { Button } from "@/components/ui/button"; import { useRouter } from "next/navigation"; interface CategoriesProps { categories: Category[]; selectedCategory: string | null; } const Categories = ({ categories, selectedCategory }: CategoriesProps) => { const router = useRouter(); const onClick = (categoryId: string | null) => { router.push(categoryId ? `/categories/${categoryId}` : "/"); }; return (
{categories.map((category) => ( ))}
); }; export default Categories; ================================================ FILE: components/custom/ComboBox.tsx ================================================ "use client" import * as React from "react" import { Check, ChevronsUpDown } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" interface ComboBoxProps { options: { label: string, value: string }[] value?: string onChange: (value: string) => void } export function ComboBox({ options, value, onChange }: ComboBoxProps) { const [open, setOpen] = React.useState(false) return ( No option found. {options.map((option) => ( { onChange(option.value === value ? "" : option.value) setOpen(false) }} > {option.label} ))} ) } ================================================ FILE: components/custom/DataTable.tsx ================================================ "use client"; import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel, useReactTable, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input" import { useState } from "react"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; } export function DataTable({ columns, data, }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState( [] ) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), state: { sorting, columnFilters } }); return (
table.getColumn("title")?.setFilterValue(event.target.value) } className="max-w-sm" />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) ) : ( No results. )}
); } ================================================ FILE: components/custom/Delete.tsx ================================================ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import axios from "axios"; import { Loader2, Trash } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; import { Button } from "../ui/button"; interface DeleteProps { item: string; courseId: string; sectionId?: string; } const Delete = ({ item, courseId, sectionId }: DeleteProps) => { const router = useRouter(); const [isDeleting, setIsDeleting] = useState(false); const onDelete = async () => { try { setIsDeleting(true); const url = item === "course" ? `/api/courses/${courseId}` : `/api/courses/${courseId}/sections/${sectionId}`; await axios.delete(url); setIsDeleting(false); const pushedUrl = item === "course" ? "/instructor/courses" : `/instructor/courses/${courseId}/sections`; router.push(pushedUrl); router.refresh(); toast.success(`${item} deleted`); } catch (err) { toast.error(`Something went wrong!`); console.log(`Failed to delete the ${item}`, err); } }; return ( Are you absolutely sure? This action cannot be undone. This will permanently delete your {item} Cancel Delete ); }; export default Delete; ================================================ FILE: components/custom/FileUpload.tsx ================================================ "use client"; import { ourFileRouter } from "@/app/api/uploadthing/core"; import { UploadDropzone } from "@/lib/uploadthing"; import Image from "next/image"; import toast from "react-hot-toast"; interface FileUploadProps { value: string; onChange: (url?: string) => void; endpoint: keyof typeof ourFileRouter; page: string; } const FileUpload = ({ value, onChange, endpoint, page }: FileUploadProps) => { return (
{page === "Edit Course" && value !== "" && ( image )} {page === "Edit Section" && value !== "" && (

{value}

)} { onChange(res?.[0].url); }} onUploadError={(error: Error) => { toast.error(error.message); }} className="w-[280px] h-[200px]" />
); }; export default FileUpload; ================================================ FILE: components/custom/PublishButton.tsx ================================================ "use client"; import { Button } from "@/components/ui/button"; import axios from "axios"; import { Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; interface PublishButtonProps { disabled: boolean; courseId: string; sectionId?: string; isPublished: boolean; page: string; } const PublishButton = ({ disabled, courseId, sectionId, isPublished, page, }: PublishButtonProps) => { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const onClick = async () => { let url = `/api/courses/${courseId}`; if (page === "Section") { url += `/sections/${sectionId}`; } try { setIsLoading(true); isPublished ? await axios.post(`${url}/unpublish`) : await axios.post(`${url}/publish`); toast.success(`${page} ${isPublished ? "unpublished" : "published"}`); router.refresh(); } catch (err) { toast.error("Something went wrong!"); console.log( `Failed to ${isPublished ? "unpublish" : "publish"} ${page}`, err ); } finally { setIsLoading(false); } }; return ( ); }; export default PublishButton; ================================================ FILE: components/custom/ReadText.tsx ================================================ "use client" import dynamic from "next/dynamic"; import { useMemo } from "react"; import "react-quill/dist/quill.bubble.css"; const ReadText = ({ value }: { value: string }) => { const ReactQuill = useMemo( () => dynamic(() => import("react-quill"), { ssr: false }), [] ); return ( ); }; export default ReadText; ================================================ FILE: components/custom/RichEditor.tsx ================================================ "use client" import "react-quill/dist/quill.snow.css"; import dynamic from "next/dynamic"; import { useMemo } from "react"; interface RichEditorProps { placeholder: string; onChange: (value: string) => void; value?: string; } const RichEditor = ({ placeholder, onChange, value }: RichEditorProps) => { const ReactQuill = useMemo( () => dynamic(() => import("react-quill"), { ssr: false }), [] ); return ( ); }; export default RichEditor; ================================================ FILE: components/layout/CourseSideBar.tsx ================================================ import { db } from "@/lib/db"; import { Course, Section } from "@prisma/client"; import Link from "next/link"; import { Progress } from "../ui/progress"; interface CourseSideBarProps { course: Course & { sections: Section[] }; studentId: string; } const CourseSideBar = async ({ course, studentId }: CourseSideBarProps) => { const publishedSections = await db.section.findMany({ where: { courseId: course.id, isPublished: true, }, orderBy: { position: "asc", }, }); const publishedSectionIds = publishedSections.map((section) => section.id); const purchase = await db.purchase.findUnique({ where: { customerId_courseId: { customerId: studentId, courseId: course.id, }, }, }); const completedSections = await db.progress.count({ where:{ studentId, sectionId: { in: publishedSectionIds, }, isCompleted: true, } }); const progressPercentage = (completedSections / publishedSectionIds.length) * 100; return (

{course.title}

{purchase && (

{Math.round(progressPercentage)}% completed

)} Overview {publishedSections.map((section) => ( {section.title} ))}
); }; export default CourseSideBar; ================================================ FILE: components/layout/SectionMenu.tsx ================================================ import { Course, Section } from "@prisma/client"; import React from "react"; import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet"; import { Button } from "../ui/button"; import Link from "next/link"; interface SectionMenuProps { course: Course & { sections: Section[] }; } const SectionMenu = ({ course }: SectionMenuProps) => { return (
Overview {course.sections.map((section) => ( {section.title} ))}
); }; export default SectionMenu; ================================================ FILE: components/layout/Sidebar.tsx ================================================ "use client"; import { BarChart4, MonitorPlay } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; const Sidebar = () => { const pathname = usePathname(); const sidebarRoutes = [ { icon: , label: "Courses", path: "/instructor/courses" }, { icon: , label: "Performance", path: "/instructor/performance", }, ]; return (
{sidebarRoutes.map((route) => ( {route.icon} {route.label} ))}
); }; export default Sidebar; ================================================ FILE: components/layout/Topbar.tsx ================================================ "use client"; import { UserButton, useAuth } from "@clerk/nextjs"; import { Menu, Search } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; const Topbar = () => { const { isSignedIn } = useAuth(); const router = useRouter(); const pathName = usePathname(); const topRoutes = [ { label: "Instructor", path: "/instructor/courses" }, { label: "Learning", path: "/learning" }, ]; const sidebarRoutes = [ { label: "Courses", path: "/instructor/courses" }, { label: "Performance", path: "/instructor/performance", }, ]; const [searchInput, setSearchInput] = useState(""); const handleSearch = () => { if (searchInput.trim() !== "") { router.push(`/search?query=${searchInput}`); } setSearchInput(""); }; return (
logo
setSearchInput(e.target.value)} />
{topRoutes.map((route) => ( {route.label} ))}
{topRoutes.map((route) => ( {route.label} ))}
{pathName.startsWith("/instructor") && (
{sidebarRoutes.map((route) => ( {route.label} ))}
)}
{isSignedIn ? ( ) : ( )}
); }; export default Topbar; ================================================ FILE: components/performance/Chart.tsx ================================================ "use client" import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, } from "recharts"; import { Card } from "@/components/ui/card"; const Chart = ({ data }: { data: { name: string; total: number }[] }) => { return ( `$${value}`} /> ); }; export default Chart; ================================================ FILE: components/performance/DataCard.tsx ================================================ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { formatPrice } from "@/lib/formatPrice"; interface DataCardProps { value: number; label: string; shouldFormat?: boolean; } const DataCard = ({ value, label, shouldFormat }: DataCardProps) => { return ( {label}
{shouldFormat ? formatPrice(value) : value}
); }; export default DataCard; ================================================ FILE: components/providers/ToasterProvider.tsx ================================================ "use client" import { Toaster } from "react-hot-toast" const ToasterProvider = () => { return ( ) } export default ToasterProvider ================================================ FILE: components/sections/CreateSectionForm.tsx ================================================ "use client"; import { Course, Section } from "@prisma/client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import toast from "react-hot-toast"; import axios from "axios"; import SectionList from "@/components/sections/SectionList"; import { Loader2 } from "lucide-react"; const formSchema = z.object({ title: z.string().min(2, { message: "Title is required and must be at least 2 characters long", }), }); const CreateSectionForm = ({ course, }: { course: Course & { sections: Section[] }; }) => { const pathname = usePathname(); const router = useRouter(); const routes = [ { label: "Basic Information", path: `/instructor/courses/${course.id}/basic`, }, { label: "Curriculum", path: `/instructor/courses/${course.id}/sections` }, ]; // 1. Define your form. const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { title: "", }, }); const { isValid, isSubmitting } = form.formState; // 2. Define a submit handler. const onSubmit = async (values: z.infer) => { try { const response = await axios.post( `/api/courses/${course.id}/sections`, values ); router.push( `/instructor/courses/${course.id}/sections/${response.data.id}` ); toast.success("New Section created!"); } catch (err) { toast.error("Something went wrong!"); console.log("Failed to create a new section", err); } }; const onReorder = async (updateData: { id: string; position: number }[]) => { try { await axios.put(`/api/courses/${course.id}/sections/reorder`, { list: updateData, }); toast.success("Sections reordered successfully"); } catch (err) { console.log("Failed to reorder sections", err); toast.error("Something went wrong!"); } }; return (
{routes.map((route) => ( ))}
router.push(`/instructor/courses/${course.id}/sections/${id}`) } />

Add New Section

( Title )} />
); }; export default CreateSectionForm; ================================================ FILE: components/sections/EditSectionForm.tsx ================================================ "use client"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { MuxData, Resource, Section } from "@prisma/client"; import Link from "next/link"; import axios from "axios"; import { useRouter } from "next/navigation"; import toast from "react-hot-toast"; import { ArrowLeft, Loader2, Trash } from "lucide-react"; import MuxPlayer from "@mux/mux-player-react"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import RichEditor from "@/components/custom/RichEditor"; import FileUpload from "../custom/FileUpload"; import { Switch } from "@/components/ui/switch"; import ResourceForm from "@/components/sections/ResourceForm"; import Delete from "@/components/custom/Delete"; import PublishButton from "@/components/custom/PublishButton"; const formSchema = z.object({ title: z.string().min(2, { message: "Title is required and must be at least 2 characters long", }), description: z.string().optional(), videoUrl: z.string().optional(), isFree: z.boolean().optional(), }); interface EditSectionFormProps { section: Section & { resources: Resource[]; muxData?: MuxData | null }; courseId: string; isCompleted: boolean; } const EditSectionForm = ({ section, courseId, isCompleted, }: EditSectionFormProps) => { const router = useRouter(); // 1. Define your form. const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { title: section.title, description: section.description || "", videoUrl: section.videoUrl || "", isFree: section.isFree, }, }); const { isValid, isSubmitting } = form.formState; // 2. Define a submit handler. const onSubmit = async (values: z.infer) => { try { await axios.post( `/api/courses/${courseId}/sections/${section.id}`, values ); toast.success("Section Updated"); router.refresh(); } catch (err) { console.log("Failed to update the section", err); toast.error("Something went wrong!"); } }; return ( <>

Section Details

Complete this section with detailed information, good video and resources to give your students the best learning experience

( Title * )} /> ( Description * )} /> {section.videoUrl && (
)} ( Video * field.onChange(url)} endpoint="sectionVideo" page="Edit Section" /> )} /> (
Accessibility Everyone can access this section for FREE
)} />
); }; export default EditSectionForm; ================================================ FILE: components/sections/ProgressButton.tsx ================================================ "use client"; import axios from "axios"; import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; import { Button } from "@/components/ui/button"; import { CheckCircle, Loader2 } from "lucide-react"; interface ProgressButtonProps { courseId: string; sectionId: string; isCompleted: boolean; } const ProgressButton = ({ courseId, sectionId, isCompleted, }: ProgressButtonProps) => { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const onClick = async () => { try { setIsLoading(true); await axios.post(`/api/courses/${courseId}/sections/${sectionId}/progress`, { isCompleted: !isCompleted, }); toast.success("Progress updated!"); router.refresh(); } catch (err) { console.log("Failed to update progress", err); toast.error("Something went wrong!"); } finally { setIsLoading(false); } }; return ( ); }; export default ProgressButton; ================================================ FILE: components/sections/ResourceForm.tsx ================================================ "use client"; import { Resource, Section } from "@prisma/client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import axios from "axios"; import { File, Loader2, PlusCircle, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import FileUpload from "@/components/custom/FileUpload"; const formSchema = z.object({ name: z.string().min(2, { message: "Name is required and must be at least 2 characters long", }), fileUrl: z.string().min(1, { message: "File is required", }), }); interface ResourceFormProps { section: Section & { resources: Resource[] }; courseId: string; } const ResourceForm = ({ section, courseId }: ResourceFormProps) => { const router = useRouter(); // 1. Define your form. const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: "", fileUrl: "", }, }); const { isValid, isSubmitting } = form.formState; // 2. Define a submit handler. const onSubmit = async (values: z.infer) => { try { await axios.post( `/api/courses/${courseId}/sections/${section.id}/resources`, values ); toast.success("New Resource uploaded!"); form.reset(); router.refresh(); } catch (err) { toast.error("Something went wrong!"); console.log("Failed to upload resource", err); } }; const onDelete = async (id: string) => { try { await axios.post( `/api/courses/${courseId}/sections/${section.id}/resources/${id}` ); toast.success("Resource deleted!"); router.refresh(); } catch (err) { toast.error("Something went wrong!"); console.log("Failed to delete resource", err); } }; return ( <>
Add Resources (optional)

Add resources to this section to help students learn better.

{section.resources.map((resource) => (
{resource.name}
))}
( File Name )} /> ( Upload File field.onChange(url)} endpoint="sectionResource" page="Edit Section" /> )} />
); }; export default ResourceForm; ================================================ FILE: components/sections/SectionList.tsx ================================================ import { Section } from "@prisma/client"; import { DragDropContext, Droppable, Draggable, DropResult, } from "@hello-pangea/dnd"; import { useEffect, useState } from "react"; import { Grip, Pencil } from "lucide-react"; interface SectionListProps { items: Section[]; onReorder: (updateData: { id: string; position: number }[]) => void; onEdit: (id: string) => void; } const SectionList = ({ items, onReorder, onEdit }: SectionListProps) => { const [isMounted, setIsMounted] = useState(false); const [sections, setSections] = useState(items); useEffect(() => { setIsMounted(true); }, []); useEffect(() => { setSections(items); }, [items]); const onDragEnd = (result: DropResult) => { if (!result.destination) return; const items = Array.from(sections); const [reorderedItem] = items.splice(result.source.index, 1); items.splice(result.destination.index, 0, reorderedItem); const startIndex = Math.min(result.source.index, result.destination.index); const endIndex = Math.max(result.source.index, result.destination.index); const updatedSections = items.slice(startIndex, endIndex + 1); setSections(items); const bulkUpdateData = updatedSections.map((section) => ({ id: section.id, position: items.findIndex((item) => item.id === section.id), })); onReorder(bulkUpdateData); }; if (!isMounted) return null; return ( {(provided) => (
0 ? "my-10" : "mt-7" } flex flex-col gap-5`} > {sections.map((section, index) => ( {(provided) => (
{section.title}
onEdit(section.id)} />
)}
))} {provided.placeholder}
)}
); }; export default SectionList; ================================================ FILE: components/sections/SectionsDetails.tsx ================================================ "use client"; import { Course, MuxData, Progress, Purchase, Resource, Section, } from "@prisma/client"; import toast from "react-hot-toast"; import { useState } from "react"; import axios from "axios"; import { File, Loader2, Lock } from "lucide-react"; import { Button } from "@/components/ui/button"; import ReadText from "@/components/custom/ReadText"; import MuxPlayer from "@mux/mux-player-react"; import Link from "next/link"; import ProgressButton from "./ProgressButton"; import SectionMenu from "../layout/SectionMenu"; interface SectionsDetailsProps { course: Course & { sections: Section[] }; section: Section; purchase: Purchase | null; muxData: MuxData | null; resources: Resource[] | []; progress: Progress | null; } const SectionsDetails = ({ course, section, purchase, muxData, resources, progress, }: SectionsDetailsProps) => { const [isLoading, setIsLoading] = useState(false); const isLocked = !purchase && !section.isFree; const buyCourse = async () => { try { setIsLoading(true); const response = await axios.post(`/api/courses/${course.id}/checkout`); window.location.assign(response.data.url); } catch (err) { console.log("Failed to chechout course", err); toast.error("Something went wrong!"); } finally { setIsLoading(false); } }; return (

{section.title}

{!purchase ? ( ) : ( // !! converts falsy values to boolean false )}
{isLocked ? (

Video for this section is locked!. Please buy the course to access

) : ( )}

Resources

{resources.map((resource) => ( {resource.name} ))}
); }; export default SectionsDetails; ================================================ FILE: components/ui/alert-dialog.tsx ================================================ "use client" import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName const AlertDialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) AlertDialogHeader.displayName = "AlertDialogHeader" const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) AlertDialogFooter.displayName = "AlertDialogFooter" const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName const AlertDialogAction = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName const AlertDialogCancel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, } ================================================ FILE: components/ui/alert.tsx ================================================ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( "relative w-full rounded-lg border p-2 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { variants: { variant: { default: "bg-background text-foreground", complete: "bg-[#C5DDD6] text-foreground", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, }, defaultVariants: { variant: "default", }, } ) const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => (
)) Alert.displayName = "Alert" const AlertTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertTitle.displayName = "AlertTitle" const AlertDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertDescription.displayName = "AlertDescription" export { Alert, AlertTitle, AlertDescription } ================================================ FILE: components/ui/badge.tsx ================================================ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", outline: "text-foreground", }, }, defaultVariants: { variant: "default", }, } ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return (
) } export { Badge, badgeVariants } ================================================ FILE: components/ui/button.tsx ================================================ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-[#FDAB04] text-black-foreground hover:bg-[#FDAB04]/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-[#FFF8EB] hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", complete: "bg-[#C5DDD6] text-black-foreground hover:bg-[#C5DDD6]/90" }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export { Button, buttonVariants } ================================================ FILE: components/ui/card.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) Card.displayName = "Card" const CardHeader = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardHeader.displayName = "CardHeader" const CardTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardFooter.displayName = "CardFooter" export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ================================================ FILE: components/ui/command.tsx ================================================ "use client" import * as React from "react" import { type DialogProps } from "@radix-ui/react-dialog" import { Command as CommandPrimitive } from "cmdk" import { Search } from "lucide-react" import { cn } from "@/lib/utils" import { Dialog, DialogContent } from "@/components/ui/dialog" const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Command.displayName = CommandPrimitive.displayName interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( {children} ) } const CommandInput = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => (
)) CommandInput.displayName = CommandPrimitive.Input.displayName const CommandList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandList.displayName = CommandPrimitive.List.displayName const CommandEmpty = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >((props, ref) => ( )) CommandEmpty.displayName = CommandPrimitive.Empty.displayName const CommandGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandGroup.displayName = CommandPrimitive.Group.displayName const CommandSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandSeparator.displayName = CommandPrimitive.Separator.displayName const CommandItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandItem.displayName = CommandPrimitive.Item.displayName const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ) } CommandShortcut.displayName = "CommandShortcut" export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, } ================================================ FILE: components/ui/dialog.tsx ================================================ "use client" import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react" import { cn } from "@/lib/utils" const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger const DialogPortal = DialogPrimitive.Portal const DialogClose = DialogPrimitive.Close const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} Close )) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } ================================================ FILE: components/ui/form.tsx ================================================ import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot" import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext, } from "react-hook-form" import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath > = { name: TName } const FormFieldContext = React.createContext( {} as FormFieldContextValue ) const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath >({ ...props }: ControllerProps) => { return ( ) } const useFormField = () => { const fieldContext = React.useContext(FormFieldContext) const itemContext = React.useContext(FormItemContext) const { getFieldState, formState } = useFormContext() const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { throw new Error("useFormField should be used within ") } const { id } = itemContext return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, } } type FormItemContextValue = { id: string } const FormItemContext = React.createContext( {} as FormItemContextValue ) const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { const id = React.useId() return (
) }) FormItem.displayName = "FormItem" const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField() return (