From 3ea627381786cd844ccfd66498eabf2e182466bf Mon Sep 17 00:00:00 2001 From: Junhaeng Date: Sat, 25 Nov 2023 17:50:45 -0500 Subject: [PATCH 1/6] convert app router to pages router --- auth.ts | 32 + components.json | 16 + components/AddCourseDialog.tsx | 107 ++++ components/Navbar.tsx | 67 +++ components/QuizEditor.tsx | 182 ++++++ components/Sidebar.tsx | 57 ++ components/ui/avatar.tsx | 50 ++ components/ui/button.tsx | 56 ++ components/ui/dialog.tsx | 122 ++++ components/ui/dropdown-menu.tsx | 200 +++++++ components/ui/form.tsx | 176 ++++++ components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/navigation-menu.tsx | 128 ++++ components/ui/radio-group.tsx | 44 ++ components/ui/scroll-area.tsx | 48 ++ components/ui/table.tsx | 117 ++++ components/ui/textarea.tsx | 24 + components/user-nav.tsx | 58 ++ context/courseContext.tsx | 35 ++ context/quizContext.tsx | 31 + context/userContext.tsx | 63 ++ context/userTypeContext.tsx | 35 ++ lib/types.ts | 37 ++ lib/utils.ts | 6 + package-lock.json | 558 ++++++++++++++---- package.json | 11 +- pages/_app.tsx | 49 +- .../[courseName]/assignments/index.tsx | 1 + pages/courses/[courseName]/files/index.tsx | 62 ++ pages/courses/[courseName]/index.tsx | 59 ++ pages/courses/[courseName]/modules/index.tsx | 1 + .../[courseName]/quizzes/[quizName]/edit.tsx | 67 +++ .../[courseName]/quizzes/[quizName]/index.tsx | 38 ++ .../[courseName]/quizzes/[quizName]/take.tsx | 107 ++++ pages/courses/[courseName]/quizzes/create.tsx | 18 + pages/courses/[courseName]/quizzes/index.tsx | 85 +++ pages/dashboard/index.tsx | 3 + styles/globals.css | 89 ++- tailwind.config.js | 78 +++ tsconfig.json | 28 +- 41 files changed, 2866 insertions(+), 130 deletions(-) create mode 100644 auth.ts create mode 100644 components.json create mode 100644 components/AddCourseDialog.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/QuizEditor.tsx create mode 100644 components/Sidebar.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/user-nav.tsx create mode 100644 context/courseContext.tsx create mode 100644 context/quizContext.tsx create mode 100644 context/userContext.tsx create mode 100644 context/userTypeContext.tsx create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 pages/courses/[courseName]/assignments/index.tsx create mode 100644 pages/courses/[courseName]/files/index.tsx create mode 100644 pages/courses/[courseName]/index.tsx create mode 100644 pages/courses/[courseName]/modules/index.tsx create mode 100644 pages/courses/[courseName]/quizzes/[quizName]/edit.tsx create mode 100644 pages/courses/[courseName]/quizzes/[quizName]/index.tsx create mode 100644 pages/courses/[courseName]/quizzes/[quizName]/take.tsx create mode 100644 pages/courses/[courseName]/quizzes/create.tsx create mode 100644 pages/courses/[courseName]/quizzes/index.tsx create mode 100644 pages/dashboard/index.tsx create mode 100644 tailwind.config.js diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..0f9fd39 --- /dev/null +++ b/auth.ts @@ -0,0 +1,32 @@ +import NextAuth from "next-auth"; +import GithubProvider from "next-auth/providers/github"; +import PostgresAdapter from "@auth/pg-adapter"; +const { Pool } = require("pg"); + +// Store user accounts in CosmosDB Postgres as well +const pool = new Pool({ + max: 300, + connectionTimeoutMillis: 5000, + host: process.env.AZURE_COSMOSDB_PG_URL, + port: 5432, + user: process.env.AZURE_COSMOSDB_PG_USER, + password: process.env.AZURE_COSMOSDB_PG_PASSWORD, + database: process.env.AZURE_COSMOSDB_PG_DBNAME, + ssl: true, +}); + +export const { handlers, auth } = NextAuth({ + adapter: PostgresAdapter(pool), + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET, + }), + ], + callbacks: { + async session({ session, user }) { + session.user.id = user.id; + return session; + }, + }, +}); diff --git a/components.json b/components.json new file mode 100644 index 0000000..a7488d8 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/AddCourseDialog.tsx b/components/AddCourseDialog.tsx new file mode 100644 index 0000000..32f424c --- /dev/null +++ b/components/AddCourseDialog.tsx @@ -0,0 +1,107 @@ +"use client"; + +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { Button } from "@/components/ui/button"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogClose, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +const formSchema = z.object({ + courseName: z.string().min(2), + ListTAs: z.string().min(2), +}); + +export function AddCourseDialog() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + courseName: "", + ListTAs: "", + }, + }); + + async function onSubmit(values: z.infer) { + let courseName = values.courseName; + // make API call to create course by api at /api/courses/[courseName]/create + let response = await fetch( + "http://localhost:3000/api/" + courseName + "/create", + { + method: "GET", + } + ); + + response = await response; + + if (response.status == 200) { + console.log("Course created"); + } else { + console.log("Course not created"); + } + } + + return ( + + + Create new course + + Add course details and save when you're done. + + +
+ + ( + + Course Name + + + + + + )} + /> + ( + + List TAs + + + + Enter as comma-seperated list + + + )} + /> + + + + + + + +
+ ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..cc8fcee --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,67 @@ +"use client"; +import Link from "next/link"; +import { FC } from "react"; +import { useCourseContext } from "../context/courseContext"; +import { Course, CourseList, User } from "@/lib/types"; + +import { UserNav } from "./user-nav"; +import { useRouter } from "next/router"; + +type NavbarProps = { + courseList: CourseList; + // user: User; +}; + +const Navbar: FC = ({ courseList }) => { + const { currentCourse, setCurrentCourse } = useCourseContext(); + + const pathName = useRouter(); + + const isActive = (route: string) => { + const pathList = pathName.asPath.split("/"); + /*pathList = ["", dashboard] or ["", courses, [courseName], ...]*/ + return pathList.length > 2 ? route === pathList[2] : route === pathList[1]; + }; + return ( + + ); +}; + +export default Navbar; diff --git a/components/QuizEditor.tsx b/components/QuizEditor.tsx new file mode 100644 index 0000000..a0af62f --- /dev/null +++ b/components/QuizEditor.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +import { ScrollArea } from "@/components/ui/scroll-area"; + +import { Dispatch, SetStateAction, useState } from "react"; +import { Quiz, QuizQuestion } from "@/lib/types"; + +interface quizQuestionsProps { + questions: QuizQuestion[]; + setQuestions: Dispatch>; +} + +const QuizEditor: React.FC = ({ + questions, + setQuestions, +}) => { + // const [questions, setQuestions] = useState(questions); + + const defaultMCQQuestion: QuizQuestion = { + questionNum: questions.length + 1, + questionType: "MCQ", + question: "", + options: ["", "", "", ""], // Assuming a default of 4 options + answer: 1, // Assuming no answer selected by default + points: 10, // Default points value + }; + + const defaultFRQQuestion: QuizQuestion = { + questionNum: questions.length + 1, + questionType: "FRQ", + question: "", + options: [], + answer: "", + points: 80, // Default points value for FRQ + }; + + // Function to add a new MCQ question + const addMCQQuestion = () => { + setQuestions( + questions.concat({ + ...defaultMCQQuestion, + questionNum: questions.length + 1, + }) + ); + }; + + // Function to add a new FRQ question + const addFRQQuestion = () => { + setQuestions( + questions.concat({ + ...defaultFRQQuestion, + questionNum: questions.length + 1, + }) + ); + }; + + const deleteQuestion = (questionNum: number) => { + setQuestions(questions.filter((q) => q.questionNum !== questionNum)); + }; + + // Function to handle reordering the question numbers after deletion + const reorderQuestions = (questions: QuizQuestion[]) => { + return questions.map((q, index) => ({ + ...q, + questionNum: index + 1, // Reset question numbers to be in order + })); + }; + + // Adjusted delete function that also reorders question numbers after deletion + const handleDeleteQuestion = (questionNum: number) => { + const updatedQuestions = questions.filter( + (q) => q.questionNum !== questionNum + ); + setQuestions(reorderQuestions(updatedQuestions)); + }; + + const handleQuestionChange = (index: number, newText: string) => { + const updatedQuestions = [...questions]; + updatedQuestions[index].question = newText; + setQuestions(updatedQuestions); + }; + + const handleOptionChange = ( + questionIndex: number, + optionIndex: number, + newOptionText: string + ) => { + const updatedQuestions = [...questions]; + updatedQuestions[questionIndex].options[optionIndex] = newOptionText; + setQuestions(updatedQuestions); + }; + + const addOption = (questionIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions[questionIndex].options.push(""); + setQuestions(updatedQuestions); + }; + + const deleteOption = (questionIndex: number, optionIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions[questionIndex].options.splice(optionIndex, 1); + setQuestions(updatedQuestions); + }; + + const handleSubmit = () => { + /* + 1. Need a popup to make sure the Ta finished editing. + 2. update the quesitons + 3. back to the previous page + */ + }; + + return ( +
+ +
+ {questions.map((q, qIndex) => ( +
+ {q.questionNum}. + handleQuestionChange(qIndex, e.target.value)} + placeholder="Type your question here." + key={q.questionNum} + /> + {q.questionType == "MCQ" && ( + <> + {q.options.map((option, oIndex) => ( +
+ + handleOptionChange(qIndex, oIndex, e.target.value) + } + key={`option${oIndex}`} + /> + +
+ ))} +
+ +
+ + )} + +
+ ))} +
+ + +
+ +
+ ); +}; + +export default QuizEditor; diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx new file mode 100644 index 0000000..40d293f --- /dev/null +++ b/components/Sidebar.tsx @@ -0,0 +1,57 @@ +"use client"; +import Link from "next/link"; +import { FC, useEffect, useState } from "react"; +import { useCourseContext } from "../context/courseContext"; +import { useRouter } from "next/router"; + +const Sidebar: FC = () => { + const { currentCourse, setCurrentCourse } = useCourseContext(); + const [tabList, setTableList] = useState([ + "Home", + "Assignments", + "Quizzes", + "Modules", + "Files", + ]); + const originPath = `/courses/${currentCourse}`; + const pathName = useRouter(); + + const isActive = (it: string) => { + // If the item is 'Home', check against the base path ('/') + const paramList = pathName.asPath.split("/"); + if (it === "Home") { + return paramList.length === 3; + } + // Otherwise, compare against the lowercased item + return paramList[3] === it.toLocaleLowerCase(); + }; + + return ( + + ); +}; + +export default Sidebar; diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/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/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,56 @@ +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", + }, + 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/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..cad6f58 --- /dev/null +++ b/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/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,176 @@ +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 ( +