diff --git a/studyAi/graphql/schema.graphql b/studyAi/graphql/schema.graphql index b1214814..e843d2fa 100644 --- a/studyAi/graphql/schema.graphql +++ b/studyAi/graphql/schema.graphql @@ -505,7 +505,7 @@ type AggregateVerificationToken { } type AnswerData { - correctAnswer: [String!]! + correctAnswer: [AnswerOption!]! } input AnswerDataCompositeFilter { @@ -519,19 +519,15 @@ input AnswerDataCreateEnvelopeInput { } input AnswerDataCreateInput { - correctAnswer: AnswerDataCreatecorrectAnswerInput -} - -input AnswerDataCreatecorrectAnswerInput { - set: [String!]! + correctAnswer: [AnswerOptionCreateInput!] } input AnswerDataObjectEqualityInput { - correctAnswer: [String!] + correctAnswer: [AnswerOptionObjectEqualityInput!] } input AnswerDataOrderByInput { - correctAnswer: SortOrder + correctAnswer: AnswerOptionOrderByCompositeAggregateInput } input AnswerDataUpdateEnvelopeInput { @@ -540,19 +536,33 @@ input AnswerDataUpdateEnvelopeInput { } input AnswerDataUpdateInput { - correctAnswer: AnswerDataUpdatecorrectAnswerInput -} - -input AnswerDataUpdatecorrectAnswerInput { - push: [String!] - set: [String!] + correctAnswer: [AnswerOptionCreateInput!] } input AnswerDataWhereInput { AND: [AnswerDataWhereInput!] NOT: [AnswerDataWhereInput!] OR: [AnswerDataWhereInput!] - correctAnswer: StringNullableListFilter + correctAnswer: [AnswerOptionObjectEqualityInput!] +} + +type AnswerOption { + id: String! + value: String! +} + +input AnswerOptionCreateInput { + id: String! + value: String! +} + +input AnswerOptionObjectEqualityInput { + id: String! + value: String! +} + +input AnswerOptionOrderByCompositeAggregateInput { + _count: SortOrder } input BoolFieldUpdateOperationsInput { @@ -1282,7 +1292,7 @@ type QuestionGroupBy { type QuestionInfoData { description: String! - options: [String!]! + options: [AnswerOption!]! title: String! } @@ -1298,23 +1308,19 @@ input QuestionInfoDataCreateEnvelopeInput { input QuestionInfoDataCreateInput { description: String! - options: QuestionInfoDataCreateoptionsInput + options: [AnswerOptionCreateInput!] title: String! } -input QuestionInfoDataCreateoptionsInput { - set: [String!]! -} - input QuestionInfoDataObjectEqualityInput { description: String! - options: [String!] + options: [AnswerOptionObjectEqualityInput!] title: String! } input QuestionInfoDataOrderByInput { description: SortOrder - options: SortOrder + options: AnswerOptionOrderByCompositeAggregateInput title: SortOrder } @@ -1325,21 +1331,16 @@ input QuestionInfoDataUpdateEnvelopeInput { input QuestionInfoDataUpdateInput { description: StringFieldUpdateOperationsInput - options: QuestionInfoDataUpdateoptionsInput + options: [AnswerOptionCreateInput!] title: StringFieldUpdateOperationsInput } -input QuestionInfoDataUpdateoptionsInput { - push: [String!] - set: [String!] -} - input QuestionInfoDataWhereInput { AND: [QuestionInfoDataWhereInput!] NOT: [QuestionInfoDataWhereInput!] OR: [QuestionInfoDataWhereInput!] description: StringFilter - options: StringNullableListFilter + options: [AnswerOptionObjectEqualityInput!] title: StringFilter } diff --git a/studyAi/package.json b/studyAi/package.json index 255a5903..0ce29275 100644 --- a/studyAi/package.json +++ b/studyAi/package.json @@ -47,6 +47,7 @@ "nexus": "^1.3.0", "openai": "^3.3.0", "react": "18", + "react-device-detect": "^2.2.3", "react-dom": "18", "react-select": "^5.7.7", "react-sweet-state": "^2.7.1", diff --git a/studyAi/prisma/schema.prisma b/studyAi/prisma/schema.prisma index e16201d8..85649838 100644 --- a/studyAi/prisma/schema.prisma +++ b/studyAi/prisma/schema.prisma @@ -15,7 +15,10 @@ datasource db { provider = "mongodb" url = env("DATABASE_URL") } - +type AnswerOption { + id String @db.ObjectId + value String +} type LocationData { locationType String //latitude and longitude @@ -32,7 +35,7 @@ type QuestionInfoData { title String description String //for mc and select multiple. short answer will contain empty arr - options String[] + options AnswerOption[] } type UserQuestionData { @@ -41,7 +44,7 @@ type UserQuestionData { } type AnswerData { - correctAnswer String[] + correctAnswer AnswerOption[] } type LikeCounter { diff --git a/studyAi/public/logo/android-chrome-192x192.png b/studyAi/public/logo/android-chrome-192x192.png new file mode 100644 index 00000000..ab5fa801 Binary files /dev/null and b/studyAi/public/logo/android-chrome-192x192.png differ diff --git a/studyAi/public/logo/android-chrome-512x512.png b/studyAi/public/logo/android-chrome-512x512.png new file mode 100644 index 00000000..afcac6d0 Binary files /dev/null and b/studyAi/public/logo/android-chrome-512x512.png differ diff --git a/studyAi/public/logo/apple-touch-icon.png b/studyAi/public/logo/apple-touch-icon.png new file mode 100644 index 00000000..20501f90 Binary files /dev/null and b/studyAi/public/logo/apple-touch-icon.png differ diff --git a/studyAi/public/logo/favicon-16x16.png b/studyAi/public/logo/favicon-16x16.png new file mode 100644 index 00000000..e02c4d48 Binary files /dev/null and b/studyAi/public/logo/favicon-16x16.png differ diff --git a/studyAi/public/logo/favicon-32x32.png b/studyAi/public/logo/favicon-32x32.png new file mode 100644 index 00000000..d4141f7c Binary files /dev/null and b/studyAi/public/logo/favicon-32x32.png differ diff --git a/studyAi/public/logo/favicon.ico b/studyAi/public/logo/favicon.ico new file mode 100644 index 00000000..74747952 Binary files /dev/null and b/studyAi/public/logo/favicon.ico differ diff --git a/studyAi/public/logo/logo.png b/studyAi/public/logo/logo.png new file mode 100644 index 00000000..4a54467c Binary files /dev/null and b/studyAi/public/logo/logo.png differ diff --git a/studyAi/public/site.webmanifest b/studyAi/public/site.webmanifest new file mode 100644 index 00000000..555c61bf --- /dev/null +++ b/studyAi/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Study AI", + "short_name": "Study AI", + "icons": [ + { + "src": "/logo/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/logo/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/studyAi/src/app/api/generateQuestion/generateQuestion.ts b/studyAi/src/app/api/generateQuestion/generateQuestion.ts index cfd70cb9..cd2f63c4 100644 --- a/studyAi/src/app/api/generateQuestion/generateQuestion.ts +++ b/studyAi/src/app/api/generateQuestion/generateQuestion.ts @@ -1,28 +1,136 @@ import generatePrompts from "../../util/openAI/openAI"; import { NextResponse } from "next/server"; import * as z from "zod"; - //schema for validating user inputs const questionSchema = z.object({ type: z.string(), tags: z.array(z.string()), + title: z.array(z.string()), question: z.string(), - numberOfOptions: z.number() + answers: z.array( + z.object({ + value: z.string(), + id: z.string(), + }) + ), + numberOfOptions: z.number(), }); - +type QuesitonSchema = z.infer; +const determinePromptTemplateStr = (questionType: string) => { + let str: string; + switch (questionType) { + case "Multiple Choice": + str = ` + { + description: 'QuestionDescription' + title: 'QuestionTitle' + options: 'QuestionOptions' + answer: 'CorrectAnswers' + } + `; + return str; + case "Short Answer": + str = ` + { + description: 'QuestionDescription' + title: 'QuestionTitle' + answer: 'CorrectAnswers' + } + `; + return str; + case "Select Multiple": + str = ` + { + description: 'QuestionDescription' + title: 'QuestionTitle' + options: 'QuestionOptions' + answer: 'CorrectAnswers' + } + `; + return str; + default: + str = ` + { + description: 'QuestionDescription' + title: 'QuestionTitle' + answer: 'CorrectAnswers' + } + `; + return str; + } +}; +const determineVariableTypeTemplateStr = ( + questionType: string, + numberOfOptions: number +) => { + let str: string; + switch (questionType) { + case "Multiple Choice": + str = `the new options as a list of strings with length ${numberOfOptions} called 'QuestionOptions', and the new correct answer inside a list of strings, with length 1, called 'CorrectAnswers'.`; + break; + case "Short Answer": + str = `the new correct answer inside a list of strings with length 1, called 'CorrectAnswers'`; + break; + case "Select Multiple": + str = `the new options as a list of strings with length ${numberOfOptions} called 'QuestionOptions', and the new correct answer/answers as a list of strings called 'CorrectAnswers'`; + break; + default: + str = `the new correct answer inside a list of strings with length 1, called 'CorrectAnswers'`; + break; + } + return str; +}; export async function generateQuestion(req: Request) { try { const bodyPromise = req.json(); const body = await bodyPromise; - const { type, tags, question, numberOfOptions } = questionSchema.parse(body); - const questionType = (type === "mcq") ? `multiple choice with ${numberOfOptions} different potential answers` : ("checkbox") ? `with ${numberOfOptions} different potential answers some correct and some incorrect` : "short answer"; + let parsedBody: QuesitonSchema; + try { + parsedBody = questionSchema.parse(body); + } catch (err) { + return NextResponse.json({ + status: 400, + message: "Invalid input", + error: err, + }); + } + const { type, tags, title, question, numberOfOptions, answers } = + parsedBody; + const questionType = + type === "Multiple Choice" + ? `A Multiple choice question, with ${numberOfOptions} unique options, but only one correct answer` + : "Select Multiple" + ? `A Select Multiple/Select All question, with ${numberOfOptions} unique options, where each option can be a correct answer OR incorrect answer. However, there MUST be at least ONE correct answer` + : "Short Answer question"; + + const prompt = `Give me a completely NEW question where the question type is: ${questionType}. + It should also be similar to the following question: + questionTitle: ${title}, description: ${question}, answer: ${answers + .map((a) => a.value) + .join(", ")}. + + Also, try to ensure that the question is related to the following subjects: ${tags.join( + ", " + )} - const prompt = `Ask me a question, ${questionType}, similar to this question: ${question} and from the following subjects: ${tags}. Indicate which is the correct response, and Return your response in a JSON object, with the following format: {"question": "", "correct": ["",...], "incorrect": ["",...]}`; - const model = "gpt-3.5-turbo"; - const questionGenerated = await generatePrompts(model, prompt) || ""; - const newQuestion = JSON.parse(questionGenerated); - newQuestion.options = [...newQuestion.correct, ...newQuestion.incorrect]; + Then, store the new question description as a string called 'QuestionDescription', the new question title as a string 'QuestionTitle', ${determineVariableTypeTemplateStr(type, numberOfOptions)} + Finally, return the new generated question data as a JSON object, in the following format: + ${determinePromptTemplateStr(type)}`; + // Indicate which is the correct response, and Return your response in a JSON object, + // with the following format: {"question": "", "correct": ["",...], "incorrect": ["",...]}; + const model = "gpt-3.5-turbo"; + const questionGenerated = (await generatePrompts(model, prompt)) || ""; + let newQuestion = null; + try { + newQuestion = JSON.parse(questionGenerated); + } catch (err) { + return NextResponse.json({ + status: 500, + message: `Failed to parse the following response ${questionGenerated}`, + error: err, + }); + } return NextResponse.json({ newQuestion, message: `Question Generated Successfully`, @@ -34,4 +142,4 @@ export async function generateQuestion(req: Request) { message: "Something went wrong", }); } -} \ No newline at end of file +} diff --git a/studyAi/src/app/dashboard/blah.tsx b/studyAi/src/app/dashboard/blah.tsx deleted file mode 100644 index c13c8ffd..00000000 --- a/studyAi/src/app/dashboard/blah.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" -import { useQuery } from "@apollo/client"; -import { gql } from "../../../graphql/generated"; - -const QuestionQueryById = gql(` - query GetFullQuestion($id: String) { - question(where: { id: $id }) { - id - creatorId - questionType - tags - questionInfo { - title - description - options - } - likeCounter { - likes - dislikes - } - } - } -`); - -const Blah = () => { - - const query = useQuery(QuestionQueryById, {variables: {id: "6547321ece45c9cdfba29c5e"}}) - - return ( -
- Hello -
- ); -}; -export default Blah; \ No newline at end of file diff --git a/studyAi/src/app/favicon.ico b/studyAi/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/studyAi/src/app/favicon.ico and /dev/null differ diff --git a/studyAi/src/app/layout.tsx b/studyAi/src/app/layout.tsx index 2983dc4a..2c9aa42f 100644 --- a/studyAi/src/app/layout.tsx +++ b/studyAi/src/app/layout.tsx @@ -6,9 +6,31 @@ import { IsClientCtxProvider } from "./util/providers/isClientProvider"; export const metadata: Metadata = { title: "StudyAI", - description: "Generated by Arky, Irha, Tiffany and Fung", + description: + "Generate an endless amount of practice questions, using the power of AI", + icons: [ + { + url: "/logo/favicon.ico", + type: "image/x-icon", + }, + { + url: "/logo/favicon-16x16.png", + type: "image/png", + sizes: "16x16", + }, + { + type: "image/png", + sizes: "32x32", + url: "/logo/favicon-32x32.png", + }, + { + rel: "apple-touch-icon", + sizes: "180x180", + url: "/logo/apple-touch-icon.png", + }, + ], + manifest: "/site.webmanifest", }; - export default function RootLayout({ children, }: { diff --git a/studyAi/src/app/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index 64b3e942..c82b2301 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -1,17 +1,18 @@ -import NavigationWrapper from "@/app/util/components/navigation/navigationWrapper"; import ServerGraphQLClient from "@/app/api/graphql/apolloServerClient"; -import QuestionPageContainer from "../components/client/questionPageContainer"; +import QuestionPageContainer from "../components/page/client/questionPageContainer"; import { Question } from "../../../../../prisma/generated/type-graphql"; import { QuestionsContainer } from "@/app/stores/questionStore"; import { QuestionTypes } from "@/app/util/types/UserData"; import { gql } from "../../../../../graphql/generated"; import { getServerSession } from "next-auth"; import { options } from "@/app/api/auth/[...nextauth]/options"; +import { Metadata, ResolvingMetadata } from "next"; +import determineOriginUrl from "@/app/util/parsers/determineOriginUrl"; const question: Partial & { id: string; questionType: (typeof QuestionTypes)[number]; } = { - id: "65429fd993f2d4403eac75ec", + id: "654e89ad8cd9123e2353ab1b", creatorId: "6533f4c7489ef223ffc31a99", questionType: "Short Answer", tags: [ @@ -35,7 +36,16 @@ const question: Partial & { questionInfo: { title: "Question 1", description: "Question 2 is the world", - options: ["the world", "the world", "the world", "the world"], + options: [ + { + id: "1", + value: "Option 1", + }, + { + id: "2", + value: "Option 1", + }, + ], }, }; const QuestionQueryById = gql(` @@ -48,7 +58,10 @@ const QuestionQueryById = gql(` questionInfo { title description - options + options { + id + value + } } likeCounter { likes @@ -68,27 +81,63 @@ export default async function QuestionPage({ variables: { id: questionId }, }; try { - const session = await getServerSession(options) - const client = ServerGraphQLClient(session); - const { data: result } = await client.query(query); - const data = result.question as (Partial & { id: string }) | null; + // const session = await getServerSession(options) + // const client = ServerGraphQLClient(session); + // const { data: result } = await client.query(query); + // const data = result.question as (Partial & { id: string }) | null; // console.log(data) - // const data = question; + const data = question; return ( - - - - - + + + ); } catch (err) { console.log(err); return <>; } } +type Props = { + params: { id: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata +): Promise { + const questionId = params.id; + const query = { + query: QuestionQueryById, + variables: { id: questionId }, + }; + const session = await getServerSession(options); + const client = ServerGraphQLClient(session); + const { data: result } = await client.query(query); + const data = result.question as (Partial & { id: string }) | null; + const title = data?.questionInfo?.title + ? `${data.questionInfo.title} - Study AI` + : "Question title is not found - Study AI"; + const description = + data?.questionInfo?.description ?? "Question description is not available"; + const origin = determineOriginUrl() as string; + return { + title, + description, + metadataBase: new URL(origin), + openGraph: { + title, + description, + locale: "en_US", + type: "website", + siteName: "Study AI", + url: origin, + images: [ + { + url: "/logo/logo.png", + width: 800, + height: 800, + }, + ], + }, + }; +} diff --git a/studyAi/src/app/library/question/components/server/answerComponent.tsx b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx similarity index 55% rename from studyAi/src/app/library/question/components/server/answerComponent.tsx rename to studyAi/src/app/library/question/components/answer/client/answerComponent.tsx index ebdf254a..611096ca 100644 --- a/studyAi/src/app/library/question/components/server/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx @@ -1,21 +1,25 @@ "use client"; import { useParams } from "next/navigation"; import { useQuestions } from "@/app/stores/questionStore"; -import ContainerBar, { Container } from "./containerBar"; +import ContainerBar, { Container } from "../../page/server/containerBar"; import { Button, IconButton } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { + faDownLeftAndUpRightToCenter, faRefresh, faUpRightAndDownLeftFromCenter, } from "@fortawesome/free-solid-svg-icons"; import { QuestionTypes } from "@/app/util/types/UserData"; import { AnswerType } from "./answerInputs"; +import { useFullscreen } from "@/app/util/providers/FullscreenProvider"; +import React from "react"; +import BtnLabelDropdown from "@/app/util/components/btnLabelDropdown/btnLabelDropdown"; const determineAnswerTitle = (str?: string) => { const matchStr = str as (typeof QuestionTypes)[number]; switch (matchStr) { - case "multipleChoice": + case "Multiple Choice": return "Select the best answer"; - case "selectMultiple": + case "Select Multiple": return "Select all that apply"; case "Short Answer": return "Add your answer below"; @@ -23,13 +27,87 @@ const determineAnswerTitle = (str?: string) => { return str; } }; +const FullScreenBtn = ({ + btnClassNames, + btnStyle, +}: { + btnClassNames?: string; + btnStyle?: React.CSSProperties; +}) => { + const { isFullscreen, toggleFullscreen } = useFullscreen(); + return ( + + {(props) => ( + { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} + size="small" + sx={btnStyle} + className={btnClassNames} + type="button" + aria-label={`toggle fullscreen ${isFullscreen ? "off" : "on"}`} + onClick={toggleFullscreen} + > + {!isFullscreen ? ( + + ) : ( + + )} + + )} + + ); +}; +const ResetAnswerBtn = ({ + btnClassNames, + btnStyle, +}: { + btnClassNames?: string; + btnStyle?: React.CSSProperties; +}) => { + return ( + + {(props) => ( + { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} + size="small" + sx={btnStyle} + className={btnClassNames} + type="button" + > + + + )} + + ); +}; const TopBar = () => { const params = useParams(); const questions = useQuestions()[0].data; const question = params.id && typeof params.id === "string" ? questions[params.id] : null; const btnClassNames = "flex items-center justify-center h-[70%]"; - const btnStyle = { + const btnStyle: React.CSSProperties = { minHeight: "unset", padding: 0, aspectRatio: 1, @@ -41,25 +119,8 @@ const TopBar = () => { {determineAnswerTitle(question?.questionType)}
- - - - - - + +
); @@ -71,6 +132,7 @@ const AnswerContainer = ({ height }: { height?: string | number }) => { overflow className="max-h-[max(30rem,45vh)] md:max-h-none md:w-3/6 md:ml-2 grow" style={{ height: height ? height + "px" : undefined }} + fullHeight={false} > diff --git a/studyAi/src/app/library/question/components/server/answerInputs.tsx b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx similarity index 84% rename from studyAi/src/app/library/question/components/server/answerInputs.tsx rename to studyAi/src/app/library/question/components/answer/client/answerInputs.tsx index 57bbc8e6..222fdc95 100644 --- a/studyAi/src/app/library/question/components/server/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx @@ -9,8 +9,8 @@ import { Radio, RadioGroup, } from "@mui/material"; -import useOptionsWithId from "../hooks/useOptionsWithId"; import { ChangeEvent, KeyboardEvent } from "react"; +import { Question } from "../../../../../../../graphql/generated/graphql"; const adjustScroll = ( event: ChangeEvent | KeyboardEvent ) => { @@ -39,14 +39,17 @@ const adjustScroll = ( return (element.scrollTop = newScrollPos <= 0 ? 0 : newScrollPos); } }; -export const MultipleChoice = ({ options }: { options: string[] }) => { - const { currOptions } = useOptionsWithId({ options }); +export const MultipleChoice = ({ + options, +}: { + options: Question["questionInfo"]["options"]; +}) => { return ( - {currOptions.map((val, idx) => ( + {options.map((val) => ( } label={val.value} /> @@ -54,14 +57,17 @@ export const MultipleChoice = ({ options }: { options: string[] }) => { ); }; -export const SelectMultiple = ({ options }: { options: string[] }) => { - const { currOptions } = useOptionsWithId({ options }); +export const SelectMultiple = ({ + options, +}: { + options: Question["questionInfo"]["options"]; +}) => { return ( - {currOptions.map((val, idx) => ( + {options.map((val) => ( } label={val.value} /> @@ -93,9 +99,9 @@ export const AnswerType = () => { questionInfo: { options: questionOptions }, } = question; switch (questionType) { - case "multipleChoice": + case "Multiple Choice": return ; - case "selectMultiple": + case "Select Multiple": return ; case "Short Answer": return ; diff --git a/studyAi/src/app/library/question/components/client/questionComponents 2.tsx b/studyAi/src/app/library/question/components/client/questionComponents 2.tsx deleted file mode 100644 index ff796496..00000000 --- a/studyAi/src/app/library/question/components/client/questionComponents 2.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; -import ContainerBar, { Container } from "../server/containerBar"; -import capitalizeEveryWord from "@/app/util/parsers/capitalizeEveryWord"; -import EditIcon from "@mui/icons-material/Edit"; -import { Button, Tab, Tabs } from "@mui/material"; -import { useState } from "react"; -import { useSession } from "next-auth/react"; -import { useQuestions } from "@/app/stores/questionStore"; -import { useParams } from "next/navigation"; -import { containerTabs, InnerContainer } from "../server/questionComponents"; -const TopBar = ({ - view, - handleChange, -}: { - view: (typeof containerTabs)[number]; - handleChange: ( - event: React.SyntheticEvent, - newValue: (typeof containerTabs)[number] - ) => void; -}) => { - const params = useParams(); - const session = useSession(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - return ( - - - {containerTabs.map((tab) => ( - - ))} - - {session.data && - question && - session.data.user.id === question.creatorId && ( - - )} - - ); -}; -export const QuestionContainer = ({ height }: { height?: string | number }) => { - const [view, setView] = - useState<(typeof containerTabs)[number]>("description"); - const handleChange = ( - event: React.SyntheticEvent, - newValue: (typeof containerTabs)[number] - ) => setView(newValue); - return ( - - - - - ); -}; -export default QuestionContainer; diff --git a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx b/studyAi/src/app/library/question/components/client/questionPageContainer.tsx deleted file mode 100644 index a719ce90..00000000 --- a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; -import { NavigationBtns, PaginationOptions } from "./navigationBtns"; -import { QuestionWrapper } from "./questionWrapper"; -import { TimeComponent } from "./timeComponent"; -const QuestionFormWrapper = ({ children }: { children: React.ReactNode }) => { - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - //grab uncontrolled inputs here form - const formData = new FormData(e.currentTarget); - const data = Object.fromEntries(formData.entries()); - const { email } = data; - }; - return ( -
- {children} -
- ); -}; -const OngoingQuestionBar = ({ - pagination, -}: { - pagination?: PaginationOptions; -}) => { - if (pagination) - return ( - - - - ); - return ( -
- -
- ); -}; -const QuestionPageContainer = () => { - return ( - - {}, onNext: () => {} }} /> - - - ); -}; -export default QuestionPageContainer; diff --git a/studyAi/src/app/library/question/components/client/questionWrapper 2.tsx b/studyAi/src/app/library/question/components/client/questionWrapper 2.tsx deleted file mode 100644 index df55872f..00000000 --- a/studyAi/src/app/library/question/components/client/questionWrapper 2.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; -import QuestionComponent from "./questionComponents"; -import AnswerComponent from "../server/answerComponent"; -import useElementPosition from "@/app/util/hooks/useElementSize"; -import useWindowWidth from "@/app/util/hooks/useWindowWidth"; -export const QuestionWrapper = () => { - const { - setRef, - position: { height }, - } = useElementPosition(); - const windowWidth = useWindowWidth(); - return ( -
- - -
- ); -}; diff --git a/studyAi/src/app/library/question/components/client/questionWrapper.tsx b/studyAi/src/app/library/question/components/client/questionWrapper.tsx deleted file mode 100644 index 09ec0e7a..00000000 --- a/studyAi/src/app/library/question/components/client/questionWrapper.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; -import QuestionComponent from "./questionComponents"; -import AnswerComponent from "../server/answerComponent"; -import useElementPosition from "@/app/util/hooks/useElementSize"; -import useWindowWidth from "@/app/util/hooks/useWindowWidth"; -export const QuestionWrapper = () => { - const { - setRef, - position: { height }, - } = useElementPosition(); - const windowWidth = useWindowWidth(); - return ( -
- - -
- ); -}; diff --git a/studyAi/src/app/library/question/components/client/questionsView 2.tsx b/studyAi/src/app/library/question/components/client/questionsView 2.tsx deleted file mode 100644 index 3b384333..00000000 --- a/studyAi/src/app/library/question/components/client/questionsView 2.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; -import { Container } from "../server/containerBar"; -import { Chip, IconButton } from "@mui/material"; -import { useQuestions } from "@/app/stores/questionStore"; -import { useParams } from "next/navigation"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Share } from "@mui/icons-material"; -import { faThumbsUp, faThumbsDown } from "@fortawesome/free-regular-svg-icons"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { parseInteger } from "@/app/util/parsers/parseInt"; -const QuestionActionBtns = () => { - return ( -
- - - - - - -
- ); -}; -const LikeCounterBtns = () => { - const params = useParams(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - return ( -
- - - - {parseInteger(question?.likeCounter?.likes)} - - - - - - {parseInteger(question?.likeCounter?.dislikes)} - - -
- ); -}; -export const QuestionView = () => { - const params = useParams(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - - if (!question) return <>; - return ( - - {question.question && ( -

- {question.question.title} -

- )} -
- - -
- {question.tags && ( -
- {question.tags.map((tag, idx) => ( - - ))} -
- )} - - {question.question && ( -

{question.question.description}

- )} -
- ); -}; diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/client/questionsView.tsx deleted file mode 100644 index ae4c314f..00000000 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; -import { Container } from "../server/containerBar"; -import { Button, Chip, IconButton } from "@mui/material"; -import { useQuestions } from "@/app/stores/questionStore"; -import { useParams } from "next/navigation"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Share } from "@mui/icons-material"; -import { faThumbsUp, faThumbsDown } from "@fortawesome/free-regular-svg-icons"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { parseInteger } from "@/app/util/parsers/parseInt"; -import { Carousel } from "@/app/util/components/carousel/carousel"; - -const QuestionActionBtns = () => { - return ( -
- - - - - - -
- ); -}; -const LikeCounterBtns = () => { - const params = useParams(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - return ( -
- - -
- ); -}; -export const QuestionView = () => { - const params = useParams(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - - if (!question) return <>; - return ( - - {question.questionInfo && ( -

- {question.questionInfo.title} -

- )} -
- - -
- {question.tags && ( -
- - {question.tags.map((tag, idx) => ( - - ))} - -
- )} - - {question.questionInfo && ( -

{question.questionInfo.description}

- )} -
- ); -}; diff --git a/studyAi/src/app/library/question/components/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeComponent.tsx deleted file mode 100644 index bc4c8975..00000000 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ /dev/null @@ -1,261 +0,0 @@ -"use client"; -import { - ChangeEvent, - Dispatch, - FormEvent, - SetStateAction, - SyntheticEvent, - useRef, - useState, -} from "react"; -import StopWatch from "@/app/util/components/time/stopwatch"; -import Timer from "@/app/util/components/time/timer"; -import { - Box, - Button, - FormControlLabel, - Modal, - Radio, - RadioGroup, - SxProps, - TextField, - Typography, -} from "@mui/material"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { TimeOptions } from "../../../../../../prisma/generated/type-graphql"; -import formatMilliseconds, { - extractTime, -} from "@/app/util/parsers/formatMilliseconds"; -import removeNonIntegerChars from "@/app/util/parsers/removeNonIntegerChars"; -//we can manage time on the frontend -//because time measurements are only -//for the user's benefit -//if we need to ensure compliance to time -//we must manage it using a websocket connection -type TimeProps = TimeOptions & { - initialTime: number; -}; -const defaultTime = formatMilliseconds(0) as string; -const timeOrder = ["h", "m", "s"]; -const splitTimeStrBy2 = (str: string) => { - const arr = []; - for (let i = 0; i < str.length; i += 2) { - const chunk = str.slice(i, i + 2); - arr.push(chunk); - } - return arr; -}; -function TimerInput() { - const ref = useRef(); - const [totalTime, setTotalTime] = useState( - defaultTime - .split(":") - .map((a, idx) => a + timeOrder[idx]) - .reduce((a, b) => a + " " + b) - ); - const onChange = (e: ChangeEvent) => { - const target = e.target as HTMLInputElement; - const value = target.value; - const selectionEnd = target.selectionEnd; - setTotalTime((prevVal) => { - const prevIntegers = removeNonIntegerChars(prevVal); - let currIntegers = removeNonIntegerChars(value); - //this means that incorrect values were entered - //this is therefore not a correct input - if (prevIntegers.length < currIntegers.length) return prevVal; - const diff = currIntegers.length - prevIntegers.length; - //we remove the difference from the start of the string - //therefore maintaing the default string length - currIntegers = currIntegers.substring(diff, currIntegers.length); - if (currIntegers.length !== prevIntegers.length) return prevVal; - const newValArr = splitTimeStrBy2(currIntegers); - const newVal = newValArr.reduce((a, b, idx) => a + timeOrder[idx] + b); - console.log(newVal) - return newVal; - }); - - //every hour minute and second is represented with two digits - //therefore we return the new string parsed - // console.log(value, selectionEnd); - // setTotalTime((prevVal) => { - // const currVal = target.value - // let newVal = "" - // if (prevVal < currVal) newVal = "0" + currVal.substring(1, currVal.length) - // else - // const { hours, minutes, seconds } = extractTime(currVal, false); - - // }) - // if (!(hours) || !(minutes) || !(seconds)) return; - // console.log(hours, minutes, seconds) - - // const timeTotalSeconds = - // parseInt(hours.toString().padStart(2, "0"), 10) * 3600 + - // parseInt(minutes.toString().padStart(2, "0"), 10) * 60 + - // parseInt(seconds.toString().padStart(2, "0"), 10); - - // const timeInMilliseconds = timeTotalSeconds * 1000; - // const formattedTime = formatMilliseconds(timeInMilliseconds) as string; - // const userReadableTime = formattedTime - // .split(":") - // .map((a, idx) => a + timeOrder[idx]) - // .reduce((a, b) => a + " " + b); - // console.log(userReadableTime); - //setTotalTime(userReadableTime); - }; - return ( -
- -
- ); -} - -const TimeForm = ({ - setCurrType, - setCurrTotalTimeGiven, -}: { - setCurrType: Dispatch>; - setCurrTotalTimeGiven: Dispatch>; -}) => { - const [timeType, setTimeType] = useState("timer"); - const onTimeTypeChange = (e: SyntheticEvent) => { - const target = e.target as HTMLInputElement; - setTimeType(target.value); - }; - const onSubmit = (e: FormEvent) => { - e.preventDefault(); - //grab uncontrolled inputs here form - const formData = new FormData(e.currentTarget); - const data = Object.fromEntries(formData.entries()); - const { totalTime } = data; - const { hours, minutes, seconds } = extractTime(totalTime.toString()); - const timeTotalSeconds = - parseInt(hours.toString(), 10) * 3600 + - parseInt(minutes.toString(), 10) * 60 + - parseInt(seconds.toString(), 10); - console.log(data); - }; - const borderStyle: SxProps = { - borderWidth: 1, - borderStyle: "solid", - }; - return ( -
- - Track Your Time - -
- - - } - label="Stopwatch" - labelPlacement="bottom" - onChange={onTimeTypeChange} - checked={timeType === "stopwatch"} - className="flex items-center justify-center aspect-square h-[5rem] md:h-[10rem]" - /> - - - } - label="Timer" - labelPlacement="bottom" - onChange={onTimeTypeChange} - checked={timeType === "timer"} - className="flex items-center justify-center aspect-square h-[5rem] md:h-[10rem]" - /> - - - {timeType === "timer" && } - - -
- ); -}; - -export const TimeComponent = ({ props }: { props?: TimeProps }) => { - const { timeType, initialTime, totalTimeGiven } = props || { - initialTime: 0, - }; - const [currType, setCurrType] = useState(timeType); - const [currInitTime, setCurrInitTime] = useState(initialTime); - const [currTotalTimeGiven, setCurrTotalTimeGiven] = useState(totalTimeGiven); - const [modalOpen, setModalOpen] = useState(true); - switch (currType) { - case "stopwatch": - return ; - case "timer": - return ( - - ); - //create timer component - default: - return ( - <> - {!modalOpen && ( - - )} - setModalOpen(false)} - aria-labelledby="track-your-time" - aria-describedby="attach-stopwatch-or-timer" - className="flex justify-center items-center" - > - <> - - - - - ); - } -}; diff --git a/studyAi/src/app/library/question/components/client/navigationBtns.tsx b/studyAi/src/app/library/question/components/page/client/navigationBtns.tsx similarity index 100% rename from studyAi/src/app/library/question/components/client/navigationBtns.tsx rename to studyAi/src/app/library/question/components/page/client/navigationBtns.tsx diff --git a/studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx b/studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx new file mode 100644 index 00000000..b8c52b4f --- /dev/null +++ b/studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx @@ -0,0 +1,100 @@ +"use client"; +import React from "react"; +import { NavigationBtns, PaginationOptions } from "./navigationBtns"; +import { QuestionWrapper } from "../server/questionWrapper"; +import { TimeComponent } from "../../time/client/timeModal"; +import NavigationWrapper from "@/app/util/components/navigation/navigationWrapper"; +import FullscreenProvider, { + useFullscreen, +} from "@/app/util/providers/FullscreenProvider"; +const ConditionalWrapper = ({ + condition, + wrapper, + children, +}: { + condition: boolean; + wrapper: (children: React.ReactNode) => React.ReactNode; + children: React.ReactNode; +}) => { + return condition ? wrapper(children) : children; +}; +const QuestionFormWrapper = ({ children }: { children: React.ReactNode }) => { + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + //grab uncontrolled inputs here form + const formData = new FormData(e.currentTarget); + const data = Object.fromEntries(formData.entries()); + const { email } = data; + }; + return ( +
+ {children} +
+ ); +}; +const OngoingQuestionBar = ({ + pagination, +}: { + pagination?: PaginationOptions; +}) => { + if (pagination) + return ( + + + + ); + return ( +
+ +
+ ); +}; +const QuestionPageNavigation = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isFullscreen } = useFullscreen(); + return ( + ( + + {children} + + )} + > + ( +
+ {children} +
+ )} + > + {children} +
+
+ ); +}; +const QuestionPageContainer = () => { + return ( + + + + {}, onNext: () => {} }} + /> + + + + + ); +}; +export default QuestionPageContainer; diff --git a/studyAi/src/app/library/question/components/server/containerBar.tsx b/studyAi/src/app/library/question/components/page/server/containerBar.tsx similarity index 70% rename from studyAi/src/app/library/question/components/server/containerBar.tsx rename to studyAi/src/app/library/question/components/page/server/containerBar.tsx index 8ca688d0..de319aa4 100644 --- a/studyAi/src/app/library/question/components/server/containerBar.tsx +++ b/studyAi/src/app/library/question/components/page/server/containerBar.tsx @@ -5,23 +5,25 @@ export const Container = ( overflow?: boolean; border?: boolean; fullWidth?: boolean; + fullHeight?: boolean; } & DetailedHTMLProps, HTMLDivElement> ) => { const copyProps = { ...props }; - const { children, overflow, border, fullWidth } = props; + const { children, overflow, border, fullWidth, fullHeight } = props; const borderClasses = " border-Black border border-solid"; - if (copyProps.children) delete copyProps.children; - if (copyProps.overflow) delete copyProps.overflow; - if (copyProps.border) delete copyProps.border; + if ("children" in copyProps) delete copyProps.children; + if ("border" in copyProps) delete copyProps.border; if ("overflow" in copyProps) delete copyProps.overflow; - if (copyProps.fullWidth) delete copyProps.fullWidth; + if ("fullWidth" in copyProps) delete copyProps.fullWidth; + if ("fullHeight" in copyProps) delete copyProps.fullHeight; return (
{ + return ( +
+ + +
+ ); +}; diff --git a/studyAi/src/app/library/question/components/client/questionComponents.tsx b/studyAi/src/app/library/question/components/question/client/questionComponents.tsx similarity index 66% rename from studyAi/src/app/library/question/components/client/questionComponents.tsx rename to studyAi/src/app/library/question/components/question/client/questionComponents.tsx index c72a122e..87eee777 100644 --- a/studyAi/src/app/library/question/components/client/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/question/client/questionComponents.tsx @@ -1,13 +1,50 @@ "use client"; -import ContainerBar, { Container } from "../server/containerBar"; +import ContainerBar, { Container } from "../../page/server/containerBar"; import capitalizeEveryWord from "@/app/util/parsers/capitalizeEveryWord"; import EditIcon from "@mui/icons-material/Edit"; -import { IconButton, Tab, Tabs } from "@mui/material"; +import { + IconButton, + Menu, + MenuProps, + Tab, + Tabs, + Typography, +} from "@mui/material"; import React, { useState } from "react"; import { useSession } from "next-auth/react"; import { useQuestions } from "@/app/stores/questionStore"; import { useParams } from "next/navigation"; -import { containerTabs, InnerContainer } from "../server/questionComponents"; +import { containerTabs, InnerContainer } from "../server/questionViewContainer"; +import BtnLabelDropdown from "@/app/util/components/btnLabelDropdown/btnLabelDropdown"; +const EditBtn = ({ + btnStyles, + btnClassNames, +}: { + btnStyles: React.CSSProperties; + btnClassNames: string; +}) => { + return ( + + {(props) => ( + { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} + type="button" + sx={btnStyles} + className={btnClassNames + " aspect-square h-[70%]"} + > + + + )} + + ); +}; + const TopBar = ({ view, handleChange, @@ -23,7 +60,7 @@ const TopBar = ({ const questions = useQuestions()[0].data; const question = params.id && typeof params.id === "string" ? questions[params.id] : null; - const btnStyles = { + const btnStyles: React.CSSProperties = { textTransform: "none", padding: 0, margin: 0, @@ -55,13 +92,7 @@ const TopBar = ({ {session.data && question && session.data.user.id === question.creatorId && ( - - - + )} ); @@ -76,6 +107,7 @@ export const QuestionContainer = ({ height }: { height?: string | number }) => { return ( diff --git a/studyAi/src/app/library/question/components/question/client/questionsView.tsx b/studyAi/src/app/library/question/components/question/client/questionsView.tsx new file mode 100644 index 00000000..10d3b5ec --- /dev/null +++ b/studyAi/src/app/library/question/components/question/client/questionsView.tsx @@ -0,0 +1,278 @@ +"use client"; +import { Container } from "../../page/server/containerBar"; +import { + Button, + Chip, + IconButton, + Menu, + MenuProps, + Typography, +} from "@mui/material"; +import { useQuestions } from "@/app/stores/questionStore"; +import { useParams, usePathname } from "next/navigation"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Share } from "@mui/icons-material"; +import { faThumbsUp, faThumbsDown } from "@fortawesome/free-regular-svg-icons"; +import { parseInteger } from "@/app/util/parsers/parseInt"; +import { Carousel } from "@/app/util/components/carousel/carousel"; +import { MouseEvent, useState } from "react"; +import useOrigin from "@/app/util/hooks/useOrigin"; +import { + faFacebook, + faLinkedin, + faReddit, + faTwitter, + faWhatsapp, +} from "@fortawesome/free-brands-svg-icons"; +import { faCheck, faLink } from "@fortawesome/free-solid-svg-icons"; +import useDropdown from "@/app/util/hooks/useDropdown"; +import BtnLabelDropdown from "@/app/util/components/btnLabelDropdown/btnLabelDropdown"; +const platformsToShare = [ + { + platform: "link", + icon: , + }, + { platform: "facebook", icon: }, + { platform: "twitter", icon: }, + { platform: "linkedin", icon: }, + { platform: "reddit", icon: }, + { platform: "whatsapp", icon: }, +] as const; +const determineShareUrl = (url: string, platform?: string) => { + // Set the URL to share based on the social media platform + let shareUrl; + switch (platform) { + case "facebook": + shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent( + url + )}`; + break; + case "twitter": + shareUrl = `https://twitter.com/share?url=${encodeURIComponent(url)}`; + break; + case "linkedin": + shareUrl = `https://www.linkedin.com/sharing/share-offsite/?&url=${encodeURIComponent( + url + )}`; + break; + case "reddit": + shareUrl = `https://reddit.com/submit?url=${encodeURIComponent(url)}`; + break; + case "whatsapp": + shareUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent( + url + )}`; + break; + default: + shareUrl = url; + } + return shareUrl; +}; +const ShareBtn = () => { + const origin = useOrigin(); + const pathName = usePathname(); + const { + anchorEl: shareBtnEl, + setAnchorEl, + handleClick, + handleClose, + open, + } = useDropdown(); + const [copied, setCopied] = useState(false); + const fullUrl = origin + pathName; + const onShareClick = ( + e: MouseEvent + ) => { + const target = e.currentTarget as HTMLButtonElement; + // Get the social media platform from the data list + const dataset = target.dataset; + const platform = dataset["platformId"]; + // Get the URL of the current page + const shareUrl = determineShareUrl(fullUrl, platform); + if (platform !== "link") window.open(shareUrl, "_blank"); + //when user wants to only copy url/link + else navigator.clipboard.writeText(shareUrl).then(() => setCopied(true)); + handleClose(); + }; + const shareMenuProps: Omit = { + anchorEl: shareBtnEl, + onClose: () => { + setCopied(false); + handleClose(); + }, + anchorOrigin: { + horizontal: "center", + vertical: "bottom", + }, + transformOrigin: { + vertical: "top", + horizontal: "center", + }, + MenuListProps: { + disablePadding: true, + }, + slotProps: { + paper: { + sx: { minHeight: "unset" }, + }, + }, + sx: { + minHeight: "unset", + }, + }; + return ( + <> + + {(props) => ( + { + setAnchorEl(ref); + props.setAnchorEl(ref); + }} + onPointerEnter={(e) => { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} + className="h-[70%]" + type="button" + onClick={(e) => { + handleClick(e); + }} + > + + + )} + + +
+ {" "} + Link Copied! +
+
+ +
+ {platformsToShare.map((platform) => { + const onClick = + platform.platform === "link" + ? (e: MouseEvent) => { + onShareClick(e); + setCopied(true); + setTimeout(() => setCopied(false), 5000); + } + : onShareClick; + return ( + + {platform.icon} + + ); + })} +
+
+ + ); +}; +const QuestionActionBtns = () => { + return ( +
+ {/* this is for when a user can add the question to quiz */} + {/* + + */} + +
+ ); +}; +const LikeCounterBtns = () => { + const params = useParams(); + const questions = useQuestions()[0].data; + const question = + params.id && typeof params.id === "string" ? questions[params.id] : null; + return ( +
+ + +
+ ); +}; +export const QuestionView = () => { + const params = useParams(); + const questions = useQuestions()[0].data; + const question = + params.id && typeof params.id === "string" ? questions[params.id] : null; + + if (!question) return <>; + return ( + + {question.questionInfo && ( +

+ {question.questionInfo.title} +

+ )} +
+ + +
+ {question.tags && ( +
+ + {question.tags.map((tag, idx) => ( + + ))} + +
+ )} + + {question.questionInfo && ( +

{question.questionInfo.description}

+ )} +
+ ); +}; diff --git a/studyAi/src/app/library/question/components/client/solutionView.tsx b/studyAi/src/app/library/question/components/question/client/solutionView.tsx similarity index 63% rename from studyAi/src/app/library/question/components/client/solutionView.tsx rename to studyAi/src/app/library/question/components/question/client/solutionView.tsx index 7ba50e86..0fe5ac69 100644 --- a/studyAi/src/app/library/question/components/client/solutionView.tsx +++ b/studyAi/src/app/library/question/components/question/client/solutionView.tsx @@ -1,16 +1,19 @@ "use client"; import { useQuery } from "@apollo/client"; -import { Question } from "../../../../../../prisma/generated/type-graphql"; +import { Question } from "../../../../../../../prisma/generated/type-graphql"; import { useParams } from "next/navigation"; import { useQuestions } from "@/app/stores/questionStore"; -import { Container } from "../server/containerBar"; -import { gql } from "../../../../../../graphql/generated"; +import { Container } from "../../page/server/containerBar"; +import { gql } from "../../../../../../../graphql/generated"; const getAnswerById = gql(` query GetAnswerById($id: String) { question(where: { id: $id }) { id answer { - correctAnswer + correctAnswer { + id + value + } } } } @@ -18,8 +21,11 @@ const getAnswerById = gql(` const SolutionView = () => { const params = useParams(); const questions = useQuestions()[0].data; - const questionId = params?.id as string | undefined; - if (!questionId) return <>; + const questionId = params + ? typeof params.id === "string" + ? params.id + : params.id[0] + : ""; const { loading, error, @@ -33,7 +39,9 @@ const SolutionView = () => { const question = questions[questionId]; return ( - {question?.answer?.correctAnswer} + {question?.answer?.correctAnswer.map((e) => { + return e.value; + })} ); }; diff --git a/studyAi/src/app/library/question/components/server/submissionView.tsx b/studyAi/src/app/library/question/components/question/client/submissionView.tsx similarity index 82% rename from studyAi/src/app/library/question/components/server/submissionView.tsx rename to studyAi/src/app/library/question/components/question/client/submissionView.tsx index cadc2630..d7b4befa 100644 --- a/studyAi/src/app/library/question/components/server/submissionView.tsx +++ b/studyAi/src/app/library/question/components/question/client/submissionView.tsx @@ -1,10 +1,10 @@ "use client"; -import { QuestionSubmission } from "../../../../../../prisma/generated/type-graphql"; +import { QuestionSubmission } from "../../../../../../../prisma/generated/type-graphql"; import { useQuery } from "@apollo/client"; -import { Container } from "./containerBar"; +import { Container } from "../../page/server/containerBar"; import { useParams } from "next/navigation"; import { useSession } from "next-auth/react"; -import { gql } from "../../../../../../graphql/generated"; +import { gql } from "../../../../../../../graphql/generated"; const getSubmissionByQuestionId = gql(` query GetQuestionSubmissionByQuestionId($questionId: String, $userId: String ) { questionSubmissions( @@ -32,10 +32,10 @@ export const SubmissionView = () => { const params = useParams(); const { data: session } = useSession(); if (!params?.id) return <>; - const userId = session ? session.user.id : '' + const userId = session ? session.user.id : ""; const queryOptions = { variables: { - questionId: typeof params.id === 'string'? params.id : params.id[0], + questionId: typeof params.id === "string" ? params.id : params.id[0], userId: userId, }, }; diff --git a/studyAi/src/app/library/question/components/server/questionComponents.tsx b/studyAi/src/app/library/question/components/question/server/questionViewContainer.tsx similarity index 89% rename from studyAi/src/app/library/question/components/server/questionComponents.tsx rename to studyAi/src/app/library/question/components/question/server/questionViewContainer.tsx index 2d3b4296..22bc7f7e 100644 --- a/studyAi/src/app/library/question/components/server/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/question/server/questionViewContainer.tsx @@ -1,6 +1,6 @@ import { QuestionView } from "../client/questionsView"; import SolutionView from "../client/solutionView"; -import { SubmissionView } from "./submissionView"; +import { SubmissionView } from "../client/submissionView"; export const containerTabs = ["description", "solution", "attempts"] as const; export const InnerContainer = ({ view, diff --git a/studyAi/src/app/library/question/components/server/answerComponent 2.tsx b/studyAi/src/app/library/question/components/server/answerComponent 2.tsx deleted file mode 100644 index ea0093b9..00000000 --- a/studyAi/src/app/library/question/components/server/answerComponent 2.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; -import { useParams } from "next/navigation"; -import { useQuestions } from "@/app/stores/questionStore"; -import ContainerBar, { Container } from "./containerBar"; -import { IconButton } from "@mui/material"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faRefresh, - faUpRightAndDownLeftFromCenter, -} from "@fortawesome/free-solid-svg-icons"; -import { QuestionTypes } from "@/app/util/types/UserData"; -const determineAnswerTitle = (str?: string) => { - const matchStr = str as (typeof QuestionTypes)[number]; - switch (matchStr) { - case "multipleChoice": - return "Select the best answer"; - case "selectMultiple": - return "Select all that apply"; - case "shortAnswer": - return "Type your answer below"; - default: - return str; - } -}; -const TopBar = () => { - const params = useParams(); - const questions = useQuestions()[0].data; - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - return ( - -

{determineAnswerTitle(question?.questionType)}

-
- - - - - - -
-
- ); -}; -const AnswerContainer = ({ height }: { height?: string | number }) => { - return ( - - - - ); -}; -export default AnswerContainer; diff --git a/studyAi/src/app/library/question/components/server/questionComponents 2.tsx b/studyAi/src/app/library/question/components/server/questionComponents 2.tsx deleted file mode 100644 index c83540c0..00000000 --- a/studyAi/src/app/library/question/components/server/questionComponents 2.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { QuestionView } from "../client/questionView"; -import SolutionView from "../client/solutionView"; - -export const containerTabs = ["description", "solution", "attempts"] as const; -export const InnerContainer = ({ view }: { view: (typeof containerTabs)[number] }) => { - switch (view) { - case "description": - return ; - case "solution": - return ; - case "attempts": - return
attempts
; - default: - return ; - } -}; diff --git a/studyAi/src/app/library/question/components/time/client/timeForm.tsx b/studyAi/src/app/library/question/components/time/client/timeForm.tsx new file mode 100644 index 00000000..c40ee313 --- /dev/null +++ b/studyAi/src/app/library/question/components/time/client/timeForm.tsx @@ -0,0 +1,311 @@ +"use client"; +import { + ChangeEvent, + Dispatch, + FormEvent, + SetStateAction, + SyntheticEvent, + useEffect, + useRef, + useState, +} from "react"; +import { Button, Tab, Tabs, TextField, Typography } from "@mui/material"; +import removeNonIntegerChars from "@/app/util/parsers/removeNonIntegerChars"; +import { unstable_batchedUpdates } from "react-dom"; +import { extractTime } from "@/app/util/parsers/formatMilliseconds"; +export const timeLabelData: { + abbrev: "h" | "m" | "s"; + label: "hours" | "minutes" | "seconds"; +}[] = [ + { abbrev: "h", label: "hours" }, + { abbrev: "m", label: "minutes" }, + { abbrev: "s", label: "seconds" }, +]; +const determineNewVal = ( + newValArr: string[], + name: string, + prevVal: string +) => { + //determine new value from parsed arr + let newVal: string; + switch (name) { + case "hours": + newVal = newValArr[0]; + break; + case "minutes": + newVal = newValArr[1]; + break; + case "seconds": + newVal = newValArr[2]; + break; + default: + newVal = prevVal; + break; + } + return newVal; +}; +export const splitTimeStrBy2 = (str: string) => { + const arr = []; + for (let i = 0; i < str.length; i += 2) { + const chunk = str.slice(i, i + 2); + arr.push(chunk); + } + return arr; +}; +const FieldInput = ({ + onChange, + value, + name, + label, + abbrev, +}: { + onChange: (e: ChangeEvent) => void; + value: string; + name: string; + label: string; + abbrev: "h" | "m" | "s"; +}) => { + const ref = useRef(); + const [cursor, setCursor] = useState(null); + useEffect(() => { + const input = ref.current; + if (input) input.setSelectionRange(cursor, cursor); + }, [ref, cursor, value]); + return ( +
+ { + const target = e.target as HTMLInputElement; + const selectionEnd = target.selectionEnd; + setCursor(selectionEnd); + }} + inputProps={{ + className: "text-5xl sm:text-7xl w-12 sm:w-18 tracking-wider", + style: { + minHeight: "unset", + minWidth: "unset", + textAlign: "center", + height: "inherit", + }, + }} + /> + +
+ ); +}; +const StopWatchPlaceholder = () => { + return ( +
+
+ + {"0"} + + + {"s"} + + + {"00"} + +
+
+ ); +}; +function TimerInput() { + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [totalTime, setTotalTime] = useState("00h 00m 00s"); + const timeVals: { + [key: string]: string; + } = { + hours, + minutes, + seconds, + }; + const onChange = (e: ChangeEvent) => { + const target = e.target as HTMLInputElement; + const { name, value } = target; + const dispatchVals: { + [key: string]: Dispatch>; + } = { + hours: setHours, + minutes: setMinutes, + seconds: setSeconds, + totalTime: setTotalTime, + }; + let setAction = dispatchVals[name]; + setAction((prevVal) => { + const maxLength = timeLabelData.length * 2; + //set the current values + const parsedVal = removeNonIntegerChars(value); + timeVals[name] = parsedVal; + let currIntegers = removeNonIntegerChars( + `${timeVals.hours}${timeVals.minutes}${timeVals.seconds}` + ); + //we remove the difference from the start of the string + //therefore maintaing the default string length + const diff = currIntegers.length - maxLength; + if (currIntegers.length > maxLength) + currIntegers = currIntegers.substring(diff, currIntegers.length); + //we pad the beginning with zeros in case of delete + if (currIntegers.length < maxLength) + currIntegers = currIntegers.padStart(maxLength, "0"); + //this means that incorrect values were entered + //this is therefore not a correct input + if (currIntegers.length !== maxLength) return prevVal; + const newValArr = splitTimeStrBy2(currIntegers); + const newVal = determineNewVal(newValArr, name, prevVal); + //this creates the new total time string + const newTotalTime = + newValArr.reduce( + (a, b, idx) => a + timeLabelData[idx - 1].abbrev + " " + b + ) + timeLabelData[timeLabelData.length - 1].abbrev; + //we can do this because we are using + //updating from the same component + unstable_batchedUpdates(() => { + //update new total time + if (name !== "hours") setHours(newValArr[0]); + if (name !== "minutes") setMinutes(newValArr[1]); + if (name !== "seconds") setSeconds(newValArr[2]); + setTotalTime(newTotalTime); + }); + return newVal; + }); + }; + return ( +
+ {timeLabelData.map((a) => ( + + ))} + +
+ ); +} +const btnStyles = { + textTransform: "none", + padding: 0, + margin: 0, + minHeight: "unset", +}; +export const TimeForm = ({ + setCurrType, + setCurrTotalTimeGiven, + setModalOpen, +}: { + setCurrType: Dispatch>; + setCurrTotalTimeGiven: Dispatch>; + setModalOpen: Dispatch>; +}) => { + const [timeType, setTimeType] = useState("stopwatch"); + const onTimeTypeChange = ( + e: SyntheticEvent, + newValue: string + ) => { + setTimeType(newValue); + }; + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + //grab uncontrolled inputs here form + const formData = new FormData(e.currentTarget); + const data = Object.fromEntries(formData.entries()); + if (timeType === "stopwatch") + return unstable_batchedUpdates(() => { + setCurrType(timeType); + setModalOpen(false); + }); + const { totalTime } = data; + const { hours, minutes, seconds } = extractTime( + totalTime.toString(), + false + ); + const timeTotalSeconds = + parseInt(hours.toString(), 10) * 3600 + + parseInt(minutes.toString(), 10) * 60 + + parseInt(seconds.toString(), 10); + unstable_batchedUpdates(() => { + setCurrType(timeType); + setCurrTotalTimeGiven(timeTotalSeconds * 1000); + setModalOpen(false); + }); + }; + return ( +
+ + Track Your Time + +
+ + + + + {timeType === "timer" && } + {timeType === "stopwatch" && } + + +
+ ); +}; +export default TimeForm; diff --git a/studyAi/src/app/library/question/components/time/client/timeModal.tsx b/studyAi/src/app/library/question/components/time/client/timeModal.tsx new file mode 100644 index 00000000..d16b6c98 --- /dev/null +++ b/studyAi/src/app/library/question/components/time/client/timeModal.tsx @@ -0,0 +1,252 @@ +"use client"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import StopWatch from "@/app/util/components/time/stopwatch"; +import Timer from "@/app/util/components/time/timer"; +import { Button, IconButton, Modal, Typography } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { TimeOptions } from "../../../../../../../prisma/generated/type-graphql"; +import TimeForm, { splitTimeStrBy2 } from "./timeForm"; +import { unstable_batchedUpdates } from "react-dom"; +import { getLocalStorageObj } from "@/app/util/parsers/localStorageWrappers"; +import { + TimeProvider, + useTimeHook, +} from "@/app/util/components/time/context/useTimeContext"; +import onTimeEventChangeHandler from "../../../eventHandlers/onTimeEventChangeHandler"; +import formatMilliseconds from "@/app/util/parsers/formatMilliseconds"; +import { timeLabelData } from "./timeForm"; +import removeNonIntegerChars from "@/app/util/parsers/removeNonIntegerChars"; +//we can manage time on the frontend +//because time measurements are only +//for the user's benefit +//if we need to ensure compliance to time +//we must manage it using a websocket connection +type TimeProps = TimeOptions & { + id?: string; + initialTime: number; +}; +export const TimerFinishedModal = ({ + setTimerCompleteModalOpen, + modalOpen, + totalTimeGiven, +}: { + totalTimeGiven: number; + modalOpen: boolean; + setTimerCompleteModalOpen: Dispatch>; +}) => { + const timeContext = useTimeHook(); + const timeElapsed = formatMilliseconds(totalTimeGiven) as string; + const timeStr = removeNonIntegerChars(timeElapsed); + const timeArr = splitTimeStrBy2(timeStr); + //this creates the new total time string + const parsedTimeElapsed = + timeArr.reduce((a, b, idx) => a + timeLabelData[idx - 1].abbrev + " " + b) + + timeLabelData[timeLabelData.length - 1].abbrev; + return ( + setTimerCompleteModalOpen(false)} + aria-labelledby="timer-complete-modal" + aria-describedby="timer-complete" + className="flex justify-center items-center" + > +
+ Your Time is Up! + Time Passed: {parsedTimeElapsed} + +
+
+ ); +}; +export const DeleteTimeBtn = ({ + setCurrTotalTimeGiven, + setCurrType, + label, +}: { + setCurrTotalTimeGiven: Dispatch>; + setCurrType: Dispatch>; + label: string; +}) => { + return ( + { + unstable_batchedUpdates(() => { + setCurrTotalTimeGiven(null); + setCurrType(undefined); + }); + }} + > + + + ); +}; +export const TimeComponent = ({ props }: { props?: TimeProps }) => { + const { timeType, initialTime, totalTimeGiven, id } = props || { + initialTime: 0, + }; + const [currType, setCurrType] = useState(timeType); + const [currInitTime, setCurrInitTime] = useState(initialTime); + const [currTotalTimeGiven, setCurrTotalTimeGiven] = useState(totalTimeGiven); + const [modalOpen, setModalOpen] = useState(false); + const [timerCompleteModalOpen, setTimerCompleteModalOpen] = useState(false); + //every time we modify the time component we should ensure this is false + //because that means that the timer has been added with new values, or + //we no longer have a timer + useEffect(() => { + setTimerCompleteModalOpen(false); + }, [currType]); + //update initial time with stored values + useEffect(() => { + const storedData = getLocalStorageObj< + Pick + >(`${id}-timer-data`); + if (storedData) setCurrInitTime(storedData.initialTime); + }, []); + switch (currType) { + case "stopwatch": + return ( + + + } + /> + + ); + case "timer": + if (typeof currTotalTimeGiven === "number") + return ( + + + } + /> + {timerCompleteModalOpen && ( + + )} + + ); + else { + unstable_batchedUpdates(() => { + setCurrTotalTimeGiven(null); + setCurrType("stopwatch"); + }); + return ( + + + } + /> + + ); + } + //create timer component + default: + return ( + <> + {!modalOpen && ( + + )} + setModalOpen(false)} + aria-labelledby="track-your-time" + aria-describedby="attach-stopwatch-or-timer" + className="flex justify-center items-center" + > + <> + + + + + ); + } +}; diff --git a/studyAi/src/app/library/question/eventHandlers/onTimeEventChangeHandler.tsx b/studyAi/src/app/library/question/eventHandlers/onTimeEventChangeHandler.tsx new file mode 100644 index 00000000..bff8c2ff --- /dev/null +++ b/studyAi/src/app/library/question/eventHandlers/onTimeEventChangeHandler.tsx @@ -0,0 +1,59 @@ +import { Dispatch, SetStateAction } from "react"; +import { + deleteLocalStorageObj, + addLocalStorageObj, +} from "@/app/util/parsers/localStorageWrappers"; +import { TimeEventProps } from "@/app/util/components/time/context/useTimeContext"; +const onTimeEventChangeHandler = + ({ + id, + currType, + setCurrInitTime, + setTimerCompleteModalOpen, + }: { + id?: string; + currType: string; + setCurrInitTime: Dispatch>; + setTimerCompleteModalOpen: Dispatch>; + }) => + (e?: TimeEventProps) => { + if (!e) return; + const { eventType, time } = e; + const dataId = id ? `${id}-time-data` : null; + if (currType !== "stopwatch" && currType !== "timer") return; + //if we're dealing with timer + switch (eventType) { + case "start": + if (dataId) + addLocalStorageObj(dataId, { + time, + timeType: currType, + }); + break; + case "interval": + if (dataId) + addLocalStorageObj(dataId, { + time, + timeType: currType, + }); + break; + case "stop": + if (dataId) + addLocalStorageObj(dataId, { + time, + timeType: currType, + }); + break; + case "reset": + if (dataId) deleteLocalStorageObj(dataId); + setCurrInitTime(0); + break; + case "finished": + if (dataId) deleteLocalStorageObj(dataId); + if (currType === "timer") setTimerCompleteModalOpen(true); + break; + default: + return; + } + }; +export default onTimeEventChangeHandler \ No newline at end of file diff --git a/studyAi/src/app/library/question/components/hooks/useOptionsWithId.tsx b/studyAi/src/app/library/question/hooks/useOptionsWithId.tsx similarity index 100% rename from studyAi/src/app/library/question/components/hooks/useOptionsWithId.tsx rename to studyAi/src/app/library/question/hooks/useOptionsWithId.tsx diff --git a/studyAi/src/app/util/components/btnLabelDropdown/btnLabelDropdown.tsx b/studyAi/src/app/util/components/btnLabelDropdown/btnLabelDropdown.tsx new file mode 100644 index 00000000..739d5fe5 --- /dev/null +++ b/studyAi/src/app/util/components/btnLabelDropdown/btnLabelDropdown.tsx @@ -0,0 +1,75 @@ +import { Menu, MenuProps, Typography } from "@mui/material"; +import useDropdown from "../../hooks/useDropdown"; +import { Dispatch, MouseEvent, SetStateAction } from "react"; +type BtnLabelDropdownProps = { + anchorEl: HTMLAnchorElement | HTMLButtonElement | null; + setAnchorEl: Dispatch< + SetStateAction + >; + handleClick: ( + event?: MouseEvent< + HTMLButtonElement | HTMLAnchorElement, + globalThis.MouseEvent + > + ) => void; + handleClose: () => void; + open: boolean; +}; +const BtnLabelDropdown = ({ + children, + pointerEvents, + text, +}: { + children: (props: BtnLabelDropdownProps) => React.ReactNode; + pointerEvents?: boolean; + text: string; +}) => { + const { anchorEl, setAnchorEl, handleClick, handleClose, open } = + useDropdown(); + const dropdownMenuProps: Omit = { + anchorOrigin: { + horizontal: "center", + vertical: "bottom", + }, + transformOrigin: { + vertical: "top", + horizontal: "center", + }, + MenuListProps: { + disablePadding: true, + }, + slotProps: { + paper: { + sx: { minHeight: "unset", pointerEvents: "none" }, + }, + }, + sx: { + minHeight: "unset", + }, + }; + const props = { + anchorEl, + setAnchorEl, + handleClick, + handleClose, + open, + }; + return ( + <> + +
+ {text} +
+
+ {children(props)} + + ); +}; +export default BtnLabelDropdown; diff --git a/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx b/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx index 165cc624..e219e76f 100644 --- a/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx +++ b/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx @@ -2,14 +2,13 @@ import React from "react"; import { MenuItem, Menu, Link } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faCaretDown, -} from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown } from "@fortawesome/free-solid-svg-icons"; import useRemToPixel from "@/app/util/hooks/useRemToPixel"; import useElementPosition from "@/app/util/hooks/useElementSize"; import { faWandMagicSparkles } from "@fortawesome/free-solid-svg-icons/faWandMagicSparkles"; import { faFileLines } from "@fortawesome/free-regular-svg-icons"; import useDropdown from "@/app/util/hooks/useDropdown"; +import { unstable_batchedUpdates } from "react-dom"; export const menuItemLinks = [ { href: "/", @@ -29,11 +28,17 @@ export const menuItemLinks = [ export const GenerateDropdown = () => { const currRemToPixelVal = useRemToPixel("1rem"); const { setRef, position: dropdownButtonPos } = useElementPosition(); - const { anchorEl, handleClick, handleClose } = useDropdown(); + const { anchorEl, setAnchorEl, handleClick, handleClose, open } = + useDropdown(); return (
{ + unstable_batchedUpdates(() => { + setRef(ref); + setAnchorEl(ref); + }); + }} component="button" aria-controls="dropdown-menu" aria-haspopup="true" @@ -47,7 +52,7 @@ export const GenerateDropdown = () => { {
); }; - - diff --git a/studyAi/src/app/util/components/navigation/client/userProfile.tsx b/studyAi/src/app/util/components/navigation/client/userProfile.tsx index cc2fe446..63eb1a19 100644 --- a/studyAi/src/app/util/components/navigation/client/userProfile.tsx +++ b/studyAi/src/app/util/components/navigation/client/userProfile.tsx @@ -10,6 +10,7 @@ import { faCaretDown, faChartLine } from "@fortawesome/free-solid-svg-icons"; import { faFileLines, faUserCircle } from "@fortawesome/free-regular-svg-icons"; import NextLink from "next/link"; import { useSession } from "next-auth/react"; +import { unstable_batchedUpdates } from "react-dom"; const userItemLinks = (userId?: string) => [ { href: `/dashboard`, @@ -54,7 +55,7 @@ export const UserProfile = ({
{name && ( - { name} + {name} )} @@ -71,8 +72,10 @@ export const ProfileDropdown = ({ userDropdownPos, handleClose, userId, + open, }: { anchorEl: HTMLElement | null; + open: boolean; userDropdownPos: { x: number; y: number; width: number; height: number }; handleClose: () => void; userId: Partial["id"]; @@ -82,7 +85,7 @@ export const ProfileDropdown = ({ { const session = useSession(); const { setRef, position: userDropdownPos } = useElementPosition(); - const { anchorEl, handleClick, handleClose } = useDropdown(); + const { anchorEl, setAnchorEl, handleClick, handleClose, open } = + useDropdown(); const userProfileProps = session.data?.user; if (!userProfileProps) return <>; return ( <> {dropdown && ( { + unstable_batchedUpdates(() => { + setRef(ref); + setAnchorEl(ref); + }); + }} className="flex items-center h-5/6" component={"button"} aria-label="open-user-navigation" @@ -180,6 +189,7 @@ export const UserProfileNav = ({ {dropdown && ( , isLoading: string, setQuestionData: React.Dispatch | null>> - ) => { +) => { if (!questionData) return; if (isLoading === "loading") return; try { - const questionProvided = { - type: questionData.questionType, - tags: questionData.tags, - question: questionData.questionInfo?.description, - numberOfOptions: questionData.questionInfo?.options.length - } - const result = await axios({ - url: "/api/generateQuestion", - method: "POST", - data: questionProvided, + const questionProvided = { + type: questionData.questionType, + tags: questionData.tags, + question: questionData.questionInfo?.description, + numberOfOptions: questionData.questionInfo?.options.length, + }; + const result = await axios({ + url: "/api/generateQuestion", + method: "POST", + data: questionProvided, }); setQuestionData((prev) => ({ ...prev, questionInfo: { title: prev?.questionInfo?.title || "", description: result?.data?.newQuestion?.question || "", - options: result?.data?.newQuestion?.options || [""] + options: result?.data?.newQuestion?.options || [""], }, answer: { - correctAnswer: result?.data?.newQuestion?.correct || [""] - }})); + correctAnswer: result?.data?.newQuestion?.correct || [""], + }, + })); } catch (err) { console.error(err); return null; } -} +}; const AddQuestion = gql(` mutation CreateOneQuestionResolver( - $creatorId: String, - $questionType: String, - $tags: [String], - $questionInfo: { - title: String, - descriptin: String, - options: [String] - }, - $answer: { - correctAnswer: [string] - }, - $likeCounter: { - likes: Int, - dislikes: Int - }, - $private: boolean + $creatorId: String!, + $questionType: String!, + $tags: QuestionCreatetagsInput, + $questionInfo: QuestionInfoDataCreateEnvelopeInput!, + $answer: AnswerDataCreateEnvelopeInput!, + $likeCounter: LikeCounterCreateEnvelopeInput!, + $private: Boolean! ){ createOneQuestion( data: { creatorId: $creatorId, questionType: $questionType, tags: $tags, - questionInfo: $questionInfoData, - answer: $answerData, + questionInfo: $questionInfo, + answer: $answer, likeCounter: $likeCounter, private: $private - }) + } + ) { id } + } `); const styles = { @@ -107,57 +101,88 @@ const styles = { ].join(" "), }; -const uploadQuestion = async (mutationQuery: any, isLoading: string, e: any) => { +const uploadQuestion = async ( + mutationQuery: any, + isLoading: string, + e: any +) => { e.preventDefault(); if (isLoading === "loading") return; - mutationQuery() - -} + mutationQuery(); +}; const Controls = ({ setIsOpen, setQuestionData, - questionData + questionData, }: QuestionProps) => { - const label = { inputProps: { 'aria-label': 'Switch demo' } }; + const label = { inputProps: { "aria-label": "Switch demo" } }; const [isLoading, setIsLoading] = useState("success"); - // const session = useSession() - // const creatorId = session?.data?.user.id; - // const [mutationQuery, { loading, error, data }] = useMutation( - // AddQuestion, - // { - // variables: { - // creatorId, - // likeCounter: { - // likes: 0, - // dislikes: 0 - // }, - // ...questionData - // }, - // } - // ); - console.log(questionData) - + const session = useSession(); + const creatorId = session?.data?.user.id; + const [mutationQuery, { loading, error, data }] = useMutation(AddQuestion, { + variables: { + questionType: questionData?.questionType + ? questionData.questionType + : "Short Answer", + tags: { + set: questionData?.tags ? questionData.tags : [], + }, + questionInfo: { + set: questionData?.questionInfo + ? { + ...questionData.questionInfo, + } + : { + title: "", + description: "", + options: [], + }, + }, + creatorId: creatorId ? creatorId : "", + likeCounter: { + set: { + likes: 0, + dislikes: 0, + }, + }, + answer: { + set: { + correctAnswer: questionData?.answer?.correctAnswer + ? questionData?.answer?.correctAnswer + : [], + }, + }, + private: !!questionData?.private, + }, + }); return (
- +
-
Private
+
Private
{setQuestionData((prev) => ({...prev, private: !prev?.private}))}} defaultChecked/> + onChange={() => { + setQuestionData((prev) => ({ ...prev, private: !prev?.private })); + }} + defaultChecked + />
); - } - +}; export default Controls; diff --git a/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx b/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx new file mode 100644 index 00000000..4f4f22aa --- /dev/null +++ b/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx @@ -0,0 +1,44 @@ +import { TimeStartAndResetProps } from "./useTimeContext"; + +const stopwatchStartAndReset = ({ + time, + setPause, + setTime, + callback, + intervalRef, + callbackIntervalRef, + mounted, +}: TimeStartAndResetProps) => { + const startTimer = () => { + setPause(false); + intervalRef.current = setInterval(() => { + if (!mounted.current) return; + setTime((prevTime) => { + return prevTime + 1000; + }); + }, 1000); + callbackIntervalRef.current = setInterval(() => { + if (!mounted.current) return; + //keep this slower occuring action in sync with locally changing one + if (!intervalRef.current && callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + //update below function with time value + if (callback) + callback({ + eventType: "interval", + time: time, + }); + }, 5000); + if (callback) callback({ eventType: "start", time: time }); + }; + const resetTimer = () => { + setPause(true); + if (intervalRef.current) clearInterval(intervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + setTime(0); + if (callback) callback({ eventType: "reset", time: 0 }); + }; + return [startTimer, resetTimer]; +}; +export default stopwatchStartAndReset; \ No newline at end of file diff --git a/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx b/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx new file mode 100644 index 00000000..7b257d3c --- /dev/null +++ b/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx @@ -0,0 +1,60 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { TimeStartAndResetProps } from "./useTimeContext"; +const timerStartAndReset = ({ + time, + initialTimeLeft, + totalTimeGiven, + setPause, + setTime, + callback, + intervalRef, + callbackIntervalRef, + mounted, +}: TimeStartAndResetProps) => { + const startTimer = () => { + setPause(false); + //we change local state every second, as a balance between performance and accuracy + intervalRef.current = setInterval(() => { + if (!mounted.current) return; + setTime((prevTime) => { + const newTime = prevTime - 1000; + if (newTime > 0) return newTime; + if (newTime <= 0 && intervalRef.current) { + clearInterval(intervalRef.current); + } + return 0; + }); + }, 1000); + callbackIntervalRef.current = setInterval( + () => { + if (!mounted.current) return; + //keep this slower occuring action in sync with locally changing one + if (!intervalRef.current && callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + //update below function with time value + if (callback) + callback({ + eventType: "interval", + time: time, + }); + }, + //we update every 5 second as this can be costly computation (i.e writing to local state) + initialTimeLeft < 5000 ? initialTimeLeft : 5000 + ); + if (callback) callback({ eventType: "start", time: time }); + }; + const resetTimer = () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + const newTime = totalTimeGiven ? totalTimeGiven : 0; + unstable_batchedUpdates(() => { + setTime(newTime); + setPause(true); + if (callback) + callback({ eventType: "reset", time: newTime }); + }); + }; + return [startTimer, resetTimer]; +}; +export default timerStartAndReset; \ No newline at end of file diff --git a/studyAi/src/app/util/components/time/context/useTimeContext.tsx b/studyAi/src/app/util/components/time/context/useTimeContext.tsx new file mode 100644 index 00000000..d89d5aed --- /dev/null +++ b/studyAi/src/app/util/components/time/context/useTimeContext.tsx @@ -0,0 +1,150 @@ +"use client"; +import React, { useState, useRef, useEffect, useContext } from "react"; +import timerStartAndReset from "./timerStartAndReset"; +import stopwatchStartAndReset from "./stopwatchStartAndReset"; +import { unstable_batchedUpdates } from "react-dom"; +export type TimeEventProps = { + time: number; + eventType: "start" | "stop" | "reset" | "interval" | "finished"; +}; +export type TimeStartAndResetProps = { + time: number; + initialTimeLeft: number; + totalTimeGiven?: number | null; + setPause: React.Dispatch>; + setTime: React.Dispatch>; + callback?: (props?: TimeEventProps) => void; + intervalRef: React.MutableRefObject; + callbackIntervalRef: React.MutableRefObject; + mounted: React.MutableRefObject; +}; +export type TimeContextProps = { + time: number; + paused: boolean; + autoPlay: boolean | undefined; + setPause: React.Dispatch>; + startTimer: () => void; + resetTimer: () => void; + stopTimer: () => void; + setTime: React.Dispatch>; + callbackIntervalRef: React.MutableRefObject; + intervalRef: React.MutableRefObject; + mounted: React.MutableRefObject; +}; +// Create a new context +const TimeContext = React.createContext(null); +// Create a provider component +const TimeProvider = ({ + initialTime, + callback, + autoPlay, + children, + timeType, + totalTimeGiven, +}: { + timeType: "stopwatch" | "timer"; + initialTime: number; + callback?: (props?: TimeEventProps) => void; + autoPlay?: boolean; + children: React.ReactNode; + totalTimeGiven?: number | null; +}) => { + const [time, setTime] = useState(initialTime); + const [paused, setPause] = useState(true); + const callbackIntervalRef = useRef(null); + const intervalRef = useRef(null); + const mounted = useRef(true); + //every context is re-rendered clear the interval and restart it + useEffect(() => { + mounted.current = true; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + mounted.current = false; + }; + }, []); + //handle timer side effects, by reseting time + //if timer has ended. This is a non-issue on stopwatch + useEffect(() => { + if (timeType !== "timer") return; + if (!paused && time <= 0 && intervalRef.current) { + if (callback) + callback({ + eventType: "finished", + time: 0, + }); + unstable_batchedUpdates(() => { + setPause(true); + if (totalTimeGiven) setTime(totalTimeGiven); + }); + } + }, [paused, timeType, time, totalTimeGiven, callback]); + //every time timeType changes, clear the interval and restart it + useEffect(() => { + mounted.current = true; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); + mounted.current = false; + }; + }, [timeType]); + const stopTimer = () => { + setPause(true); + if (intervalRef.current) clearInterval(intervalRef.current); + if (callbackIntervalRef.current) { + if (callback) { + callback({ + eventType: "stop", + time: time, + }); + } + clearInterval(callbackIntervalRef.current); + } + }; + const [startTimer, resetTimer] = + timeType === "timer" + ? timerStartAndReset({ + time, + initialTimeLeft: initialTime, + setPause, + setTime, + callback: callback, + intervalRef, + callbackIntervalRef, + mounted, + totalTimeGiven, + }) + : stopwatchStartAndReset({ + time, + setPause, + setTime, + callback: callback, + intervalRef, + callbackIntervalRef, + mounted, + initialTimeLeft: initialTime, + }); + const value = { + time, + paused, + autoPlay, + setPause, + startTimer, + resetTimer, + stopTimer, + setTime, + callbackIntervalRef, + intervalRef, + mounted, + }; + return {children}; +}; + +// Custom hook to consume the context value +const useTimeHook = () => { + return useContext(TimeContext); +}; + +export { TimeProvider, useTimeHook }; diff --git a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx deleted file mode 100644 index c5d2aa02..00000000 --- a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; -import { useEffect, useRef, useState } from "react"; -const useTimeHook = ({ - initialTime, - callback, -}: { - initialTime: number; - callback?: (time: number) => void; -}) => { - const [time, setTime] = useState(initialTime); - const [paused, setPause] = useState(true); - const updateTimeActionIntervalRef = useRef(null); - const intervalRef = useRef(null); - const mounted = useRef(true); - useEffect(() => { - mounted.current = true; - //clean up any side effects so we dont cause a memory leak - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - mounted.current = false; - }; - }, []); - const stopTimer = () => { - setPause(true); - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) { - //update with curr time value - if (callback) callback(time); - clearInterval(updateTimeActionIntervalRef.current); - } - }; - return { - time, - paused, - setPause, - stopTimer, - setTime, - updateTimeActionIntervalRef, - intervalRef, - mounted, - }; -}; -export default useTimeHook; diff --git a/studyAi/src/app/util/components/time/stopwatch.tsx b/studyAi/src/app/util/components/time/stopwatch.tsx index bf4ece52..8c9fb9ee 100644 --- a/studyAi/src/app/util/components/time/stopwatch.tsx +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -1,61 +1,23 @@ "use client"; import formatMilliseconds from "../../parsers/formatMilliseconds"; -import useTimeHook from "./hooks/useTimeHook"; +import { useTimeHook } from "./context/useTimeContext"; import TimeControlsWrapper from "./timeControls"; - +type StopWatchProps = { + customBtns?: React.ReactNode; + showTimer?: boolean; +}; const StopWatch = ({ - initialTimeUsed, - updateTimeAction, -}: { - updateTimeAction?: () => void; - initialTimeUsed: number; -}) => { - const { - time, - stopTimer, - setTime, - updateTimeActionIntervalRef, - intervalRef, - mounted, - paused, - setPause, - } = useTimeHook({ - initialTime: initialTimeUsed, - callback: (time) => { - if (updateTimeAction) updateTimeAction(); - }, - }); - const startTimer = () => { - setPause(false); - intervalRef.current = setInterval(() => { - if (!mounted.current) return; - setTime((prevTime) => { - return prevTime + 1000; - }); - }, 1000); - updateTimeActionIntervalRef.current = setInterval(() => { - if (!mounted.current) return; - //keep this slower occuring action in sync with locally changing one - if (!intervalRef.current && updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - //update below function with time value - if (updateTimeAction) updateTimeAction(); - }, 5000); - }; - const resetTimer = () => { - setPause(true); - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - setTime(0); - }; + customBtns, + showTimer, +}: StopWatchProps) => { + const timeContext = useTimeHook(); + if (!timeContext) return <>; + const { time } = timeContext; const timeArr = formatMilliseconds(time, true); return (
void; - resetTimer: () => void; - stopTimer: () => void; - paused: boolean; - showTimer?: boolean; -}) => { + customBtns, +}: TimeControlsWrapper) => { + const timeContext = useTimeHook(); const [show, setShow] = useState(showTimer); + const playAuto = useRef(timeContext && timeContext.autoPlay); + useEffect(() => { + if (playAuto.current && timeContext) timeContext.startTimer(); + //es-lint-disable-next-line + }, []); + const showTimeVisibility = (callback?: () => void) => (e: MouseEvent) => { @@ -52,10 +55,10 @@ const TimeControlsWrapper = ({ size="large" className="flex justify-center items-center p-0 aspect-square h-[80%]" onClick={ - paused && show - ? showTimeVisibility(startTimer) - : !paused && show - ? showTimeVisibility(stopTimer) + timeContext && timeContext.paused && show + ? showTimeVisibility(timeContext.startTimer) + : timeContext && !timeContext.paused && show + ? showTimeVisibility(timeContext.stopTimer) : !show ? () => setShow(true) : () => setShow(false) @@ -64,18 +67,22 @@ const TimeControlsWrapper = ({ > {!show && ( )} - {paused && show && ( + {timeContext && timeContext.paused && show && ( )} - {!paused && show && ( + {timeContext && !timeContext.paused && show && ( + {customBtns}
); }; diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx index 2e19a753..d76a2a11 100644 --- a/studyAi/src/app/util/components/time/timer.tsx +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -1,75 +1,27 @@ //we store state in react-sweet state "use client"; import formatMilliseconds from "../../parsers/formatMilliseconds"; -import useTimeHook from "./hooks/useTimeHook"; +import { useTimeHook } from "./context/useTimeContext"; import TimeControlsWrapper from "./timeControls"; - +type TimerProps = { + showTimer?: boolean; + customBtns?: React.ReactNode; +}; const Timer = ({ - initialTimeLeft, - updateTimeAction, - totalTimeGiven, -}: { - updateTimeAction?: () => void; - initialTimeLeft: number; - totalTimeGiven?: number | null; -}) => { + showTimer, + customBtns, +}: TimerProps) => { + const timeContext = useTimeHook(); + if (!timeContext) return <> const { time, - stopTimer, - setTime, - updateTimeActionIntervalRef, - intervalRef, - mounted, - setPause, - paused, - } = useTimeHook({ - initialTime: initialTimeLeft, - callback: (time) => { - if (updateTimeAction) updateTimeAction(); - }, - }); - const startTimer = () => { - setPause(false); - //we change local state every second, as a balance between performance and accuracy - intervalRef.current = setInterval(() => { - if (!mounted.current) return; - setTime((prevTime) => { - const newTime = prevTime - 1000; - if (newTime > 0) return newTime; - if (newTime <= 0 && intervalRef.current) { - setPause(true); - clearInterval(intervalRef.current); - } - return 0; - }); - }, 1000); - updateTimeActionIntervalRef.current = setInterval( - () => { - if (!mounted.current) return; - //keep this slower occuring action in sync with locally changing one - if (!intervalRef.current && updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - //update below function with time value - if (updateTimeAction) updateTimeAction(); - }, - //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) - initialTimeLeft < 5000 ? initialTimeLeft : 5000 - ); - }; - const resetTimer = () => { - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - setTime(totalTimeGiven ? totalTimeGiven : 100000); - }; + } = timeContext; const timeArr = formatMilliseconds(time, true); return (
{ const [anchorEl, setAnchorEl] = useState< - (EventTarget & HTMLButtonElement) | null + (HTMLAnchorElement | HTMLButtonElement) | null >(null); - - const handleClick: React.MouseEventHandler = (event) => { + const [open, setIsOpen] = useState(false); + const handleClick = ( + event?: MouseEvent< + HTMLButtonElement | HTMLAnchorElement, + globalThis.MouseEvent + > + ) => { + if (!event) return; const target = event.currentTarget; - setAnchorEl(target); + unstable_batchedUpdates(() => { + if (anchorEl) setAnchorEl(target); + setIsOpen(true); + }); }; const handleClose = () => { - setAnchorEl(null); + setIsOpen(false); }; return { anchorEl, + open, handleClick, handleClose, setAnchorEl, }; }; -export default useDropdown \ No newline at end of file +export default useDropdown; diff --git a/studyAi/src/app/util/hooks/useOrigin.tsx b/studyAi/src/app/util/hooks/useOrigin.tsx new file mode 100644 index 00000000..35b666d2 --- /dev/null +++ b/studyAi/src/app/util/hooks/useOrigin.tsx @@ -0,0 +1,21 @@ +"use client"; // this is Next 13 App Router stuff + +import { useEffect, useState } from "react"; + +export default function useOrigin() { + const [mounted, setMounted] = useState(false); + const origin = + typeof window !== "undefined" && window.location.origin + ? window.location.origin + : ""; + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return origin; +} diff --git a/studyAi/src/app/util/parsers/capitalizeEveryWord.tsx b/studyAi/src/app/util/parsers/capitalizeEveryWord.ts similarity index 100% rename from studyAi/src/app/util/parsers/capitalizeEveryWord.tsx rename to studyAi/src/app/util/parsers/capitalizeEveryWord.ts diff --git a/studyAi/src/app/util/parsers/determineOriginUrl.ts b/studyAi/src/app/util/parsers/determineOriginUrl.ts new file mode 100644 index 00000000..b109d72b --- /dev/null +++ b/studyAi/src/app/util/parsers/determineOriginUrl.ts @@ -0,0 +1,8 @@ +const determineOriginUrl = () => { + const env = process.env.NODE_ENV; + const prod = process.env.NEXT_PUBLIC_GRAPHQL_DOMAIN_PROD; + const dev = process.env.NEXT_PUBLIC_GRAPHQL_DOMAIN_DEV; + if (env === "development") return dev; + return prod; +}; +export default determineOriginUrl; diff --git a/studyAi/src/app/util/parsers/formatMilliseconds.tsx b/studyAi/src/app/util/parsers/formatMilliseconds.ts similarity index 86% rename from studyAi/src/app/util/parsers/formatMilliseconds.tsx rename to studyAi/src/app/util/parsers/formatMilliseconds.ts index 5f32b4c5..9ab6f612 100644 --- a/studyAi/src/app/util/parsers/formatMilliseconds.tsx +++ b/studyAi/src/app/util/parsers/formatMilliseconds.ts @@ -1,9 +1,9 @@ function formatMilliseconds(milliseconds: number, inArr?: boolean) { const totalSeconds = Math.floor(milliseconds / 1000); let seconds = totalSeconds; - const hours = Math.floor(totalSeconds / 3600); + const hours = Math.floor(seconds / 3600); seconds = seconds % 3600; - const minutes = Math.floor(totalSeconds / 60); + const minutes = Math.floor(seconds / 60); seconds = seconds % 60; const formattedHours = String(hours).padStart(2, "0"); const formattedMinutes = String(minutes).padStart(2, "0"); @@ -20,7 +20,7 @@ export function extractTime(timeString: string, splitByColon: boolean = true) { const time = { hours: hours.padStart(2, "0"), minutes: minutes.padStart(2, "0"), - seconds: seconds.padStart(2, "0") + seconds: seconds.padStart(2, "0"), }; return time; } diff --git a/studyAi/src/app/util/parsers/localStorageWrappers.ts b/studyAi/src/app/util/parsers/localStorageWrappers.ts new file mode 100644 index 00000000..679a9399 --- /dev/null +++ b/studyAi/src/app/util/parsers/localStorageWrappers.ts @@ -0,0 +1,16 @@ +'use client' +// Get the LocalStorageObj from local storage +export function getLocalStorageObj(key: string) { + const storedValue = localStorage.getItem(key); + const value = storedValue ? JSON.parse(storedValue) : null; + return value as T | null; +} +// Delete the LocalStorageObj from local storage +export function deleteLocalStorageObj(key: string) { + localStorage.removeItem(key); +} +// Add or update the LocalStorageObj in local storage +export function addLocalStorageObj(key: string, value: T) { + const stringValue = JSON.stringify(value); + localStorage.setItem(key, stringValue); +} diff --git a/studyAi/src/app/util/parsers/removeNonIntegerChars.tsx b/studyAi/src/app/util/parsers/removeNonIntegerChars.ts similarity index 100% rename from studyAi/src/app/util/parsers/removeNonIntegerChars.tsx rename to studyAi/src/app/util/parsers/removeNonIntegerChars.ts diff --git a/studyAi/src/app/util/prisma/seedData.ts b/studyAi/src/app/util/prisma/seedData.ts index f61a4faf..5be7b7c4 100644 --- a/studyAi/src/app/util/prisma/seedData.ts +++ b/studyAi/src/app/util/prisma/seedData.ts @@ -1,6 +1,13 @@ import { PrismaClient } from "@prisma/client"; import { Question } from "../../../../prisma/generated/type-graphql"; +import { ObjectId } from "bson"; export const prismaDb = new PrismaClient(); +const createOptions = (e: T[]) => { + return e.map((val) => ({ + id: new ObjectId().toString(), + value: val, + })); +}; const questions: Omit[] = [ { questionType: "Short Answer", @@ -11,48 +18,53 @@ const questions: Omit[] = [ options: [], }, answer: { - correctAnswer: ["H20"], + correctAnswer: createOptions(["H20"]), }, likeCounter: { likes: 1, dislikes: 0, }, - private: false + private: false, }, { questionType: "Multiple Choice", - tags: ['Maths', 'Technology'], + tags: ["Maths", "Technology"], questionInfo: { - title: 'Recursion', - description: 'When does recursion end?', - options: ['When the loop ends.', 'When the loop starts.', 'At the second iteration.'] + title: "Recursion", + description: "When does recursion end?", + options: createOptions([ + "When the loop ends.", + "When the loop starts.", + "At the second iteration.", + ]), }, answer: { - correctAnswer: ['When we reach base case.'], + correctAnswer: createOptions(["When we reach base case."]), }, likeCounter: { likes: 1, - dislikes: 0 + dislikes: 0, }, - private: false + private: false, }, { - questionType: "Checkbox", - tags: ['Science', 'Chemistry'], + questionType: "Select Multiple", + tags: ["Science", "Chemistry"], questionInfo: { - title: 'Molecular Compound', - description: 'Which of the following elements are found in the molecular formula H2O (water)?', - options: ['Carbon', 'Nitrogen'] + title: "Molecular Compound", + description: + "Which of the following elements are found in the molecular formula H2O (water)?", + options: createOptions(["Carbon", "Nitrogen", "Hydrogen", "Oxygen"]), }, answer: { - correctAnswer: ['Hydrogen', 'Oxygen'], + correctAnswer: createOptions(["Hydrogen", "Oxygen"]), }, likeCounter: { likes: 1, - dislikes: 0 + dislikes: 0, }, - private: true - } + private: true, + }, ]; export const allQuestions = async () => { diff --git a/studyAi/src/app/util/providers/FullscreenProvider.tsx b/studyAi/src/app/util/providers/FullscreenProvider.tsx new file mode 100644 index 00000000..05ae4a89 --- /dev/null +++ b/studyAi/src/app/util/providers/FullscreenProvider.tsx @@ -0,0 +1,67 @@ +"use client"; +import React, { createContext, useEffect, useState } from "react"; + +interface FullscreenContextType { + isFullscreen: boolean; + toggleFullscreen: () => void; +} + +export const FullscreenContext = createContext({ + isFullscreen: false, + toggleFullscreen: () => {}, +}); +const FullscreenProvider = ({ children }: { children: React.ReactNode }) => { + const [isFullscreen, setIsFullscreen] = useState(false); + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(document.fullscreenElement !== null); + }; + + document.addEventListener("fullscreenchange", handleFullscreenChange); + + return () => { + document.removeEventListener("fullscreenchange", handleFullscreenChange); + }; + }, []); + + const toggleFullscreen = () => { + let currDocument: any = document; + if (!isFullscreen) { + // Go fullscreen + if (currDocument.documentElement.requestFullscreen) { + currDocument.documentElement.requestFullscreen(); + } else if (currDocument.documentElement.mozRequestFullScreen) { + currDocument.documentElement.mozRequestFullScreen(); + } else if (currDocument.documentElement.webkitRequestFullscreen) { + currDocument.documentElement.webkitRequestFullscreen(); + } else if (currDocument.documentElement.msRequestFullscreen) { + currDocument.documentElement.msRequestFullscreen(); + } + } else { + // Exit fullscreen + if (currDocument.exitFullscreen) { + currDocument.exitFullscreen(); + } else if (currDocument.mozCancelFullScreen) { + currDocument.mozCancelFullScreen(); + } else if (currDocument.webkitExitFullscreen) { + currDocument.webkitExitFullscreen(); + } else if (currDocument.msExitFullscreen) { + currDocument.msExitFullscreen(); + } + } + }; + + return ( + + {children} + + ); +}; +export const useFullscreen = () => React.useContext(FullscreenContext); +export default FullscreenProvider; diff --git a/studyAi/src/app/util/types/UserData.ts b/studyAi/src/app/util/types/UserData.ts index c2bcc735..c2f7f93e 100644 --- a/studyAi/src/app/util/types/UserData.ts +++ b/studyAi/src/app/util/types/UserData.ts @@ -1,7 +1,7 @@ import { User } from "@prisma/client"; export const QuestionTypes = [ - "multipleChoice", - "selectMultiple", + "Multiple Choice", + "Select Multiple", "Short Answer", ] as const; -export type UserInfo = User \ No newline at end of file +export type UserInfo = User; diff --git a/studyAi/theme.ts b/studyAi/theme.ts index efbb6650..e6efe7b8 100644 --- a/studyAi/theme.ts +++ b/studyAi/theme.ts @@ -7,7 +7,7 @@ const theme = { colors: { Black: "#000000", White: "#ffffff", - "Light Grey": "#f4f4f4", + LightGrey: "#f4f4f4", M3: { white: "#ffffff", black: "#000000" }, light: { primary: "#b30086",