├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── images
├── lj-3.jpg
├── lj-4.jpg
├── lj-5.jpg
├── lj-8.png
├── lj1.jpg
└── lj2.jpg
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── chapter
│ │ │ └── getInfo
│ │ │ │ └── route.ts
│ │ └── course
│ │ │ └── createChapters
│ │ │ └── route.ts
│ ├── course
│ │ └── [...slug]
│ │ │ └── page.tsx
│ ├── create
│ │ ├── [courseId]
│ │ │ └── page.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── gallery
│ │ └── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── ChapterCard.tsx
│ ├── ConfirmChapters.tsx
│ ├── CourseSideBar.tsx
│ ├── CreateCourseForm.tsx
│ ├── GalleryCourseCard.tsx
│ ├── MainVideoSummary.tsx
│ ├── Navbar.tsx
│ ├── Providers.tsx
│ ├── QuizCards.tsx
│ ├── SignInButton.tsx
│ ├── ThemeToggle.tsx
│ ├── UserAccountNav.tsx
│ ├── UserAvatar.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── radio-group.tsx
│ │ ├── separator.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── use-toast.ts
├── lib
│ ├── auth.ts
│ ├── db.ts
│ ├── gpt.ts
│ ├── unsplash.ts
│ ├── utils.ts
│ └── youtube.ts
└── validators
│ └── course.ts
├── tailwind.config.js
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 | .env
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Learning Journey- Full-Stack-AI-Course-Generator
2 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). It is a fully-functioning AI web application that generates courses based on the title and the units you provide it. It uses OpenAI's API using prompt-engineering to utilize user provided data to generate the courses. Any topic can be provided, the ideas are ENDLESS! It has a login feature to login through your Google account (used Google Cloud API) and also generates relevant images using Unsplash API. The relevant YouTube videos are generated through YouTube's API and the Summary section summarizes the video transcript. It uses Prisma, an open-source ORM which generates all the configuartions for the MySQL-compatible serverless database - PlanetScale.
3 |
4 | ## Getting Started
5 |
6 | First, run the development server on your terminal - no need to use an IDE. Make sure that you are in the current directory of learning-journey-yt before beginning.
7 |
8 | ```bash
9 | npm run dev
10 | ```
11 |
12 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
13 |
14 | The page should be like the one shown below once you sign in:
15 |
16 |
17 |
18 | 
19 |
20 |
21 |
22 |
23 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24 |
25 |
26 |
27 | The Gallery page shows the courses you have generated using Learning Journey. Here are some I generated below! You can generate courses from the simplest to the most complicated topics....
28 |
29 |
30 |
31 | 
32 |
33 |
34 |
35 | Click on the create course button to create your first course!! Input the titles and units you want. You can input as many units as you want by clicking on "Add Unit". The default value is 3 units.
36 |
37 |
38 |
39 | 
40 |
41 |
42 |
43 | All the pages can be viewed in both light and dark themes ! My favourite is the dark theme.
44 |
45 |
46 |
47 | 
48 |
49 |
50 |
51 | Once the course has been generated the page should look like the one shown below.
52 |
53 |
54 |
55 | 
56 |
57 |
58 |
59 | Each chapter within the course will have a relevant video generated from YouTube along with a short summary summarising the video's transcript.
60 |
61 |
62 |
63 | 
64 |
65 |
66 |
67 | You can return to the course generated anytime from the course gallery.
68 | Have fun creating courses!
69 |
70 |
71 |
72 | ## Learn More
73 |
74 | To learn more about Next.js, take a look at the following resources:
75 |
76 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
77 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
78 |
79 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
80 |
81 | ## Deploy on Vercel
82 |
83 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
84 |
85 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
86 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/images/lj-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj-3.jpg
--------------------------------------------------------------------------------
/images/lj-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj-4.jpg
--------------------------------------------------------------------------------
/images/lj-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj-5.jpg
--------------------------------------------------------------------------------
/images/lj-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj-8.png
--------------------------------------------------------------------------------
/images/lj1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj1.jpg
--------------------------------------------------------------------------------
/images/lj2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/images/lj2.jpg
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images:{
4 | domains:["lh3.googleusercontent.com","s3.us-west-2.amazonaws.com"]
5 | }
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "learning-journey-yt",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.3.0",
13 | "@next-auth/prisma-adapter": "^1.0.7",
14 | "@prisma/client": "^5.2.0",
15 | "@radix-ui/react-avatar": "^1.0.3",
16 | "@radix-ui/react-dropdown-menu": "^2.0.5",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-radio-group": "^1.1.3",
19 | "@radix-ui/react-separator": "^1.0.3",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "@radix-ui/react-toast": "^1.1.4",
22 | "@tanstack/react-query": "^4.33.0",
23 | "@types/node": "20.5.3",
24 | "@types/react": "18.2.21",
25 | "@types/react-dom": "18.2.7",
26 | "autoprefixer": "10.4.15",
27 | "axios": "^1.4.0",
28 | "class-variance-authority": "^0.7.0",
29 | "clsx": "^2.0.0",
30 | "create-next-app": "^13.4.19",
31 | "eslint": "8.47.0",
32 | "eslint-config-next": "13.4.19",
33 | "framer-motion": "^10.16.1",
34 | "init": "^0.1.2",
35 | "lucide-react": "^0.268.0",
36 | "next": "13.4.19",
37 | "next-auth": "^4.23.1",
38 | "next-themes": "^0.2.1",
39 | "openai": "^3.3.0",
40 | "postcss": "8.4.28",
41 | "react": "18.2.0",
42 | "react-dom": "18.2.0",
43 | "react-hook-form": "^7.45.4",
44 | "shadcn-ui": "^0.3.0",
45 | "tailwind-merge": "^1.14.0",
46 | "tailwindcss": "3.3.3",
47 | "tailwindcss-animate": "^1.0.6",
48 | "typescript": "5.1.6",
49 | "youtube-transcript": "^1.0.6",
50 | "zod": "^3.22.2"
51 | },
52 | "description": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).",
53 | "main": "next.config.js",
54 | "author": "",
55 | "license": "ISC",
56 | "devDependencies": {
57 | "concurrently": "^8.2.1",
58 | "http-server": "^14.1.1",
59 | "prisma": "^5.2.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 |
5 | generator client {
6 | provider = "prisma-client-js"
7 | }
8 |
9 | datasource db {
10 | provider = "mysql"
11 | url = env("DATABASE_URL")
12 | relationMode = "prisma"
13 | }
14 |
15 | model Account {
16 | id String @id @default(cuid())
17 | userId String
18 | type String
19 | provider String
20 | providerAccountId String
21 | refresh_token String? @db.Text
22 | access_token String? @db.Text
23 | expires_at Int?
24 | token_type String?
25 | scope String?
26 | id_token String? @db.Text
27 | session_state String?
28 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
29 | @@index([userId], name:"userId")
30 | @@unique([provider, providerAccountId])
31 | }
32 |
33 | model Session {
34 | id String @id @default(cuid())
35 | sessionToken String @unique
36 | userId String
37 | expires DateTime
38 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
39 | @@index([userId], name:"userId")
40 | }
41 |
42 | model User {
43 | id String @id @default(cuid())
44 | name String?
45 | email String? @unique
46 | emailVerified DateTime?
47 | image String?
48 | credits Int @default(10)
49 | accounts Account[]
50 | sessions Session[]
51 | }
52 |
53 | model Course{
54 | id String @id @default(cuid())
55 | name String
56 | image String
57 | units Unit[]
58 | }
59 |
60 | model Unit{
61 | id String @id @default(cuid())
62 | courseId String
63 | name String
64 | course Course @relation(fields: [courseId], references:[id])
65 | chapters Chapter[]
66 | @@index([courseId], name:"courseId")
67 | }
68 |
69 | model Chapter{
70 | id String @id @default(cuid())
71 | unitId String
72 | name String
73 | youtubeSearchQuery String
74 | videoId String?
75 | summary String? @db.VarChar(3000)
76 | unit Unit @relation(fields:[unitId], references:[id])
77 | questions Question[]
78 |
79 | @@index([unitId], name:"unitId")
80 | }
81 |
82 | model Question{
83 | id String @id @default(cuid())
84 | chapterId String
85 | question String @db.VarChar(3000)
86 | answer String @db.VarChar(3000)
87 | options String @db.VarChar(3000)
88 | chapter Chapter @relation(fields:[chapterId], references:[id])
89 |
90 | @@index([chapterId], name:"chapterId")
91 |
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import {authOptions} from "@/lib/auth";
2 | import NextAuth from "next-auth/next";
3 |
4 | const handler=NextAuth(authOptions)
5 |
6 | export {handler as GET, handler as POST};
--------------------------------------------------------------------------------
/src/app/api/chapter/getInfo/route.ts:
--------------------------------------------------------------------------------
1 | // /api/chapter/getInfo
2 | import { prisma } from "@/lib/db"
3 | import { strict_output } from "@/lib/gpt"
4 | import { getQuestionsFromTranscript, getTranscript, searchYoutube } from "@/lib/youtube"
5 | import { NextResponse } from "next/server"
6 | import { z } from "zod"
7 |
8 | const bodyParser = z.object({
9 | chapterId: z.string()
10 | })
11 |
12 | export async function POST(req: Request, res: Response) {
13 | try {
14 | const body = await req.json()
15 | const { chapterId } = bodyParser.parse(body)
16 | const chapter = await prisma.chapter.findUnique({
17 | where: {
18 | id: chapterId
19 | }
20 | })
21 | if (!chapter) {
22 | return NextResponse.json({
23 | success: false,
24 | error: "Chapter not found"
25 | }, { status: 404 })
26 | }
27 | const videoId = await searchYoutube(chapter.youtubeSearchQuery)
28 | let transcript = await getTranscript(videoId)
29 | let maxLength = 500
30 | transcript = transcript.split(" ").slice(0, maxLength).join(" ")
31 | const {summary }:{ summary:string } = await strict_output("You are an AI capable of summarizing a youtube transcript",
32 | "summarize in 250 words or less and do not talk of the sponsors or anything unrelated to the main topic, also do not introduce what the summary is about.\n" + transcript, { summary: "summary of the transcript" })
33 | const questions = await getQuestionsFromTranscript(transcript, chapter.name)
34 | await prisma.question.createMany({
35 | data: questions.map((question) => {
36 | let options = [question.answer, question.option1, question.option2, question.option3,]
37 | options = options.sort(() => Math.random() - 0.5)
38 | return {
39 | question: question.question,
40 | answer: question.answer,
41 | options: JSON.stringify(options),
42 | chapterId: chapterId,
43 | }
44 | })
45 | })
46 | await prisma.chapter.update({
47 | where: {
48 | id: chapterId
49 | },
50 | data: {
51 | videoId: videoId,
52 | summary: summary,
53 | },
54 | })
55 | return NextResponse.json({success:true})
56 | } catch (error) {
57 | if (error instanceof z.ZodError) {
58 | return NextResponse.json({
59 | success: false, error: 'Invalid body'
60 | }, { status: 400 })
61 | }
62 | else {
63 | return NextResponse.json({
64 | success: false,
65 | error: "unknown"
66 | }, { status: 500 })
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/app/api/course/createChapters/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { createChaptersSchema } from "@/validators/course";
3 | import { ZodError } from "zod";
4 | import { getUnsplashImage } from "@/lib/unsplash";
5 | import { prisma } from "@/lib/db";
6 | import { strict_output } from "@/lib/gpt";
7 |
8 |
9 | export async function POST(req: Request, res: Response) {
10 | try {
11 | const body = await req.json();
12 | const { title, units } = createChaptersSchema.parse(body)
13 | type outputUnits = {
14 | title: String;
15 | chapters: {
16 | youtube_search_query: string;
17 | chapter_title: string;
18 | }[];
19 | }[];
20 | let output_units: outputUnits = await strict_output(
21 | "You are an AI capable of curating course content, coming up with relevant chapter titles, and finding relevant youtube videos for each chapter",
22 | new Array(units.length).fill(
23 | `It is your job to create a course about ${title}. The user has requested to create chapters for each of the units. Then, for each chapter, provide a detailed youtube search query that can be used to find an informative educational vidoe for each chapter. Each query should give an educational informative course in youtube.`
24 | ),
25 | {
26 | title: "title of the unit",
27 | chapters:
28 | "an array of chapters, each chapter should have a youtube_search_query and a chapter_title key in the JSON object",
29 | }
30 | );
31 | const imageSearchTerm = await strict_output(
32 | "you are an AI capable of finding the most relevant image for a course",
33 | `please provide a good image search term for the title of a course about ${title}. This search term will be fed into the unsplash api so make sure it is a good search term that will return good results`,
34 | {
35 | image_search_term: "a good search term for the title of the course"
36 | }
37 | )
38 | const course_image = await getUnsplashImage(imageSearchTerm.image_search_term);
39 | const course = await prisma.course.create({
40 | data: {
41 | name: title,
42 | image: course_image,
43 | },
44 | })
45 |
46 | for (const unit of output_units) {
47 | const title = unit.title;
48 | const prismaUnit = await prisma.unit.create({
49 | data: {
50 | name: title,
51 | courseId: course.id
52 | }
53 | })
54 | await prisma.chapter.createMany({
55 | data: unit.chapters.map((chapter) => {
56 | return {
57 | name: chapter.chapter_title,
58 | youtubeSearchQuery: chapter.youtube_search_query,
59 | unitId: prismaUnit.id
60 |
61 | }
62 | })
63 | })
64 | }
65 | console.log(output_units);
66 | return NextResponse.json({ course_id: course.id });
67 |
68 | } catch (error) {
69 | if (error instanceof ZodError) {
70 | return new NextResponse("invalid body", { status: 400 })
71 | }
72 | console.error(error)
73 | }
74 | }
--------------------------------------------------------------------------------
/src/app/course/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import CourseSideBar from '@/components/CourseSideBar'
2 | import MainVideoSummary from '@/components/MainVideoSummary'
3 | import QuizCards from '@/components/QuizCards'
4 | import { prisma } from "@/lib/db"
5 | import { ChevronLeft, ChevronRight } from 'lucide-react'
6 | import Link from 'next/link'
7 | import { redirect } from 'next/navigation'
8 | import React from 'react'
9 |
10 | type Props = {
11 | params:{
12 | slug: string[]
13 | }
14 | }
15 |
16 | const CoursePage = async ({params:{slug}}: Props) => {
17 | const[courseId,unitIndexParam,chapterIndexParam]=slug
18 | const course=await prisma.course.findUnique({
19 | where:{id:courseId},
20 | include:{
21 | units:{
22 | include:{chapters:{
23 | include:{questions:true}
24 | }}
25 | }
26 | }
27 | })
28 | if(!course){
29 | return redirect('/gallery')
30 |
31 | }
32 | let unitIndex=parseInt(unitIndexParam)
33 | let chapterIndex=parseInt(chapterIndexParam)
34 |
35 | const unit = course.units[unitIndex]
36 | if (!unit){
37 | return redirect('/gallery')
38 | }
39 | const chapter=unit.chapters[chapterIndex]
40 | if(!chapter){
41 | return redirect('/gallery')
42 | }
43 |
44 | const nextChapter=unit.chapters[chapterIndex+1]
45 | const prevChapter=unit.chapters[chapterIndex-1]
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
61 |
62 |
63 | {prevChapter&&(
64 |
66 |
67 |
68 |
69 |
70 | Previous
71 |
72 |
73 | {prevChapter.name}
74 |
75 |
76 |
77 |
78 | )}
79 |
80 | {nextChapter&&(
81 |
83 |
84 |
85 |
86 |
87 | Next
88 |
89 |
90 | {nextChapter.name}
91 |
92 |
93 |
94 |
95 |
96 | )}
97 |
98 |
99 |
100 |
101 | )
102 |
103 |
104 | }
105 |
106 | export default CoursePage
--------------------------------------------------------------------------------
/src/app/create/[courseId]/page.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmChapters from '@/components/ConfirmChapters'
2 | import { getAuthSession } from '@/lib/auth'
3 | import { prisma } from '@/lib/db'
4 | import { Info } from 'lucide-react'
5 | import { redirect } from 'next/navigation'
6 | import React from 'react'
7 |
8 | type Props = {
9 | params :{
10 | courseId: string
11 | }
12 | }
13 |
14 | const CreateChapters = async ({params:{ courseId}}: Props) => {
15 | const session = await getAuthSession()
16 | if (!session?.user){
17 | return redirect('/gallery')
18 | }
19 | const course = await prisma.course.findUnique({
20 | where:{
21 | id:courseId,
22 |
23 | },
24 | include:{
25 | units:{
26 | include:{
27 | chapters:true
28 | },
29 | },
30 | },
31 |
32 | })
33 | if (!course){
34 | return redirect('/create')
35 | }
36 | return (
37 |
38 |
39 | Course Name
40 |
41 |
42 | {course.name}
43 |
44 |
45 |
46 |
47 |
48 | We generated chapters for each of your units. Look over them and then click the button to confirm and continue
49 |
50 |
51 |
52 |
53 | )
54 |
55 | }
56 |
57 | export default CreateChapters
--------------------------------------------------------------------------------
/src/app/create/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from 'lucide-react'
2 | import React from 'react'
3 |
4 | type Props = {}
5 |
6 | const LoadingComponent = (props: Props) => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default LoadingComponent
--------------------------------------------------------------------------------
/src/app/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { getAuthSession } from '@/lib/auth'
2 | import React from 'react'
3 | import {redirect} from 'next/navigation'
4 | import { InfoIcon } from 'lucide-react'
5 | import CreateCourseForm from '@/components/CreateCourseForm'
6 |
7 | type Props = {}
8 |
9 | const CreatePage = async (props: Props) => {
10 | const session= await getAuthSession()
11 | if(!session?.user){
12 | return redirect('/gallery')
13 | }
14 | return (
15 |
16 |
17 |
18 |
19 | Learning Journey
20 |
21 |
22 |
23 |
24 | Enter in a course title, or what you want to learn about. Then enter a list of units which are the specifics you want to learn. And our AI will generate a course for you!
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default CreatePage
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggike/Full-Stack-AI-Course-Creator-Learning-Journey/15ea7bf160f72fb4edc3127dcc6df49921dd9cf9/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/gallery/page.tsx:
--------------------------------------------------------------------------------
1 | import GalleryCourseCard from '@/components/GalleryCourseCard'
2 | import { prisma } from '@/lib/db'
3 | import React from 'react'
4 |
5 | type Props = {}
6 |
7 | const GalleryPage = async (props: Props) => {
8 | const courses=await prisma.course.findMany({
9 | include:{
10 | units:{
11 | include:{chapters:true}
12 | }
13 | }
14 | })
15 | return (
16 |
17 |
18 | {courses.map((course)=>{
19 | return
20 | })}
21 |
22 |
23 | )
24 | }
25 |
26 | export default GalleryPage
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import './globals.css'
3 | import type { Metadata } from 'next'
4 | import { Lexend } from 'next/font/google'
5 | import Navbar from '@/components/Navbar';
6 | import { Provider } from '@/components/Providers';
7 | import { Toaster } from '@/components/ui/toaster';
8 |
9 | const lexend = Lexend({ subsets: ['latin'] })
10 |
11 | export const metadata: Metadata = {
12 | title: 'Learning Journey',
13 |
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: {
19 | children: React.ReactNode
20 | }) {
21 | return (
22 |
23 |
26 |
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 |
3 |
4 | export default function Home() {
5 | return Hello World! ;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/ChapterCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {Chapter} from '@prisma/client'
3 |
4 | import { cn } from '@/lib/utils'
5 | import React from 'react'
6 | import { useMutation } from '@tanstack/react-query'
7 | import axios from 'axios'
8 | import { useToast } from './ui/use-toast'
9 | import { Loader2 } from 'lucide-react'
10 |
11 |
12 | type Props = {
13 | chapter: Chapter
14 | chapterIndex: number
15 | completedChapters:Set;
16 | setCompletedChapters:React.Dispatch>>;
17 | }
18 |
19 | export type ChapterCardHandler = {
20 | triggerLoad: () => void;
21 | };
22 |
23 | const ChapterCard = React.forwardRef(({ chapter, chapterIndex, setCompletedChapters, completedChapters }, ref) => {
24 | const {toast}=useToast()
25 | const [ success, setSuccess ] = React.useState(null);
26 | const { mutate: getchapterInfo, isLoading } = useMutation({
27 | mutationFn: async () => {
28 | const response = await axios.post('/api/chapter/getInfo', { chapterId: chapter.id })
29 | return response.data
30 | }
31 | })
32 |
33 | const addChapterIdToSet=React.useCallback((
34 | )=>{
35 | // const newSet=new Set(completedChapters);
36 | // newSet.add(chapter.id);
37 | // setCompletedChapters(newSet)
38 | setCompletedChapters((prev)=>{
39 | const newSet=new Set(prev)
40 | newSet.add(chapter.id)
41 | return newSet
42 | }
43 | )
44 |
45 | },[ chapter.id, setCompletedChapters])
46 |
47 | React.useEffect(()=>{
48 | if(chapter.videoId){
49 | setSuccess(true);
50 | addChapterIdToSet
51 | }
52 | }, [chapter,addChapterIdToSet])
53 | React.useImperativeHandle(ref, () => ({
54 | async triggerLoad() {
55 | if (chapter.videoId){
56 | addChapterIdToSet()
57 | return
58 | }
59 | getchapterInfo(undefined, {
60 | onSuccess: ({success}) => {
61 | setSuccess(true)
62 | addChapterIdToSet()
63 | },
64 | onError:(error)=>{
65 | console.error(error)
66 | setSuccess(false)
67 | toast({
68 | title:"Error",
69 | description:"There was an error loading your chapter",
70 | variant:"destructive",
71 | })
72 | addChapterIdToSet()
73 |
74 | }
75 |
76 | })
77 | }
78 | }))
79 | return (
80 |
82 |
{chapter.name}
83 | {isLoading&& }
84 | )
85 | })
86 | ChapterCard.displayName = "ChapterCard"
87 |
88 | export default ChapterCard
89 |
90 |
--------------------------------------------------------------------------------
/src/components/ConfirmChapters.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import {Chapter,Course,Unit} from "@prisma/client";
4 | import ChapterCard, { ChapterCardHandler } from './ChapterCard';
5 | import { Separator } from './ui/separator';
6 | import { Button, buttonVariants } from './ui/button';
7 | import Link from 'next/link';
8 | import { ChevronLeft, ChevronRight } from 'lucide-react';
9 |
10 |
11 |
12 |
13 |
14 |
15 | type Props = {
16 | course:Course&{
17 | units:(Unit& {
18 | chapters:Chapter[];
19 | })[]
20 | }
21 | }
22 |
23 | const ConfirmChapters = ({course}: Props) => {
24 | const [loading, setLoading]=React.useState(false);
25 | const chapterRefs: Record>={};
26 | course.units.forEach((unit)=>{
27 | unit.chapters.forEach((chapter)=>{
28 | //eslint-disable-next-line react-hooks/rule of hooks
29 | chapterRefs[chapter.id] = React.useRef(null);
30 | })
31 | })
32 | const [completedChapters, setCompletedChapters]=React.useState>(new Set())
33 | const totalChaptersCount =React.useMemo(()=>{
34 | return course.units.reduce ((acc, unit)=>
35 | {
36 | return acc+unit.chapters.length
37 | }, 0)
38 | }, [course.units])
39 |
40 | return (
41 |
42 | {course.units.map((unit, unitIndex)=>{
43 | return(
44 |
45 | Unit {unitIndex+1}
46 |
47 |
48 | {unit.name}
49 |
50 |
51 | {unit.chapters.map((chapter, chapterIndex)=>{
52 | return ()
59 | })}
60 |
61 |
62 | )
63 |
64 | })}
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 | Back
75 |
76 | {
77 | totalChaptersCount===completedChapters.size?(
78 |
79 | Save & Continue
80 |
81 |
82 | ):(
83 | {
84 | setLoading(true)
85 | Object.values(chapterRefs).forEach((ref)=>{
86 | ref.current?.triggerLoad();
87 | })
88 | }}>
89 | Generate
90 |
91 |
92 | )
93 | }
94 |
95 |
96 |
97 |
98 |
99 | )}
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | export default ConfirmChapters;
--------------------------------------------------------------------------------
/src/components/CourseSideBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Chapter, Course, Unit } from "@prisma/client"
3 | import Link from 'next/link'
4 | import { cn } from '@/lib/utils'
5 | import { Separator } from './ui/separator'
6 |
7 | type Props = {
8 | course:Course&{
9 | units:(Unit&{
10 | chapters:Chapter[]
11 | })[]
12 | }
13 | currentChapterId:string
14 | }
15 |
16 | const CourseSideBar = async ({course, currentChapterId}: Props) => {
17 | return (
18 |
19 |
20 | {course.name}
21 |
22 |
23 | {course.units.map((unit,unitIndex)=>{
24 | return (
25 |
26 | Unit {unitIndex+1}
27 |
28 |
29 | {unit.name}
30 |
31 | {unit.chapters.map((chapter,chapterIndex)=>{
32 | return(
33 |
34 |
38 | {chapter.name}
39 |
40 | )
41 | })}
42 |
43 |
44 | )
45 | })}
46 |
47 | )
48 | }
49 |
50 |
51 |
52 | export default CourseSideBar
--------------------------------------------------------------------------------
/src/components/CreateCourseForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import { Form, FormField, FormItem, FormLabel, FormControl } from './ui/form'
4 | import { createChaptersSchema } from '@/validators/course'
5 | import {z} from "zod";
6 | import {useForm} from "react-hook-form"
7 |
8 | import {zodResolver} from '@hookform/resolvers/zod'
9 | import { Input } from './ui/input';
10 | import { Separator } from './ui/separator';
11 | import { Button } from './ui/button';
12 | import { Plus, Trash } from 'lucide-react';
13 | import {motion, AnimatePresence}from 'framer-motion'
14 | import { useMutation } from '@tanstack/react-query';
15 | import axios from 'axios';
16 | import { useToast } from './ui/use-toast';
17 | import { useRouter } from 'next/navigation';
18 |
19 |
20 |
21 | type Props = {};
22 |
23 | type Input= z.infer
24 |
25 | const CreateCourseForm = (props: Props) => {
26 | const router=useRouter();
27 | const {toast}=useToast()
28 | const {mutate:createChapters, isLoading}=useMutation({
29 | mutationFn: async({title,units}:Input)=>{
30 | const response=await axios.post('/api/course/createChapters', {title,units})
31 | return response.data
32 |
33 | }
34 | })
35 | const form=useForm ({
36 | resolver:zodResolver(createChaptersSchema),
37 | defaultValues:{
38 | title:'',
39 | units:['','','']
40 | }
41 | })
42 |
43 | function onSubmit(data:Input){
44 | if (data.units.some(unit=>unit==='')){
45 | toast({
46 | title:"error",
47 | description:"please fill all the units",
48 | variant:"destructive",
49 |
50 | })
51 | return;
52 | }
53 | createChapters(data,{
54 | onSuccess:({course_id})=>{
55 | toast({
56 | title:"success",
57 | description:"course created successfully",
58 | })
59 | router.push(`/create/${course_id}`)
60 |
61 | },
62 | onError:(error)=>{
63 | console.error(error)
64 | toast({
65 | title:"Error",
66 | description:"Something went wrong",
67 | variant:"destructive",
68 | })
69 | }
70 | })
71 | }
72 | form.watch();
73 |
74 |
75 | return (
76 |
159 | )
160 | }
161 |
162 | export default CreateCourseForm
--------------------------------------------------------------------------------
/src/components/GalleryCourseCard.tsx:
--------------------------------------------------------------------------------
1 | import { Chapter, Course, Unit } from '@prisma/client'
2 | import Link from 'next/link'
3 | import Image from 'next/image'
4 |
5 |
6 | import React from 'react'
7 |
8 | type Props = {
9 | course:Course&{
10 | units:(Unit&{
11 | chapters:Chapter[]
12 | })[]
13 | }
14 | }
15 |
16 | const GalleryCourseCard = async ({course}: Props) => {
17 | return (
18 | <>
19 |
20 |
21 |
22 |
27 |
28 | {course.name}
29 |
30 |
31 |
32 |
33 |
34 | Units
35 |
36 |
37 | {course.units.map((unit,unitIndex)=>{
38 | return(
39 |
42 | {unit.name}
43 |
44 | )
45 | })}
46 |
47 |
48 |
49 | >
50 | )
51 | }
52 |
53 | export default GalleryCourseCard
--------------------------------------------------------------------------------
/src/components/MainVideoSummary.tsx:
--------------------------------------------------------------------------------
1 | import { Chapter, Unit } from '@prisma/client'
2 | import React from 'react'
3 |
4 | type Props = {
5 | chapter: Chapter
6 | unit: Unit
7 | unitIndex: number
8 | chapterIndex: number
9 | }
10 |
11 | const MainVideoSummary = ({ unit, unitIndex, chapter, chapterIndex }: Props) => {
12 | return (
13 |
14 | Unit{unitIndex + 1} &bull:Chapter{chapterIndex + 1}
15 |
16 | {chapter.name}
17 |
VIDEO
19 |
20 |
21 | Summary
22 |
23 |
24 |
25 | {chapter.summary}
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default MainVideoSummary
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 |
4 | import SignInButton from './SignInButton'
5 | import { getAuthSession } from '@/lib/auth'
6 | import UserAccountNav from './UserAccountNav'
7 | import { ThemeToggle } from './ThemeToggle'
8 | import Link from 'next/link'
9 |
10 | type Props = {}
11 |
12 | const Navbar = async (props: Props) => {
13 | const session=await getAuthSession()
14 | console.log(session)
15 | return (
16 |
17 |
18 |
19 |
20 | Learning Journey
21 |
22 |
23 |
24 |
25 | Gallery
26 |
27 | {session?.user && (
28 | <>
29 |
30 | Create Course
31 |
32 |
33 | Settings
34 |
35 | >
36 |
37 | )}
38 |
39 |
40 | {session?.user ? : }
41 |
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default Navbar
--------------------------------------------------------------------------------
/src/components/Providers.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 | import {QueryClient,QueryClientProvider} from '@tanstack/react-query'
7 |
8 | const queryClient=new QueryClient();
9 |
10 | export function Provider({ children, ...props }: ThemeProviderProps) {
11 | return (
12 |
13 | {children}
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/QuizCards.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Chapter, Question } from '@prisma/client'
3 | import React from 'react'
4 | import { RadioGroup, RadioGroupItem } from './ui/radio-group'
5 | import { Label } from './ui/label'
6 | import { cn } from '@/lib/utils'
7 | // import { Button } from './ui/button'
8 | import { ChevronRight } from 'lucide-react'
9 | import { Button } from './ui/button'
10 | // import { Button } from './ui/button'
11 |
12 | type Props = {
13 | chapter:Chapter&{
14 | questions:Question[]
15 | }
16 | }
17 |
18 | const QuizCards = ({chapter}: Props) => {
19 |
20 | const[answers,setAnswers]=React.useState>({})
21 | const [questionState,setQuestionState]=React.useState>({})
22 | // {
23 | // 'question1.id':true,
24 | // 'question2.id':false,
25 | // 'question3.id':null
26 | // }
27 |
28 | const checkAnswer=React.useCallback(()=>{
29 | const newQuestionState={...questionState}
30 | chapter.questions.forEach((question)=>{
31 | const user_answer=answers[question.id]
32 | if(!user_answer) return
33 | if(user_answer===question.answer){
34 | newQuestionState[question.id]=true
35 | }else{
36 | newQuestionState[question.id]=false
37 | }
38 | setQuestionState(newQuestionState)
39 | })
40 |
41 | },[answers,questionState,chapter.questions])
42 |
43 | return (
44 |
45 |
46 | Concept Check
47 |
48 |
49 |
50 | {chapter.questions.map(question=>{
51 |
52 | const options=JSON.parse(question.options) as string[]
53 | return
58 |
59 |
60 |
61 | {question.question}
62 |
63 |
64 |
{
65 | setAnswers((prev)=>{
66 | return{
67 | ...prev,
68 | [question.id]:e,
69 | }
70 |
71 | })
72 | }}>
73 | {options.map((option,index)=>{
74 | return(
75 |
76 |
77 |
78 | {option}
79 |
80 |
81 |
82 | )
83 | })}
84 |
85 |
86 |
87 |
88 |
89 | })}
90 |
91 |
92 |
93 | Check Answer!
94 |
95 |
96 |
97 |
98 | )
99 | }
100 |
101 | export default QuizCards
--------------------------------------------------------------------------------
/src/components/SignInButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React from 'react'
3 | import { Button } from './ui/button'
4 | import { signIn } from 'next-auth/react'
5 |
6 | type Props = {}
7 |
8 | const SignInButton = (props: Props) => {
9 | return (
10 | {
13 | signIn('google')
14 | }}>
15 | Sign In
16 |
17 | )
18 | }
19 |
20 | export default SignInButton
--------------------------------------------------------------------------------
/src/components/ThemeToggle.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 ThemeToggle({className, ...props}:React.HTMLAttributes) {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | Toggle theme
26 |
27 |
28 |
29 | setTheme("light")}>
30 | Light
31 |
32 | setTheme("dark")}>
33 | Dark
34 |
35 | setTheme("system")}>
36 | System
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/UserAccountNav.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { Button } from './ui/button'
5 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu'
6 | //import { UserInfo } from 'os'
7 | //import { AdapterUser } from 'next-auth/adapters'
8 | import { User } from 'next-auth'
9 | import { signOut } from 'next-auth/react'
10 | import { LogOut } from 'lucide-react'
11 | import UserAvatar from './UserAvatar'
12 | //import { userAgent } from 'next/server'
13 |
14 |
15 | type Props = {
16 | user:User
17 | }
18 |
19 | const UserAccountNav = ({user}: Props) => {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {user?.name && (
{user.name}
)}
31 | {user?.email && (
{user.email}
)}
32 |
33 |
34 | {
35 | signOut();
36 |
37 |
38 | }}
39 | className='text-red-600 cursor-pointer'
40 | >Sign out
41 |
42 |
43 | )
44 | }
45 |
46 | export default UserAccountNav
--------------------------------------------------------------------------------
/src/components/UserAvatar.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback } from './ui/avatar'
2 | import { User } from 'next-auth'
3 | import React from 'react'
4 | import Image from 'next/image'
5 |
6 | type Props = {
7 | user:User
8 | }
9 |
10 | const UserAvatar = ({user}: Props) => {
11 | return (
12 |
13 | {user.image?(
14 |
15 |
16 |
17 |
18 | ):(
19 |
20 | {user?.name}
21 |
22 | )}
23 |
24 | )
25 | }
26 |
27 | export default UserAvatar
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/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/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, children, ...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/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { DefaultSession, NextAuthOptions, getServerSession } from "next-auth";
2 | import {prisma} from "./db";
3 | import { PrismaAdapter } from "@next-auth/prisma-adapter";
4 | import GoogleProvider from 'next-auth/providers/google';
5 |
6 |
7 | declare module 'next-auth'{
8 | interface Session extends DefaultSession{
9 | user:{
10 | id:string;
11 | credits:number;
12 | }& DefaultSession["user"];
13 | }
14 | }
15 |
16 | declare module 'next-auth/jwt'{
17 | interface JWT{
18 | id:string;
19 | credits:number;
20 | }
21 | }
22 |
23 | export const authOptions: NextAuthOptions={
24 | session:{
25 | strategy:'jwt'
26 | },
27 | callbacks:{
28 | jwt:async({token})=>{
29 | const db_user=await prisma.user.findFirst({
30 | where:{
31 | email:token.email,
32 | },
33 | });
34 | if (db_user){
35 | token.id=db_user.id
36 | token.credits=db_user.credits
37 | }
38 | return token;
39 | },
40 | session: ({session,token})=>{
41 | if (token){
42 | session.user.id=token.id;
43 | session.user.name=token.name;
44 | session.user.email=token.email;
45 | session.user.image=token.picture;
46 | session.user.credits=token.credits;
47 | }
48 | return session;
49 | },
50 | },
51 |
52 | secret:process.env.NEXTAUTH_SECRET as string,
53 | adapter:PrismaAdapter(prisma),
54 | providers:[
55 | GoogleProvider({
56 | clientId:process.env.GOOGLE_CLIENT_ID as string,
57 | clientSecret:process.env.GOOGLE_CLIENT_SECRET as string,
58 | }),
59 | ],
60 |
61 |
62 |
63 | };
64 | export const getAuthSession=()=>{
65 | return getServerSession(authOptions)
66 | }
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import "server-only";
3 |
4 | declare global {
5 | // eslint-disable-next-line no-var, no-unused-vars
6 | var cachedPrisma: PrismaClient;
7 | }
8 |
9 | export let prisma: PrismaClient;
10 | if (process.env.NODE_ENV === "production") {
11 | prisma = new PrismaClient();
12 | } else {
13 | if (!global.cachedPrisma) {
14 | global.cachedPrisma = new PrismaClient();
15 | }
16 | prisma = global.cachedPrisma;
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/gpt.ts:
--------------------------------------------------------------------------------
1 | import { Configuration, OpenAIApi } from "openai";
2 |
3 | const configuration = new Configuration({
4 | apiKey: process.env.OPENAI_API_KEY,
5 | });
6 | const openai = new OpenAIApi(configuration);
7 |
8 | interface OutputFormat {
9 | [key: string]: string | string[] | OutputFormat;
10 | }
11 |
12 | export async function strict_output(
13 | system_prompt: string,
14 | user_prompt: string | string[],
15 | output_format: OutputFormat,
16 | default_category: string = "",
17 | output_value_only: boolean = false,
18 | model: string = "gpt-3.5-turbo",
19 | temperature: number = 1,
20 | num_tries: number = 3,
21 | verbose: boolean = false
22 | ): Promise<
23 | {
24 | question: string;
25 | answer: string;
26 | }[]
27 | > {
28 | // if the user input is in a list, we also process the output as a list of json
29 | const list_input: boolean = Array.isArray(user_prompt);
30 | // if the output format contains dynamic elements of < or >, then add to the prompt to handle dynamic elements
31 | const dynamic_elements: boolean = /<.*?>/.test(JSON.stringify(output_format));
32 | // if the output format contains list elements of [ or ], then we add to the prompt to handle lists
33 | const list_output: boolean = /\[.*?\]/.test(JSON.stringify(output_format));
34 |
35 | // start off with no error message
36 | let error_msg: string = "";
37 |
38 | for (let i = 0; i < num_tries; i++) {
39 | let output_format_prompt: string = `\nYou are to output the following in json format: ${JSON.stringify(
40 | output_format
41 | )}. \nDo not put quotation marks or escape character \\ in the output fields.`;
42 |
43 | if (list_output) {
44 | output_format_prompt += `\nIf output field is a list, classify output into the best element of the list.`;
45 | }
46 |
47 | // if output_format contains dynamic elements, process it accordingly
48 | if (dynamic_elements) {
49 | output_format_prompt += `\nAny text enclosed by < and > indicates you must generate content to replace it. Example input: Go to , Example output: Go to the garden\nAny output key containing < and > indicates you must generate the key name to replace it. Example input: {'': 'description of location'}, Example output: {school: a place for education}`;
50 | }
51 |
52 | // if input is in a list format, ask it to generate json in a list
53 | if (list_input) {
54 | output_format_prompt += `\nGenerate a list of json, one json for each input element.`;
55 | }
56 |
57 | // Use OpenAI to get a response
58 | const response = await openai.createChatCompletion({
59 | temperature: temperature,
60 | model: model,
61 | messages: [
62 | {
63 | role: "system",
64 | content: system_prompt + output_format_prompt + error_msg,
65 | },
66 | { role: "user", content: user_prompt.toString() },
67 | ],
68 | });
69 |
70 | let res: string =
71 | response.data.choices[0].message?.content?.replace(/'/g, '"') ?? "";
72 |
73 | // ensure that we don't replace away apostrophes in text
74 | res = res.replace(/(\w)"(\w)/g, "$1'$2");
75 |
76 | if (verbose) {
77 | console.log(
78 | "System prompt:",
79 | system_prompt + output_format_prompt + error_msg
80 | );
81 | console.log("\nUser prompt:", user_prompt);
82 | console.log("\nGPT response:", res);
83 | }
84 |
85 | // try-catch block to ensure output format is adhered to
86 | try {
87 | let output: any = JSON.parse(res);
88 |
89 | if (list_input) {
90 | if (!Array.isArray(output)) {
91 | throw new Error("Output format not in a list of json");
92 | }
93 | } else {
94 | output = [output];
95 | }
96 |
97 | // check for each element in the output_list, the format is correctly adhered to
98 | for (let index = 0; index < output.length; index++) {
99 | for (const key in output_format) {
100 | // unable to ensure accuracy of dynamic output header, so skip it
101 | if (/<.*?>/.test(key)) {
102 | continue;
103 | }
104 |
105 | // if output field missing, raise an error
106 | if (!(key in output[index])) {
107 | throw new Error(`${key} not in json output`);
108 | }
109 |
110 | // check that one of the choices given for the list of words is an unknown
111 | if (Array.isArray(output_format[key])) {
112 | const choices = output_format[key] as string[];
113 | // ensure output is not a list
114 | if (Array.isArray(output[index][key])) {
115 | output[index][key] = output[index][key][0];
116 | }
117 | // output the default category (if any) if GPT is unable to identify the category
118 | if (!choices.includes(output[index][key]) && default_category) {
119 | output[index][key] = default_category;
120 | }
121 | // if the output is a description format, get only the label
122 | if (output[index][key].includes(":")) {
123 | output[index][key] = output[index][key].split(":")[0];
124 | }
125 | }
126 | }
127 |
128 | // if we just want the values for the outputs
129 | if (output_value_only) {
130 | output[index] = Object.values(output[index]);
131 | // just output without the list if there is only one element
132 | if (output[index].length === 1) {
133 | output[index] = output[index][0];
134 | }
135 | }
136 | }
137 |
138 | return list_input ? output : output[0];
139 | } catch (e) {
140 | error_msg = `\n\nResult: ${res}\n\nError message: ${e}`;
141 | console.log("An exception occurred:", e);
142 | console.log("Current invalid json format:", res);
143 | }
144 | }
145 |
146 | return [];
147 | }
--------------------------------------------------------------------------------
/src/lib/unsplash.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | export const getUnsplashImage=async (query:string)=>{
3 | const {data}=await axios.get(`
4 | https://api.unsplash.com/search/photos?per_page=1&query=${query}&client_id=${process.env.UNSPLASH_API_KEY}`);
5 |
6 | return data.results[0].urls.small_s3;
7 | };
8 |
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/youtube.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import {YoutubeTranscript} from 'youtube-transcript'
3 | import { strict_output } from "./gpt";
4 |
5 |
6 | export async function searchYoutube(searchQuery:string){
7 | searchQuery=encodeURIComponent(searchQuery);
8 | const {data}=await axios.get(
9 | `https://www.googleapis.com/youtube/v3/search?key=${process.env.YOUTUBE_API_KEY}&q=${searchQuery}&videoDuration=medium&videoEmbeddable=true&type=video&maxResults=5`
10 | )
11 | if (!data){
12 | console.log('youtube fail')
13 | return null
14 | }
15 | if (data.items[0]==undefined){
16 | console.log('youtube fail')
17 | return null
18 | }
19 | return data.items[0].id.videoId;
20 | }
21 | export async function getTranscript(videoId:string){
22 | try{
23 | let transcript_arr=await YoutubeTranscript.fetchTranscript(videoId,{
24 | lang: "en",
25 | country:"EN"
26 | })
27 | let transcript=''
28 | for (let t of transcript_arr){
29 | transcript+=t.text+' '
30 |
31 | }
32 | return transcript.replaceAll("/n","")
33 | } catch (error){
34 | return ""
35 | }
36 | }
37 | export async function getQuestionsFromTranscript(transcript:string, course_title:string){
38 | type Question={
39 | question:string,
40 | answer:string,
41 | option1:string,
42 | option2:string,
43 | option3:string,
44 | }
45 | const questions:Question[]= await strict_output(
46 | 'You are a helpful AI that is able to generate mcq questions and answers, the length of each answer should not be more than 15 words',
47 | new Array(5).fill(
48 | `You are to generate a random hard mcq question about ${course_title} with context of the following transcript: ${transcript}`
49 | ),
50 | {
51 | questions:'question',
52 | answer:'answer with max length of 15 words',
53 | option1:'option1 with max length of 15 words',
54 | option2:'option2 with max length of 15 words',
55 | option3:'option3 with max length of 15 words',
56 | }
57 | )
58 | return questions
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/validators/course.ts:
--------------------------------------------------------------------------------
1 |
2 | import {z} from 'zod';
3 |
4 | export const createChaptersSchema =z.object({
5 | title:z.string().min(3).max(100),
6 | units:z.array(z.string())
7 | });
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------