) => {
54 | if (e.key === "Enter") {
55 | e.preventDefault();
56 | const search = form.getValues("search");
57 | router.push(`/courses?search=${encodeURIComponent(search)}`);
58 | }
59 | };
60 |
61 | return (
62 |
69 | );
70 | };
71 |
72 | export default SearchInput;
73 |
--------------------------------------------------------------------------------
/src/app/(common)/courses/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import React from "react";
3 |
4 | const loading = () => {
5 | const user = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
6 | return (
7 |
8 |
9 |
10 |
Courses
11 |
12 | (Choose your course and dive into career)
13 |
14 |
15 |
16 |
17 |
18 | {user.map((_, index) => (
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ))}
30 |
31 |
32 | );
33 | };
34 |
35 | export default loading;
36 |
--------------------------------------------------------------------------------
/src/app/(common)/courses/page.tsx:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import Categories from "./_components/Categories";
3 | import Courses from "./_components/Courses";
4 | import { Input } from "@/components/ui/input";
5 | import SearchInput from "./_components/SearchInput";
6 |
7 | const page = async ({ searchParams }: { searchParams: { search: string } }) => {
8 | const cats = await prisma.category.findMany();
9 | return (
10 |
11 |
12 |
13 |
Courses
14 |
15 | (Choose your course and dive into career)
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default page;
31 |
--------------------------------------------------------------------------------
/src/app/(common)/dashboard/_components/CourseCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { currencyFormater } from "@/lib/utils";
3 | import { Course, User } from "@prisma/client";
4 | import { BookOpenCheck } from "lucide-react";
5 | import Image from "next/image";
6 | import Link from "next/link";
7 | import React, { useEffect } from "react";
8 | import { GiPayMoney, GiReceiveMoney } from "react-icons/gi";
9 | import { Progress } from "@/components/ui/progress";
10 | import {
11 | courseAccess,
12 | getTotalCourseProgress,
13 | } from "@/app/(common)/courses/actions";
14 | import { Button } from "@/components/ui/button";
15 | interface CourseCardProps {
16 | course: Course & { user: User };
17 | chapters: number;
18 | }
19 | const CourseCard = ({ course, chapters }: CourseCardProps) => {
20 | const [visitedUser, setVisitedUser] = React.useState(false);
21 | const [isCourseAccessableByTheUser, setIsCourseAccessableByTheUser] =
22 | React.useState(false);
23 | const [value, setValue] = React.useState(0);
24 |
25 | useEffect(() => {
26 | courseAccess(course.id).then((data) => {
27 | setVisitedUser(data.visitedUser);
28 | setIsCourseAccessableByTheUser(data.isCourseAccessableByTheUser);
29 | });
30 | getTotalCourseProgress(course.id).then((data) => {
31 | setValue(data);
32 | });
33 | }, []);
34 |
35 | return (
36 |
40 |
47 |
48 |
{course.title}
49 |
50 |
51 |
52 | {chapters}
53 |
54 |
55 | {!visitedUser &&
56 | (isCourseAccessableByTheUser ? (
57 |
58 | ) : (
59 |
60 | ))}
61 |
62 | {isCourseAccessableByTheUser &&
}
63 |
64 |
68 | by {course.user.name}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default CourseCard;
77 |
--------------------------------------------------------------------------------
/src/app/(common)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import React from "react";
3 |
4 | const loading = () => {
5 | const user = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
6 | return (
7 |
8 |
9 |
10 |
Courses
11 |
12 | (View your registered courses)
13 |
14 |
15 |
16 |
17 | {user.map((access) => (
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ))}
29 |
30 |
31 | );
32 | };
33 |
34 | export default loading;
35 |
--------------------------------------------------------------------------------
/src/app/(common)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { redirect } from "next/navigation";
4 | import React from "react";
5 | import CourseCard from "./_components/CourseCard";
6 | import { SignInButton } from "@clerk/nextjs";
7 | import { Button } from "@/components/ui/button";
8 | import Link from "next/link";
9 |
10 | const page = async ({ params }: { params: string }) => {
11 | const { userId } = auth();
12 | if (!userId)
13 | return (
14 |
15 | Sign in to view your courses
16 |
17 |
18 |
19 |
20 | );
21 |
22 | const user = await prisma.user.findUnique({
23 | where: {
24 | authId: userId,
25 | },
26 | include: {
27 | accesses: {
28 | include: {
29 | course: {
30 | include: {
31 | _count: {
32 | select: {
33 | chapters: {
34 | where: {
35 | isPublished: true,
36 | },
37 | },
38 | },
39 | },
40 | user: true,
41 | },
42 | },
43 | },
44 | },
45 | },
46 | });
47 | if (!user) redirect("/not-authorized");
48 | return (
49 |
50 |
51 |
52 |
Courses
53 |
54 | (Here are your registered courses)
55 |
56 |
57 |
58 | {
59 | // If user has no courses
60 | user.accesses.length === 0 && (
61 |
62 |
63 | You have not registered for any courses yet.
64 |
65 |
66 |
Browse Courses
67 |
68 |
69 | )
70 | }
71 |
72 | {user.accesses.map((access) => (
73 |
78 | ))}
79 |
80 |
81 | );
82 | };
83 |
84 | export default page;
85 |
--------------------------------------------------------------------------------
/src/app/(common)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from "@/components/Navbar";
2 | import Sidebar from "@/components/Sidebar";
3 | import SidebarWraper from "@/components/SidebarWraper";
4 | import { prisma } from "@/lib/db";
5 | import { auth } from "@clerk/nextjs/server";
6 | import { redirect } from "next/navigation";
7 |
8 | import React from "react";
9 |
10 | const layout = async ({ children }: { children: React.ReactNode }) => {
11 | let isTeacher = false;
12 | let visitedUser = false;
13 | const { userId } = auth();
14 | if (userId) {
15 | const user = await prisma.user.findUnique({
16 | where: {
17 | authId: userId,
18 | },
19 | });
20 |
21 | if (!user) redirect("/onboarding");
22 |
23 | if (!user.onBoarded) redirect("/onboarding");
24 | if (user.role === "TEACHER") {
25 | isTeacher = true;
26 | }
27 | } else {
28 | visitedUser = true;
29 | }
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default layout;
38 |
39 | //
40 | //
41 | //
42 | //
43 | //
44 | //
45 | // {children}
46 | //
47 | //
48 |
--------------------------------------------------------------------------------
/src/app/(common)/preview/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import React from "react";
3 |
4 | const loading = () => {
5 | return (
6 |
7 |
8 | {/* Left Column */}
9 |
10 |
11 | {/* Right Column */}
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default loading;
22 |
--------------------------------------------------------------------------------
/src/app/(common)/preview/page.tsx:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import React from "react";
3 | import CoursePreviewPage from "./_components/CoursePreviewPage";
4 | import { auth } from "@clerk/nextjs/server";
5 |
6 | const Page = async ({
7 | searchParams,
8 | }: {
9 | searchParams: { courseId: string };
10 | }) => {
11 | const courseId = searchParams.courseId;
12 | if (!courseId) {
13 | return (
14 |
15 |
Course not found
16 |
17 | );
18 | }
19 |
20 | const course = await prisma.course.findUnique({
21 | where: {
22 | id: courseId,
23 | },
24 | include: {
25 | _count: {
26 | select: {
27 | accesses: {
28 | where: { courseId },
29 | },
30 | },
31 | },
32 | category: true,
33 | user: true,
34 | chapters: {
35 | select: {
36 | title: true,
37 | isFree: true,
38 | id: true,
39 | },
40 | },
41 | },
42 | });
43 |
44 | if (!course) {
45 | return (
46 |
47 |
Course not found
48 |
49 | );
50 | }
51 |
52 | let isEnrolled = false;
53 | let visitedUser = true;
54 | let isAuthor = false;
55 |
56 | const { userId } = auth();
57 | if (userId) {
58 | const user = await prisma.user.findUnique({
59 | where: {
60 | authId: userId,
61 | },
62 | });
63 |
64 | if (user) {
65 | const access = await prisma.access.findFirst({
66 | where: {
67 | userId: user.id,
68 | courseId,
69 | },
70 | });
71 |
72 | isEnrolled = !!access;
73 | }
74 | visitedUser = false;
75 | isAuthor = course.user.authId === userId;
76 | }
77 |
78 | return (
79 |
80 |
86 |
87 | );
88 | };
89 |
90 | export default Page;
91 |
--------------------------------------------------------------------------------
/src/app/(common)/profile/[profileId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 |
6 | const loading = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {[1, 2, 3].map((i) => (
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {[1, 2, 3].map((i) => (
47 |
48 | ))}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {[1, 2, 3].map((i) => (
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {[1, 2, 3].map((i) => (
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default loading;
89 |
--------------------------------------------------------------------------------
/src/app/(common)/profile/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 |
6 | const loading = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {[1, 2, 3].map((i) => (
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {[1, 2, 3].map((i) => (
47 |
48 | ))}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {[1, 2, 3].map((i) => (
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {[1, 2, 3].map((i) => (
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default loading;
89 |
--------------------------------------------------------------------------------
/src/app/(common)/settings/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { prisma } from "@/lib/db";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { render } from "@react-email/render";
6 | import nodemailer from "nodemailer";
7 | import WelcomeToLMS from "../../../../emails/WelcomeToLMS";
8 |
9 | export const onboarding = async () => {
10 | try {
11 | const { userId } = auth();
12 | if (!userId) {
13 | return { message: "Unauthorized" };
14 | }
15 | const user = await prisma.user.findUnique({
16 | where: {
17 | authId: userId,
18 | },
19 | });
20 | if (!user) {
21 | return { message: "User not found" };
22 | }
23 |
24 | const transport = nodemailer.createTransport({
25 | service: "Gmail",
26 | host: "smtp.gmail.com",
27 | port: 465,
28 | secure: true,
29 | auth: {
30 | user: process.env.MAIL_USER,
31 | pass: process.env.MAIL_PASS,
32 | },
33 | });
34 |
35 | const mailOptions = {
36 | from: process.env.MAIL_USER,
37 | to: user.email,
38 | subject: "Welcome To YourLMS Portal (Role Change)",
39 | html: render(
40 | WelcomeToLMS({
41 | name: user?.name || user.email.split("@")[0],
42 | role: "TEACHER",
43 | })
44 | ),
45 | };
46 |
47 | transport.sendMail(mailOptions, function (error, info) {
48 | if (error) {
49 | console.log(error);
50 | throw new Error("Error sending email");
51 | }
52 | });
53 | } catch (error) {
54 | throw new Error("Error in onboarding");
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/src/app/(common)/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import { cn } from "@/lib/utils";
3 | import React from "react";
4 |
5 | const loading = () => {
6 | const settings = [1, 2, 3, 4, 5];
7 | return (
8 |
9 |
10 |
11 |
Settings
12 |
13 | (Manage your account settings)
14 |
15 |
16 |
17 |
18 |
19 | {settings.map((_, index) => (
20 |
21 | ))}
22 |
23 |
24 | {settings.map((_, index) => (
25 |
29 | ))}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default loading;
37 |
--------------------------------------------------------------------------------
/src/app/(common)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { prisma } from "@/lib/db";
3 | import { SignInButton } from "@clerk/nextjs";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { redirect } from "next/navigation";
6 | import React from "react";
7 | import SettingsField from "./_components/SettingsField";
8 |
9 | const page = async () => {
10 | const { userId } = auth();
11 | if (!userId)
12 | return (
13 |
14 | Sign in to view your settings
15 |
16 |
17 |
18 |
19 | );
20 |
21 | const user = await prisma.user.findUnique({
22 | where: {
23 | authId: userId,
24 | },
25 | });
26 | if (!user) redirect("/not-authorized");
27 |
28 | return (
29 |
30 |
31 |
32 |
Settings
33 |
34 | (Manage your account settings)
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default page;
44 |
--------------------------------------------------------------------------------
/src/app/api/student/request/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function POST(req: Request, res: Response) {
6 | const data: {
7 | email: string;
8 | request: string;
9 | phone: string;
10 | } = await req.json();
11 |
12 | if (!data.email || !data.request || !data.phone) {
13 | return NextResponse.json({ message: "Invalid request" }, { status: 400 });
14 | }
15 |
16 | const { userId } = auth();
17 | if (!userId) {
18 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
19 | }
20 | const user = await prisma.user.findUnique({
21 | where: {
22 | authId: userId,
23 | },
24 | });
25 |
26 | if (!user) {
27 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
28 | }
29 |
30 | await prisma.requests.create({
31 | data: {
32 | email: data.email,
33 | message: data.request,
34 | phone: data.phone,
35 | userId: user.id,
36 | },
37 | });
38 |
39 | return NextResponse.json(user, { status: 200 });
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/api/student/update/chapter/[chapterId]/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | export async function POST(
6 | req: NextRequest,
7 | { params }: { params: { chapterId: string } }
8 | ) {
9 | try {
10 | const { userId } = auth();
11 | if (!userId) {
12 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
13 | }
14 | if (!params.chapterId) {
15 | return NextResponse.json(
16 | { message: "chapterID is required" },
17 | { status: 400 }
18 | );
19 | }
20 | const value = await req.json();
21 |
22 | const user = await prisma.user.findUnique({
23 | where: {
24 | authId: userId,
25 | },
26 | });
27 |
28 | if (!user) {
29 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
30 | }
31 |
32 | const chapter = await prisma.progress.upsert({
33 | where: {
34 | userId_chapterId: {
35 | chapterId: params.chapterId,
36 | userId: user.id,
37 | },
38 | },
39 | update: {
40 | status: value.completed ? "COMPLETED" : "IN_PROGRESS",
41 | },
42 | create: {
43 | chapterId: params.chapterId,
44 | userId: user.id,
45 | status: value.completed ? "COMPLETED" : "IN_PROGRESS",
46 | },
47 | });
48 |
49 | return NextResponse.json({ message: "progress updated" }, { status: 200 });
50 | } catch (error) {
51 | return NextResponse.json(
52 | { message: "Internal server error" },
53 | { status: 500 }
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/api/student/update/settings/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function PUT(req: Request, res: Response) {
6 | const { name, bio, roleChange, signature } = await req.json();
7 | if (!name || !bio) {
8 | return NextResponse.json(
9 | { message: "Name and Bio are required" },
10 | { status: 400 }
11 | );
12 | }
13 | const { userId } = auth();
14 | if (!userId) {
15 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
16 | }
17 | let user;
18 | if (!roleChange) {
19 | user = await prisma.user.update({
20 | where: {
21 | authId: userId,
22 | },
23 | data: {
24 | name,
25 | bio,
26 | },
27 | });
28 | } else {
29 | if (!name || !bio || !signature) {
30 | return NextResponse.json(
31 | { message: "Name and Bio are required" },
32 | { status: 400 }
33 | );
34 | }
35 | user = await prisma.user.update({
36 | where: {
37 | authId: userId,
38 | },
39 | data: {
40 | name,
41 | bio,
42 | role: "TEACHER",
43 | signature,
44 | },
45 | });
46 | }
47 | return NextResponse.json(user, { status: 200 });
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/api/teacher/add-users/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | import { render } from "@react-email/render";
6 | import nodemailer from "nodemailer";
7 | import { WelcomeToLMS } from "@/templates/WelcomeToLMS";
8 |
9 | export async function POST(req: Request) {
10 | try {
11 | const values: string[] = await req.json();
12 | if (!values) {
13 | return NextResponse.json(
14 | { message: "field is required to create" },
15 | { status: 400 }
16 | );
17 | }
18 |
19 | const { userId } = auth();
20 |
21 | if (!userId) {
22 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
23 | }
24 |
25 | const user = await prisma.user.findUnique({
26 | where: {
27 | authId: userId,
28 | },
29 | });
30 |
31 | if (!user) {
32 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
33 | }
34 |
35 | if (user?.role !== "TEACHER") {
36 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
37 | }
38 |
39 | const users = await prisma.user.createManyAndReturn({
40 | data: values.map((email) => {
41 | return {
42 | role: "STUDENT",
43 | email,
44 | name: email.split("@")[0],
45 | authId: email,
46 | };
47 | }),
48 | skipDuplicates: true,
49 | });
50 |
51 | //node mailer
52 | const emails = users.map((user) => user.email);
53 |
54 | const transport = nodemailer.createTransport({
55 | service: "gmail",
56 | auth: {
57 | user: process.env.MAIL_USER,
58 | pass: process.env.MAIL_PASS,
59 | },
60 | });
61 |
62 | const sendAllEmails = emails.map((email) => {
63 | const user = users.find((user) => user.email === email);
64 | return transport.sendMail({
65 | from: process.env.MAIL_USER,
66 | to: email,
67 | subject: "Welcome To LMS",
68 | html: render(
69 | WelcomeToLMS({
70 | email,
71 | studentFirstName: user?.name || "student",
72 | })
73 | ),
74 | });
75 | });
76 | const data = await Promise.all(sendAllEmails);
77 |
78 | return NextResponse.json(
79 | { mesage: "users created and mails sent..." },
80 | { status: 201 }
81 | );
82 | } catch (error) {
83 | return NextResponse.json(
84 | { message: "Internal server error" },
85 | { status: 500 }
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/api/teacher/create/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function POST(req: Request) {
6 | try {
7 | const { title } = await req.json();
8 | if (!title) {
9 | return NextResponse.json(
10 | { message: "Title is required" },
11 | { status: 400 }
12 | );
13 | }
14 |
15 | const { userId } = auth();
16 | if (!userId) {
17 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
18 | }
19 |
20 | const user = await prisma.user.findUnique({
21 | where: {
22 | authId: userId,
23 | },
24 | });
25 |
26 | if (!user) {
27 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
28 | }
29 |
30 | if (user.role !== "TEACHER") {
31 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
32 | }
33 |
34 | const course = await prisma.course.findFirst({
35 | where: {
36 | title,
37 | userId: user.id,
38 | },
39 | });
40 | if (course) {
41 | return NextResponse.json(
42 | { message: "Course with this title already present" },
43 | { status: 400 }
44 | );
45 | }
46 |
47 | const res = await prisma.course.create({
48 | data: {
49 | title,
50 | userId: user.id,
51 | },
52 | });
53 |
54 | const data = {
55 | id: res.id,
56 | title: res.title,
57 | };
58 |
59 | return NextResponse.json(
60 | { ...data, mesage: "course created..." },
61 | { status: 201 }
62 | );
63 | } catch (error) {
64 | return NextResponse.json(
65 | { message: "Internal server error" },
66 | { status: 500 }
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/api/teacher/queries/[queryId]/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function PUT(
6 | req: Request,
7 | { params }: { params: { queryId: string } }
8 | ) {
9 | try {
10 | if (!params.queryId) {
11 | return NextResponse.json(
12 | { message: "QueryId is required" },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | const { userId } = auth();
18 | if (!userId) {
19 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
20 | }
21 |
22 | const value = await req.json();
23 |
24 | if (!value) {
25 | return NextResponse.json(
26 | { message: "Invalid request body" },
27 | { status: 400 }
28 | );
29 | }
30 |
31 | await prisma.requests.update({
32 | where: {
33 | id: params.queryId,
34 | },
35 | data: {
36 | status: value.status === "COMPLETED" ? "IN_PROGRESS" : "COMPLETED",
37 | },
38 | });
39 |
40 | return NextResponse.json(
41 | { message: "Query resolved successfully" },
42 | { status: 200 }
43 | );
44 | } catch (error) {
45 | return NextResponse.json(
46 | { message: "Internal server error" },
47 | { status: 500 }
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/api/teacher/update/[courseId]/chapter/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | export async function POST(
6 | req: NextRequest,
7 | { params }: { params: { courseId: string } }
8 | ) {
9 | try {
10 | if (!params.courseId) {
11 | return NextResponse.json(
12 | { message: "CourseId is required" },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | const { userId } = auth();
18 | if (!userId) {
19 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
20 | }
21 |
22 | const user = await prisma.user.findUnique({
23 | where: {
24 | authId: userId,
25 | },
26 | });
27 |
28 | if (!user) {
29 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
30 | }
31 |
32 | const course = await prisma.course.findUnique({
33 | where: {
34 | id: params.courseId,
35 | userId: user.id,
36 | },
37 | include: {
38 | chapters: true,
39 | },
40 | });
41 |
42 | if (!course) {
43 | return NextResponse.json(
44 | { message: "Course with this id not found" },
45 | { status: 400 }
46 | );
47 | }
48 |
49 | const value = await req.json();
50 | if (!value) {
51 | return NextResponse.json(
52 | { message: "title is required to create" },
53 | { status: 400 }
54 | );
55 | }
56 |
57 | const chapter = await prisma.chapter.create({
58 | data: {
59 | title: value.title,
60 | courseId: params.courseId,
61 | order: course.chapters.length ? course.chapters.length + 1 : 1,
62 | },
63 | });
64 |
65 | return NextResponse.json(
66 | { message: "Chapter created", ...chapter },
67 | { status: 201 }
68 | );
69 | } catch (error) {
70 | return NextResponse.json(
71 | { message: "Internal server error" },
72 | { status: 500 }
73 | );
74 | }
75 | }
76 |
77 | export async function PUT(
78 | req: NextRequest,
79 | { params }: { params: { courseId: string } }
80 | ) {
81 | try {
82 | if (!params.courseId) {
83 | return NextResponse.json(
84 | { message: "CourseId is required" },
85 | { status: 400 }
86 | );
87 | }
88 | const value = await req.json();
89 | if (!value) {
90 | return NextResponse.json(
91 | { message: "chapters are required to reorder" },
92 | { status: 400 }
93 | );
94 | }
95 |
96 | for (let i = 0; i < value.length; i++) {
97 | await prisma.chapter.update({
98 | where: {
99 | id: value[i].id,
100 | courseId: params.courseId,
101 | },
102 | data: {
103 | order: value[i].order,
104 | },
105 | });
106 | }
107 |
108 | return NextResponse.json({ message: "Chapter reorderd" }, { status: 200 });
109 | } catch (error) {
110 | return NextResponse.json(
111 | { message: "Internal server error" },
112 | { status: 500 }
113 | );
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/app/api/teacher/update/[courseId]/users/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { auth } from "@clerk/nextjs/server";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function PUT(
6 | req: Request,
7 | { params }: { params: { courseId: string } }
8 | ) {
9 | try {
10 | if (!params.courseId) {
11 | return NextResponse.json(
12 | { message: "CourseId is required" },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | const { userId } = auth();
18 | if (!userId) {
19 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
20 | }
21 |
22 | const { id } = await req.json();
23 |
24 | if (!id) {
25 | return NextResponse.json(
26 | { message: "student id is required to remove" },
27 | { status: 400 }
28 | );
29 | }
30 |
31 | const user = await prisma.user.findUnique({
32 | where: {
33 | authId: userId,
34 | },
35 | });
36 |
37 | if (!user) {
38 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
39 | }
40 |
41 | const course = await prisma.course.findFirst({
42 | where: {
43 | id: params.courseId,
44 | userId: user.id,
45 | },
46 | });
47 | if (!course) {
48 | return NextResponse.json(
49 | { message: "Course with this id not found" },
50 | { status: 400 }
51 | );
52 | }
53 |
54 | const access = await prisma.access.findFirst({
55 | where: {
56 | courseId: params.courseId,
57 | userId: id,
58 | },
59 | });
60 | if (!access) {
61 | return NextResponse.json(
62 | { message: "Student with this id not found" },
63 | { status: 400 }
64 | );
65 | }
66 |
67 | await prisma.access.delete({
68 | where: {
69 | id: access.id,
70 | },
71 | });
72 |
73 | return NextResponse.json({ mesage: "course deleted..." }, { status: 201 });
74 | } catch (error) {
75 | return NextResponse.json(
76 | { message: "Internal server error" },
77 | { status: 500 }
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/core.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server";
2 | import { redirect } from "next/navigation";
3 | import { createUploadthing, type FileRouter } from "uploadthing/next";
4 |
5 | const f = createUploadthing();
6 |
7 | const funAuth = async () => {
8 | const { userId } = auth();
9 | if (!userId) {
10 | return redirect("/");
11 | }
12 | return { userId };
13 | };
14 |
15 | export const ourFileRouter = {
16 | uploadThumbnail: f({ image: { maxFileCount: 1 } })
17 | .middleware(() => funAuth())
18 | .onUploadComplete(() => {}),
19 | uploadChapterVideo: f({
20 | "video/mp4": { maxFileCount: 1, maxFileSize: "8GB" },
21 | "video/ogg": { maxFileCount: 1, maxFileSize: "8GB" },
22 | "video/webm": { maxFileCount: 1, maxFileSize: "8GB" },
23 | })
24 | .middleware(() => funAuth())
25 | .onUploadComplete(() => {}),
26 | uploadChapterAttachement: f({
27 | image: { maxFileCount: 5 },
28 | text: { maxFileCount: 5 },
29 | pdf: { maxFileCount: 5 },
30 | "application/docbook+xml": { maxFileCount: 3 },
31 | })
32 | .middleware(() => funAuth())
33 | .onUploadComplete(() => {}),
34 | uploadBasicStuff: f({ image: { maxFileCount: 1 } })
35 | .middleware(() => funAuth())
36 | .onUploadComplete(() => {}),
37 | } satisfies FileRouter;
38 |
39 | export type OurFileRouter = typeof ourFileRouter;
40 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandler } from "uploadthing/next";
2 |
3 | import { ourFileRouter } from "./core";
4 |
5 | // Export routes for Next App Router
6 | export const { GET, POST } = createRouteHandler({
7 | router: ourFileRouter,
8 |
9 | // Apply an (optional) custom config:
10 | // config: { ... },
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "@/lib/db";
2 | import { Webhook } from "svix";
3 |
4 | const webhookSecret: string = process.env.CLERK_WEBHOOK_SECRET!;
5 |
6 | export async function POST(req: Request) {
7 | const svix_id = req.headers.get("svix-id") ?? "";
8 | const svix_timestamp = req.headers.get("svix-timestamp") ?? "";
9 | const svix_signature = req.headers.get("svix-signature") ?? "";
10 |
11 | const body = await req.text();
12 |
13 | const sivx = new Webhook(webhookSecret);
14 |
15 | let msg: any;
16 |
17 | try {
18 | msg = sivx.verify(body, {
19 | "svix-id": svix_id,
20 | "svix-timestamp": svix_timestamp,
21 | "svix-signature": svix_signature,
22 | });
23 | } catch (err) {
24 | return new Response("Bad Request", { status: 400 });
25 | }
26 |
27 | // Rest
28 | if (msg && msg?.type) {
29 | if (msg.type == "user.created") {
30 | await prisma.user.create({
31 | data: {
32 | authId: msg.data.id,
33 | email: msg.data.email,
34 | name: msg.data.firstName,
35 | role: "STUDENT",
36 | },
37 | });
38 | } else if (msg.type == "user.updated") {
39 | await prisma.user.update({
40 | where: {
41 | authId: msg.data.id,
42 | },
43 | data: {
44 | email: msg.data.email,
45 | name: msg.data.firstName,
46 | },
47 | });
48 | } else if (msg.type == "user.deleted") {
49 | await prisma.user.delete({
50 | where: {
51 | authId: msg.data.id,
52 | },
53 | });
54 | }
55 | }
56 |
57 | return new Response("OK", { status: 200 });
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const layout = ({ children }: { children: React.ReactNode }) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
11 | export default layout;
12 |
--------------------------------------------------------------------------------
/src/app/auth/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import { SignIn, ClerkLoading } from "@clerk/nextjs";
3 |
4 | export default function Page() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/auth/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import { ClerkLoading, SignUp } from "@clerk/nextjs";
3 |
4 | export default function Page() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/course/[courseId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 |
4 | const loading = () => {
5 | return (
6 |
7 | {/* Left Part Skeleton */}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {[...Array(8)].map((_, index) => (
16 |
17 | ))}
18 |
19 |
20 |
21 |
22 | {/* Right Part Skeleton */}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default loading;
47 |
--------------------------------------------------------------------------------
/src/app/course/[courseId]/page.tsx:
--------------------------------------------------------------------------------
1 | import LeftPart from "../_components/LeftPart";
2 | import RightPart from "../_components/RightPart";
3 | import Navbar from "@/components/Navbar";
4 | import { courseAccess } from "@/app/(common)/courses/actions";
5 | import MobileLeftPart from "../_components/MobileLeftPart";
6 | import { redirect } from "next/navigation";
7 | import { prisma } from "@/lib/db";
8 |
9 | const Page = async ({ params }: { params: { courseId: string } }) => {
10 | const courseId = params.courseId;
11 |
12 | if (!courseId) {
13 | redirect("/not-found");
14 | }
15 |
16 | const course = await prisma.course.findUnique({
17 | where: {
18 | id: courseId,
19 | },
20 | });
21 |
22 | if (!course) {
23 | return (
24 |
25 |
Course not found
26 |
27 | );
28 | }
29 |
30 | const { isCourseAccessableByTheUser, visitedUser, isauther } =
31 | await courseAccess(courseId);
32 |
33 | return (
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
51 |
52 |
53 |
54 |
55 |
56 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default Page;
68 |
--------------------------------------------------------------------------------
/src/app/course/_components/ChapterBanner.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | BookMarked,
4 | LucideMessageSquareWarning,
5 | MessageSquareWarning,
6 | } from "lucide-react";
7 | import React from "react";
8 | interface ChapterBannerProps {
9 | isCompleted: boolean;
10 | }
11 |
12 | const ChapterBanner = ({ isCompleted }: ChapterBannerProps) => {
13 | const content =
14 | isCompleted &&
15 | "You have completed this chapter. You can move to the next chapter";
16 |
17 | return (
18 |
27 | );
28 | };
29 |
30 | export default ChapterBanner;
31 |
--------------------------------------------------------------------------------
/src/app/course/_components/ChapterButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import { Circle } from "lucide-react";
4 | import { useRouter, useSearchParams } from "next/navigation";
5 | import React from "react";
6 | import { IoMdCheckmarkCircleOutline } from "react-icons/io";
7 | import { FaLock } from "react-icons/fa";
8 | import { FaLockOpen } from "react-icons/fa";
9 | import * as SheetPrimitive from "@radix-ui/react-dialog";
10 | import { toast } from "sonner";
11 |
12 | interface ChapterButtonProps {
13 | isCompleted: boolean;
14 | title: string;
15 | courseId: string;
16 | chapterId: string;
17 | isFree: boolean;
18 | isAccessable: boolean;
19 | visitedUser: boolean;
20 | isSidebar: boolean;
21 | }
22 |
23 | const ChapterButton = ({
24 | courseId,
25 | isCompleted,
26 | title,
27 | chapterId,
28 | isFree,
29 | isAccessable,
30 | visitedUser,
31 | isSidebar,
32 | }: ChapterButtonProps) => {
33 | const router = useRouter();
34 | const searchParams = useSearchParams();
35 | const chapter = searchParams.get("chapter");
36 |
37 | const openChapter = () => {
38 | if (chapter === chapterId) return;
39 | toast.loading("loading chapter...", { id: "chapter" });
40 | router.push(`/course/${courseId}?chapter=${chapterId}`);
41 | toast.success("chapter loaded", { id: "chapter" });
42 | };
43 |
44 | return isSidebar ? (
45 |
46 |
56 | {isCompleted ? (
57 |
58 | ) : isAccessable ? (
59 |
60 | ) : isFree ? (
61 |
62 | ) : (
63 |
64 | )}
65 |
{title}
66 |
67 |
68 | ) : (
69 |
79 | {isCompleted ? (
80 |
81 | ) : isAccessable ? (
82 |
83 | ) : isFree ? (
84 |
85 | ) : (
86 |
87 | )}
88 |
{title}
89 |
90 | );
91 | };
92 |
93 | export default ChapterButton;
94 |
--------------------------------------------------------------------------------
/src/app/course/_components/LeftPart.tsx:
--------------------------------------------------------------------------------
1 | import { Progress } from "@/components/ui/progress";
2 | import ChapterButton from "./ChapterButton";
3 | import { getTotalCourseProgress } from "@/app/(common)/courses/actions";
4 | import { getProgressWithIds } from "../action";
5 | import { prisma } from "@/lib/db";
6 | import Loading from "@/components/Loading";
7 | import { cn } from "@/lib/utils";
8 | import { Skeleton } from "@/components/ui/skeleton";
9 |
10 | interface LeftPartProps {
11 | courseId: string;
12 | isAccessable: boolean;
13 | visitedUser: boolean;
14 | isSidebar?: boolean;
15 | }
16 |
17 | const LeftPart = async ({
18 | courseId,
19 | isAccessable,
20 | visitedUser,
21 | isSidebar = false,
22 | }: LeftPartProps) => {
23 | const course = await prisma.course.findUnique({
24 | where: { id: courseId },
25 | include: {
26 | chapters: {
27 | where: { isPublished: true },
28 | orderBy: { order: "asc" },
29 | },
30 | },
31 | });
32 | const progress = await getTotalCourseProgress(courseId);
33 | const progressWithIds = await getProgressWithIds(courseId);
34 |
35 | if (!course) {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {[...Array(8)].map((_, index) => (
45 |
46 | ))}
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | return (
54 |
55 |
56 |
{course?.title}
57 |
64 |
65 |
66 | {course?.chapters.map((chapter) => {
67 | const isCompleted = progressWithIds.includes(chapter.id);
68 | return (
69 |
80 | );
81 | })}
82 |
83 |
84 | );
85 | };
86 |
87 | export default LeftPart;
88 |
--------------------------------------------------------------------------------
/src/app/course/_components/MobileLeftPart.tsx:
--------------------------------------------------------------------------------
1 | import { Sheet, SheetTrigger, SheetContent } from "@/components/ui/sheet";
2 | import { FaBars } from "react-icons/fa";
3 | import LeftPart from "./LeftPart";
4 |
5 | interface MobileLeftPartProps {
6 | courseId: string;
7 | isAccessable: boolean;
8 | visitedUser: boolean;
9 | }
10 |
11 | const MobileLeftPart = ({
12 | courseId,
13 | isAccessable,
14 | visitedUser,
15 | }: MobileLeftPartProps) => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default MobileLeftPart;
34 |
--------------------------------------------------------------------------------
/src/app/course/_components/VideoPlayer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Course } from "@prisma/client";
4 | import { useEffect, useState } from "react";
5 | import { Player } from "video-react";
6 | import { getCourse } from "../action";
7 | // import Video from 'next-video';
8 |
9 | interface VideoPlayerProps {
10 | videoUrl: string;
11 | courseId: string;
12 | }
13 | const VideoPlayer = ({ videoUrl, courseId }: VideoPlayerProps) => {
14 | const [courseDetails, setCourseDetails] = useState(null);
15 | useEffect(() => {
16 | const getCourseDeatils = async () => {
17 | const course = await getCourse(courseId);
18 | setCourseDetails(course);
19 | };
20 | getCourseDeatils();
21 | }, [videoUrl]);
22 | const nextVideo = async () => {};
23 | return (
24 |
30 | //
31 | // {
35 | // alert("Video ended");
36 | // }}
37 | // />
38 | //
39 | );
40 | };
41 |
42 | export default VideoPlayer;
43 |
--------------------------------------------------------------------------------
/src/app/course/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { prisma } from "@/lib/db";
3 | import { auth } from "@clerk/nextjs/server";
4 |
5 | export const getProgressWithIds = async (courseId: string) => {
6 | const { userId } = auth();
7 | if (!userId) {
8 | return [];
9 | }
10 | const user = await prisma.user.findUnique({
11 | where: {
12 | authId: userId,
13 | },
14 | });
15 |
16 | if (!user) {
17 | return [];
18 | }
19 |
20 | const allChapters = await prisma.course.findUnique({
21 | where: {
22 | id: courseId,
23 | isPublished: true,
24 | },
25 | include: {
26 | chapters: {
27 | where: {
28 | isPublished: true,
29 | },
30 | select: {
31 | id: true,
32 | },
33 | },
34 | },
35 | });
36 |
37 | const allChapterIds =
38 | allChapters?.chapters.map((chapter) => chapter.id) || [];
39 | const chapterProgressIds = await prisma.progress.findMany({
40 | where: {
41 | status: "COMPLETED",
42 | userId: user.id,
43 | chapterId: {
44 | in: allChapterIds,
45 | },
46 | },
47 | select: {
48 | chapterId: true,
49 | },
50 | });
51 |
52 | const ids = chapterProgressIds.map((id) => id.chapterId);
53 |
54 | return ids;
55 | };
56 |
57 | export const getChapterProgress = async (chapterId: string) => {
58 | const { userId } = auth();
59 | if (!userId) {
60 | return false;
61 | }
62 | const user = await prisma.user.findUnique({
63 | where: {
64 | authId: userId,
65 | },
66 | });
67 | if (!user) {
68 | return false;
69 | }
70 | const chapterProgress = await prisma.progress.findUnique({
71 | where: {
72 | userId_chapterId: {
73 | chapterId: chapterId,
74 | userId: user.id,
75 | },
76 | },
77 | });
78 | return chapterProgress?.status === "COMPLETED" ? true : false;
79 | };
80 |
81 | export const getFullChapter = async (chapterId: string) => {
82 | const fullDetails = await prisma.chapter.findUnique({
83 | where: {
84 | id: chapterId,
85 | },
86 | include: {
87 | attachments: true,
88 | },
89 | });
90 | const course = await prisma.course.findUnique({
91 | where: {
92 | id: fullDetails?.courseId || "",
93 | },
94 | });
95 | return fullDetails;
96 | };
97 |
98 | export const getCourse = async (courseId: string) => {
99 | const course = await prisma.course.findUnique({
100 | where: {
101 | id: courseId,
102 | },
103 | });
104 | return course;
105 | };
106 |
107 | export type progressType = Awaited>;
108 | export type fullChapterType = Awaited>;
109 |
--------------------------------------------------------------------------------
/src/app/custom.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/src/app/custom.css
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DevKrishnasai/lms/0833f4a49292910e5c459ddcc38b9aa3254e31c4/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import { ClerkProvider, ClerkLoaded, ClerkLoading } from "@clerk/nextjs";
4 | import { ThemeProvider } from "@/providers/theme-provider";
5 | import ToastProvider from "@/providers/toast-provider";
6 | import { ContextProvider } from "@/providers/context-provider";
7 | import Loading from "@/components/Loading";
8 | import "./globals.css";
9 | import "@uploadthing/react/styles.css";
10 | import NextTopLoader from "nextjs-toploader";
11 |
12 | const inter = Inter({ subsets: ["latin"] });
13 | ;
17 | export const metadata: Metadata = {
18 | title: "LMS Portal",
19 | description: "LMS portal for students and teachers",
20 | };
21 |
22 | export default function RootLayout({
23 | children,
24 | }: Readonly<{
25 | children: React.ReactNode;
26 | }>) {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
39 | {/*
40 |
41 | */}
42 | {/* {children} */}
43 | {children}
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./custom.css";
3 | import Link from "next/link";
4 | //
5 | //
6 | //
7 | //
8 | //
9 | //
10 | //
404
11 | //
12 |
13 | //
14 | //
Look like you're lost
15 |
16 | //
the page you are looking for not avaible!
17 |
18 | //
19 | // Go to Home
20 | //
21 | //
22 | //
23 | //
24 | //
25 | //
26 | //
27 |
28 | const NotFound = () => {
29 | return (
30 |
31 | Error 404. The page does not exist
32 |
33 | Sorry! The page you are looking for can not be found. Perhaps the page
34 | you requested was moved or deleted. It is also possible that you made a
35 | small typo when entering the address. Go to the main page.
36 |
37 |
38 |
42 |
43 |
44 |
45 |
46 | go home
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default NotFound;
54 |
--------------------------------------------------------------------------------
/src/app/onboarding/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { prisma } from "@/lib/db";
3 | import { auth } from "@clerk/nextjs/server";
4 | import { Role } from "@prisma/client";
5 | import { render } from "@react-email/render";
6 | import { redirect } from "next/navigation";
7 | import nodemailer from "nodemailer";
8 | import WelcomeToLMS from "../../../emails/WelcomeToLMS";
9 |
10 | export const updateOnboarding = async (data: {
11 | selectedCategories: string[];
12 | selectedGoals: string[];
13 | bio: string;
14 | role: Role;
15 | }) => {
16 | const { userId } = auth();
17 | if (!userId) {
18 | redirect("/");
19 | }
20 |
21 | const user = await prisma.user.update({
22 | where: {
23 | authId: userId,
24 | },
25 | data: {
26 | onBoarded: true,
27 | bio: data.bio,
28 | role: data.role,
29 | },
30 | });
31 |
32 | const categories = await prisma.category.findMany({
33 | where: {
34 | title: {
35 | in: data.selectedCategories,
36 | },
37 | },
38 | select: { id: true, title: true },
39 | });
40 |
41 | await prisma.userCategory.createMany({
42 | data: categories.map((category) => ({
43 | userId: user.id,
44 | categoryId: category.id,
45 | })),
46 | skipDuplicates: true,
47 | });
48 |
49 | const goals = await prisma.goal.findMany({
50 | where: {
51 | title: {
52 | in: data.selectedGoals,
53 | },
54 | },
55 | select: { id: true, title: true },
56 | });
57 |
58 | await prisma.userGoal.createMany({
59 | data: goals.map((goal) => ({
60 | userId: user.id,
61 | goalId: goal.id,
62 | })),
63 | skipDuplicates: true,
64 | });
65 |
66 | const transport = nodemailer.createTransport({
67 | service: "Gmail",
68 | host: "smtp.gmail.com",
69 | port: 465,
70 | secure: true,
71 | auth: {
72 | user: process.env.MAIL_USER,
73 | pass: process.env.MAIL_PASS,
74 | },
75 | });
76 |
77 | const mailOptions = {
78 | from: process.env.MAIL_USER,
79 | to: user.email,
80 | subject: "Welcome To YourLMS Portal",
81 | html: render(
82 | WelcomeToLMS({
83 | name: user?.name || user.email.split("@")[0],
84 | role: user.role,
85 | })
86 | ),
87 | };
88 |
89 | transport.sendMail(mailOptions, function (error, info) {
90 | if (error) {
91 | console.log(error);
92 | throw new Error("Error sending email");
93 | }
94 | });
95 | };
96 |
--------------------------------------------------------------------------------
/src/app/onboarding/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 | import React from "react";
3 |
4 | const loading = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {[1, 2, 3, 4, 5, 7, 8, 9].map((goal) => (
14 |
15 |
16 |
17 | ))}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {[1, 2, 3, 4, 5, 7, 8, 9].map((goal) => (
26 |
27 |
28 |
29 | ))}
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default loading;
39 |
--------------------------------------------------------------------------------
/src/app/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth, currentUser } from "@clerk/nextjs/server";
2 | import OnboardingForm from "./_components/OnboardingForm";
3 | import { redirect } from "next/navigation";
4 | import { prisma } from "@/lib/db";
5 |
6 | const OnboardingPage = async () => {
7 | const user = await currentUser();
8 |
9 | if (!user) {
10 | redirect("auth/sign-in");
11 | }
12 |
13 | const data = await prisma.user.findUnique({
14 | where: {
15 | authId: user.id,
16 | },
17 | });
18 | if (data) {
19 | if (data.onBoarded) {
20 | redirect("/dashboard");
21 | }
22 | } else {
23 | await prisma.user.create({
24 | data: {
25 | authId: user.id,
26 | email: user.emailAddresses[0].emailAddress,
27 | name: user.fullName || user.firstName,
28 | profilePic: user.imageUrl,
29 | },
30 | });
31 | }
32 |
33 | const categories: string[] = [
34 | "Web Development",
35 | "Mobile App Development",
36 | "Data Science",
37 | "Machine Learning",
38 | "Artificial Intelligence",
39 | "Cloud Computing",
40 | "Cybersecurity",
41 | "DevOps",
42 | "Blockchain",
43 | "Digital Marketing",
44 | "Graphic Design",
45 | "UX/UI Design",
46 | "Business Analytics",
47 | "Project Management",
48 | "Photography",
49 | "Music Production",
50 | "Language Learning",
51 | "Personal Development",
52 | "Fitness and Health",
53 | "Cooking and Nutrition",
54 | ];
55 |
56 | const learningGoals: string[] = [
57 | "Career Advancement",
58 | "Personal Interest",
59 | "Academic Requirements",
60 | "Skill Development",
61 | "Certification",
62 | "Starting a Business",
63 | ];
64 | const roles: string[] = ["Student", "Teacher"];
65 |
66 | return (
67 |
68 |
69 |
70 | Welcome to YourLMS, {user?.firstName}!
71 |
72 |
73 | Let's personalize your learning journey. Tell us a bit about
74 | yourself and your goals.
75 |
76 |
77 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default OnboardingPage;
89 |
--------------------------------------------------------------------------------
/src/components/Banner.tsx:
--------------------------------------------------------------------------------
1 | import { AlertTriangle } from "lucide-react";
2 | import React from "react";
3 |
4 | interface BannerProps {
5 | isCourse: boolean;
6 | }
7 |
8 | const Banner = ({ isCourse }: BannerProps) => {
9 | const content = isCourse
10 | ? "This Course is not published. Students can't see this course."
11 | : "This Chapter is not published. Students can't see this chapter.";
12 |
13 | return (
14 |
23 | );
24 | };
25 |
26 | export default Banner;
27 |
--------------------------------------------------------------------------------
/src/components/Editor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useCallback, useState } from "react";
2 | import { useQuill } from "react-quilljs";
3 | import "quill/dist/quill.snow.css";
4 | import { useUploadThing } from "@/lib/uploadthing";
5 | import { toast } from "sonner";
6 |
7 | interface EditorProps {
8 | content: string;
9 | onChange: (content: string) => void;
10 | }
11 |
12 | const Editor: React.FC = ({ content, onChange }) => {
13 | const { quill, quillRef } = useQuill();
14 | const [isInitialized, setIsInitialized] = useState(false);
15 |
16 | const { startUpload } = useUploadThing("uploadThumbnail", {
17 | skipPolling: true,
18 | onUploadBegin: () => {
19 | toast.loading("uploading...", {
20 | id: "uploading-image",
21 | });
22 | },
23 | onClientUploadComplete: (res) => {
24 | toast.success("uploaded successfully", {
25 | id: "uploading-image",
26 | });
27 | },
28 | onUploadProgress(p) {
29 | toast.loading(`uploading ${p}%`, {
30 | id: "uploading-image",
31 | });
32 | },
33 | onUploadError: (error: Error) => {
34 | toast.error(error.message, {
35 | id: "uploading-image",
36 | });
37 | },
38 | });
39 |
40 | const insertToEditor = useCallback(
41 | (url: string) => {
42 | const range = quill?.getSelection();
43 | if (range && quill) {
44 | quill.insertEmbed(range.index, "image", url);
45 | }
46 | },
47 | [quill]
48 | );
49 |
50 | const saveToServer = useCallback(
51 | async (file: File) => {
52 | try {
53 | const data = await startUpload([file]);
54 | if (data && data[0]) {
55 | insertToEditor(data[0].url);
56 | }
57 | } catch (error) {
58 | console.error("Error uploading image:", error);
59 | }
60 | },
61 | [startUpload, insertToEditor]
62 | );
63 |
64 | const selectLocalImage = useCallback(() => {
65 | const input = document.createElement("input");
66 | input.setAttribute("type", "file");
67 | input.setAttribute("accept", "image/*");
68 | input.click();
69 | input.onchange = () => {
70 | const file = input.files?.[0];
71 | if (file) {
72 | saveToServer(file);
73 | }
74 | };
75 | }, [saveToServer]);
76 |
77 | useEffect(() => {
78 | if (quill && !isInitialized) {
79 | quill.root.innerHTML = content;
80 | setIsInitialized(true);
81 |
82 | quill.on("text-change", () => {
83 | onChange(quill.root.innerHTML);
84 | });
85 | //@ts-ignore
86 | quill.getModule("toolbar").addHandler("image", selectLocalImage);
87 | }
88 | }, [quill, content, onChange, selectLocalImage, isInitialized]);
89 |
90 | // Update content when it changes externally
91 | useEffect(() => {
92 | if (quill && isInitialized && content !== quill.root.innerHTML) {
93 | quill.root.innerHTML = content;
94 | }
95 | }, [quill, content, isInitialized]);
96 |
97 | return (
98 |
101 | );
102 | };
103 |
104 | export default Editor;
105 |
--------------------------------------------------------------------------------
/src/components/Fileupload.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ourFileRouter } from "@/app/api/uploadthing/core";
4 | import { UploadButton, UploadDropzone } from "@/lib/uploadthing";
5 | import { toast } from "sonner";
6 | import { ClientUploadedFileData } from "uploadthing/types";
7 | interface FileuploadProps {
8 | endpoint: keyof typeof ourFileRouter;
9 | onChange: (url: string, res?: ClientUploadedFileData[]) => void;
10 | }
11 | export default function Fileupload({ endpoint, onChange }: FileuploadProps) {
12 | return (
13 | {
15 | toast.loading("uploading...", {
16 | id: "uploading",
17 | });
18 | }}
19 | endpoint={endpoint}
20 | onClientUploadComplete={(res) => {
21 | onChange(res[0].url, res);
22 | toast.success("uploaded successfully", {
23 | id: "uploading",
24 | });
25 | }}
26 | onUploadError={(error: Error) => {
27 | toast.error(error.message, {
28 | id: "uploading",
29 | });
30 | }}
31 | />
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/GridPattern.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import AnimatedGridPattern from "@/components/magicui/animated-grid-pattern";
5 | import { ReactNode } from "react";
6 |
7 | const GridPattern = ({ children }: { children: ReactNode }) => {
8 | return (
9 |
10 | {children}
11 |
21 |
22 | );
23 | };
24 |
25 | export default GridPattern;
26 |
--------------------------------------------------------------------------------
/src/components/GroupButtons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { ThemeSwitch } from "./ThemeSwitch";
4 | import { UserButton } from "@clerk/nextjs";
5 | import { cn } from "@/lib/utils";
6 | import { useTheme } from "next-themes";
7 | import { dark } from "@clerk/themes";
8 |
9 | const GroupButtons = ({ open }: { open: boolean }) => {
10 | const { theme } = useTheme();
11 | return (
12 |
18 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default GroupButtons;
30 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default Loading;
13 |
--------------------------------------------------------------------------------
/src/components/MobileSideBar.tsx:
--------------------------------------------------------------------------------
1 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
2 | import { MenuIcon } from "lucide-react";
3 | import Sidebar from "./Sidebar";
4 |
5 | const MobileSideBar = () => {
6 | // useEffect(() => {
7 | // setOpen(false);
8 | // }, [path]);
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default MobileSideBar;
23 |
--------------------------------------------------------------------------------
/src/components/NewCourseButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
3 | import CreateCourseForm from "./CreateCourseForm";
4 |
5 | export function NewCourseButton() {
6 | return (
7 |
8 |
9 | Add Course
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Preview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import "react-quill/dist/quill.bubble.css";
3 | import ReactQuill from "react-quill";
4 | interface PreviewProps {
5 | content: string;
6 | }
7 | const Preview = ({ content }: PreviewProps) => {
8 | return ;
9 | };
10 |
11 | export default Preview;
12 |
--------------------------------------------------------------------------------
/src/components/SidebarItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | interface SideBarItemType {
6 | icon: React.ReactNode;
7 | label: string;
8 | link: string;
9 | }
10 |
11 | interface SidebarItemProps {
12 | item: SideBarItemType;
13 | }
14 |
15 | const SidebarItem = ({ item }: { item: SideBarItemType }) => {
16 | const current = usePathname();
17 |
18 | const isActive = current.includes(item.link);
19 | return (
20 |
21 |
30 |
{item.icon}
31 |
32 | {item.label}
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default SidebarItem;
40 |
41 | //
42 | //
51 | // {item.icon}
52 | // {item.label}
53 | //
54 | //
55 |
--------------------------------------------------------------------------------
/src/components/Sliders.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import Marquee from "@/components/magicui/marquee";
3 |
4 | const reviews = [
5 | {
6 | name: "Jack",
7 | username: "@jack",
8 | body: "I've never seen anything like this before. It's amazing. I love it.",
9 | img: "https://avatar.vercel.sh/jack",
10 | },
11 | {
12 | name: "Jill",
13 | username: "@jill",
14 | body: "I don't know what to say. I'm speechless. This is amazing.",
15 | img: "https://avatar.vercel.sh/jill",
16 | },
17 | {
18 | name: "John",
19 | username: "@john",
20 | body: "I'm at a loss for words. This is amazing. I love it.",
21 | img: "https://avatar.vercel.sh/john",
22 | },
23 | {
24 | name: "Jane",
25 | username: "@jane",
26 | body: "I'm at a loss for words. This is amazing. I love it.",
27 | img: "https://avatar.vercel.sh/jane",
28 | },
29 | {
30 | name: "Jenny",
31 | username: "@jenny",
32 | body: "I'm at a loss for words. This is amazing. I love it.",
33 | img: "https://avatar.vercel.sh/jenny",
34 | },
35 | {
36 | name: "James",
37 | username: "@james",
38 | body: "I'm at a loss for words. This is amazing. I love it.",
39 | img: "https://avatar.vercel.sh/james",
40 | },
41 | ];
42 |
43 | const firstRow = reviews.slice(0, reviews.length / 2);
44 | const secondRow = reviews.slice(reviews.length / 2);
45 |
46 | const ReviewCard = ({
47 | img,
48 | name,
49 | username,
50 | body,
51 | }: {
52 | img: string;
53 | name: string;
54 | username: string;
55 | body: string;
56 | }) => {
57 | return (
58 |
67 |
68 |
69 |
70 |
71 | {name}
72 |
73 |
{username}
74 |
75 |
76 | {body}
77 |
78 | );
79 | };
80 |
81 | export function Sliders() {
82 | return (
83 |
84 |
85 | {firstRow.map((review) => (
86 |
87 | ))}
88 |
89 |
90 | {secondRow.map((review) => (
91 |
92 | ))}
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/ThemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | export function ThemeSwitch() {
16 | const { setTheme, theme } = useTheme();
17 |
18 | return (
19 | setTheme(theme === "dark" ? "light" : "dark")}
23 | >
24 |
25 |
26 | Toggle theme
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/magicui/animated-shiny-text.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, FC, ReactNode } from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | interface AnimatedShinyTextProps {
6 | children: ReactNode;
7 | className?: string;
8 | shimmerWidth?: number;
9 | }
10 |
11 | const AnimatedShinyText: FC = ({
12 | children,
13 | className,
14 | shimmerWidth = 100,
15 | }) => {
16 | return (
17 |
35 | {children}
36 |
37 | );
38 | };
39 |
40 | export default AnimatedShinyText;
41 |
--------------------------------------------------------------------------------
/src/components/magicui/gradual-spacing.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AnimatePresence, motion, Variants } from "framer-motion";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | interface GradualSpacingProps {
8 | text: string;
9 | duration?: number;
10 | delayMultiple?: number;
11 | framerProps?: Variants;
12 | className?: string;
13 | }
14 |
15 | export default function GradualSpacing({
16 | text,
17 | duration = 0.5,
18 | delayMultiple = 0.04,
19 | framerProps = {
20 | hidden: { opacity: 0, x: -20 },
21 | visible: { opacity: 1, x: 0 },
22 | },
23 | className,
24 | }: GradualSpacingProps) {
25 | return (
26 |
27 |
28 | {text.split("").map((char, i) => (
29 |
38 | {char === " " ? : char}
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/magicui/letter-pullup.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | interface LetterPullupProps {
8 | className?: string;
9 | words: string;
10 | delay?: number;
11 | }
12 |
13 | export default function LetterPullup({
14 | className,
15 | words,
16 | delay,
17 | }: LetterPullupProps) {
18 | const letters = words.split("");
19 |
20 | const pullupVariant = {
21 | initial: { y: 100, opacity: 0 },
22 | animate: (i: any) => ({
23 | y: 0,
24 | opacity: 1,
25 | transition: {
26 | delay: i * (delay ? delay : 0.05), // By default, delay each letter's animation by 0.05 seconds
27 | },
28 | }),
29 | };
30 |
31 | return (
32 |
33 | {letters.map((letter, i) => (
34 |
45 | {letter === " " ? : letter}
46 |
47 | ))}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/magicui/magic-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { motion, useMotionTemplate, useMotionValue } from "framer-motion";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | export interface MagicCardProps {
9 | children: React.ReactNode;
10 | className?: string;
11 | gradientSize?: number;
12 | gradientColor?: string;
13 | gradientOpacity?: number;
14 | }
15 |
16 | export function MagicCard({
17 | children,
18 | className = "",
19 | gradientSize = 200,
20 | gradientColor = "#262626",
21 | }: MagicCardProps) {
22 | const mouseX = useMotionValue(0);
23 | const mouseY = useMotionValue(0);
24 |
25 | return (
26 | {
28 | const { left, top } = e.currentTarget.getBoundingClientRect();
29 |
30 | mouseX.set(e.clientX - left);
31 | mouseY.set(e.clientY - top);
32 | }}
33 | className={cn(
34 | "group relative flex size-full overflow-hidden rounded-xl bg-neutral-900 border text-white",
35 | className
36 | )}
37 | >
38 |
{children}
39 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/magicui/marquee.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface MarqueeProps {
4 | className?: string;
5 | reverse?: boolean;
6 | pauseOnHover?: boolean;
7 | children?: React.ReactNode;
8 | vertical?: boolean;
9 | repeat?: number;
10 | [key: string]: any;
11 | }
12 |
13 | export default function Marquee({
14 | className,
15 | reverse,
16 | pauseOnHover = false,
17 | children,
18 | vertical = false,
19 | repeat = 4,
20 | ...props
21 | }: MarqueeProps) {
22 | return (
23 |
34 | {Array(repeat)
35 | .fill(0)
36 | .map((_, i) => (
37 |
46 | {children}
47 |
48 | ))}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/magicui/number-ticker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 | import { useInView, useMotionValue, useSpring } from "framer-motion";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | export default function NumberTicker({
9 | value,
10 | direction = "up",
11 | delay = 0,
12 | className,
13 | }: {
14 | value: number;
15 | direction?: "up" | "down";
16 | className?: string;
17 | delay?: number; // delay in s
18 | }) {
19 | const ref = useRef(null);
20 | const motionValue = useMotionValue(direction === "down" ? value : 0);
21 | const springValue = useSpring(motionValue, {
22 | damping: 60,
23 | stiffness: 100,
24 | });
25 | const isInView = useInView(ref, { once: true, margin: "0px" });
26 |
27 | useEffect(() => {
28 | isInView &&
29 | setTimeout(() => {
30 | motionValue.set(direction === "down" ? 0 : value);
31 | }, delay * 1000);
32 | }, [motionValue, isInView, delay, value, direction]);
33 |
34 | useEffect(
35 | () =>
36 | springValue.on("change", (latest) => {
37 | if (ref.current) {
38 | ref.current.textContent = Intl.NumberFormat("en-US").format(
39 | latest.toFixed(0),
40 | );
41 | }
42 | }),
43 | [springValue],
44 | );
45 |
46 | return (
47 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/starter/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import { CoolMode } from "../magicui/cool-mode";
4 | import { ClerkLoaded, ClerkLoading, SignInButton } from "@clerk/nextjs";
5 |
6 | const Navbar = () => {
7 | const links = [
8 | { name: "Home", link: "/" },
9 | { name: "Features", link: "#features" },
10 | ];
11 | return (
12 |
13 |
14 |
15 | YourLMS
16 |
17 |
18 | {links.map((link) => (
19 |
20 |
25 | {link.name}
26 |
27 |
28 | ))}
29 |
33 | Testimonials
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Start Learning
43 |
44 |
45 |
46 | loading...
47 |
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Navbar;
56 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "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",
8 | {
9 | variants: {
10 | variant: {
11 | default: "border-transparent bg-primary text-primary-foreground ",
12 | secondary:
13 | "border-transparent bg-secondary text-secondary-foreground h",
14 | destructive:
15 | "border-transparent bg-destructive text-destructive-foreground ",
16 | outline: "text-foreground",
17 | },
18 | },
19 | defaultVariants: {
20 | variant: "default",
21 | },
22 | }
23 | );
24 |
25 | export interface BadgeProps
26 | extends React.HTMLAttributes,
27 | VariantProps {}
28 |
29 | function Badge({ className, variant, ...props }: BadgeProps) {
30 | return (
31 |
32 | );
33 | }
34 |
35 | export { Badge, badgeVariants };
36 |
--------------------------------------------------------------------------------
/src/components/ui/bento-grid.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | export const BentoGrid = ({
4 | className,
5 | children,
6 | }: {
7 | className?: string;
8 | children?: React.ReactNode;
9 | }) => {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export const BentoGridItem = ({
23 | className,
24 | title,
25 | description,
26 | header,
27 | icon,
28 | }: {
29 | className?: string;
30 | title?: string | React.ReactNode;
31 | description?: string | React.ReactNode;
32 | header?: React.ReactNode;
33 | icon?: React.ReactNode;
34 | }) => {
35 | return (
36 |
42 | {header}
43 |
44 | {icon}
45 |
46 | {title}
47 |
48 |
49 | {description}
50 |
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/components/ui/flip-words.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useCallback, useEffect, useRef, useState } from "react";
3 | import { AnimatePresence, motion, LayoutGroup } from "framer-motion";
4 | import { cn } from "@/lib/utils";
5 |
6 | export const FlipWords = ({
7 | words,
8 | duration = 3000,
9 | className,
10 | }: {
11 | words: string[];
12 | duration?: number;
13 | className?: string;
14 | }) => {
15 | const [currentWord, setCurrentWord] = useState(words[0]);
16 | const [isAnimating, setIsAnimating] = useState(false);
17 |
18 | // thanks for the fix Julian - https://github.com/Julian-AT
19 | const startAnimation = useCallback(() => {
20 | const word = words[words.indexOf(currentWord) + 1] || words[0];
21 | setCurrentWord(word);
22 | setIsAnimating(true);
23 | }, [currentWord, words]);
24 |
25 | useEffect(() => {
26 | if (!isAnimating)
27 | setTimeout(() => {
28 | startAnimation();
29 | }, duration);
30 | }, [isAnimating, duration, startAnimation]);
31 |
32 | return (
33 | {
35 | setIsAnimating(false);
36 | }}
37 | >
38 |
66 | {currentWord.split("").map((letter, index) => (
67 |
77 | {letter}
78 |
79 | ))}
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/components/ui/text-generate-effect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect } from "react";
3 | import { motion, stagger, useAnimate } from "framer-motion";
4 | import { cn } from "@/lib/utils";
5 |
6 | export const TextGenerateEffect = ({
7 | words,
8 | className,
9 | filter = true,
10 | duration = 0.5,
11 | }: {
12 | words: string;
13 | className?: string;
14 | filter?: boolean;
15 | duration?: number;
16 | }) => {
17 | const [scope, animate] = useAnimate();
18 | let wordsArray = words.split(" ");
19 | useEffect(() => {
20 | animate(
21 | "span",
22 | {
23 | opacity: 1,
24 | filter: filter ? "blur(0px)" : "none",
25 | },
26 | {
27 | duration: duration ? duration : 1,
28 | delay: stagger(0.2),
29 | }
30 | );
31 | }, [scope.current]);
32 |
33 | const renderWords = () => {
34 | return (
35 |
36 | {wordsArray.map((word, idx) => {
37 | return (
38 |
45 | {word}{" "}
46 |
47 | );
48 | })}
49 |
50 | );
51 | };
52 |
53 | return (
54 |
55 |
56 |
57 | {renderWords()}
58 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/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 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 |
3 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
4 |
5 | export const prisma =
6 | globalForPrisma.prisma || new PrismaClient()
7 |
8 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
9 |
--------------------------------------------------------------------------------
/src/lib/uploadthing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateReactHelpers,
3 | generateUploadButton,
4 | generateUploadDropzone,
5 | } from "@uploadthing/react";
6 |
7 | import type { OurFileRouter } from "@/app/api/uploadthing/core";
8 |
9 | export const UploadButton = generateUploadButton();
10 | export const UploadDropzone = generateUploadDropzone();
11 | export const { useUploadThing } = generateReactHelpers();
12 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const isProtected = createRouteMatcher([
4 | "/dashboard(.*), /access(.*),/create(.*), /settings(.*)",
5 | ]);
6 |
7 | export default clerkMiddleware((auth, req) => {
8 | if (isProtected(req)) auth().protect();
9 | });
10 |
11 | export const config = {
12 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
13 | };
14 |
--------------------------------------------------------------------------------
/src/providers/context-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { createContext, useState } from "react";
3 |
4 | interface ContextState {
5 | search: string;
6 | setSearch: (search: string) => void;
7 | }
8 |
9 | export const MyContext = createContext({
10 | search: "",
11 | setSearch: () => {},
12 | });
13 |
14 | export const ContextProvider = ({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) => {
19 | const [search, setSearch] = useState("");
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/providers/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTheme } from "next-themes";
3 | import React from "react";
4 | import { Toaster } from "sonner";
5 |
6 | const ToastProvider = () => {
7 | const { theme, systemTheme } = useTheme();
8 | const system = theme === "dark" ? "dark" : theme ? "light" : systemTheme;
9 | return ;
10 | };
11 |
12 | export default ToastProvider;
13 |
--------------------------------------------------------------------------------
/src/schema/zod-schemes.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const titleSchema = z.object({
4 | title: z
5 | .string()
6 | .min(2, {
7 | message: "Title is too short",
8 | })
9 | .max(50, {
10 | message: "Title is too long",
11 | }),
12 | });
13 |
14 | export const durationSchema = z.object({
15 | duration: z.coerce.number().min(1, {
16 | message: "duration is too short",
17 | }),
18 | });
19 |
20 | export const descriptionSchema = z.object({
21 | description: z.string().min(10, {
22 | message: "description is too short",
23 | }),
24 | });
25 |
26 | export const thumbnailSchema = z.object({
27 | thumbnail: z.string({
28 | message: "thumbnail is required",
29 | }),
30 | });
31 |
32 | export const categorySchema = z.object({
33 | category: z.string({
34 | message: "category is required",
35 | }),
36 | });
37 |
38 | export const isFreeSchema = z.object({
39 | isFree: z.boolean(),
40 | price: z.number(),
41 | });
42 |
43 | export const contentSchema = z.object({
44 | content: z.string().min(50, {
45 | message: "content is too short",
46 | }),
47 | });
48 |
49 | export const isChapterFreeSchema = z.object({
50 | isFree: z.boolean(),
51 | });
52 |
53 | export const videoSchema = z.object({
54 | videoUrl: z.string({
55 | message: "video is required",
56 | }),
57 | });
58 |
59 | export const searchSchema = z.object({
60 | search: z
61 | .string()
62 | .min(3, {
63 | message: "type more to get best courses",
64 | })
65 | .max(50, {
66 | message: "too long search query",
67 | }),
68 | });
69 |
70 | export const addUsersToDBSchema = z.object({
71 | email: z.string().email({
72 | message: "Invalid email",
73 | }),
74 | });
75 |
76 | export const enrollUsersToACourseSchema = z.object({
77 | email: z.string().email({
78 | message: "Invalid email",
79 | }),
80 | courseId: z.string(),
81 | });
82 |
83 | export const requestFormSchema = z.object({
84 | request: z.string().min(10, {
85 | message: "request is too short, please provide more details",
86 | }),
87 | email: z.string().email({
88 | message: "Invalid email",
89 | }),
90 | phone: z
91 | .string({
92 | coerce: true,
93 | })
94 | .regex(/^\d{10}$/, {
95 | message: "Invalid phone number",
96 | }),
97 | });
98 |
--------------------------------------------------------------------------------
/src/styles/canvas.css:
--------------------------------------------------------------------------------
1 | @media print {
2 | body * {
3 | visibility: hidden;
4 | }
5 | canvas,
6 | canvas * {
7 | visibility: visible;
8 | }
9 | canvas {
10 | position: absolute;
11 | left: 0;
12 | top: 0;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/types/general-types.ts:
--------------------------------------------------------------------------------
1 | type SideBarItemType = {
2 | icon: React.ReactNode;
3 | label: string;
4 | link: string;
5 | };
6 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | marquee: {
71 | from: { transform: "translateX(0)" },
72 | to: { transform: "translateX(calc(-100% - var(--gap)))" },
73 | },
74 | "marquee-vertical": {
75 | from: { transform: "translateY(0)" },
76 | to: { transform: "translateY(calc(-100% - var(--gap)))" },
77 | },
78 | shimmer: {
79 | "0%, 90%, 100%": {
80 | "background-position": "calc(-100% - var(--shimmer-width)) 0",
81 | },
82 | "30%, 60%": {
83 | "background-position": "calc(100% + var(--shimmer-width)) 0",
84 | },
85 | },
86 | },
87 | animation: {
88 | "accordion-down": "accordion-down 0.2s ease-out",
89 | "accordion-up": "accordion-up 0.2s ease-out",
90 | marquee: "marquee var(--duration) linear infinite",
91 | "marquee-vertical": "marquee-vertical var(--duration) linear infinite",
92 | shimmer: "shimmer 8s infinite",
93 | },
94 | },
95 | },
96 | plugins: [require("tailwindcss-animate")],
97 | } satisfies Config;
98 |
99 | export default config;
100 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": [
25 | "next-env.d.ts",
26 | "**/*.ts",
27 | "**/*.tsx",
28 | ".next/types/**/*.ts",
29 | "scripts/seed.js"
30 | ],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------