From 6ab33e41e7c375266380071891fb686fce97ceca Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Tue, 7 Nov 2023 18:06:18 -0500 Subject: [PATCH 01/23] add new timer component input logic --- .../components/client/timeComponent.tsx | 212 +++++++++++++----- studyAi/theme.ts | 2 +- 2 files changed, 153 insertions(+), 61 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeComponent.tsx index bc4c8975..e0805630 100644 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ b/studyAi/src/app/library/question/components/client/timeComponent.tsx @@ -5,6 +5,7 @@ import { FormEvent, SetStateAction, SyntheticEvent, + useEffect, useRef, useState, } from "react"; @@ -24,10 +25,9 @@ import { 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"; +import { unstable_batchedUpdates } from "react-dom"; +import { extractTime } from "@/app/util/parsers/formatMilliseconds"; //we can manage time on the frontend //because time measurements are only //for the user's benefit @@ -36,8 +36,37 @@ import removeNonIntegerChars from "@/app/util/parsers/removeNonIntegerChars"; type TimeProps = TimeOptions & { initialTime: number; }; -const defaultTime = formatMilliseconds(0) as string; -const timeOrder = ["h", "m", "s"]; +const timeOrder: { + 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; +}; const splitTimeStrBy2 = (str: string) => { const arr = []; for (let i = 0; i < str.length; i += 2) { @@ -46,74 +75,137 @@ const splitTimeStrBy2 = (str: string) => { } return arr; }; -function TimerInput() { +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 [totalTime, setTotalTime] = useState( - defaultTime - .split(":") - .map((a, idx) => a + timeOrder[idx]) - .reduce((a, b) => a + " " + b) + 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); + }} + value={value} + /> + +
); +}; +function TimerInput() { + // const ref = useRef(); + 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 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; + 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 = timeOrder.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 - currIntegers = currIntegers.substring(diff, currIntegers.length); - if (currIntegers.length !== prevIntegers.length) return prevVal; + 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 = newValArr.reduce((a, b, idx) => a + timeOrder[idx] + b); - console.log(newVal) + const newVal = determineNewVal(newValArr, name, prevVal); + //this creates the new total time string + const newTotalTime = + newValArr.reduce( + (a, b, idx) => a + timeOrder[idx - 1].abbrev + " " + b + ) + timeOrder[timeOrder.length - 1].abbrev; + + 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; }); - - //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 ( -
+
+ {timeOrder.map((a) => ( + + ))}
); 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", From 79a0460a1c9b404a33a6ee74b67e83ab8083d703 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Tue, 7 Nov 2023 19:23:07 -0500 Subject: [PATCH 02/23] fixed millisecond parser, and added auto play feature to time controls --- .../components/client/timeComponent.tsx | 35 +++++++++++++------ .../components/time/hooks/useTimeHook.tsx | 10 ++++++ .../app/util/components/time/stopwatch.tsx | 4 +++ .../app/util/components/time/timeControls.tsx | 9 ++++- .../src/app/util/components/time/timer.tsx | 9 ++++- .../app/util/parsers/formatMilliseconds.tsx | 6 ++-- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeComponent.tsx index e0805630..548e5cd3 100644 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ b/studyAi/src/app/library/question/components/client/timeComponent.tsx @@ -117,7 +117,9 @@ const FieldInput = ({ }} value={value} /> - +
); }; @@ -228,14 +230,22 @@ const TimeForm = ({ //grab uncontrolled inputs here form const formData = new FormData(e.currentTarget); const data = Object.fromEntries(formData.entries()); + if (timeType === "stopwatch") return setCurrType(timeType); const { totalTime } = data; - const { hours, minutes, seconds } = extractTime(totalTime.toString()); + const { hours, minutes, seconds } = extractTime( + totalTime.toString(), + false + ); const timeTotalSeconds = parseInt(hours.toString(), 10) * 3600 + parseInt(minutes.toString(), 10) * 60 + parseInt(seconds.toString(), 10); - console.log(data); + unstable_batchedUpdates(() => { + setCurrType(timeType); + setCurrTotalTimeGiven(timeTotalSeconds * 1000); + }); }; + const borderStyle: SxProps = { borderWidth: 1, borderStyle: "solid", @@ -307,16 +317,21 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { const [currInitTime, setCurrInitTime] = useState(initialTime); const [currTotalTimeGiven, setCurrTotalTimeGiven] = useState(totalTimeGiven); const [modalOpen, setModalOpen] = useState(true); + console.log(currTotalTimeGiven, currType, "total time given"); switch (currType) { case "stopwatch": - return ; + return ; case "timer": - return ( - - ); + if (typeof currTotalTimeGiven === "number") + return ( + + ); + else return setCurrType("stopwatch"); //create timer component default: return ( diff --git a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx index c5d2aa02..7aff006b 100644 --- a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx +++ b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx @@ -3,10 +3,13 @@ import { useEffect, useRef, useState } from "react"; const useTimeHook = ({ initialTime, callback, + autoPlay, }: { initialTime: number; callback?: (time: number) => void; + autoPlay?: boolean; }) => { + const playAuto = useRef(autoPlay); const [time, setTime] = useState(initialTime); const [paused, setPause] = useState(true); const updateTimeActionIntervalRef = useRef(null); @@ -22,6 +25,13 @@ const useTimeHook = ({ mounted.current = false; }; }, []); + useEffect(() => { + if (playAuto.current) + setPause((state) => { + if (state) return false; + else return state; + }); + }, []); const stopTimer = () => { setPause(true); if (intervalRef.current) clearInterval(intervalRef.current); diff --git a/studyAi/src/app/util/components/time/stopwatch.tsx b/studyAi/src/app/util/components/time/stopwatch.tsx index bf4ece52..d5e58659 100644 --- a/studyAi/src/app/util/components/time/stopwatch.tsx +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -6,9 +6,11 @@ import TimeControlsWrapper from "./timeControls"; const StopWatch = ({ initialTimeUsed, updateTimeAction, + autoPlay, }: { updateTimeAction?: () => void; initialTimeUsed: number; + autoPlay?: boolean; }) => { const { time, @@ -24,6 +26,7 @@ const StopWatch = ({ callback: (time) => { if (updateTimeAction) updateTimeAction(); }, + autoPlay, }); const startTimer = () => { setPause(false); @@ -56,6 +59,7 @@ const StopWatch = ({ startTimer={startTimer} resetTimer={resetTimer} paused={paused} + autoPlay={autoPlay} >
void; @@ -22,8 +23,14 @@ const TimeControlsWrapper = ({ stopTimer: () => void; paused: boolean; showTimer?: boolean; + autoPlay?: boolean; }) => { const [show, setShow] = useState(showTimer); + const playAuto = useRef(autoPlay); + useEffect(() => { + if (playAuto.current) startTimer(); + //es-lint-disable-next-line + }, []); const showTimeVisibility = (callback?: () => void) => (e: MouseEvent) => { diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx index 2e19a753..67ef4b68 100644 --- a/studyAi/src/app/util/components/time/timer.tsx +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -8,10 +8,14 @@ const Timer = ({ initialTimeLeft, updateTimeAction, totalTimeGiven, + showTimer, + autoPlay, }: { updateTimeAction?: () => void; initialTimeLeft: number; totalTimeGiven?: number | null; + showTimer?: boolean; + autoPlay?: boolean; }) => { const { time, @@ -27,6 +31,7 @@ const Timer = ({ callback: (time) => { if (updateTimeAction) updateTimeAction(); }, + autoPlay, }); const startTimer = () => { setPause(false); @@ -60,7 +65,7 @@ const Timer = ({ if (intervalRef.current) clearInterval(intervalRef.current); if (updateTimeActionIntervalRef.current) clearInterval(updateTimeActionIntervalRef.current); - setTime(totalTimeGiven ? totalTimeGiven : 100000); + setTime(totalTimeGiven ? totalTimeGiven : 0); }; const timeArr = formatMilliseconds(time, true); return ( @@ -70,6 +75,8 @@ const Timer = ({ startTimer={startTimer} resetTimer={resetTimer} paused={paused} + showTimer={showTimer} + autoPlay={autoPlay} >
Date: Tue, 7 Nov 2023 20:03:13 -0500 Subject: [PATCH 03/23] finalized timer/stopwatch logic to auto play timer and routed it with timer modal pop up --- .../app/library/question/components/client/timeComponent.tsx | 1 - .../library/question/components/server/answerComponent 2.tsx | 4 ++-- .../library/question/components/server/answerComponent.tsx | 2 +- .../app/library/question/components/server/answerInputs.tsx | 2 +- studyAi/src/app/util/components/time/timeControls.tsx | 5 +++-- studyAi/src/app/util/components/time/timer.tsx | 1 + studyAi/src/app/util/types/UserData.ts | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeComponent.tsx index 548e5cd3..6fbc1865 100644 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ b/studyAi/src/app/library/question/components/client/timeComponent.tsx @@ -317,7 +317,6 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { const [currInitTime, setCurrInitTime] = useState(initialTime); const [currTotalTimeGiven, setCurrTotalTimeGiven] = useState(totalTimeGiven); const [modalOpen, setModalOpen] = useState(true); - console.log(currTotalTimeGiven, currType, "total time given"); switch (currType) { case "stopwatch": return ; diff --git a/studyAi/src/app/library/question/components/server/answerComponent 2.tsx b/studyAi/src/app/library/question/components/server/answerComponent 2.tsx index ea0093b9..6d2868da 100644 --- a/studyAi/src/app/library/question/components/server/answerComponent 2.tsx +++ b/studyAi/src/app/library/question/components/server/answerComponent 2.tsx @@ -14,9 +14,9 @@ const determineAnswerTitle = (str?: string) => { switch (matchStr) { case "multipleChoice": return "Select the best answer"; - case "selectMultiple": + case "Checkbox": return "Select all that apply"; - case "shortAnswer": + case "Short Answer": return "Type your answer below"; default: return str; diff --git a/studyAi/src/app/library/question/components/server/answerComponent.tsx b/studyAi/src/app/library/question/components/server/answerComponent.tsx index ebdf254a..7d4cb66b 100644 --- a/studyAi/src/app/library/question/components/server/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/server/answerComponent.tsx @@ -15,7 +15,7 @@ const determineAnswerTitle = (str?: string) => { switch (matchStr) { case "multipleChoice": return "Select the best answer"; - case "selectMultiple": + case "Checkbox": return "Select all that apply"; case "Short Answer": return "Add your answer below"; diff --git a/studyAi/src/app/library/question/components/server/answerInputs.tsx b/studyAi/src/app/library/question/components/server/answerInputs.tsx index 57bbc8e6..0f882097 100644 --- a/studyAi/src/app/library/question/components/server/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/server/answerInputs.tsx @@ -95,7 +95,7 @@ export const AnswerType = () => { switch (questionType) { case "multipleChoice": return ; - case "selectMultiple": + case "Checkbox": return ; case "Short Answer": return ; diff --git a/studyAi/src/app/util/components/time/timeControls.tsx b/studyAi/src/app/util/components/time/timeControls.tsx index b2912026..0fb8b746 100644 --- a/studyAi/src/app/util/components/time/timeControls.tsx +++ b/studyAi/src/app/util/components/time/timeControls.tsx @@ -71,7 +71,9 @@ const TimeControlsWrapper = ({ > {!show && ( @@ -92,7 +94,6 @@ const TimeControlsWrapper = ({ Date: Tue, 7 Nov 2023 20:05:12 -0500 Subject: [PATCH 04/23] commented out add quiz button, until a future date --- .../app/library/question/components/client/questionsView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/client/questionsView.tsx index ae4c314f..f36f185d 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/client/questionsView.tsx @@ -13,9 +13,10 @@ import { Carousel } from "@/app/util/components/carousel/carousel"; const QuestionActionBtns = () => { return (
- + {/* this is for when a user can add the question to quiz */} + {/* - + */} From dec32676f0e6afb03389a4a4e98099ed3fdcdce1 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Wed, 8 Nov 2023 00:56:02 -0500 Subject: [PATCH 05/23] styled and made timer form responsive --- .../src/app/library/question/[id]/page.tsx | 10 +- .../components/client/timeComponent.tsx | 154 +++++++++--------- 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/studyAi/src/app/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index 64b3e942..eb0ba1f3 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -68,12 +68,12 @@ 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 ( +
{ const target = e.target as HTMLInputElement; const selectionEnd = target.selectionEnd; setCursor(selectionEnd); }} - value={value} + 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 ref = useRef(); const [hours, setHours] = useState("00"); @@ -186,7 +200,7 @@ function TimerInput() { }); }; return ( -
+
{timeOrder.map((a) => ( ))} ); } - +const btnStyles = { + textTransform: "none", + padding: 0, + margin: 0, + minHeight: "unset", +}; const TimeForm = ({ setCurrType, setCurrTotalTimeGiven, @@ -220,10 +240,12 @@ const TimeForm = ({ setCurrType: Dispatch>; setCurrTotalTimeGiven: Dispatch>; }) => { - const [timeType, setTimeType] = useState("timer"); - const onTimeTypeChange = (e: SyntheticEvent) => { - const target = e.target as HTMLInputElement; - setTimeType(target.value); + const [timeType, setTimeType] = useState("stopwatch"); + const onTimeTypeChange = ( + e: SyntheticEvent, + newValue: string + ) => { + setTimeType(newValue); }; const onSubmit = (e: FormEvent) => { e.preventDefault(); @@ -245,64 +267,50 @@ const TimeForm = ({ setCurrTotalTimeGiven(timeTotalSeconds * 1000); }); }; - - 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" && } + {timeType === "stopwatch" && }
From e17b83f0d5e37af6dba7bf884076171e3f756a1d Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Wed, 8 Nov 2023 13:46:51 -0500 Subject: [PATCH 06/23] added time change handler, so that intial time can be saved every time an event occurred --- .../components/client/timeComponent.tsx | 1 + .../components/time/hooks/useTimeHook.tsx | 12 +++++- .../app/util/components/time/stopwatch.tsx | 27 ++++++++----- .../app/util/components/time/timeControls.tsx | 19 +++++----- .../src/app/util/components/time/timer.tsx | 38 ++++++++++++------- 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeComponent.tsx index 77b72cb3..196d7427 100644 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ b/studyAi/src/app/library/question/components/client/timeComponent.tsx @@ -336,6 +336,7 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { totalTimeGiven={currTotalTimeGiven} showTimer autoPlay + updateTimeAction={(e) => {} } /> ); else return setCurrType("stopwatch"); diff --git a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx index 7aff006b..02c26af2 100644 --- a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx +++ b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx @@ -1,12 +1,17 @@ "use client"; import { useEffect, useRef, useState } from "react"; + +export type TimeEventProps = { + time: number; + eventType: "start" | "stop" | "reset" | "interval"; +}; const useTimeHook = ({ initialTime, callback, autoPlay, }: { initialTime: number; - callback?: (time: number) => void; + callback?: (props?: TimeEventProps) => void; autoPlay?: boolean; }) => { const playAuto = useRef(autoPlay); @@ -37,7 +42,10 @@ const useTimeHook = ({ if (intervalRef.current) clearInterval(intervalRef.current); if (updateTimeActionIntervalRef.current) { //update with curr time value - if (callback) callback(time); + if (callback) callback({ + eventType: "stop", + time: time, + }); clearInterval(updateTimeActionIntervalRef.current); } }; diff --git a/studyAi/src/app/util/components/time/stopwatch.tsx b/studyAi/src/app/util/components/time/stopwatch.tsx index d5e58659..49d60aff 100644 --- a/studyAi/src/app/util/components/time/stopwatch.tsx +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -1,17 +1,17 @@ "use client"; import formatMilliseconds from "../../parsers/formatMilliseconds"; -import useTimeHook from "./hooks/useTimeHook"; +import useTimeHook, { TimeEventProps } from "./hooks/useTimeHook"; import TimeControlsWrapper from "./timeControls"; - +type StopWatchProps = { + updateTimeAction?: (props?: TimeEventProps) => void; + initialTimeUsed: number; + autoPlay?: boolean; +}; const StopWatch = ({ initialTimeUsed, updateTimeAction, autoPlay, -}: { - updateTimeAction?: () => void; - initialTimeUsed: number; - autoPlay?: boolean; -}) => { +}: StopWatchProps) => { const { time, stopTimer, @@ -23,8 +23,8 @@ const StopWatch = ({ setPause, } = useTimeHook({ initialTime: initialTimeUsed, - callback: (time) => { - if (updateTimeAction) updateTimeAction(); + callback: (event) => { + if (updateTimeAction) updateTimeAction(event); }, autoPlay, }); @@ -42,8 +42,13 @@ const StopWatch = ({ if (!intervalRef.current && updateTimeActionIntervalRef.current) clearInterval(updateTimeActionIntervalRef.current); //update below function with time value - if (updateTimeAction) updateTimeAction(); + if (updateTimeAction) + updateTimeAction({ + eventType: "interval", + time: time, + }); }, 5000); + if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); }; const resetTimer = () => { setPause(true); @@ -51,6 +56,8 @@ const StopWatch = ({ if (updateTimeActionIntervalRef.current) clearInterval(updateTimeActionIntervalRef.current); setTime(0); + if (updateTimeAction) + updateTimeAction({ eventType: "reset", time: 0 }); }; const timeArr = formatMilliseconds(time, true); return ( diff --git a/studyAi/src/app/util/components/time/timeControls.tsx b/studyAi/src/app/util/components/time/timeControls.tsx index 0fb8b746..3e89ca28 100644 --- a/studyAi/src/app/util/components/time/timeControls.tsx +++ b/studyAi/src/app/util/components/time/timeControls.tsx @@ -8,6 +8,15 @@ import { } from "@fortawesome/free-regular-svg-icons"; import TimerIcon from "../../icons/timerIcon"; import { MouseEvent, useEffect, useRef, useState } from "react"; +type TimeControlsWrapper = { + children: React.ReactNode; + startTimer: () => void; + resetTimer: () => void; + stopTimer: () => void; + paused: boolean; + showTimer?: boolean; + autoPlay?: boolean; +} const TimeControlsWrapper = ({ children, paused, @@ -16,15 +25,7 @@ const TimeControlsWrapper = ({ stopTimer, showTimer, autoPlay, -}: { - children: React.ReactNode; - startTimer: () => void; - resetTimer: () => void; - stopTimer: () => void; - paused: boolean; - showTimer?: boolean; - autoPlay?: boolean; -}) => { +}:TimeControlsWrapper) => { const [show, setShow] = useState(showTimer); const playAuto = useRef(autoPlay); useEffect(() => { diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx index cae60575..63c44f8f 100644 --- a/studyAi/src/app/util/components/time/timer.tsx +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -3,20 +3,22 @@ import formatMilliseconds from "../../parsers/formatMilliseconds"; import useTimeHook from "./hooks/useTimeHook"; import TimeControlsWrapper from "./timeControls"; - +import { TimeEventProps } from "./hooks/useTimeHook"; +import { unstable_batchedUpdates } from "react-dom"; +type TimerProps = { + updateTimeAction?: (props?: TimeEventProps) => void; + initialTimeLeft: number; + totalTimeGiven?: number | null; + showTimer?: boolean; + autoPlay?: boolean; +}; const Timer = ({ initialTimeLeft, updateTimeAction, totalTimeGiven, showTimer, autoPlay, -}: { - updateTimeAction?: () => void; - initialTimeLeft: number; - totalTimeGiven?: number | null; - showTimer?: boolean; - autoPlay?: boolean; -}) => { +}: TimerProps) => { const { time, stopTimer, @@ -28,8 +30,8 @@ const Timer = ({ paused, } = useTimeHook({ initialTime: initialTimeLeft, - callback: (time) => { - if (updateTimeAction) updateTimeAction(); + callback: (props?: TimeEventProps) => { + if (updateTimeAction) updateTimeAction(props); }, autoPlay, }); @@ -55,18 +57,28 @@ const Timer = ({ if (!intervalRef.current && updateTimeActionIntervalRef.current) clearInterval(updateTimeActionIntervalRef.current); //update below function with time value - if (updateTimeAction) updateTimeAction(); + if (updateTimeAction) + updateTimeAction({ + eventType: "interval", + time: time, + }); }, //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) initialTimeLeft < 5000 ? initialTimeLeft : 5000 ); + if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); }; const resetTimer = () => { if (intervalRef.current) clearInterval(intervalRef.current); if (updateTimeActionIntervalRef.current) clearInterval(updateTimeActionIntervalRef.current); - setTime(totalTimeGiven ? totalTimeGiven : 0); - setPause(true) + const newTime = totalTimeGiven ? totalTimeGiven : 0; + unstable_batchedUpdates(() => { + setTime(newTime); + setPause(true); + if (updateTimeAction) + updateTimeAction({ eventType: "reset", time: newTime }); + }); }; const timeArr = formatMilliseconds(time, true); return ( From 49d13237e36bc9fed3f4fe4d2d14e4b31b78f772 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 10:13:54 -0500 Subject: [PATCH 07/23] added ability to trash timer and get local storage data for question timer based off id --- .../src/app/library/question/[id]/page.tsx | 2 +- .../{server => client}/answerComponent.tsx | 2 +- .../{server => client}/answerInputs.tsx | 0 .../client/questionComponents 2.tsx | 64 ------ .../client/questionPageContainer.tsx | 2 +- .../components/client/questionWrapper 2.tsx | 18 -- .../components/client/questionWrapper.tsx | 2 +- .../components/client/questionsView 2.tsx | 84 ------- .../components/client/questionsView.tsx | 1 - .../components/client/solutionView.tsx | 7 +- .../{server => client}/submissionView.tsx | 6 +- .../{timeComponent.tsx => timeForm.tsx} | 88 +------- .../question/components/client/timeModal.tsx | 207 ++++++++++++++++++ .../components/server/answerComponent 2.tsx | 51 ----- .../server/questionComponents 2.tsx | 16 -- .../components/server/questionComponents.tsx | 2 +- .../components/time/hooks/useTimeHook.tsx | 14 +- .../app/util/components/time/stopwatch.tsx | 6 + .../app/util/components/time/timeControls.tsx | 3 + .../src/app/util/components/time/timer.tsx | 8 + ...zeEveryWord.tsx => capitalizeEveryWord.ts} | 0 ...Milliseconds.tsx => formatMilliseconds.ts} | 0 .../app/util/parsers/localStorageWrappers.ts | 16 ++ ...egerChars.tsx => removeNonIntegerChars.ts} | 0 24 files changed, 273 insertions(+), 326 deletions(-) rename studyAi/src/app/library/question/components/{server => client}/answerComponent.tsx (97%) rename studyAi/src/app/library/question/components/{server => client}/answerInputs.tsx (100%) delete mode 100644 studyAi/src/app/library/question/components/client/questionComponents 2.tsx delete mode 100644 studyAi/src/app/library/question/components/client/questionWrapper 2.tsx delete mode 100644 studyAi/src/app/library/question/components/client/questionsView 2.tsx rename studyAi/src/app/library/question/components/{server => client}/submissionView.tsx (90%) rename studyAi/src/app/library/question/components/client/{timeComponent.tsx => timeForm.tsx} (76%) create mode 100644 studyAi/src/app/library/question/components/client/timeModal.tsx delete mode 100644 studyAi/src/app/library/question/components/server/answerComponent 2.tsx delete mode 100644 studyAi/src/app/library/question/components/server/questionComponents 2.tsx rename studyAi/src/app/util/parsers/{capitalizeEveryWord.tsx => capitalizeEveryWord.ts} (100%) rename studyAi/src/app/util/parsers/{formatMilliseconds.tsx => formatMilliseconds.ts} (100%) create mode 100644 studyAi/src/app/util/parsers/localStorageWrappers.ts rename studyAi/src/app/util/parsers/{removeNonIntegerChars.tsx => removeNonIntegerChars.ts} (100%) diff --git a/studyAi/src/app/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index eb0ba1f3..c15aef68 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -11,7 +11,7 @@ const question: Partial & { id: string; questionType: (typeof QuestionTypes)[number]; } = { - id: "65429fd993f2d4403eac75ec", + id: "6549b35d98536604d74f3b22", creatorId: "6533f4c7489ef223ffc31a99", questionType: "Short Answer", tags: [ diff --git a/studyAi/src/app/library/question/components/server/answerComponent.tsx b/studyAi/src/app/library/question/components/client/answerComponent.tsx similarity index 97% rename from studyAi/src/app/library/question/components/server/answerComponent.tsx rename to studyAi/src/app/library/question/components/client/answerComponent.tsx index 7d4cb66b..c8d83b96 100644 --- a/studyAi/src/app/library/question/components/server/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/client/answerComponent.tsx @@ -1,7 +1,7 @@ "use client"; import { useParams } from "next/navigation"; import { useQuestions } from "@/app/stores/questionStore"; -import ContainerBar, { Container } from "./containerBar"; +import ContainerBar, { Container } from "../server/containerBar"; import { Button, IconButton } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { diff --git a/studyAi/src/app/library/question/components/server/answerInputs.tsx b/studyAi/src/app/library/question/components/client/answerInputs.tsx similarity index 100% rename from studyAi/src/app/library/question/components/server/answerInputs.tsx rename to studyAi/src/app/library/question/components/client/answerInputs.tsx 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 index a719ce90..e5f47721 100644 --- a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx +++ b/studyAi/src/app/library/question/components/client/questionPageContainer.tsx @@ -1,7 +1,7 @@ "use client"; import { NavigationBtns, PaginationOptions } from "./navigationBtns"; import { QuestionWrapper } from "./questionWrapper"; -import { TimeComponent } from "./timeComponent"; +import { TimeComponent } from "./timeModal"; const QuestionFormWrapper = ({ children }: { children: React.ReactNode }) => { const onSubmit = (e: React.FormEvent) => { e.preventDefault(); 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 index 09ec0e7a..8a00d6d7 100644 --- a/studyAi/src/app/library/question/components/client/questionWrapper.tsx +++ b/studyAi/src/app/library/question/components/client/questionWrapper.tsx @@ -1,6 +1,6 @@ "use client"; import QuestionComponent from "./questionComponents"; -import AnswerComponent from "../server/answerComponent"; +import AnswerComponent from "./answerComponent"; import useElementPosition from "@/app/util/hooks/useElementSize"; import useWindowWidth from "@/app/util/hooks/useWindowWidth"; export const QuestionWrapper = () => { 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 index f36f185d..e9de02f3 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/client/questionsView.tsx @@ -6,7 +6,6 @@ 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"; diff --git a/studyAi/src/app/library/question/components/client/solutionView.tsx b/studyAi/src/app/library/question/components/client/solutionView.tsx index 7ba50e86..681024a3 100644 --- a/studyAi/src/app/library/question/components/client/solutionView.tsx +++ b/studyAi/src/app/library/question/components/client/solutionView.tsx @@ -18,8 +18,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, diff --git a/studyAi/src/app/library/question/components/server/submissionView.tsx b/studyAi/src/app/library/question/components/client/submissionView.tsx similarity index 90% rename from studyAi/src/app/library/question/components/server/submissionView.tsx rename to studyAi/src/app/library/question/components/client/submissionView.tsx index cadc2630..9b898227 100644 --- a/studyAi/src/app/library/question/components/server/submissionView.tsx +++ b/studyAi/src/app/library/question/components/client/submissionView.tsx @@ -1,7 +1,7 @@ "use client"; import { QuestionSubmission } from "../../../../../../prisma/generated/type-graphql"; import { useQuery } from "@apollo/client"; -import { Container } from "./containerBar"; +import { Container } from "../server/containerBar"; import { useParams } from "next/navigation"; import { useSession } from "next-auth/react"; import { gql } from "../../../../../../graphql/generated"; @@ -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/client/timeComponent.tsx b/studyAi/src/app/library/question/components/client/timeForm.tsx similarity index 76% rename from studyAi/src/app/library/question/components/client/timeComponent.tsx rename to studyAi/src/app/library/question/components/client/timeForm.tsx index 196d7427..57160974 100644 --- a/studyAi/src/app/library/question/components/client/timeComponent.tsx +++ b/studyAi/src/app/library/question/components/client/timeForm.tsx @@ -9,25 +9,10 @@ import { useRef, useState, } from "react"; -import StopWatch from "@/app/util/components/time/stopwatch"; -import Timer from "@/app/util/components/time/timer"; -import { Button, Modal, Tab, Tabs, 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 { 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"; -import useElementPosition from "@/app/util/hooks/useElementSize"; -//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 timeOrder: { abbrev: "h" | "m" | "s"; label: "hours" | "minutes" | "seconds"; @@ -138,7 +123,6 @@ const StopWatchPlaceholder = () => { ); }; function TimerInput() { - // const ref = useRef(); const [hours, setHours] = useState("00"); const [minutes, setMinutes] = useState("00"); const [seconds, setSeconds] = useState("00"); @@ -233,12 +217,14 @@ const btnStyles = { margin: 0, minHeight: "unset", }; -const TimeForm = ({ +export const TimeForm = ({ setCurrType, setCurrTotalTimeGiven, + setModalOpen, }: { setCurrType: Dispatch>; setCurrTotalTimeGiven: Dispatch>; + setModalOpen: Dispatch>; }) => { const [timeType, setTimeType] = useState("stopwatch"); const onTimeTypeChange = ( @@ -252,7 +238,11 @@ const TimeForm = ({ //grab uncontrolled inputs here form const formData = new FormData(e.currentTarget); const data = Object.fromEntries(formData.entries()); - if (timeType === "stopwatch") return setCurrType(timeType); + if (timeType === "stopwatch") + return unstable_batchedUpdates(() => { + setCurrType(timeType); + setModalOpen(false); + }); const { totalTime } = data; const { hours, minutes, seconds } = extractTime( totalTime.toString(), @@ -265,6 +255,7 @@ const TimeForm = ({ unstable_batchedUpdates(() => { setCurrType(timeType); setCurrTotalTimeGiven(timeTotalSeconds * 1000); + setModalOpen(false); }); }; return ( @@ -316,61 +307,4 @@ const TimeForm = ({
); }; - -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": - if (typeof currTotalTimeGiven === "number") - return ( - {} } - /> - ); - else return setCurrType("stopwatch"); - //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" - > - <> - - - - - ); - } -}; +export default TimeForm; diff --git a/studyAi/src/app/library/question/components/client/timeModal.tsx b/studyAi/src/app/library/question/components/client/timeModal.tsx new file mode 100644 index 00000000..379e8025 --- /dev/null +++ b/studyAi/src/app/library/question/components/client/timeModal.tsx @@ -0,0 +1,207 @@ +"use client"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import StopWatch from "@/app/util/components/time/stopwatch"; +import Timer from "@/app/util/components/time/timer"; +import { Button, IconButton, Modal } 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 from "./timeForm"; +import { unstable_batchedUpdates } from "react-dom"; +import { TimeEventProps } from "@/app/util/components/time/hooks/useTimeHook"; +import { + getLocalStorageObj, + deleteLocalStorageObj, + addLocalStorageObj, +} from "@/app/util/parsers/localStorageWrappers"; +//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; +}; +const onChangeHandler = + ({ + 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 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); + //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 ( + + } + /> + ); + 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/components/server/answerComponent 2.tsx b/studyAi/src/app/library/question/components/server/answerComponent 2.tsx deleted file mode 100644 index 6d2868da..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 "Checkbox": - return "Select all that apply"; - case "Short Answer": - 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/server/questionComponents.tsx b/studyAi/src/app/library/question/components/server/questionComponents.tsx index 2d3b4296..22bc7f7e 100644 --- a/studyAi/src/app/library/question/components/server/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/server/questionComponents.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/util/components/time/hooks/useTimeHook.tsx b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx index 02c26af2..737ab6dc 100644 --- a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx +++ b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; export type TimeEventProps = { time: number; - eventType: "start" | "stop" | "reset" | "interval"; + eventType: "start" | "stop" | "reset" | "interval" | "finished"; }; const useTimeHook = ({ initialTime, @@ -20,6 +20,9 @@ const useTimeHook = ({ const updateTimeActionIntervalRef = useRef(null); const intervalRef = useRef(null); const mounted = useRef(true); + // useEffect(() => { + // setTime(initialTime); + // }, [initialTime]); useEffect(() => { mounted.current = true; //clean up any side effects so we dont cause a memory leak @@ -42,10 +45,11 @@ const useTimeHook = ({ if (intervalRef.current) clearInterval(intervalRef.current); if (updateTimeActionIntervalRef.current) { //update with curr time value - if (callback) callback({ - eventType: "stop", - time: time, - }); + if (callback) + callback({ + eventType: "stop", + time: time, + }); clearInterval(updateTimeActionIntervalRef.current); } }; diff --git a/studyAi/src/app/util/components/time/stopwatch.tsx b/studyAi/src/app/util/components/time/stopwatch.tsx index 49d60aff..e31a6c76 100644 --- a/studyAi/src/app/util/components/time/stopwatch.tsx +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -6,11 +6,15 @@ type StopWatchProps = { updateTimeAction?: (props?: TimeEventProps) => void; initialTimeUsed: number; autoPlay?: boolean; + customBtns?: React.ReactNode; + showTimer?: boolean; }; const StopWatch = ({ initialTimeUsed, updateTimeAction, autoPlay, + customBtns, + showTimer, }: StopWatchProps) => { const { time, @@ -65,8 +69,10 @@ const StopWatch = ({ stopTimer={stopTimer} startTimer={startTimer} resetTimer={resetTimer} + showTimer={showTimer} paused={paused} autoPlay={autoPlay} + customBtns={customBtns} >
{ const [show, setShow] = useState(showTimer); const playAuto = useRef(autoPlay); @@ -101,6 +103,7 @@ const TimeControlsWrapper = ({ > + {customBtns}
); }; diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx index 63c44f8f..ebe83c8a 100644 --- a/studyAi/src/app/util/components/time/timer.tsx +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -11,6 +11,7 @@ type TimerProps = { totalTimeGiven?: number | null; showTimer?: boolean; autoPlay?: boolean; + customBtns?: React.ReactNode; }; const Timer = ({ initialTimeLeft, @@ -18,6 +19,7 @@ const Timer = ({ totalTimeGiven, showTimer, autoPlay, + customBtns }: TimerProps) => { const { time, @@ -45,6 +47,11 @@ const Timer = ({ if (newTime > 0) return newTime; if (newTime <= 0 && intervalRef.current) { setPause(true); + if (updateTimeAction) + updateTimeAction({ + eventType: "finished", + time: 0, + }); clearInterval(intervalRef.current); } return 0; @@ -90,6 +97,7 @@ const Timer = ({ paused={paused} showTimer={showTimer} autoPlay={autoPlay} + customBtns={customBtns} >
(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 From fef484e4485e24667df9e98a833c75679f2f5dcd Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 10:34:28 -0500 Subject: [PATCH 08/23] added initial time finished modal --- .../question/components/client/timeModal.tsx | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeModal.tsx b/studyAi/src/app/library/question/components/client/timeModal.tsx index 379e8025..7842e14a 100644 --- a/studyAi/src/app/library/question/components/client/timeModal.tsx +++ b/studyAi/src/app/library/question/components/client/timeModal.tsx @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import StopWatch from "@/app/util/components/time/stopwatch"; import Timer from "@/app/util/components/time/timer"; -import { Button, IconButton, Modal } from "@mui/material"; +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"; @@ -111,6 +111,12 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { 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< @@ -143,25 +149,48 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { case "timer": if (typeof currTotalTimeGiven === "number") return ( - - } - /> + <> + + } + /> + {timerCompleteModalOpen && ( + setTimerCompleteModalOpen(false)} + aria-labelledby="timer-complete-modal" + aria-describedby="timer-complete" + className="flex justify-center items-center" + > +
+ Your Time is Up! + Time Elapsed: {totalTimeGiven} + +
+
+ )} + ); else { unstable_batchedUpdates(() => { From d810273105a9c2356d9f75829596c3d1bd4fe594 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 12:14:57 -0500 Subject: [PATCH 09/23] converted use time hook, to context provider and adjusted passing of state to components --- .../question/components/client/timeModal.tsx | 137 ++++++++---------- .../onTimeEventChangeHandler.tsx | 59 ++++++++ .../time/context/stopwatchStartAndReset.tsx | 44 ++++++ .../time/context/timerStartAndReset.tsx | 67 +++++++++ .../time/context/useTimeContext.tsx | 135 +++++++++++++++++ .../components/time/hooks/useTimeHook.tsx | 67 --------- .../app/util/components/time/stopwatch.tsx | 63 +------- .../app/util/components/time/timeControls.tsx | 43 +++--- .../src/app/util/components/time/timer.tsx | 86 +---------- 9 files changed, 390 insertions(+), 311 deletions(-) create mode 100644 studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx create mode 100644 studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx create mode 100644 studyAi/src/app/util/components/time/context/timerStartAndReset.tsx create mode 100644 studyAi/src/app/util/components/time/context/useTimeContext.tsx delete mode 100644 studyAi/src/app/util/components/time/hooks/useTimeHook.tsx diff --git a/studyAi/src/app/library/question/components/client/timeModal.tsx b/studyAi/src/app/library/question/components/client/timeModal.tsx index 7842e14a..af7187f2 100644 --- a/studyAi/src/app/library/question/components/client/timeModal.tsx +++ b/studyAi/src/app/library/question/components/client/timeModal.tsx @@ -1,5 +1,5 @@ "use client"; -import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +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"; @@ -8,12 +8,11 @@ import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { TimeOptions } from "../../../../../../prisma/generated/type-graphql"; import TimeForm from "./timeForm"; import { unstable_batchedUpdates } from "react-dom"; -import { TimeEventProps } from "@/app/util/components/time/hooks/useTimeHook"; import { getLocalStorageObj, - deleteLocalStorageObj, - addLocalStorageObj, } from "@/app/util/parsers/localStorageWrappers"; +import { TimeProvider } from "@/app/util/components/time/context/useTimeContext"; +import onTimeEventChangeHandler from "../eventHandlers/onTimeEventChangeHandler"; //we can manage time on the frontend //because time measurements are only //for the user's benefit @@ -23,58 +22,7 @@ type TimeProps = TimeOptions & { id?: string; initialTime: number; }; -const onChangeHandler = - ({ - 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 const DeleteTimeBtn = ({ setCurrTotalTimeGiven, setCurrType, @@ -127,40 +75,46 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { switch (currType) { case "stopwatch": return ( - - } - /> + > + + } + /> + ); case "timer": if (typeof currTotalTimeGiven === "number") return ( - <> + {
)} - + ); else { unstable_batchedUpdates(() => { setCurrTotalTimeGiven(null); setCurrType("stopwatch"); }); - return ; + return ( + + + } + /> + + ); } //create timer component default: diff --git a/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx b/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx new file mode 100644 index 00000000..ffbff815 --- /dev/null +++ b/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx @@ -0,0 +1,59 @@ +import { Dispatch, SetStateAction } from "react"; +import { TimeEventProps } from "@/app/util/components/time/hooks/useTimeHook"; +import { + deleteLocalStorageObj, + addLocalStorageObj, +} from "@/app/util/parsers/localStorageWrappers"; +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/util/components/time/context/stopwatchStartAndReset.tsx b/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx new file mode 100644 index 00000000..522661ae --- /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, + updateTimeAction, + intervalRef, + updateTimeActionIntervalRef, + mounted, +}: TimeStartAndResetProps) => { + 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({ + eventType: "interval", + time: time, + }); + }, 5000); + if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); + }; + const resetTimer = () => { + setPause(true); + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + setTime(0); + if (updateTimeAction) updateTimeAction({ 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..4a2e7c1a --- /dev/null +++ b/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx @@ -0,0 +1,67 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { TimeStartAndResetProps } from "./useTimeContext"; + +const timerStartAndReset = ({ + time, + initialTimeLeft, + totalTimeGiven, + setPause, + setTime, + updateTimeAction, + intervalRef, + updateTimeActionIntervalRef, + 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) { + setPause(true); + if (updateTimeAction) + updateTimeAction({ + eventType: "finished", + time: 0, + }); + 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({ + eventType: "interval", + time: time, + }); + }, + //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) + initialTimeLeft < 5000 ? initialTimeLeft : 5000 + ); + if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); + }; + const resetTimer = () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + const newTime = totalTimeGiven ? totalTimeGiven : 0; + unstable_batchedUpdates(() => { + setTime(newTime); + setPause(true); + if (updateTimeAction) + updateTimeAction({ 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..d41d677d --- /dev/null +++ b/studyAi/src/app/util/components/time/context/useTimeContext.tsx @@ -0,0 +1,135 @@ +"use client"; +import React, { useState, useRef, useEffect, useContext } from "react"; +import timerStartAndReset from "./timerStartAndReset"; +import stopwatchStartAndReset from "./stopwatchStartAndReset"; + +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>; + updateTimeAction?: (props?: TimeEventProps) => void; + intervalRef: React.MutableRefObject; + updateTimeActionIntervalRef: 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>; + updateTimeActionIntervalRef: 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 updateTimeActionIntervalRef = 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 (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + mounted.current = false; + }; + }, []); + //every time timeType changes, clear the interval and restart it + useEffect(() => { + mounted.current = true; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + mounted.current = false; + }; + }, [timeType]); + const stopTimer = () => { + setPause(true); + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) { + if (callback) { + callback({ + eventType: "stop", + time: time, + }); + } + clearInterval(updateTimeActionIntervalRef.current); + } + }; + const [startTimer, resetTimer] = + timeType === "timer" + ? timerStartAndReset({ + time, + initialTimeLeft: initialTime, + setPause, + setTime, + updateTimeAction: callback, + intervalRef, + updateTimeActionIntervalRef, + mounted, + totalTimeGiven, + }) + : stopwatchStartAndReset({ + time, + setPause, + setTime, + updateTimeAction: callback, + intervalRef, + updateTimeActionIntervalRef, + mounted, + initialTimeLeft: initialTime, + }); + const value = { + time, + paused, + autoPlay, + setPause, + startTimer, + resetTimer, + stopTimer, + setTime, + updateTimeActionIntervalRef, + 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 737ab6dc..00000000 --- a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; -import { useEffect, useRef, useState } from "react"; - -export type TimeEventProps = { - time: number; - eventType: "start" | "stop" | "reset" | "interval" | "finished"; -}; -const useTimeHook = ({ - initialTime, - callback, - autoPlay, -}: { - initialTime: number; - callback?: (props?: TimeEventProps) => void; - autoPlay?: boolean; -}) => { - const playAuto = useRef(autoPlay); - const [time, setTime] = useState(initialTime); - const [paused, setPause] = useState(true); - const updateTimeActionIntervalRef = useRef(null); - const intervalRef = useRef(null); - const mounted = useRef(true); - // useEffect(() => { - // setTime(initialTime); - // }, [initialTime]); - 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; - }; - }, []); - useEffect(() => { - if (playAuto.current) - setPause((state) => { - if (state) return false; - else return state; - }); - }, []); - const stopTimer = () => { - setPause(true); - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) { - //update with curr time value - if (callback) - callback({ - eventType: "stop", - time: 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 e31a6c76..8c9fb9ee 100644 --- a/studyAi/src/app/util/components/time/stopwatch.tsx +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -1,77 +1,22 @@ "use client"; import formatMilliseconds from "../../parsers/formatMilliseconds"; -import useTimeHook, { TimeEventProps } from "./hooks/useTimeHook"; +import { useTimeHook } from "./context/useTimeContext"; import TimeControlsWrapper from "./timeControls"; type StopWatchProps = { - updateTimeAction?: (props?: TimeEventProps) => void; - initialTimeUsed: number; - autoPlay?: boolean; customBtns?: React.ReactNode; showTimer?: boolean; }; const StopWatch = ({ - initialTimeUsed, - updateTimeAction, - autoPlay, customBtns, showTimer, }: StopWatchProps) => { - const { - time, - stopTimer, - setTime, - updateTimeActionIntervalRef, - intervalRef, - mounted, - paused, - setPause, - } = useTimeHook({ - initialTime: initialTimeUsed, - callback: (event) => { - if (updateTimeAction) updateTimeAction(event); - }, - autoPlay, - }); - 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({ - eventType: "interval", - time: time, - }); - }, 5000); - if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); - }; - const resetTimer = () => { - setPause(true); - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - setTime(0); - if (updateTimeAction) - updateTimeAction({ eventType: "reset", time: 0 }); - }; + const timeContext = useTimeHook(); + if (!timeContext) return <>; + const { time } = timeContext; const timeArr = formatMilliseconds(time, true); return (
void; - resetTimer: () => void; - stopTimer: () => void; - paused: boolean; showTimer?: boolean; - autoPlay?: boolean; - customBtns?: React.ReactNode -} + customBtns?: React.ReactNode; +}; const TimeControlsWrapper = ({ children, - paused, - startTimer, - resetTimer, - stopTimer, showTimer, - autoPlay, - customBtns -}:TimeControlsWrapper) => { + customBtns, +}: TimeControlsWrapper) => { + const timeContext = useTimeHook(); const [show, setShow] = useState(showTimer); - const playAuto = useRef(autoPlay); + const playAuto = useRef(timeContext && timeContext.autoPlay); useEffect(() => { - if (playAuto.current) startTimer(); + if (playAuto.current && timeContext) timeContext.startTimer(); //es-lint-disable-next-line }, []); + const showTimeVisibility = (callback?: () => void) => (e: MouseEvent) => { @@ -62,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) @@ -75,19 +68,21 @@ const TimeControlsWrapper = ({ {!show && ( )} - {paused && show && ( + {timeContext && timeContext.paused && show && ( )} - {!paused && show && ( + {timeContext && !timeContext.paused && show && ( diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx index ebe83c8a..d76a2a11 100644 --- a/studyAi/src/app/util/components/time/timer.tsx +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -1,102 +1,26 @@ //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"; -import { TimeEventProps } from "./hooks/useTimeHook"; -import { unstable_batchedUpdates } from "react-dom"; type TimerProps = { - updateTimeAction?: (props?: TimeEventProps) => void; - initialTimeLeft: number; - totalTimeGiven?: number | null; showTimer?: boolean; - autoPlay?: boolean; customBtns?: React.ReactNode; }; const Timer = ({ - initialTimeLeft, - updateTimeAction, - totalTimeGiven, showTimer, - autoPlay, - customBtns + customBtns, }: TimerProps) => { + const timeContext = useTimeHook(); + if (!timeContext) return <> const { time, - stopTimer, - setTime, - updateTimeActionIntervalRef, - intervalRef, - mounted, - setPause, - paused, - } = useTimeHook({ - initialTime: initialTimeLeft, - callback: (props?: TimeEventProps) => { - if (updateTimeAction) updateTimeAction(props); - }, - autoPlay, - }); - 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); - if (updateTimeAction) - updateTimeAction({ - eventType: "finished", - time: 0, - }); - 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({ - eventType: "interval", - time: time, - }); - }, - //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) - initialTimeLeft < 5000 ? initialTimeLeft : 5000 - ); - if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); - }; - const resetTimer = () => { - if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); - const newTime = totalTimeGiven ? totalTimeGiven : 0; - unstable_batchedUpdates(() => { - setTime(newTime); - setPause(true); - if (updateTimeAction) - updateTimeAction({ eventType: "reset", time: newTime }); - }); - }; + } = timeContext; const timeArr = formatMilliseconds(time, true); return (
Date: Thu, 9 Nov 2023 14:00:22 -0500 Subject: [PATCH 10/23] fixed side effect error where we updated a component from inside the function body of a different component, due to embedded setState functions --- .../question/components/client/timeModal.tsx | 71 +++++++++++++------ .../onTimeEventChangeHandler.tsx | 2 +- .../time/context/stopwatchStartAndReset.tsx | 22 +++--- .../time/context/timerStartAndReset.tsx | 35 ++++----- .../time/context/useTimeContext.tsx | 51 ++++++++----- 5 files changed, 107 insertions(+), 74 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeModal.tsx b/studyAi/src/app/library/question/components/client/timeModal.tsx index af7187f2..dcb6ba59 100644 --- a/studyAi/src/app/library/question/components/client/timeModal.tsx +++ b/studyAi/src/app/library/question/components/client/timeModal.tsx @@ -8,10 +8,11 @@ import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { TimeOptions } from "../../../../../../prisma/generated/type-graphql"; import TimeForm from "./timeForm"; import { unstable_batchedUpdates } from "react-dom"; +import { getLocalStorageObj } from "@/app/util/parsers/localStorageWrappers"; import { - getLocalStorageObj, -} from "@/app/util/parsers/localStorageWrappers"; -import { TimeProvider } from "@/app/util/components/time/context/useTimeContext"; + TimeProvider, + useTimeHook, +} from "@/app/util/components/time/context/useTimeContext"; import onTimeEventChangeHandler from "../eventHandlers/onTimeEventChangeHandler"; //we can manage time on the frontend //because time measurements are only @@ -22,7 +23,45 @@ type TimeProps = TimeOptions & { id?: string; initialTime: number; }; - +export const TimerFinishedModal = ({ + setTimerCompleteModalOpen, + modalOpen, + totalTimeGiven, +}: { + totalTimeGiven: number; + modalOpen: boolean; + setTimerCompleteModalOpen: Dispatch>; +}) => { + const timeContext = useTimeHook(); + return ( + setTimerCompleteModalOpen(false)} + aria-labelledby="timer-complete-modal" + aria-describedby="timer-complete" + className="flex justify-center items-center" + > +
+ Your Time is Up! + Time Elapsed: {totalTimeGiven} + +
+
+ ); +}; export const DeleteTimeBtn = ({ setCurrTotalTimeGiven, setCurrType, @@ -124,25 +163,11 @@ export const TimeComponent = ({ props }: { props?: TimeProps }) => { } /> {timerCompleteModalOpen && ( - setTimerCompleteModalOpen(false)} - aria-labelledby="timer-complete-modal" - aria-describedby="timer-complete" - className="flex justify-center items-center" - > -
- Your Time is Up! - Time Elapsed: {totalTimeGiven} - -
-
+ )} ); diff --git a/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx b/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx index ffbff815..bff8c2ff 100644 --- a/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx +++ b/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx @@ -1,9 +1,9 @@ import { Dispatch, SetStateAction } from "react"; -import { TimeEventProps } from "@/app/util/components/time/hooks/useTimeHook"; import { deleteLocalStorageObj, addLocalStorageObj, } from "@/app/util/parsers/localStorageWrappers"; +import { TimeEventProps } from "@/app/util/components/time/context/useTimeContext"; const onTimeEventChangeHandler = ({ id, diff --git a/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx b/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx index 522661ae..4f4f22aa 100644 --- a/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx +++ b/studyAi/src/app/util/components/time/context/stopwatchStartAndReset.tsx @@ -4,9 +4,9 @@ const stopwatchStartAndReset = ({ time, setPause, setTime, - updateTimeAction, + callback, intervalRef, - updateTimeActionIntervalRef, + callbackIntervalRef, mounted, }: TimeStartAndResetProps) => { const startTimer = () => { @@ -17,27 +17,27 @@ const stopwatchStartAndReset = ({ return prevTime + 1000; }); }, 1000); - updateTimeActionIntervalRef.current = setInterval(() => { + callbackIntervalRef.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); + if (!intervalRef.current && callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); //update below function with time value - if (updateTimeAction) - updateTimeAction({ + if (callback) + callback({ eventType: "interval", time: time, }); }, 5000); - if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); + if (callback) callback({ eventType: "start", time: time }); }; const resetTimer = () => { setPause(true); if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); setTime(0); - if (updateTimeAction) updateTimeAction({ eventType: "reset", time: 0 }); + if (callback) callback({ eventType: "reset", time: 0 }); }; return [startTimer, resetTimer]; }; diff --git a/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx b/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx index 4a2e7c1a..7b257d3c 100644 --- a/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx +++ b/studyAi/src/app/util/components/time/context/timerStartAndReset.tsx @@ -1,15 +1,14 @@ import { unstable_batchedUpdates } from "react-dom"; import { TimeStartAndResetProps } from "./useTimeContext"; - const timerStartAndReset = ({ time, initialTimeLeft, totalTimeGiven, setPause, setTime, - updateTimeAction, + callback, intervalRef, - updateTimeActionIntervalRef, + callbackIntervalRef, mounted, }: TimeStartAndResetProps) => { const startTimer = () => { @@ -21,47 +20,41 @@ const timerStartAndReset = ({ const newTime = prevTime - 1000; if (newTime > 0) return newTime; if (newTime <= 0 && intervalRef.current) { - setPause(true); - if (updateTimeAction) - updateTimeAction({ - eventType: "finished", - time: 0, - }); clearInterval(intervalRef.current); } return 0; }); }, 1000); - updateTimeActionIntervalRef.current = setInterval( + callbackIntervalRef.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); + if (!intervalRef.current && callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); //update below function with time value - if (updateTimeAction) - updateTimeAction({ + if (callback) + callback({ eventType: "interval", time: time, }); }, - //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) + //we update every 5 second as this can be costly computation (i.e writing to local state) initialTimeLeft < 5000 ? initialTimeLeft : 5000 ); - if (updateTimeAction) updateTimeAction({ eventType: "start", time: time }); + if (callback) callback({ eventType: "start", time: time }); }; const resetTimer = () => { if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); const newTime = totalTimeGiven ? totalTimeGiven : 0; unstable_batchedUpdates(() => { setTime(newTime); setPause(true); - if (updateTimeAction) - updateTimeAction({ eventType: "reset", time: newTime }); + if (callback) + callback({ eventType: "reset", time: newTime }); }); }; return [startTimer, resetTimer]; }; -export default timerStartAndReset \ No newline at end of file +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 index d41d677d..d89d5aed 100644 --- a/studyAi/src/app/util/components/time/context/useTimeContext.tsx +++ b/studyAi/src/app/util/components/time/context/useTimeContext.tsx @@ -2,21 +2,20 @@ 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>; - updateTimeAction?: (props?: TimeEventProps) => void; + callback?: (props?: TimeEventProps) => void; intervalRef: React.MutableRefObject; - updateTimeActionIntervalRef: React.MutableRefObject; + callbackIntervalRef: React.MutableRefObject; mounted: React.MutableRefObject; }; export type TimeContextProps = { @@ -28,7 +27,7 @@ export type TimeContextProps = { resetTimer: () => void; stopTimer: () => void; setTime: React.Dispatch>; - updateTimeActionIntervalRef: React.MutableRefObject; + callbackIntervalRef: React.MutableRefObject; intervalRef: React.MutableRefObject; mounted: React.MutableRefObject; }; @@ -52,7 +51,7 @@ const TimeProvider = ({ }) => { const [time, setTime] = useState(initialTime); const [paused, setPause] = useState(true); - const updateTimeActionIntervalRef = useRef(null); + const callbackIntervalRef = useRef(null); const intervalRef = useRef(null); const mounted = useRef(true); //every context is re-rendered clear the interval and restart it @@ -60,32 +59,48 @@ const TimeProvider = ({ mounted.current = true; return () => { if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.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 (updateTimeActionIntervalRef.current) - clearInterval(updateTimeActionIntervalRef.current); + if (callbackIntervalRef.current) + clearInterval(callbackIntervalRef.current); mounted.current = false; - }; + }; }, [timeType]); const stopTimer = () => { setPause(true); if (intervalRef.current) clearInterval(intervalRef.current); - if (updateTimeActionIntervalRef.current) { + if (callbackIntervalRef.current) { if (callback) { callback({ eventType: "stop", time: time, }); } - clearInterval(updateTimeActionIntervalRef.current); + clearInterval(callbackIntervalRef.current); } }; const [startTimer, resetTimer] = @@ -95,9 +110,9 @@ const TimeProvider = ({ initialTimeLeft: initialTime, setPause, setTime, - updateTimeAction: callback, + callback: callback, intervalRef, - updateTimeActionIntervalRef, + callbackIntervalRef, mounted, totalTimeGiven, }) @@ -105,9 +120,9 @@ const TimeProvider = ({ time, setPause, setTime, - updateTimeAction: callback, + callback: callback, intervalRef, - updateTimeActionIntervalRef, + callbackIntervalRef, mounted, initialTimeLeft: initialTime, }); @@ -120,7 +135,7 @@ const TimeProvider = ({ resetTimer, stopTimer, setTime, - updateTimeActionIntervalRef, + callbackIntervalRef, intervalRef, mounted, }; From 0c2c755c6b77efb75dff7a35012e4fe36fbe1f67 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 14:19:45 -0500 Subject: [PATCH 11/23] finalized timer modal --- .../question/components/client/timeForm.tsx | 15 ++++++------ .../question/components/client/timeModal.tsx | 23 +++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/timeForm.tsx b/studyAi/src/app/library/question/components/client/timeForm.tsx index 57160974..c40ee313 100644 --- a/studyAi/src/app/library/question/components/client/timeForm.tsx +++ b/studyAi/src/app/library/question/components/client/timeForm.tsx @@ -13,7 +13,7 @@ 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"; -const timeOrder: { +export const timeLabelData: { abbrev: "h" | "m" | "s"; label: "hours" | "minutes" | "seconds"; }[] = [ @@ -44,7 +44,7 @@ const determineNewVal = ( } return newVal; }; -const splitTimeStrBy2 = (str: string) => { +export const splitTimeStrBy2 = (str: string) => { const arr = []; for (let i = 0; i < str.length; i += 2) { const chunk = str.slice(i, i + 2); @@ -147,7 +147,7 @@ function TimerInput() { }; let setAction = dispatchVals[name]; setAction((prevVal) => { - const maxLength = timeOrder.length * 2; + const maxLength = timeLabelData.length * 2; //set the current values const parsedVal = removeNonIntegerChars(value); timeVals[name] = parsedVal; @@ -170,9 +170,10 @@ function TimerInput() { //this creates the new total time string const newTotalTime = newValArr.reduce( - (a, b, idx) => a + timeOrder[idx - 1].abbrev + " " + b - ) + timeOrder[timeOrder.length - 1].abbrev; - + (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]); @@ -185,7 +186,7 @@ function TimerInput() { }; return (
- {timeOrder.map((a) => ( + {timeLabelData.map((a) => ( >; }) => { 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 ( -
- Your Time is Up! - Time Elapsed: {totalTimeGiven} +
+ Your Time is Up! + Time Passed: {parsedTimeElapsed}
); }; +const QuestionPageNavigation = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isFullscreen } = useFullscreen(); + return ( + ( + + {children} + + )} + > + ( +
+ {children} +
+ )} + > + {children} +
+
+ ); +}; const QuestionPageContainer = () => { return ( - - {}, onNext: () => {} }} /> - - + + + + {}, onNext: () => {} }} + /> + + + + ); }; export default QuestionPageContainer; diff --git a/studyAi/src/app/library/question/components/client/questionWrapper.tsx b/studyAi/src/app/library/question/components/client/questionWrapper.tsx index 8a00d6d7..8cfcb170 100644 --- a/studyAi/src/app/library/question/components/client/questionWrapper.tsx +++ b/studyAi/src/app/library/question/components/client/questionWrapper.tsx @@ -14,8 +14,12 @@ export const QuestionWrapper = () => { ref={setRef} className="flex w-full flex-col mt-3 mb-5 h-full grow space-y-5 md:flex-row md:space-y-0 md:justify-between" > - - + +
); }; diff --git a/studyAi/src/app/library/question/components/server/containerBar.tsx b/studyAi/src/app/library/question/components/server/containerBar.tsx index 8ca688d0..de319aa4 100644 --- a/studyAi/src/app/library/question/components/server/containerBar.tsx +++ b/studyAi/src/app/library/question/components/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 (
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; From 16e25df1eafabf6de4f26c61b5a664fdf33d2c1c Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 22:27:06 -0500 Subject: [PATCH 14/23] added share btn logic. Meta tags are still needed --- .../components/client/answerComponent.tsx | 18 ++- .../components/client/answerInputs.tsx | 2 +- .../components/client/questionsView.tsx | 136 +++++++++++++++++- studyAi/src/app/util/hooks/useOrigin.tsx | 21 +++ studyAi/src/app/util/types/UserData.ts | 4 +- 5 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 studyAi/src/app/util/hooks/useOrigin.tsx diff --git a/studyAi/src/app/library/question/components/client/answerComponent.tsx b/studyAi/src/app/library/question/components/client/answerComponent.tsx index 388220c4..9b0087c4 100644 --- a/studyAi/src/app/library/question/components/client/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/client/answerComponent.tsx @@ -5,6 +5,7 @@ import ContainerBar, { Container } from "../server/containerBar"; import { Button, IconButton } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { + faDownLeftAndUpRightToCenter, faRefresh, faUpRightAndDownLeftFromCenter, } from "@fortawesome/free-solid-svg-icons"; @@ -14,7 +15,7 @@ import { useFullscreen } from "@/app/util/providers/FullscreenProvider"; const determineAnswerTitle = (str?: string) => { const matchStr = str as (typeof QuestionTypes)[number]; switch (matchStr) { - case "multipleChoice": + case "Multiple Choice": return "Select the best answer"; case "Checkbox": return "Select all that apply"; @@ -59,10 +60,17 @@ const TopBar = () => { aria-label={`toggle fullscreen ${isFullscreen ? "off" : "on"}`} onClick={toggleFullscreen} > - + {!isFullscreen ? ( + + ) : ( + + )}
diff --git a/studyAi/src/app/library/question/components/client/answerInputs.tsx b/studyAi/src/app/library/question/components/client/answerInputs.tsx index 0f882097..bbc12fa6 100644 --- a/studyAi/src/app/library/question/components/client/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/client/answerInputs.tsx @@ -93,7 +93,7 @@ export const AnswerType = () => { questionInfo: { options: questionOptions }, } = question; switch (questionType) { - case "multipleChoice": + case "Multiple Choice": return ; case "Checkbox": return ; diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/client/questionsView.tsx index e9de02f3..6ccd4516 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/client/questionsView.tsx @@ -1,14 +1,140 @@ "use client"; import { Container } from "../server/containerBar"; -import { Button, Chip, IconButton } from "@mui/material"; +import { Button, Chip, IconButton, Menu } from "@mui/material"; import { useQuestions } from "@/app/stores/questionStore"; -import { useParams } from "next/navigation"; +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, useRef, useState } from "react"; +import useOrigin from "@/app/util/hooks/useOrigin"; +import { + faFacebook, + faLinkedin, + faReddit, + faTwitter, + faWhatsapp, +} from "@fortawesome/free-brands-svg-icons"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; +import useDropdown from "@/app/util/hooks/useDropdown"; +import useElementPosition from "@/app/util/hooks/useElementSize"; +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, handleClick, handleClose } = 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); + console.log(shareUrl) + if (platform !== "link") window.open(shareUrl, "_blank"); + //when user wants to only copy url/link + else navigator.clipboard.writeText(shareUrl).then(() => setCopied(true)); + }; + return ( + <> + { + handleClick(e); + }} + > + + + handleClose()} + anchorOrigin={{ + horizontal: "center", + vertical: "bottom", + }} + transformOrigin={{ + vertical: "top", + horizontal: "center", + }} + MenuListProps={{ + className: "w-36 sm:w-auto", + disablePadding: true, + }} + slotProps={{ + paper: { + sx: { minHeight: "unset" }, + }, + }} + sx={{ + minHeight: "unset", + }} + > +
+ {platformsToShare.map((platform) => ( + + {platform.icon} + + ))} +
+
+ + ); +}; const QuestionActionBtns = () => { return (
@@ -16,9 +142,7 @@ const QuestionActionBtns = () => { {/* */} - - - +
); }; 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/types/UserData.ts b/studyAi/src/app/util/types/UserData.ts index ea142e48..dc32577d 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", + "Multiple Choice", "Checkbox", "Short Answer", ] as const; -export type UserInfo = User \ No newline at end of file +export type UserInfo = User; From 4a3ea8bb678a536a52b6af687c028ea982094736 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 9 Nov 2023 23:37:29 -0500 Subject: [PATCH 15/23] add favicons and added question metadata on page. Finish Share Btn --- .../public/logo/android-chrome-192x192.png | Bin 0 -> 5126 bytes .../public/logo/android-chrome-512x512.png | Bin 0 -> 16315 bytes studyAi/public/logo/apple-touch-icon.png | Bin 0 -> 4614 bytes studyAi/public/logo/favicon-16x16.png | Bin 0 -> 307 bytes studyAi/public/logo/favicon-32x32.png | Bin 0 -> 570 bytes studyAi/public/logo/favicon.ico | Bin 0 -> 15406 bytes studyAi/public/site.webmanifest | 19 +++++++ studyAi/src/app/favicon.ico | Bin 25931 -> 0 bytes studyAi/src/app/layout.tsx | 27 +++++++++- .../src/app/library/question/[id]/page.tsx | 51 ++++++++++++++++-- .../client/questionPageContainer.tsx | 2 +- .../components/client/questionWrapper.tsx | 25 --------- .../components/server/questionWrapper.tsx | 10 ++++ .../app/util/parsers/determineOriginUrl.ts | 8 +++ 14 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 studyAi/public/logo/android-chrome-192x192.png create mode 100644 studyAi/public/logo/android-chrome-512x512.png create mode 100644 studyAi/public/logo/apple-touch-icon.png create mode 100644 studyAi/public/logo/favicon-16x16.png create mode 100644 studyAi/public/logo/favicon-32x32.png create mode 100644 studyAi/public/logo/favicon.ico create mode 100644 studyAi/public/site.webmanifest delete mode 100644 studyAi/src/app/favicon.ico delete mode 100644 studyAi/src/app/library/question/components/client/questionWrapper.tsx create mode 100644 studyAi/src/app/library/question/components/server/questionWrapper.tsx create mode 100644 studyAi/src/app/util/parsers/determineOriginUrl.ts diff --git a/studyAi/public/logo/android-chrome-192x192.png b/studyAi/public/logo/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5fa801e6d8dd50d409d7fcaa002c6b5d914e42 GIT binary patch literal 5126 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 FV1m{agp>dP literal 0 HcmV?d00001 diff --git a/studyAi/public/logo/favicon-32x32.png b/studyAi/public/logo/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..d4141f7c2792210475f7bf48225b150dbfb908bd GIT binary patch literal 570 zcmV-A0>%A_P)Px$^hrcPR9Hvt)yu0+VHC&l&vBPbNCrmAKuMUGnGjKKDWwct3;%$LqD%}7iGgH5 zBDZ2_<~BHW1`Lof=M*Com*mo_wQJkn_w37|H}$U3e&4;GwZ7}O*0c7A0o%xc^BV#T z75HxzSc3!DgIBnPJD6yDtJ)~A5PLK6>K=)H;7P*V#ds5#I)Lfelu5T^c9WoJ4AT!_ zSti|wB@L7Ia|@WKr8fPhOc`Jvc4y*s{F5d12TTBjtY>f-^QTyC)8nDSMVu`ZD5^h( zwN0aR@Oy^yxQ%adzB&Wz^S>Cp=>OslNvcP_;3m#ttopJSz#F{9Ol-v=Y)0IOHWo2L z=1cg{?_d=m$BJ z%1+G5;Ng2c`}-UlaTHrHw<*AVY)j|kF`<}+ucZK^334M_5}Pp~E~ihd=Oc&$!eY=O zMqeuwh<-<(3;T#i`QuZMDx%_8x1mZrN42p-3Pp-IjR7Wue2+zKrmy7tR{)~QxRQ0> zjVj}JT+Pi|+k(0RSK%aL@TS!`$GVM0a|*BP0=KPdWrFPL0rC?M@_pQdM%EUf2)Y6% zumiCx9^y3Omb9|207a5$N5qJJce1GhbV^!2Gz1tbFmMHa0l0iyC}w3$$^ZZW07*qo IM6N<$f=v?wZvX%Q literal 0 HcmV?d00001 diff --git a/studyAi/public/logo/favicon.ico b/studyAi/public/logo/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..747479524ce0055a0175764ed7ea9a74b05a0580 GIT binary patch literal 15406 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 LrJugUA?W`A8`#=m diff --git a/studyAi/src/app/layout.tsx b/studyAi/src/app/layout.tsx index 2983dc4a..a718148c 100644 --- a/studyAi/src/app/layout.tsx +++ b/studyAi/src/app/layout.tsx @@ -3,12 +3,35 @@ import type { Metadata } from "next"; import AuthProvider from "./util/providers/AuthContext"; import GraphQLProvider from "./util/providers/apolloProvider"; import { IsClientCtxProvider } from "./util/providers/isClientProvider"; +import Head from "next/head"; 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 65e409de..46b9f464 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -6,6 +6,8 @@ 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]; @@ -74,13 +76,54 @@ export default async function QuestionPage({ // console.log(data) 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} - 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: [ + + ] + }, + // 'og:title': data?.questionInfo?.title ?? "Question", + // 'og:description': data?.questionInfo?.description ?? "Question", + }; +} diff --git a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx b/studyAi/src/app/library/question/components/client/questionPageContainer.tsx index bac004b2..28f222f4 100644 --- a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx +++ b/studyAi/src/app/library/question/components/client/questionPageContainer.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; import { NavigationBtns, PaginationOptions } from "./navigationBtns"; -import { QuestionWrapper } from "./questionWrapper"; +import { QuestionWrapper } from "../server/questionWrapper"; import { TimeComponent } from "./timeModal"; import NavigationWrapper from "@/app/util/components/navigation/navigationWrapper"; import FullscreenProvider, { 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 8cfcb170..00000000 --- a/studyAi/src/app/library/question/components/client/questionWrapper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import QuestionComponent from "./questionComponents"; -import AnswerComponent from "./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/server/questionWrapper.tsx b/studyAi/src/app/library/question/components/server/questionWrapper.tsx new file mode 100644 index 00000000..4ef7c163 --- /dev/null +++ b/studyAi/src/app/library/question/components/server/questionWrapper.tsx @@ -0,0 +1,10 @@ +import QuestionComponent from "../client/questionComponents"; +import AnswerComponent from "../client/answerComponent"; +export const QuestionWrapper = () => { + return ( +
+ + +
+ ); +}; 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; From dd572aef9c550a73e24ce9fe8c4012d01fe07e72 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 02:36:24 -0500 Subject: [PATCH 16/23] modifed use dropdown to store element so that share btn can have an extra dropdown that appears when a link is copied --- studyAi/public/logo/logo.png | Bin 0 -> 10519 bytes studyAi/src/app/layout.tsx | 1 - .../src/app/library/question/[id]/page.tsx | 8 +- .../components/client/questionsView.tsx | 113 ++++++++++++------ .../navigation/client/desktopNavbar.tsx | 19 +-- .../navigation/client/userProfile.tsx | 18 ++- studyAi/src/app/util/hooks/useDropdown.tsx | 27 +++-- 7 files changed, 126 insertions(+), 60 deletions(-) create mode 100644 studyAi/public/logo/logo.png diff --git a/studyAi/public/logo/logo.png b/studyAi/public/logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4a54467cfbf811b265fa64fae27c7f00d09b6213 GIT binary patch literal 10519 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 { const ShareBtn = () => { const origin = useOrigin(); const pathName = usePathname(); - const { anchorEl: shareBtnEl, handleClick, handleClose } = useDropdown(); + const { + anchorEl: shareBtnEl, + setAnchorEl, + handleClick, + handleClose, + open, + } = useDropdown(); const [copied, setCopied] = useState(false); const fullUrl = origin + pathName; const onShareClick = ( @@ -76,14 +89,41 @@ const ShareBtn = () => { const platform = dataset["platformId"]; // Get the URL of the current page const shareUrl = determineShareUrl(fullUrl, platform); - console.log(shareUrl) 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 ( <> { @@ -92,44 +132,43 @@ const ShareBtn = () => { > + +
+ {" "} + Link Copied! +
+
handleClose()} - anchorOrigin={{ - horizontal: "center", - vertical: "bottom", - }} - transformOrigin={{ - vertical: "top", - horizontal: "center", - }} + {...shareMenuProps} + open={open} MenuListProps={{ className: "w-36 sm:w-auto", disablePadding: true, }} - slotProps={{ - paper: { - sx: { minHeight: "unset" }, - }, - }} - sx={{ - minHeight: "unset", - }} > -
- {platformsToShare.map((platform) => ( - - {platform.icon} - - ))} +
+ {platformsToShare.map((platform) => { + const onClick = + platform.platform === "link" + ? (e: MouseEvent) => { + onShareClick(e); + setCopied(true); + setTimeout(() => setCopied(false), 3000); + } + : onShareClick; + return ( + + {platform.icon} + + ); + })}
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 && ( { 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); + // setAnchorEl(null); + setIsOpen(false); }; return { anchorEl, + open, handleClick, handleClose, setAnchorEl, }; }; -export default useDropdown \ No newline at end of file +export default useDropdown; From 51082a7804e278134b57bd4f33584327924923c2 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 12:57:52 -0500 Subject: [PATCH 17/23] added dropdown btn label to describe question buttons/icon buttons --- .../components/client/answerComponent.tsx | 91 ++++++++++++++----- .../components/client/questionComponents.tsx | 45 +++++++-- .../components/client/questionsView.tsx | 38 +++++--- .../btnLabelDropdown/btnLabelDropdown.tsx | 75 +++++++++++++++ studyAi/src/app/util/hooks/useDropdown.tsx | 2 - 5 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 studyAi/src/app/util/components/btnLabelDropdown/btnLabelDropdown.tsx diff --git a/studyAi/src/app/library/question/components/client/answerComponent.tsx b/studyAi/src/app/library/question/components/client/answerComponent.tsx index 9b0087c4..f7dd0d6a 100644 --- a/studyAi/src/app/library/question/components/client/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/client/answerComponent.tsx @@ -12,6 +12,8 @@ import { 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) { @@ -25,34 +27,24 @@ const determineAnswerTitle = (str?: string) => { return str; } }; -const TopBar = () => { - const params = useParams(); - const questions = useQuestions()[0].data; +const FullScreenBtn = ({ + btnClassNames, + btnStyle, +}: { + btnClassNames?: string; + btnStyle?: React.CSSProperties; +}) => { const { isFullscreen, toggleFullscreen } = useFullscreen(); - const question = - params.id && typeof params.id === "string" ? questions[params.id] : null; - const btnClassNames = "flex items-center justify-center h-[70%]"; - const btnStyle = { - minHeight: "unset", - padding: 0, - aspectRatio: 1, - }; - return ( - -

- {determineAnswerTitle(question?.questionType)} -

-
- - - + + {(props) => ( props.handleClose()} size="small" sx={btnStyle} className={btnClassNames} @@ -72,6 +64,55 @@ const TopBar = () => { /> )} + )} + + ); +}; +const ResetAnswerBtn = ({ + btnClassNames, + btnStyle, +}: { + btnClassNames?: string; + btnStyle?: React.CSSProperties; +}) => { + return ( + + {(props) => ( + 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: React.CSSProperties = { + minHeight: "unset", + padding: 0, + aspectRatio: 1, + }; + + return ( + +

+ {determineAnswerTitle(question?.questionType)} +

+
+ +
); diff --git a/studyAi/src/app/library/question/components/client/questionComponents.tsx b/studyAi/src/app/library/question/components/client/questionComponents.tsx index a8fd0e2a..76c40efd 100644 --- a/studyAi/src/app/library/question/components/client/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/client/questionComponents.tsx @@ -2,12 +2,45 @@ import ContainerBar, { Container } from "../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 BtnLabelDropdown from "@/app/util/components/btnLabelDropdown/btnLabelDropdown"; +const EditBtn = ({ + btnStyles, + btnClassNames, +}: { + btnStyles: React.CSSProperties; + btnClassNames: string; +}) => { + return ( + + {(props) => ( + props.handleClose()} + type="button" + sx={btnStyles} + className={btnClassNames + " aspect-square h-[70%]"} + > + + + )} + + ); +}; + const TopBar = ({ view, handleChange, @@ -23,7 +56,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 +88,7 @@ const TopBar = ({ {session.data && question && session.data.user.id === question.creatorId && ( - - - + )} ); diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/client/questionsView.tsx index 28acad79..b615b728 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/client/questionsView.tsx @@ -27,6 +27,7 @@ import { } 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", @@ -122,20 +123,29 @@ const ShareBtn = () => { }; return ( <> - { - handleClick(e); - }} - > - - + + {(props) => ( + { + setAnchorEl(ref); + props.setAnchorEl(ref); + }} + onMouseEnter={props.handleClick} + onMouseLeave={() => props.handleClose()} + className="h-[70%]" + type="button" + onClick={(e) => { + handleClick(e); + }} + > + + + )} + -
- {" "} - Link Copied! +
+ {" "} + Link Copied!
{ ? (e: MouseEvent) => { onShareClick(e); setCopied(true); - setTimeout(() => setCopied(false), 3000); + setTimeout(() => setCopied(false), 5000); } : onShareClick; return ( 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/hooks/useDropdown.tsx b/studyAi/src/app/util/hooks/useDropdown.tsx index 0995673c..42d33fcc 100644 --- a/studyAi/src/app/util/hooks/useDropdown.tsx +++ b/studyAi/src/app/util/hooks/useDropdown.tsx @@ -1,4 +1,3 @@ -import { PopoverVirtualElement } from "@mui/material"; import { MouseEvent, useState } from "react"; import { unstable_batchedUpdates } from "react-dom"; const useDropdown = () => { @@ -21,7 +20,6 @@ const useDropdown = () => { }; const handleClose = () => { - // setAnchorEl(null); setIsOpen(false); }; return { From 8ac9a99a85dc9d6a5d58ad37210b8b7f2d517adb Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 13:54:39 -0500 Subject: [PATCH 18/23] changed dropdown btn logic, to use pointer events handler instead of touch or mouse handlers --- studyAi/package.json | 1 + .../components/client/answerComponent.tsx | 16 ++++++++++++---- .../components/client/questionComponents.tsx | 8 ++++++-- .../question/components/client/questionsView.tsx | 11 +++++++---- 4 files changed, 26 insertions(+), 10 deletions(-) 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/src/app/library/question/components/client/answerComponent.tsx b/studyAi/src/app/library/question/components/client/answerComponent.tsx index f7dd0d6a..58c1f06d 100644 --- a/studyAi/src/app/library/question/components/client/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/client/answerComponent.tsx @@ -43,8 +43,12 @@ const FullScreenBtn = ({ {(props) => ( props.handleClose()} + onPointerEnter={(e) => { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} size="small" sx={btnStyle} className={btnClassNames} @@ -80,8 +84,12 @@ const ResetAnswerBtn = ({ {(props) => ( props.handleClose()} + onPointerEnter={(e) => { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} size="small" sx={btnStyle} className={btnClassNames} diff --git a/studyAi/src/app/library/question/components/client/questionComponents.tsx b/studyAi/src/app/library/question/components/client/questionComponents.tsx index 76c40efd..550a0a15 100644 --- a/studyAi/src/app/library/question/components/client/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/client/questionComponents.tsx @@ -28,8 +28,12 @@ const EditBtn = ({ {(props) => ( props.handleClose()} + onPointerEnter={(e) => { + 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%]"} diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/client/questionsView.tsx index b615b728..8269714e 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/client/questionsView.tsx @@ -7,7 +7,6 @@ import { Menu, MenuProps, Typography, - setRef, } from "@mui/material"; import { useQuestions } from "@/app/stores/questionStore"; import { useParams, usePathname } from "next/navigation"; @@ -16,7 +15,7 @@ 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, useRef, useState } from "react"; +import { MouseEvent, useState } from "react"; import useOrigin from "@/app/util/hooks/useOrigin"; import { faFacebook, @@ -130,8 +129,12 @@ const ShareBtn = () => { setAnchorEl(ref); props.setAnchorEl(ref); }} - onMouseEnter={props.handleClick} - onMouseLeave={() => props.handleClose()} + onPointerEnter={(e) => { + if (e.pointerType === "mouse") props.handleClick(e); + }} + onPointerLeave={(e) => { + if (e.pointerType === "mouse") props.handleClose(); + }} className="h-[70%]" type="button" onClick={(e) => { From cd08d0fb4587ee30bfd3d5185e3e57c43d285b32 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 14:53:22 -0500 Subject: [PATCH 19/23] updated prisma types for answer option, and modified gql queries to accommodate for this --- studyAi/graphql/schema.graphql | 61 ++++++++++--------- studyAi/prisma/schema.prisma | 9 ++- studyAi/src/app/dashboard/blah.tsx | 35 ----------- .../src/app/library/question/[id]/page.tsx | 16 ++++- .../components/client/answerInputs.tsx | 16 +++-- .../components/client/solutionView.tsx | 9 ++- studyAi/src/app/util/prisma/seedData.ts | 46 ++++++++------ 7 files changed, 94 insertions(+), 98 deletions(-) delete mode 100644 studyAi/src/app/dashboard/blah.tsx 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/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/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/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index 1884bbe5..3c3c37d8 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -36,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(` @@ -49,7 +58,10 @@ const QuestionQueryById = gql(` questionInfo { title description - options + options { + id + value + } } likeCounter { likes diff --git a/studyAi/src/app/library/question/components/client/answerInputs.tsx b/studyAi/src/app/library/question/components/client/answerInputs.tsx index bbc12fa6..c8128a57 100644 --- a/studyAi/src/app/library/question/components/client/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/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,13 @@ 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 +53,13 @@ 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} /> diff --git a/studyAi/src/app/library/question/components/client/solutionView.tsx b/studyAi/src/app/library/question/components/client/solutionView.tsx index 681024a3..baf46523 100644 --- a/studyAi/src/app/library/question/components/client/solutionView.tsx +++ b/studyAi/src/app/library/question/components/client/solutionView.tsx @@ -10,7 +10,10 @@ const getAnswerById = gql(` question(where: { id: $id }) { id answer { - correctAnswer + correctAnswer { + id + value + } } } } @@ -36,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/util/prisma/seedData.ts b/studyAi/src/app/util/prisma/seedData.ts index f61a4faf..1425864e 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'], + 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 () => { From f66b0f1cf16e50b02f58ca8288984719398c213e Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 15:29:59 -0500 Subject: [PATCH 20/23] restructured question id files into seperate directories for maintainability --- studyAi/src/app/library/question/[id]/page.tsx | 8 ++++---- .../{ => answer}/client/answerComponent.tsx | 2 +- .../{ => answer}/client/answerInputs.tsx | 14 +++++++++++--- .../{ => page}/client/navigationBtns.tsx | 0 .../{ => page}/client/questionPageContainer.tsx | 2 +- .../components/{ => page}/server/containerBar.tsx | 0 .../{ => page}/server/questionWrapper.tsx | 4 ++-- .../{ => question}/client/questionComponents.tsx | 4 ++-- .../{ => question}/client/questionsView.tsx | 2 +- .../{ => question}/client/solutionView.tsx | 8 ++++---- .../{ => question}/client/submissionView.tsx | 6 +++--- .../server/questionViewContainer.tsx} | 0 .../components/{ => time}/client/timeForm.tsx | 0 .../components/{ => time}/client/timeModal.tsx | 4 ++-- .../eventHandlers/onTimeEventChangeHandler.tsx | 0 .../{components => }/hooks/useOptionsWithId.tsx | 0 16 files changed, 31 insertions(+), 23 deletions(-) rename studyAi/src/app/library/question/components/{ => answer}/client/answerComponent.tsx (98%) rename studyAi/src/app/library/question/components/{ => answer}/client/answerInputs.tsx (91%) rename studyAi/src/app/library/question/components/{ => page}/client/navigationBtns.tsx (100%) rename studyAi/src/app/library/question/components/{ => page}/client/questionPageContainer.tsx (97%) rename studyAi/src/app/library/question/components/{ => page}/server/containerBar.tsx (100%) rename studyAi/src/app/library/question/components/{ => page}/server/questionWrapper.tsx (63%) rename studyAi/src/app/library/question/components/{ => question}/client/questionComponents.tsx (97%) rename studyAi/src/app/library/question/components/{ => question}/client/questionsView.tsx (99%) rename studyAi/src/app/library/question/components/{ => question}/client/solutionView.tsx (82%) rename studyAi/src/app/library/question/components/{ => question}/client/submissionView.tsx (89%) rename studyAi/src/app/library/question/components/{server/questionComponents.tsx => question/server/questionViewContainer.tsx} (100%) rename studyAi/src/app/library/question/components/{ => time}/client/timeForm.tsx (100%) rename studyAi/src/app/library/question/components/{ => time}/client/timeModal.tsx (97%) rename studyAi/src/app/library/question/{components => }/eventHandlers/onTimeEventChangeHandler.tsx (100%) rename studyAi/src/app/library/question/{components => }/hooks/useOptionsWithId.tsx (100%) diff --git a/studyAi/src/app/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index 3c3c37d8..43513a04 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -1,5 +1,5 @@ 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"; @@ -133,11 +133,11 @@ export async function generateMetadata( url: origin, images: [ { - url: '/logo/logo.png', + url: "/logo/logo.png", width: 800, height: 800, - } - ] + }, + ], }, }; } diff --git a/studyAi/src/app/library/question/components/client/answerComponent.tsx b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx similarity index 98% rename from studyAi/src/app/library/question/components/client/answerComponent.tsx rename to studyAi/src/app/library/question/components/answer/client/answerComponent.tsx index 58c1f06d..2c7edc50 100644 --- a/studyAi/src/app/library/question/components/client/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx @@ -1,7 +1,7 @@ "use client"; import { useParams } from "next/navigation"; import { useQuestions } from "@/app/stores/questionStore"; -import ContainerBar, { Container } from "../server/containerBar"; +import ContainerBar, { Container } from "../../page/server/containerBar"; import { Button, IconButton } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { diff --git a/studyAi/src/app/library/question/components/client/answerInputs.tsx b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx similarity index 91% rename from studyAi/src/app/library/question/components/client/answerInputs.tsx rename to studyAi/src/app/library/question/components/answer/client/answerInputs.tsx index c8128a57..a3f3a57e 100644 --- a/studyAi/src/app/library/question/components/client/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx @@ -10,7 +10,7 @@ import { RadioGroup, } from "@mui/material"; import { ChangeEvent, KeyboardEvent } from "react"; -import { Question } from "../../../../../../graphql/generated/graphql"; +import { Question } from "../../../../../../../graphql/generated/graphql"; const adjustScroll = ( event: ChangeEvent | KeyboardEvent ) => { @@ -39,7 +39,11 @@ const adjustScroll = ( return (element.scrollTop = newScrollPos <= 0 ? 0 : newScrollPos); } }; -export const MultipleChoice = ({ options }: { options: Question['questionInfo']['options'] }) => { +export const MultipleChoice = ({ + options, +}: { + options: Question["questionInfo"]["options"]; +}) => { return ( {options.map((val) => ( @@ -53,7 +57,11 @@ export const MultipleChoice = ({ options }: { options: Question['questionInfo'][ ); }; -export const SelectMultiple = ({ options }: { options: Question['questionInfo']['options'] }) => { +export const SelectMultiple = ({ + options, +}: { + options: Question["questionInfo"]["options"]; +}) => { return ( {options.map((val) => ( 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/client/questionPageContainer.tsx b/studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx similarity index 97% rename from studyAi/src/app/library/question/components/client/questionPageContainer.tsx rename to studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx index 28f222f4..b8c52b4f 100644 --- a/studyAi/src/app/library/question/components/client/questionPageContainer.tsx +++ b/studyAi/src/app/library/question/components/page/client/questionPageContainer.tsx @@ -2,7 +2,7 @@ import React from "react"; import { NavigationBtns, PaginationOptions } from "./navigationBtns"; import { QuestionWrapper } from "../server/questionWrapper"; -import { TimeComponent } from "./timeModal"; +import { TimeComponent } from "../../time/client/timeModal"; import NavigationWrapper from "@/app/util/components/navigation/navigationWrapper"; import FullscreenProvider, { useFullscreen, 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 100% rename from studyAi/src/app/library/question/components/server/containerBar.tsx rename to studyAi/src/app/library/question/components/page/server/containerBar.tsx diff --git a/studyAi/src/app/library/question/components/server/questionWrapper.tsx b/studyAi/src/app/library/question/components/page/server/questionWrapper.tsx similarity index 63% rename from studyAi/src/app/library/question/components/server/questionWrapper.tsx rename to studyAi/src/app/library/question/components/page/server/questionWrapper.tsx index 4ef7c163..a8f61148 100644 --- a/studyAi/src/app/library/question/components/server/questionWrapper.tsx +++ b/studyAi/src/app/library/question/components/page/server/questionWrapper.tsx @@ -1,5 +1,5 @@ -import QuestionComponent from "../client/questionComponents"; -import AnswerComponent from "../client/answerComponent"; +import QuestionComponent from "../../question/client/questionComponents"; +import AnswerComponent from "../../answer/client/answerComponent"; export const QuestionWrapper = () => { 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 97% rename from studyAi/src/app/library/question/components/client/questionComponents.tsx rename to studyAi/src/app/library/question/components/question/client/questionComponents.tsx index 550a0a15..87eee777 100644 --- a/studyAi/src/app/library/question/components/client/questionComponents.tsx +++ b/studyAi/src/app/library/question/components/question/client/questionComponents.tsx @@ -1,5 +1,5 @@ "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 { @@ -14,7 +14,7 @@ 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, diff --git a/studyAi/src/app/library/question/components/client/questionsView.tsx b/studyAi/src/app/library/question/components/question/client/questionsView.tsx similarity index 99% rename from studyAi/src/app/library/question/components/client/questionsView.tsx rename to studyAi/src/app/library/question/components/question/client/questionsView.tsx index 8269714e..10d3b5ec 100644 --- a/studyAi/src/app/library/question/components/client/questionsView.tsx +++ b/studyAi/src/app/library/question/components/question/client/questionsView.tsx @@ -1,5 +1,5 @@ "use client"; -import { Container } from "../server/containerBar"; +import { Container } from "../../page/server/containerBar"; import { Button, Chip, 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 82% rename from studyAi/src/app/library/question/components/client/solutionView.tsx rename to studyAi/src/app/library/question/components/question/client/solutionView.tsx index baf46523..0fe5ac69 100644 --- a/studyAi/src/app/library/question/components/client/solutionView.tsx +++ b/studyAi/src/app/library/question/components/question/client/solutionView.tsx @@ -1,10 +1,10 @@ "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 }) { @@ -40,7 +40,7 @@ const SolutionView = () => { return ( {question?.answer?.correctAnswer.map((e) => { - return( e.value ) + return e.value; })} ); diff --git a/studyAi/src/app/library/question/components/client/submissionView.tsx b/studyAi/src/app/library/question/components/question/client/submissionView.tsx similarity index 89% rename from studyAi/src/app/library/question/components/client/submissionView.tsx rename to studyAi/src/app/library/question/components/question/client/submissionView.tsx index 9b898227..d7b4befa 100644 --- a/studyAi/src/app/library/question/components/client/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 "../server/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( 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 100% rename from studyAi/src/app/library/question/components/server/questionComponents.tsx rename to studyAi/src/app/library/question/components/question/server/questionViewContainer.tsx diff --git a/studyAi/src/app/library/question/components/client/timeForm.tsx b/studyAi/src/app/library/question/components/time/client/timeForm.tsx similarity index 100% rename from studyAi/src/app/library/question/components/client/timeForm.tsx rename to studyAi/src/app/library/question/components/time/client/timeForm.tsx diff --git a/studyAi/src/app/library/question/components/client/timeModal.tsx b/studyAi/src/app/library/question/components/time/client/timeModal.tsx similarity index 97% rename from studyAi/src/app/library/question/components/client/timeModal.tsx rename to studyAi/src/app/library/question/components/time/client/timeModal.tsx index 71f6c4c2..d16b6c98 100644 --- a/studyAi/src/app/library/question/components/client/timeModal.tsx +++ b/studyAi/src/app/library/question/components/time/client/timeModal.tsx @@ -5,7 +5,7 @@ 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 { TimeOptions } from "../../../../../../../prisma/generated/type-graphql"; import TimeForm, { splitTimeStrBy2 } from "./timeForm"; import { unstable_batchedUpdates } from "react-dom"; import { getLocalStorageObj } from "@/app/util/parsers/localStorageWrappers"; @@ -13,7 +13,7 @@ import { TimeProvider, useTimeHook, } from "@/app/util/components/time/context/useTimeContext"; -import onTimeEventChangeHandler from "../eventHandlers/onTimeEventChangeHandler"; +import onTimeEventChangeHandler from "../../../eventHandlers/onTimeEventChangeHandler"; import formatMilliseconds from "@/app/util/parsers/formatMilliseconds"; import { timeLabelData } from "./timeForm"; import removeNonIntegerChars from "@/app/util/parsers/removeNonIntegerChars"; diff --git a/studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx b/studyAi/src/app/library/question/eventHandlers/onTimeEventChangeHandler.tsx similarity index 100% rename from studyAi/src/app/library/question/components/eventHandlers/onTimeEventChangeHandler.tsx rename to studyAi/src/app/library/question/eventHandlers/onTimeEventChangeHandler.tsx 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 From 2a9a8eee1d988ab2c3231619f028377f8886b665 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 15:33:24 -0500 Subject: [PATCH 21/23] fixed metadata title return undefined instead of default question title not found --- studyAi/src/app/library/question/[id]/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/studyAi/src/app/library/question/[id]/page.tsx b/studyAi/src/app/library/question/[id]/page.tsx index 43513a04..c82b2301 100644 --- a/studyAi/src/app/library/question/[id]/page.tsx +++ b/studyAi/src/app/library/question/[id]/page.tsx @@ -12,7 +12,7 @@ const question: Partial & { id: string; questionType: (typeof QuestionTypes)[number]; } = { - id: "6549b35d98536604d74f3b22", + id: "654e89ad8cd9123e2353ab1b", creatorId: "6533f4c7489ef223ffc31a99", questionType: "Short Answer", tags: [ @@ -114,9 +114,9 @@ export async function generateMetadata( 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} - Study AI` ?? - "Question title is not found - Study AI"; + 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; From 47deb0909d4dd62b0a95e785f2fde260281f8d74 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 17:59:10 -0500 Subject: [PATCH 22/23] Modified generate question api route, and part of control route Co-authored-by: ccrump1280 --- .../api/generateQuestion/generateQuestion.ts | 131 +++++++++++++-- .../answer/client/answerComponent.tsx | 2 +- .../components/answer/client/answerInputs.tsx | 2 +- .../questionModal/components/controls.tsx | 156 ++++++++++-------- studyAi/src/app/util/prisma/seedData.ts | 2 +- studyAi/src/app/util/types/UserData.ts | 2 +- 6 files changed, 214 insertions(+), 81 deletions(-) diff --git a/studyAi/src/app/api/generateQuestion/generateQuestion.ts b/studyAi/src/app/api/generateQuestion/generateQuestion.ts index cfd70cb9..f5bf577a 100644 --- a/studyAi/src/app/api/generateQuestion/generateQuestion.ts +++ b/studyAi/src/app/api/generateQuestion/generateQuestion.ts @@ -1,28 +1,137 @@ 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 +143,4 @@ export async function generateQuestion(req: Request) { message: "Something went wrong", }); } -} \ No newline at end of file +} diff --git a/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx index 2c7edc50..611096ca 100644 --- a/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerComponent.tsx @@ -19,7 +19,7 @@ const determineAnswerTitle = (str?: string) => { switch (matchStr) { case "Multiple Choice": return "Select the best answer"; - case "Checkbox": + case "Select Multiple": return "Select all that apply"; case "Short Answer": return "Add your answer below"; diff --git a/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx index a3f3a57e..222fdc95 100644 --- a/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx +++ b/studyAi/src/app/library/question/components/answer/client/answerInputs.tsx @@ -101,7 +101,7 @@ export const AnswerType = () => { switch (questionType) { case "Multiple Choice": return ; - case "Checkbox": + case "Select Multiple": return ; case "Short Answer": return ; diff --git a/studyAi/src/app/util/components/questionModal/components/controls.tsx b/studyAi/src/app/util/components/questionModal/components/controls.tsx index 6a6092de..88e9bb2f 100644 --- a/studyAi/src/app/util/components/questionModal/components/controls.tsx +++ b/studyAi/src/app/util/components/questionModal/components/controls.tsx @@ -5,75 +5,69 @@ import { SetStateAction, useState } from "react"; import { gql } from "../../../../../../graphql/generated"; import { useSession } from "next-auth/react"; import { useMutation } from "@apollo/client"; -import Switch from '@mui/material/Switch'; +import Switch from "@mui/material/Switch"; const generateQuestion = async ( questionData: Partial, 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/prisma/seedData.ts b/studyAi/src/app/util/prisma/seedData.ts index 1425864e..5be7b7c4 100644 --- a/studyAi/src/app/util/prisma/seedData.ts +++ b/studyAi/src/app/util/prisma/seedData.ts @@ -48,7 +48,7 @@ const questions: Omit[] = [ private: false, }, { - questionType: "Checkbox", + questionType: "Select Multiple", tags: ["Science", "Chemistry"], questionInfo: { title: "Molecular Compound", diff --git a/studyAi/src/app/util/types/UserData.ts b/studyAi/src/app/util/types/UserData.ts index dc32577d..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 = [ "Multiple Choice", - "Checkbox", + "Select Multiple", "Short Answer", ] as const; export type UserInfo = User; From e3bfba03942569088009c5923f2c0891f8b71e3b Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Fri, 10 Nov 2023 18:29:25 -0500 Subject: [PATCH 23/23] adjust question title from prompt moving to new line --- studyAi/src/app/api/generateQuestion/generateQuestion.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/studyAi/src/app/api/generateQuestion/generateQuestion.ts b/studyAi/src/app/api/generateQuestion/generateQuestion.ts index f5bf577a..cd2f63c4 100644 --- a/studyAi/src/app/api/generateQuestion/generateQuestion.ts +++ b/studyAi/src/app/api/generateQuestion/generateQuestion.ts @@ -113,8 +113,7 @@ export async function generateQuestion(req: Request) { ", " )} - Then, store the new question description as a string called 'QuestionDescription', the new question title as a string - 'QuestionTitle', ${determineVariableTypeTemplateStr(type, numberOfOptions)} + 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)}`;