diff --git a/EduBuddy/.eslintrc.json b/EduBuddy/.eslintrc.json new file mode 100644 index 0000000..957cd15 --- /dev/null +++ b/EduBuddy/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals"] +} diff --git a/EduBuddy/.gitignore b/EduBuddy/.gitignore new file mode 100644 index 0000000..00bba9b --- /dev/null +++ b/EduBuddy/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/EduBuddy/README.md b/EduBuddy/README.md new file mode 100644 index 0000000..521c8c3 --- /dev/null +++ b/EduBuddy/README.md @@ -0,0 +1,110 @@ +# EduBuddy + +*EduBuddy* is an AI-powered learning companion designed to enhance the study experience by providing personalized learning support for students of all levels. The platform leverages AI to deliver interactive educational tools that help students excel in their academic journey. + +## Features + +EduBuddy offers a comprehensive suite of learning tools: + +- *Doubt Solving*: Ask questions and receive clear explanations from AI. +- *Flashcards*: Generate custom flashcards to aid memorization and revision. +- *Quiz Generation*: Create personalized quizzes to test knowledge. +- *Code Generation*: Get code snippets and explanations for various programming topics. + +## Getting Started + +These instructions will help you set up EduBuddy on your local machine for development and testing. + +### Prerequisites + +Make sure you have the following installed: + +- [Node.js](https://nodejs.org/) (v14 or higher) +- [npm](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) + +## Installation + +1. Clone the repository: + + bash + git clone https://github.com/bhavkushwaha/EduBuddy + + +2. Navigate into the project directory: + + bash + cd EduBuddy + + +3. Install dependencies: + + bash + npm install + + +4. (Optional) Set up the database and configure environment variables: + + Create a .env file in the root directory with the following variables: + + + # Example .env file + + ``` + + DB_HOST=localhost + DB_PORT=5432 + DB_USER=your-username + DATABASE_URL=your-url + + NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in + NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up + NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard + NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard + + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "your_key" + CLERK_SECRET_KEY = "your_key" + + ``` + + +## Usage + +1. Start the development server: + + bash + npm run dev + + +2. Open your browser and navigate to: + + + http://localhost:3000 + + +3. To create a production build: + + bash + npm run build + + + +## Configuration + +EduBuddy requires certain environment variables to be set for database connections and API key integration: + +- **Database Configuration**: Modify the `.env` file as needed. +- **API Keys**: If using third-party APIs for quiz generation or code snippets, add them in the `.env` file (we used OpenAI ) + + ``` + + OPENAI_API_KEY=your-openai-api-key + + ``` + +## Scripts/Commands + +The project includes several scripts to streamline development and deployment: + +- npm start: Starts the server in production mode. +- npm run dev: Starts the server in development mode with hot-reloading. +- npm run build: Builds the project for production. \ No newline at end of file diff --git a/EduBuddy/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx b/EduBuddy/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..5ee3fe3 --- /dev/null +++ b/EduBuddy/app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,5 @@ +import { SignIn } from "@clerk/nextjs"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/EduBuddy/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx b/EduBuddy/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..20e230b --- /dev/null +++ b/EduBuddy/app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx @@ -0,0 +1,5 @@ +import { SignUp } from "@clerk/nextjs"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/EduBuddy/app/(auth)/layout.tsx b/EduBuddy/app/(auth)/layout.tsx new file mode 100644 index 0000000..818af32 --- /dev/null +++ b/EduBuddy/app/(auth)/layout.tsx @@ -0,0 +1,13 @@ +const AuthLayout = ({ + children +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +export default AuthLayout; \ No newline at end of file diff --git a/EduBuddy/app/(dashboard)/(routes)/code/constants.ts b/EduBuddy/app/(dashboard)/(routes)/code/constants.ts new file mode 100644 index 0000000..f0ca572 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/code/constants.ts @@ -0,0 +1,7 @@ +import * as z from "zod"; + +export const formSchema = z.object({ + prompt: z.string().min(1, { + message: "Prompt is required", + }), +}); \ No newline at end of file diff --git a/EduBuddy/app/(dashboard)/(routes)/code/page.tsx b/EduBuddy/app/(dashboard)/(routes)/code/page.tsx new file mode 100644 index 0000000..46f3ae2 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/code/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState } from "react"; +import axios from "axios"; +import * as z from "zod"; +import { Code } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { formSchema } from "./constants"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ChatCompletionMessageParam } from "openai/resources/index.mjs"; +import { useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; +import ReactMarkdown from "react-markdown"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +import { Heading } from "@/components/heading"; +import { Empty } from "@/components/empty"; +import { Loader } from "@/components/loader"; +import { UserAvatar } from "@/components/user-avatar"; +import { BotAvatar } from "@/components/bot-avatar"; +import { useProModal } from "@/hooks/use-pro-modal"; + +const CodePage = () => { + const router = useRouter(); + + const proModal = useProModal(); + + const [messages, setMessages] = useState([]); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + prompt: "", + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const userMessage: ChatCompletionMessageParam = { + role: "user", + content: values.prompt, + }; + + const newMessages = [...messages, userMessage]; + + const response = await axios.post("/api/code", { + messages: newMessages, + }); + + setMessages((current) => [...current, userMessage, response.data]); + + form.reset(); + } catch (error: any) { + if (error?.response?.status === 403) { + proModal.onOpen(); + } + console.log(error); + } finally { + router.refresh(); + } + }; + + return ( +
+ +
+
+
+ + ( + + + + + + )} + /> + + + +
+
+ {isLoading && ( +
+ +
+ )} + + {messages.length === 0 && !isLoading && ( +
+ +
+ )} + +
+ {messages.map((message) => ( +
+ {message.role === "user" ? : } + ( +
+
+                      
+ ), + code: ({ node, ...props }) => ( + + ), + }} + className="text-sm overflow-hidden leading-7" + > + {String(message.content) || ""} +
+
+ ))} +
+
+
+
+ ); +}; + +export default CodePage; diff --git a/EduBuddy/app/(dashboard)/(routes)/conversation/constants.ts b/EduBuddy/app/(dashboard)/(routes)/conversation/constants.ts new file mode 100644 index 0000000..f0ca572 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/conversation/constants.ts @@ -0,0 +1,7 @@ +import * as z from "zod"; + +export const formSchema = z.object({ + prompt: z.string().min(1, { + message: "Prompt is required", + }), +}); \ No newline at end of file diff --git a/EduBuddy/app/(dashboard)/(routes)/conversation/page.tsx b/EduBuddy/app/(dashboard)/(routes)/conversation/page.tsx new file mode 100644 index 0000000..aeaadb0 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/conversation/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState } from "react"; +import axios from "axios"; +import * as z from "zod"; +import { MessageSquare } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { formSchema } from "./constants"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ChatCompletionMessageParam } from "openai/resources/index.mjs"; +import { useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; +import ReactMarkdown from "react-markdown"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +import { Heading } from "@/components/heading"; +import { Empty } from "@/components/empty"; +import { Loader } from "@/components/loader"; +import { UserAvatar } from "@/components/user-avatar"; +import { BotAvatar } from "@/components/bot-avatar"; +import { useProModal } from "@/hooks/use-pro-modal"; + +const DoubtSolvePage = () => { + const router = useRouter(); + + const proModal = useProModal(); + + const [messages, setMessages] = useState([]); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + prompt: "", + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const userMessage: ChatCompletionMessageParam = { + role: "user", + content: values.prompt, + }; + + const newMessages = [...messages, userMessage]; + + const response = await axios.post("/api/conversation", { + messages: newMessages, + }); + + setMessages((current) => [...current, userMessage, response.data]); + + form.reset(); + } catch (error: any) { + if (error?.response?.status === 403) { + proModal.onOpen(); + } + console.log(error); + } finally { + router.refresh(); + } + }; + + return ( +
+ +
+
+
+ + ( + + + + + + )} + /> + + + +
+
+ {isLoading && ( +
+ +
+ )} + + {messages.length === 0 && !isLoading && ( +
+ +
+ )} + +
+ {messages.map((message) => ( +
+ {message.role === "user" ? : } + ( +
+
+                      
+ ), + code: ({ node, ...props }) => ( + + ), + }} + className="text-sm overflow-hidden leading-7" + > + {String(message.content) || ""} +
+
+ ))} +
+
+
+
+ ); +}; + +export default DoubtSolvePage; diff --git a/EduBuddy/app/(dashboard)/(routes)/dashboard/page.tsx b/EduBuddy/app/(dashboard)/(routes)/dashboard/page.tsx new file mode 100644 index 0000000..e2ca9c2 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/dashboard/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { + ArrowRight, + Code, + ImageIcon, + MessageSquare, + FileQuestion, +} from "lucide-react"; + +const tools = [ + { + label: "Doubts Solving", + icon: MessageSquare, + color: "text-violet-500", + bgColor: "bg-violet-500/10", + href: "/conversation", + }, + { + label: "Quiz Generation", + icon: FileQuestion, + color: "text-emerald-500", + bgColor: "bg-emerald-500/10", + href: "/quiz", + }, + { + label: "Flashcards", + icon: ImageIcon, + color: "text-pink-700", + bgColor: "bg-pink-700/10", + href: "/flashcards", + }, + { + label: "Coding Assistant", + icon: Code, + color: "text-green-700", + bgColor: "bg-green-700/10", + href: "/code", + }, +]; + +const DashboardPage = () => { + const router = useRouter(); + + return ( +
+
+

+ EduBuddy +

+

+ Chat with the smartest AI - Resolve your queries! +

+ +
+ {tools.map((tool) => ( + router.push(tool.href)} + key={tool.href} + className="p-4 border-black/5 flex items-center justify-between hover:shadow-md transition cursor-pointer" + > +
+
+ +
+
{tool.label}
+
+ + +
+ ))} +
+
+
+ ); +}; + +export default DashboardPage; diff --git a/EduBuddy/app/(dashboard)/(routes)/flashcards/constants.ts b/EduBuddy/app/(dashboard)/(routes)/flashcards/constants.ts new file mode 100644 index 0000000..89ab127 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/flashcards/constants.ts @@ -0,0 +1,6 @@ +import z from 'zod'; + +export const formSchema = z.object({ + topic: z.string().min(1, "Topic is required"), + numFlashcards: z.number().min(1, "At least one flashcard is required").optional(), + }); \ No newline at end of file diff --git a/EduBuddy/app/(dashboard)/(routes)/flashcards/page.tsx b/EduBuddy/app/(dashboard)/(routes)/flashcards/page.tsx new file mode 100644 index 0000000..ddc7ef7 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/flashcards/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState } from "react"; +import axios from "axios"; +import { useForm } from "react-hook-form"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Heading } from "@/components/heading"; +import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card"; +import { ImageIcon } from "lucide-react"; +import { Loader } from "@/components/loader"; +import { Empty } from "@/components/empty"; + +type Flashcard = { + title: string; + content: string; +}; + +export default function FlashcardGenerator() { + const [flashcards, setFlashcards] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const form = useForm<{ topic: string }>(); + + const onSubmit = async (data: { topic: string }) => { + setIsLoading(true); + try { + const response = await axios.post('/api/flashcards', { topic: data.topic }); + const newFlashcard: Flashcard = { + title: response.data.title, + content: response.data.content + }; + setFlashcards((prev) => [...prev, newFlashcard]); + } catch (error) { + console.error("Error generating flashcard:", error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+
+ + ( + + + + + + )} + /> + + + + +
+ {isLoading && ( +
+ +
+ )} + {flashcards.length === 0 && !isLoading && +
+ +
+ } + + {flashcards.map((flashcard, index) => ( + + + {flashcard.title} {/* Title is the topic */} + + +

{flashcard.content}

{/* Content is the response */} +
+
+ ))} +
+
+
+ ); +} diff --git a/EduBuddy/app/(dashboard)/(routes)/quiz/constants.ts b/EduBuddy/app/(dashboard)/(routes)/quiz/constants.ts new file mode 100644 index 0000000..f0ca572 --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/quiz/constants.ts @@ -0,0 +1,7 @@ +import * as z from "zod"; + +export const formSchema = z.object({ + prompt: z.string().min(1, { + message: "Prompt is required", + }), +}); \ No newline at end of file diff --git a/EduBuddy/app/(dashboard)/(routes)/quiz/page.tsx b/EduBuddy/app/(dashboard)/(routes)/quiz/page.tsx new file mode 100644 index 0000000..f4b47fe --- /dev/null +++ b/EduBuddy/app/(dashboard)/(routes)/quiz/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState } from "react"; +import axios from "axios"; +import * as z from "zod"; +import { Code } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { formSchema } from "./constants"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ChatCompletionMessageParam } from "openai/resources/index.mjs"; +import { useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; +import ReactMarkdown from "react-markdown"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +import { Heading } from "@/components/heading"; +import { Empty } from "@/components/empty"; +import { Loader } from "@/components/loader"; +import { UserAvatar } from "@/components/user-avatar"; +import { BotAvatar } from "@/components/bot-avatar"; +import { useProModal } from "@/hooks/use-pro-modal"; + +const CodePage = () => { + const router = useRouter(); + + const proModal = useProModal(); + + const [messages, setMessages] = useState([]); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + prompt: "", + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmit = async (values: z.infer) => { + try { + const userMessage: ChatCompletionMessageParam = { + role: "user", + content: values.prompt, + }; + + const newMessages = [...messages, userMessage]; + + const response = await axios.post("/api/quiz", { + messages: newMessages, + }); + + setMessages((current) => [...current, userMessage, response.data]); + + form.reset(); + } catch (error: any) { + if (error?.response?.status === 403) { + proModal.onOpen(); + } + console.log(error); + } finally { + router.refresh(); + } + }; + + return ( +
+ +
+
+
+ + ( + + + + + + )} + /> + + + +
+
+ {isLoading && ( +
+ +
+ )} + + {messages.length === 0 && !isLoading && ( +
+ +
+ )} + +
+ {messages.map((message) => ( +
+ {message.role === "user" ? : } + ( +
+
+                      
+ ), + code: ({ node, ...props }) => ( + + ), + }} + className="text-sm overflow-hidden leading-7" + > + {String(message.content) || ""} +
+
+ ))} +
+
+
+
+ ); +}; + +export default CodePage; diff --git a/EduBuddy/app/(dashboard)/layout.tsx b/EduBuddy/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..580b7d0 --- /dev/null +++ b/EduBuddy/app/(dashboard)/layout.tsx @@ -0,0 +1,21 @@ +import Navbar from "@/components/navbar"; +import Sidebar from "@/components/sidebar"; +import { getApiLimitCount } from "@/lib/api-limit"; + +const DashboardLayout = async ({ children }: { children: React.ReactNode }) => { + const apiLimitCount = await getApiLimitCount(); + + return ( +
+
+ +
+
+ + {children} +
+
+ ); +}; + +export default DashboardLayout; diff --git a/EduBuddy/app/(landing)/layout.tsx b/EduBuddy/app/(landing)/layout.tsx new file mode 100644 index 0000000..abc7839 --- /dev/null +++ b/EduBuddy/app/(landing)/layout.tsx @@ -0,0 +1,9 @@ +const LandingLayout = ({ children }: { children: React.ReactNode }) => { + return ( +
+
{children}
+
+ ); + }; + + export default LandingLayout; \ No newline at end of file diff --git a/EduBuddy/app/(landing)/page.tsx b/EduBuddy/app/(landing)/page.tsx new file mode 100644 index 0000000..a601aec --- /dev/null +++ b/EduBuddy/app/(landing)/page.tsx @@ -0,0 +1,13 @@ +import { LandingHero } from "@/components/landing-hero"; +import { LandingNavbar } from "@/components/landing-navbar"; + +const LandingPage = () => { + return ( +
+ + +
+ ); +}; + +export default LandingPage; \ No newline at end of file diff --git a/EduBuddy/app/api/code/route.ts b/EduBuddy/app/api/code/route.ts new file mode 100644 index 0000000..eeeca9d --- /dev/null +++ b/EduBuddy/app/api/code/route.ts @@ -0,0 +1,57 @@ +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; +import { checkApiLimit, increaseApiLimit } from '@/lib/api-limit'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const instructionMessage : ChatCompletionMessageParam = { + role : "system", + content : "You are a code generator, You must answer only in markdown code snippets when the user asks you to generate code. Use code comments for explanations. You have to explain the code also for the user to understand it easily. There should be 2 types of explainations, one in the code snippet and other a general explaination outside the code snippet." +} + +export async function POST( + req: Request +) { + try { + + const { userId } = auth(); + const body = await req.json(); + const { messages } = body; + + if(!userId) { + return new NextResponse("Unauthorized", {status: 401}); + } + + if(!process.env.OPENAI_API_KEY) { + return new NextResponse("OpenAI API Key not configured", { status:500 }); + } + + if(!messages) { + return new NextResponse("Messagess are required", {status:400}); + } + + const freeTrial = await checkApiLimit(); + + if(!freeTrial) { + return new NextResponse("Free tial has expired.", {status: 403}); + } + + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [instructionMessage, ...messages] + }); + + await increaseApiLimit(); + + return NextResponse.json(response.choices[0].message); + + } catch (error) { + console.log("[CODE_ERROR]", error); + return new NextResponse("Internal error", { status: 500 }); + + } +} \ No newline at end of file diff --git a/EduBuddy/app/api/conversation/route.ts b/EduBuddy/app/api/conversation/route.ts new file mode 100644 index 0000000..291eb2f --- /dev/null +++ b/EduBuddy/app/api/conversation/route.ts @@ -0,0 +1,57 @@ +import { checkApiLimit, increaseApiLimit } from '@/lib/api-limit'; +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const instructionMessage : ChatCompletionMessageParam = { + role : "system", + content : "Your sole purpose is to assist users in solving study-related doubts and queries. You will not engage in conversations or activities outside this scope. Upon interaction, prompt the user to describe their doubt or ask a study-related question. You are expected to provide clear, accurate, and well-explained answers to their academic questions. Prioritize understanding the context of the user's doubt, and ask for clarification if the question is unclear or lacks detail. Provide concise, accurate answers to the user's question, explaining the concept in simple, easy-to-understand terms. Where necessary, break down complex answers into steps or parts, using bullet points or numbered lists for clarity. Include relevant examples, diagrams (in text form), or analogies when helpful to explain difficult concepts. Use well-formatted markdown to enhance the readability of your answers. Present equations, lists, or code snippets appropriately. If the user does not understand the answer, allow them to ask follow-up questions to clarify specific points. Be patient in re-explaining concepts in different ways, offering more detailed examples, or using alternative methods of explanation until the user is satisfied. After solving the doubt, ask the user if they have any more questions or if they need help with a related concept.If the user wishes to switch topics or subjects, be prepared to handle new study-related questions as per their request. Do not respond to or engage in questions or conversations that are unrelated to academic study or doubt solving. Politely remind the user that your primary function is to assist with academic doubts." +} + +export async function POST( + req: Request +) { + try { + + const { userId } = auth(); + const body = await req.json(); + const { messages } = body; + + if(!userId) { + return new NextResponse("Unauthorized", {status: 401}); + } + + if(!process.env.OPENAI_API_KEY) { + return new NextResponse("OpenAI API Key not configured", { status:500 }); + } + + if(!messages) { + return new NextResponse("Messagess are required", {status:400}); + } + + const freeTrial = await checkApiLimit(); + + if(!freeTrial) { + return new NextResponse("Free tial has expired.", {status: 403}); + } + + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [instructionMessage, ...messages] + }); + + await increaseApiLimit(); + + return NextResponse.json(response.choices[0].message); + + } catch (error) { + console.log("[CONVERSATION_ERROR]", error); + return new NextResponse("Internal error", { status: 500 }); + + } +} \ No newline at end of file diff --git a/EduBuddy/app/api/flashcards/route.ts b/EduBuddy/app/api/flashcards/route.ts new file mode 100644 index 0000000..5906aa0 --- /dev/null +++ b/EduBuddy/app/api/flashcards/route.ts @@ -0,0 +1,59 @@ +import { checkApiLimit, increaseApiLimit } from '@/lib/api-limit'; +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export async function POST(req: Request) { + try { + const { userId } = auth(); + const body = await req.json(); + const { topic } = body; + + if (!userId) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + if (!process.env.OPENAI_API_KEY) { + return new NextResponse("OpenAI API Key not configured", { status: 500 }); + } + + if (!topic) { + return new NextResponse("Topic is required", { status: 400 }); + } + + const freeTrial = await checkApiLimit(); + if (!freeTrial) { + return new NextResponse("Free trial has expired.", { status: 403 }); + } + + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: `Generate a concise explanation for the topic in one line: ${topic}.` + } + ] + }); + + // Check if response and choices exist + if (!response || !response.choices || response.choices.length === 0) { + return new NextResponse("No response from OpenAI", { status: 500 }); + } + //@ts-ignore + const flashcardContent = response.choices[0].message.content.trim(); + + await increaseApiLimit(); + + // Return both topic and content + return NextResponse.json({ title: topic, content: flashcardContent }); + + } catch (error) { + console.log("[FLASHCARD_ERROR]", error); + return new NextResponse("Internal error", { status: 500 }); + } +} diff --git a/EduBuddy/app/api/quiz/route.ts b/EduBuddy/app/api/quiz/route.ts new file mode 100644 index 0000000..199855c --- /dev/null +++ b/EduBuddy/app/api/quiz/route.ts @@ -0,0 +1,57 @@ +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; +import { checkApiLimit, increaseApiLimit } from '@/lib/api-limit'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const instructionMessage : ChatCompletionMessageParam = { + role : "system", + content : "Your sole purpose is to function as a quiz generator. You will only respond to requests to generate, present, and evaluate quizzes. You will not engage in conversations outside this scope. Upon interaction, prompt the user to specify a topic for the quiz. This topic will guide the generation of quiz questions. For each question, generate a multiple-choice question (MCQ) relevant to the user's specified topic. Provide four options, out of which only one is correct. Ensure that options are well-formatted and listed clearly using bullet points or numbering (e.g., A, B, C, D). All questions and options must be presented in a well-formatted markdown format to ensure clarity and proper structure. After presenting the question and options, prompt the user to guess the correct answer by selecting one of the options (A, B, C, or D). Based on the user's input, provide feedback: If correct, respond with confirmation and explain why the answer is correct. If incorrect, specify the correct answer and explain why it is correct. After evaluating the user's answer, ask if they would like to continue with more questions on the same topic or switch to a new topic. Adjust the quiz accordingly based on their response. Do not respond to or entertain questions or conversations that fall outside of generating, presenting, or evaluating quiz questions. Simply remind the user that you are designed only for quiz-related tasks." +} + +export async function POST( + req: Request +) { + try { + + const { userId } = auth(); + const body = await req.json(); + const { messages } = body; + + if(!userId) { + return new NextResponse("Unauthorized", {status: 401}); + } + + if(!process.env.OPENAI_API_KEY) { + return new NextResponse("OpenAI API Key not configured", { status:500 }); + } + + if(!messages) { + return new NextResponse("Messagess are required", {status:400}); + } + + const freeTrial = await checkApiLimit(); + + if(!freeTrial) { + return new NextResponse("Free tial has expired.", {status: 403}); + } + + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [instructionMessage, ...messages] + }); + + await increaseApiLimit(); + + return NextResponse.json(response.choices[0].message); + + } catch (error) { + console.log("[CODE_ERROR]", error); + return new NextResponse("Internal error", { status: 500 }); + + } +} \ No newline at end of file diff --git a/EduBuddy/app/fonts/GeistMonoVF.woff b/EduBuddy/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/EduBuddy/app/fonts/GeistMonoVF.woff differ diff --git a/EduBuddy/app/fonts/GeistVF.woff b/EduBuddy/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/EduBuddy/app/fonts/GeistVF.woff differ diff --git a/EduBuddy/app/globals.css b/EduBuddy/app/globals.css new file mode 100644 index 0000000..7c33401 --- /dev/null +++ b/EduBuddy/app/globals.css @@ -0,0 +1,83 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +:root { + height: 100%; +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + /* --primary: 222.2 47.4% 11.2%; */ + --primary: 248 90% 66%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/EduBuddy/app/layout.tsx b/EduBuddy/app/layout.tsx new file mode 100644 index 0000000..83602b2 --- /dev/null +++ b/EduBuddy/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ClerkProvider } from "@clerk/nextjs"; +import { ModalProvider } from "@/components/modal-provider"; +import { CrispProvider } from "@/components/crisp-provider"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "EduBuddy", + description: "An AI Assistant for Students", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + + ); +} diff --git a/EduBuddy/components.json b/EduBuddy/components.json new file mode 100644 index 0000000..137bc8d --- /dev/null +++ b/EduBuddy/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/EduBuddy/components/bot-avatar.tsx b/EduBuddy/components/bot-avatar.tsx new file mode 100644 index 0000000..7a7a350 --- /dev/null +++ b/EduBuddy/components/bot-avatar.tsx @@ -0,0 +1,9 @@ +import { Avatar, AvatarImage } from "@/components/ui/avatar"; + +export const BotAvatar = () => { + return ( + + + + ); +}; diff --git a/EduBuddy/components/crisp-chat.tsx b/EduBuddy/components/crisp-chat.tsx new file mode 100644 index 0000000..56781c0 --- /dev/null +++ b/EduBuddy/components/crisp-chat.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useEffect } from "react"; +import { Crisp } from "crisp-sdk-web"; + +export const CrispChat = () => { + useEffect(() => { + Crisp.configure("a5ecf057-0032-42e3-88f9-7180efcf258e"); + }, []); + + return null; +}; diff --git a/EduBuddy/components/crisp-provider.tsx b/EduBuddy/components/crisp-provider.tsx new file mode 100644 index 0000000..287c5dc --- /dev/null +++ b/EduBuddy/components/crisp-provider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { CrispChat } from "@/components/crisp-chat"; + +export const CrispProvider = () => { + return ; +}; diff --git a/EduBuddy/components/empty.tsx b/EduBuddy/components/empty.tsx new file mode 100644 index 0000000..97d7bea --- /dev/null +++ b/EduBuddy/components/empty.tsx @@ -0,0 +1,16 @@ +import Image from "next/image"; + +interface EmptyProps { + label: string; +} + +export const Empty = ({ label }: EmptyProps) => { + return ( +
+
+ Empty +
+

{label}

+
+ ); +}; diff --git a/EduBuddy/components/free-counter.tsx b/EduBuddy/components/free-counter.tsx new file mode 100644 index 0000000..d6aaf85 --- /dev/null +++ b/EduBuddy/components/free-counter.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { MAX_FREE_COUNTS } from "@/constants"; +import { Progress } from "@/components/ui/progress"; +import { Button } from "./ui/button"; +import { Zap } from "lucide-react"; +import { useProModal } from "@/hooks/use-pro-modal"; + +interface FreeCounterProps { + apiLimitCount: number; +} + +export const FreeCounter = ({ apiLimitCount = 0 }: FreeCounterProps) => { + const proModal = useProModal(); + + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( +
+ + +
+

+ {apiLimitCount} / {MAX_FREE_COUNTS} Free Generations +

+ +
+ +
+
+
+ ); +}; diff --git a/EduBuddy/components/heading.tsx b/EduBuddy/components/heading.tsx new file mode 100644 index 0000000..9fc213c --- /dev/null +++ b/EduBuddy/components/heading.tsx @@ -0,0 +1,30 @@ +import { cn } from "@/lib/utils"; +import { Icon, LucideIcon } from "lucide-react"; + +interface HeadingProps { + title: string; + description: string; + icon: LucideIcon; + iconColor?: string; + bgColor?: string; +} + +export const Heading = ({ + title, + description, + icon: Icon, + iconColor, + bgColor, +}: HeadingProps) => { + return ( +
+
+ +
+
+

{title}

+

{description}

+
+
+ ); +}; diff --git a/EduBuddy/components/landing-hero.tsx b/EduBuddy/components/landing-hero.tsx new file mode 100644 index 0000000..2bd8106 --- /dev/null +++ b/EduBuddy/components/landing-hero.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useAuth } from "@clerk/nextjs"; +import Link from "next/link"; +import TypewriterComponent from "typewriter-effect"; +import { Button } from "@/components/ui/button"; + +export const LandingHero = () => { + const { isSignedIn } = useAuth(); + + return ( +
+
+

Your AI Study Companion

+
+ +
+
+
+
+
+ + + +
+
+ New Features lined up. +
+
+ ); +}; diff --git a/EduBuddy/components/landing-navbar.tsx b/EduBuddy/components/landing-navbar.tsx new file mode 100644 index 0000000..1683343 --- /dev/null +++ b/EduBuddy/components/landing-navbar.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Montserrat } from "next/font/google"; +import Image from "next/image"; +import Link from "next/link"; +import { useAuth } from "@clerk/nextjs"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; + +const font = Montserrat({ + weight: "600", + subsets: ["latin"], +}); + +export const LandingNavbar = () => { + const { isSignedIn } = useAuth(); + + return ( + + ); +}; diff --git a/EduBuddy/components/loader.tsx b/EduBuddy/components/loader.tsx new file mode 100644 index 0000000..c950979 --- /dev/null +++ b/EduBuddy/components/loader.tsx @@ -0,0 +1,12 @@ +import Image from "next/image"; + +export const Loader = () => { + return ( +
+
+ logo +
+

EduBuddy is thinking...

+
+ ); +}; diff --git a/EduBuddy/components/mobile-sidebar.tsx b/EduBuddy/components/mobile-sidebar.tsx new file mode 100644 index 0000000..177c6f0 --- /dev/null +++ b/EduBuddy/components/mobile-sidebar.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Menu } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import Sidebar from "@/components/sidebar"; +import { useEffect, useState } from "react"; + +interface MobileSidebarProps { + apiLimitCount: number; +} + +const MobileSidebar = ({ apiLimitCount }: MobileSidebarProps) => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return ( + + + + + + + + + ); +}; + +export default MobileSidebar; diff --git a/EduBuddy/components/modal-provider.tsx b/EduBuddy/components/modal-provider.tsx new file mode 100644 index 0000000..e677d90 --- /dev/null +++ b/EduBuddy/components/modal-provider.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { ProModal } from "@/components/pro-modal"; + +export const ModalProvider = () => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return ( + <> + + + ); +}; diff --git a/EduBuddy/components/navbar.tsx b/EduBuddy/components/navbar.tsx new file mode 100644 index 0000000..39f0181 --- /dev/null +++ b/EduBuddy/components/navbar.tsx @@ -0,0 +1,19 @@ +import { Button } from "@/components/ui/button"; +import { UserButton } from "@clerk/nextjs"; +import MobileSidebar from "@/components/mobile-sidebar"; +import { getApiLimitCount } from "@/lib/api-limit"; + +const Navbar = async () => { + const apiLimitCount = await getApiLimitCount(); + + return ( +
+ +
+ +
+
+ ); +}; + +export default Navbar; diff --git a/EduBuddy/components/pro-modal.tsx b/EduBuddy/components/pro-modal.tsx new file mode 100644 index 0000000..d4b4626 --- /dev/null +++ b/EduBuddy/components/pro-modal.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { + Check, + Code, + ImageIcon, + MessageSquare, + FileQuestion, + Zap, +} from "lucide-react"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useProModal } from "@/hooks/use-pro-modal"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; + +const tools = [ + { + label: "Doubts Solving", + icon: MessageSquare, + color: "text-violet-500", + bgColor: "bg-violet-500/10", + }, + { + label: "Quiz Generation", + icon: FileQuestion, + color: "text-orange-500", + bgColor: "bg-orange-500/10", + }, + { + label: "Flashcards", + icon: ImageIcon, + color: "text-pink-700", + bgColor: "bg-pink-700/10", + }, + { + label: "Coding Assistant", + icon: Code, + color: "text-green-700", + bgColor: "bg-green-700/10", + }, +]; + +export const ProModal = () => { + const proModal = useProModal(); + + return ( + + + + +
+ INCREASE LIMIT +
+

Since we are using a paid ChatGPT API,

+

Request for more FREE generations

+
+ + {tools.map((tool) => ( + +
+
+ +
+
{tool.label}
+
+ +
+ ))} +
+
+ + + +
+
+ ); +}; diff --git a/EduBuddy/components/sidebar.tsx b/EduBuddy/components/sidebar.tsx new file mode 100644 index 0000000..cb13932 --- /dev/null +++ b/EduBuddy/components/sidebar.tsx @@ -0,0 +1,98 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { Montserrat } from "next/font/google"; + +import { cn } from "@/lib/utils"; +import { + Code, + FileQuestion, + ImageIcon, + LayoutDashboard, + MessageSquare, +} from "lucide-react"; +import { usePathname } from "next/navigation"; +import { FreeCounter } from "@/components/free-counter"; + +const montserrat = Montserrat({ + weight: "600", + subsets: ["latin"], +}); + +const routes = [ + { + label: "Dashboard", + icon: LayoutDashboard, + href: "/dashboard", + color: "text-sky-500", + }, + { + label: "Doubts Solving", + icon: MessageSquare, + href: "/conversation", + color: "text-violet-500", + }, + { + label: "Flashcards", + icon: ImageIcon, + href: "/flashcards", + color: "text-pink-700", + }, + { + label: "Quiz Generation", + icon: FileQuestion, + href: "/quiz", + color: "text-orange-500", + }, + { + label: "Coding Assistant", + icon: Code, + href: "/code", + color: "text-green-700", + }, +]; + +interface SidebarProps { + apiLimitCount: number; +} + +const Sidebar = ({ apiLimitCount = 0 }: SidebarProps) => { + const pathname = usePathname(); + return ( +
+
+ +
+ logo +
+

+ EduBuddy +

+ +
+ {routes.map((route) => ( + +
+ + {route.label} +
+ + ))} +
+
+ +
+ ); +}; + +export default Sidebar; diff --git a/EduBuddy/components/ui/avatar.tsx b/EduBuddy/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/EduBuddy/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/EduBuddy/components/ui/badge.tsx b/EduBuddy/components/ui/badge.tsx new file mode 100644 index 0000000..4587225 --- /dev/null +++ b/EduBuddy/components/ui/badge.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + premium: + "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/EduBuddy/components/ui/button.tsx b/EduBuddy/components/ui/button.tsx new file mode 100644 index 0000000..9b9d96f --- /dev/null +++ b/EduBuddy/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white border-0", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/EduBuddy/components/ui/card.tsx b/EduBuddy/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/EduBuddy/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

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

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

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/EduBuddy/components/ui/dialog.tsx b/EduBuddy/components/ui/dialog.tsx new file mode 100644 index 0000000..01ff19c --- /dev/null +++ b/EduBuddy/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/EduBuddy/components/ui/form.tsx b/EduBuddy/components/ui/form.tsx new file mode 100644 index 0000000..ce264ae --- /dev/null +++ b/EduBuddy/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +