From 55a1129bfe1ce383b0aff73c7bf2b7edd50b8ee6 Mon Sep 17 00:00:00 2001 From: aasmal97 <74555081+aasmal97@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:26:52 -0400 Subject: [PATCH 01/24] added logout button to navbar, mobile and desktop --- studyAi/src/app/page.tsx | 1 - .../navigation/client/authentication.tsx | 40 ++++++++++++++++++- .../navigation/client/mobileNavbar.tsx | 14 +++---- .../navigation/client/userProfile.tsx | 13 +++++- .../navigation/server/desktopNavbar.tsx | 2 +- 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/studyAi/src/app/page.tsx b/studyAi/src/app/page.tsx index cd0a6863..a4f0bf0a 100644 --- a/studyAi/src/app/page.tsx +++ b/studyAi/src/app/page.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; import NavigationWrapper from "./util/components/navigation/navigationWrapper"; - export default function Home() { return ( ( ); }; - +export const LogoutBtn = ( + props: { + icon?: boolean; + } & ButtonProps +) => { + const className = props.className; + const classStyles = + (className ? className : "") + "flex w-full [&>*]:rounded-none"; + const newProps: ButtonProps = { + ...props, + className: classStyles, + sx: props.sx ? props.sx : {}, + }; + const onLogout = () => {}; + return ( + + ); +}; const AuthenticationNav = ({ classNames, authBtnClassNames, diff --git a/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx b/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx index 953645a8..2b9f2ff3 100644 --- a/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx +++ b/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBars, faClose } from "@fortawesome/free-solid-svg-icons"; import dynamic from "next/dynamic"; import AuthenticationNav, { + LogoutBtn, RecursiveClassNames, } from "../client/authentication"; import { NavButtons } from "../server/desktopNavbar"; @@ -61,17 +62,11 @@ const authBtnClassNames: RecursiveClassNames = { }, }, }; -// const userProfClassNames = { -// value: null, -// container: { -// value: "", -// }, -// }; const MobileNavbar = () => { const isLoggedIn = true; return ( -
+
{isLoggedIn && ( { authBtnClassNames={authBtnClassNames} /> )} + {isLoggedIn && ( +
+ +
+ )}
); diff --git a/studyAi/src/app/util/components/navigation/client/userProfile.tsx b/studyAi/src/app/util/components/navigation/client/userProfile.tsx index e15b1655..78200eb0 100644 --- a/studyAi/src/app/util/components/navigation/client/userProfile.tsx +++ b/studyAi/src/app/util/components/navigation/client/userProfile.tsx @@ -4,7 +4,7 @@ import { UserInfo } from "../../../types/UserData"; import useElementPosition from "@/app/util/hooks/useElementSize"; import useDropdown from "@/app/util/hooks/useDropdown"; import useRemToPixel from "@/app/util/hooks/useRemToPixel"; -import { RecursiveClassNames } from "./authentication"; +import { LogoutBtn, RecursiveClassNames } from "./authentication"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretDown, faChartLine } from "@fortawesome/free-solid-svg-icons"; import { faFileLines, faUserCircle } from "@fortawesome/free-regular-svg-icons"; @@ -126,6 +126,17 @@ export const ProfileDropdown = ({ ))} + + + ); }; diff --git a/studyAi/src/app/util/components/navigation/server/desktopNavbar.tsx b/studyAi/src/app/util/components/navigation/server/desktopNavbar.tsx index c9aa57e8..b353e766 100644 --- a/studyAi/src/app/util/components/navigation/server/desktopNavbar.tsx +++ b/studyAi/src/app/util/components/navigation/server/desktopNavbar.tsx @@ -15,7 +15,6 @@ const links = [ }, ]; - export const NavbarLinks = ({ children, }: { @@ -41,6 +40,7 @@ export const NavButtons = () => { ); }; + const authBtnClassNames: RecursiveClassNames = { value: null, container: { From fe23d049f637d4b766161bab5a0f7c4def5e6644 Mon Sep 17 00:00:00 2001 From: aasmal97 <74555081+aasmal97@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:15:43 -0400 Subject: [PATCH 02/24] added generate dropdown links in mobile sidebar, and adpated user profile styles to match low-fidelity figma design --- .../auth/components/client/authWrapper.tsx | 2 +- .../navigation/client/authentication.tsx | 8 +-- .../navigation/client/desktopNavbar.tsx | 6 +- .../navigation/client/mobileNavbar.tsx | 23 +++++++- .../navigation/client/userProfile.tsx | 58 +++++++++++-------- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/studyAi/src/app/auth/components/client/authWrapper.tsx b/studyAi/src/app/auth/components/client/authWrapper.tsx index f81c6d6e..1ea09a46 100644 --- a/studyAi/src/app/auth/components/client/authWrapper.tsx +++ b/studyAi/src/app/auth/components/client/authWrapper.tsx @@ -14,7 +14,7 @@ const AuthPageWrapper = ({ children }: { children: React.ReactNode }) => { return (
{ const isLoggedIn: boolean = true; const windowWidth = useWindowWidth(); const containerClassNames = - "flex flex-col space-y-4 xs:space-0 xs:items-center xs:justify-end xs:h-full xs:flex-row xs:grow" + + "flex flex-col xs:items-center xs:justify-end xs:h-full xs:flex-row xs:grow" + " " + (classNames ? classNames : ""); return ( @@ -105,7 +105,7 @@ const AuthenticationNav = ({ {isLoggedIn ? ( 480} - userProfClassNames={userProfClassNames} + // userProfClassNames={userProfClassNames} /> ) : ( diff --git a/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx b/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx index f865f5e2..67acf152 100644 --- a/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx +++ b/studyAi/src/app/util/components/navigation/client/desktopNavbar.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; -import { MenuItem, Menu, Link, Button } from "@mui/material"; +import { MenuItem, Menu, Link } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretDown, @@ -10,9 +10,7 @@ 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 dynamic from "next/dynamic"; - -const menuItemLinks = [ +export const menuItemLinks = [ { href: "/", text: "Create Question", diff --git a/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx b/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx index 2b9f2ff3..0719e251 100644 --- a/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx +++ b/studyAi/src/app/util/components/navigation/client/mobileNavbar.tsx @@ -9,6 +9,8 @@ import AuthenticationNav, { RecursiveClassNames, } from "../client/authentication"; import { NavButtons } from "../server/desktopNavbar"; +import { menuItemLinks } from "./desktopNavbar"; +import NextLink from "next/link"; const Drawer = dynamic(() => import("./drawer"), { ssr: false }); export const NavDrawer = ({ children }: { children: React.ReactNode }) => { const [open, setOpen] = React.useState(false); @@ -43,7 +45,7 @@ export const NavDrawer = ({ children }: { children: React.ReactNode }) => {
); }; -export const AuthForm = () => { +export const AuthForm = ({ type }: { type: "login" | "signup" }) => { + const router = useRouter(); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); //grab uncontrolled inputs here const formData = new FormData(e.currentTarget); const data = Object.fromEntries(formData.entries()); - const { email, password } = data; + const { email, password, name } = data; const creds = { + name: name.toString(), email: email.toString(), password: password.toString(), }; - signIn("credentials", { ...creds, redirect: false }).then((callback) => { - if (callback?.error) { - console.error(callback.error); - } - - if (callback?.ok && !callback?.error) { - console.log("Logged in successfully!"); - } - }); + switch (type) { + case "login": + const loginRes = await onEmailSign(creds); + if (loginRes?.error) { + console.error(loginRes.error); + } + if (loginRes?.ok && !loginRes?.error) { + console.log("Logged in successfully!"); + router.push("/dashboard"); + } + return; + case "signup": + const res = await axios({ + method: "POST", + url: "/api/user", + data: { + ...creds, + }, + }); + if (res.status === 201) router.push("/auth/login"); + else console.error("Registration Failed"); + break; + } }; return (
{ onSubmit={onSubmit} >
+ { className: "text-Black text-sm tracking-tight underline", }} /> - +
); diff --git a/studyAi/src/app/auth/context/AuthContext.tsx b/studyAi/src/app/auth/context/AuthContext.tsx index 28f416ba..43a4a5f2 100644 --- a/studyAi/src/app/auth/context/AuthContext.tsx +++ b/studyAi/src/app/auth/context/AuthContext.tsx @@ -1,12 +1,6 @@ -"use client" +import { SessionProvider } from "next-auth/react"; +import React from "react"; -import { SessionProvider } from "next-auth/react" -import React from "react" - -export default function Provider({children} : { children: React.ReactNode }) { - return ( - - {children} - - ) +export default function Provider({ children }: { children: React.ReactNode }) { + return {children}; } diff --git a/studyAi/src/app/util/prisma/helpers.ts b/studyAi/src/app/util/prisma/helpers.ts new file mode 100644 index 00000000..093ae787 --- /dev/null +++ b/studyAi/src/app/util/prisma/helpers.ts @@ -0,0 +1,33 @@ +import { PrismaClient } from "@prisma/client/edge"; +import { + PrismaTypeMap, + PrismaTypeMapAsGeneric, + assertPrismaModel, +} from "@/app/util/prisma/typeGuards"; +export const prismaDb = new PrismaClient(); +export async function findUniqueByEmail( + email: string, + collection: K +): Promise | null> { + const col = prismaDb[collection] as any; + const user = col.findUnique({ + where: { + email: email, + }, + }); + const docType = assertPrismaModel(collection, user); + return docType; +} +export async function findUniqueById( + id: string, + collection: K +): Promise | null> { + const col = prismaDb[collection] as any; + const doc = await col.findUnique({ + where: { + id: id, + }, + }); + const docType = assertPrismaModel(collection, doc); + return docType; +} diff --git a/studyAi/src/app/util/prisma/typeGuards.ts b/studyAi/src/app/util/prisma/typeGuards.ts new file mode 100644 index 00000000..91754be4 --- /dev/null +++ b/studyAi/src/app/util/prisma/typeGuards.ts @@ -0,0 +1,134 @@ +import { + User, + UserCredentials, + Question, + QuestionLikes, + Submissions, + Quiz, + QuizLikes, +} from "@prisma/client/edge"; +export type PrismaTypeMap = { + user: User; + userCredentials: UserCredentials; + question: Question; + questionLikes: QuestionLikes; + submissions: Submissions; + quiz: Quiz; + quizLikes: QuizLikes; +}; +export type PrismaTypeMapAsGeneric = { + [P in K]: PrismaTypeMap[P]; +}[K]; +function typeWrapper(func: () => boolean) { + try { + return func(); + } catch (e) { + return false; + } +} +export function isUser(obj: any): obj is User { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + typeof obj.name === "string" && + typeof obj.email === "string" && + typeof obj.usersReached === "number" && + obj.dateCreated instanceof Date + ); +} +export function isUserCreds(obj: any): obj is UserCredentials { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + typeof obj.userId === "string" && + typeof obj.email === "string" && + typeof obj.provider === "string" + ); +} +export function isQuestion(obj: any): obj is Question { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + typeof obj.creatorId === "string" && + typeof obj.type === "string" && + obj.dateCreated instanceof Date && + typeof obj.question === "object" && + typeof obj.question.title === "string" && + typeof obj.question.description === "string" && + typeof obj.answer === "object" && + typeof obj.answer.answer === "string" && + typeof obj.likeCounter === "object" && + typeof obj.likeCounter.likes === "number" && + typeof obj.likeCounter.dislikes === "number" + ); +} +export function isQuestionLikes(obj: any): obj is QuestionLikes { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + obj.dateCreated instanceof Date && + typeof obj.userId === "string" && + typeof obj.questionId === "string" + ); +} +export function isSubmissions(obj: any): obj is Submissions { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + obj.time instanceof Date && + typeof obj.score === "number" && + obj.dateCreated instanceof Date && + typeof obj.userId === "string" && + typeof obj.quizId === "string" + ); +} +export function isQuiz(obj: any): obj is Quiz { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + typeof obj.name === "string" && + typeof obj.tags === "string" && + typeof obj.likes === "number" && + typeof obj.creatorId === "string" && + obj.dateCreated instanceof Date + ); +} +export function isQuizLikes(obj: any): obj is QuizLikes { + return typeWrapper( + () => + typeof obj === "object" && + typeof obj.id === "string" && + obj.dateCreated instanceof Date && + typeof obj.userId === "string" && + typeof obj.questionId === "string" + ); +} +export const assertPrismaModel = ( + collection: K, + doc: PrismaTypeMapAsGeneric +): PrismaTypeMapAsGeneric | null => { + switch (collection) { + case "user": + return isUser(doc) ? doc : null; + case "userCredentials": + return isUserCreds(doc) ? doc : null; + case "question": + return isQuestion(doc) ? doc : null; + case "questionLikes": + return isQuestionLikes(doc) ? doc : null; + case "submissions": + return isSubmissions(doc) ? doc : null; + case "quiz": + return isQuiz(doc) ? doc : null; + case "quizLikes": + return isQuizLikes(doc) ? doc : null; + default: + return null; + } +}; diff --git a/studyAi/src/app/util/prisma/types.ts b/studyAi/src/app/util/prisma/types.ts new file mode 100644 index 00000000..55cb0433 --- /dev/null +++ b/studyAi/src/app/util/prisma/types.ts @@ -0,0 +1,12 @@ +import { + PrismaClient, +} from "@prisma/client/edge"; +export type IgnorePrismaBuiltins = string extends S + ? string + : S extends "" + ? S + : S extends `$${infer T}` + ? never + : S; +export type PrismaKeys = Exclude; +export type PrismaModelName = IgnorePrismaBuiltins; From 0eae2cf3a058287f85bc62c2b8f701c705837974 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 19 Oct 2023 18:15:46 -0400 Subject: [PATCH 04/24] added session callback and appropiate types --- .../src/app/api/auth/[...nextauth]/options.ts | 11 +++++++++++ .../components/navigation/client/userProfile.tsx | 10 ++++------ studyAi/src/app/util/types/UserData.ts | 9 +++------ studyAi/src/app/util/types/nextauth.d.ts | 16 ++++++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/studyAi/src/app/api/auth/[...nextauth]/options.ts b/studyAi/src/app/api/auth/[...nextauth]/options.ts index dfc71df6..cfcead8d 100644 --- a/studyAi/src/app/api/auth/[...nextauth]/options.ts +++ b/studyAi/src/app/api/auth/[...nextauth]/options.ts @@ -68,6 +68,17 @@ export const options: NextAuthOptions = { newUser: "/../../../auth/signup/page", }, callbacks: { + async session({ session }) { + const sessionCreds = await findUniqueByEmail( + session.user.email, + "userCredentials" + ); + if (!sessionCreds) return session; + const sessionUser = await findUniqueById(sessionCreds.userId, "user"); + if (!sessionUser) return session; + session.user = sessionUser; + return session; + }, //create a user document on oauth sign in async signIn({ profile }) { if (!profile) return true; diff --git a/studyAi/src/app/util/components/navigation/client/userProfile.tsx b/studyAi/src/app/util/components/navigation/client/userProfile.tsx index ec27528e..4a6888f6 100644 --- a/studyAi/src/app/util/components/navigation/client/userProfile.tsx +++ b/studyAi/src/app/util/components/navigation/client/userProfile.tsx @@ -28,8 +28,7 @@ const userItemLinks = (userId?: string) => [ ]; const UserProfile = ({ showUserInfo = false, - first_name, - last_name, + name, email, }: { showUserInfo?: boolean; @@ -46,12 +45,12 @@ const UserProfile = ({ : undefined } > - {first_name?.[0].toUpperCase()} + {name?.[0].toUpperCase()} {showUserInfo && (
- {first_name && last_name && first_name + " " + last_name} + {name && name} {email} @@ -159,8 +158,7 @@ export const UserProfileNav = ({ } = { showUserInfo: false, email: "arkyasmal@gmail.com", - first_name: "Arky", - last_name: "Asmal", + name: "Arky Asmal", id: "XXXXXXXXXXXXXXXXXXX", }; return ( diff --git a/studyAi/src/app/util/types/UserData.ts b/studyAi/src/app/util/types/UserData.ts index cbac80b9..a00f7c7c 100644 --- a/studyAi/src/app/util/types/UserData.ts +++ b/studyAi/src/app/util/types/UserData.ts @@ -1,6 +1,3 @@ -export interface UserInfo { - id: string; - email: string; - first_name: string; - last_name: string; -} +import { User } from "@prisma/client"; + +export type UserInfo = User \ No newline at end of file diff --git a/studyAi/src/app/util/types/nextauth.d.ts b/studyAi/src/app/util/types/nextauth.d.ts index 6058c98d..63c957d8 100644 --- a/studyAi/src/app/util/types/nextauth.d.ts +++ b/studyAi/src/app/util/types/nextauth.d.ts @@ -1,12 +1,8 @@ -import NextAuth, { DefaultSession } from "next-auth" -import { User, Session } from "next-auth" - +import NextAuth, { DefaultSession } from "next-auth"; +import { Session } from "next-auth"; +import { User } from "@prisma/client"; declare module "next-auth" { - interface Session { - user: { - id: string - email: string - password: string - } - } + interface Session { + user: User; + } } From 5ff13e41ded3efbfc31234b87e96226c0370c551 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 19 Oct 2023 18:21:10 -0400 Subject: [PATCH 05/24] removed window width state value from hook --- studyAi/src/app/util/hooks/useWindowWidth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studyAi/src/app/util/hooks/useWindowWidth.tsx b/studyAi/src/app/util/hooks/useWindowWidth.tsx index 4cbcd5d9..81b595ac 100644 --- a/studyAi/src/app/util/hooks/useWindowWidth.tsx +++ b/studyAi/src/app/util/hooks/useWindowWidth.tsx @@ -4,7 +4,7 @@ import { debounce } from "lodash"; const useWindowWidth = () => { const [windowWidth, setWindowWidth] = useState(0); useEffect(() => { - if(windowWidth === 0 ) setWindowWidth(window.innerWidth); + setWindowWidth(window.innerWidth); const handleResize = debounce(() => { setWindowWidth(window.innerWidth); }, 500); From d6e44ced2f6f62100658dc57b0a9fca4e61dd800 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 19 Oct 2023 18:26:40 -0400 Subject: [PATCH 06/24] changed imports to next router to next naviagation router --- studyAi/src/app/auth/components/client/authForm.tsx | 2 +- studyAi/src/app/auth/context/AuthContext.tsx | 1 + studyAi/src/app/auth/login/page.tsx | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/studyAi/src/app/auth/components/client/authForm.tsx b/studyAi/src/app/auth/components/client/authForm.tsx index 9bf1fd03..24f3b22e 100644 --- a/studyAi/src/app/auth/components/client/authForm.tsx +++ b/studyAi/src/app/auth/components/client/authForm.tsx @@ -3,7 +3,7 @@ import { TextFieldInput } from "@/app/auth/components/server/formInputs"; import { Button } from "@mui/material"; import { signIn } from "next-auth/react"; import axios from "axios"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; const onGoogleSign = async () => await signIn("google"); const onEmailSign = async (creds: { email: string; password: string }) => { const signInData = await signIn("credentials", { ...creds, redirect: false }); diff --git a/studyAi/src/app/auth/context/AuthContext.tsx b/studyAi/src/app/auth/context/AuthContext.tsx index 43a4a5f2..cbd701f1 100644 --- a/studyAi/src/app/auth/context/AuthContext.tsx +++ b/studyAi/src/app/auth/context/AuthContext.tsx @@ -1,3 +1,4 @@ +"use client" import { SessionProvider } from "next-auth/react"; import React from "react"; diff --git a/studyAi/src/app/auth/login/page.tsx b/studyAi/src/app/auth/login/page.tsx index 1fefaf82..e27fd205 100644 --- a/studyAi/src/app/auth/login/page.tsx +++ b/studyAi/src/app/auth/login/page.tsx @@ -1,8 +1,7 @@ "use client"; import AuthPage from "../components/authPageWrapper"; -import { useState, useEffect } from "react"; -import { signIn, useSession } from "next-auth/react"; +import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; From b09025e5aeb155cbd9de0e86567da231efad62f1 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Thu, 19 Oct 2023 18:45:56 -0400 Subject: [PATCH 07/24] Add signout button to navbar and add session hook to navbar components --- .../components/navigation/client/authentication.tsx | 12 ++++++++---- .../components/navigation/client/mobileNavbar.tsx | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/studyAi/src/app/util/components/navigation/client/authentication.tsx b/studyAi/src/app/util/components/navigation/client/authentication.tsx index 5cd4a052..43742672 100644 --- a/studyAi/src/app/util/components/navigation/client/authentication.tsx +++ b/studyAi/src/app/util/components/navigation/client/authentication.tsx @@ -1,5 +1,6 @@ "use client"; import NextLink from "next/link"; +import { signOut, useSession } from "next-auth/react"; import { UserProfileNav } from "@/app/util/components/navigation/client/userProfile"; import useWindowWidth from "@/app/util/hooks/useWindowWidth"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -59,12 +60,14 @@ export const LogoutBtn = ( const className = props.className; const classStyles = (className ? className : "") + "flex w-full [&>*]:rounded-none"; + const domProps = { ...props } + delete domProps.icon; const newProps: ButtonProps = { - ...props, + ...domProps, className: classStyles, sx: props.sx ? props.sx : {}, }; - const onLogout = () => {}; + const onLogout = () => signOut(); return ( + + +
+ ); +} diff --git a/studyAi/src/app/library/question/components/server/answerContainer.tsx b/studyAi/src/app/library/question/components/server/answerContainer.tsx new file mode 100644 index 00000000..54c924ef --- /dev/null +++ b/studyAi/src/app/library/question/components/server/answerContainer.tsx @@ -0,0 +1,4 @@ +const AnswerContainer = () => { + return <> +}; +export default AnswerContainer; diff --git a/studyAi/src/app/library/question/components/server/questionContainer.tsx b/studyAi/src/app/library/question/components/server/questionContainer.tsx new file mode 100644 index 00000000..a0cf8ad3 --- /dev/null +++ b/studyAi/src/app/library/question/components/server/questionContainer.tsx @@ -0,0 +1,4 @@ +export const QuestionContainer = () => { + return <> +} +export default QuestionContainer \ No newline at end of file diff --git a/studyAi/src/app/library/question/page.tsx b/studyAi/src/app/library/question/page.tsx deleted file mode 100644 index e0298e7f..00000000 --- a/studyAi/src/app/library/question/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function QuestionPage() { - return <>; -} diff --git a/studyAi/src/app/page.tsx b/studyAi/src/app/page.tsx index a4f0bf0a..98de47e3 100644 --- a/studyAi/src/app/page.tsx +++ b/studyAi/src/app/page.tsx @@ -14,7 +14,7 @@ export default function Home() { Dashboard Exams Library Questions Library - Questions Page + Questions Page Exam Page ); diff --git a/studyAi/src/app/util/components/navigation/client/userProfile.tsx b/studyAi/src/app/util/components/navigation/client/userProfile.tsx index 109951ff..77f2beb8 100644 --- a/studyAi/src/app/util/components/navigation/client/userProfile.tsx +++ b/studyAi/src/app/util/components/navigation/client/userProfile.tsx @@ -31,6 +31,7 @@ const UserProfile = ({ showUserInfo = false, name, email, + image }: { showUserInfo?: boolean; } & Partial) => { @@ -40,6 +41,7 @@ const UserProfile = ({ 0 ? { width: avatarPos?.width } diff --git a/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx new file mode 100644 index 00000000..c5d2aa02 --- /dev/null +++ b/studyAi/src/app/util/components/time/hooks/useTimeHook.tsx @@ -0,0 +1,45 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +const useTimeHook = ({ + initialTime, + callback, +}: { + initialTime: number; + callback?: (time: number) => void; +}) => { + const [time, setTime] = useState(initialTime); + const [paused, setPause] = useState(true); + const updateTimeActionIntervalRef = useRef(null); + const intervalRef = useRef(null); + const mounted = useRef(true); + useEffect(() => { + mounted.current = true; + //clean up any side effects so we dont cause a memory leak + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + mounted.current = false; + }; + }, []); + const stopTimer = () => { + setPause(true); + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) { + //update with curr time value + if (callback) callback(time); + clearInterval(updateTimeActionIntervalRef.current); + } + }; + return { + time, + paused, + setPause, + stopTimer, + setTime, + updateTimeActionIntervalRef, + intervalRef, + mounted, + }; +}; +export default useTimeHook; diff --git a/studyAi/src/app/util/components/time/stopwatch.tsx b/studyAi/src/app/util/components/time/stopwatch.tsx new file mode 100644 index 00000000..9eff0fd8 --- /dev/null +++ b/studyAi/src/app/util/components/time/stopwatch.tsx @@ -0,0 +1,63 @@ +'use client' +import formatMilliseconds from "../../parsers/formatMilliseconds"; +import useTimeHook from "./hooks/useTimeHook"; +import TimeControlsWrapper from "./timeControls"; + +const StopWatch = ({ + initialTimeUsed, + updateTimeAction, +}: { + updateTimeAction?: () => void; + initialTimeUsed: number; +}) => { + const { + time, + stopTimer, + setTime, + updateTimeActionIntervalRef, + intervalRef, + mounted, + paused, + setPause, + } = useTimeHook({ + initialTime: initialTimeUsed, + callback: (time) => { + if (updateTimeAction) updateTimeAction(); + }, + }); + const startTimer = () => { + setPause(false); + intervalRef.current = setInterval(() => { + if (!mounted.current) return; + setTime((prevTime) => { + return prevTime + 1000; + }); + }, 1000); + updateTimeActionIntervalRef.current = setInterval(() => { + if (!mounted.current) return; + //keep this slower occuring action in sync with locally changing one + if (!intervalRef.current && updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + //update below function with time value + if (updateTimeAction) updateTimeAction(); + }, 5000); + }; + const resetTimer = () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + setTime(0); + }; + return ( + + {formatMilliseconds(time)} + + ); +}; + +export default StopWatch; \ No newline at end of file diff --git a/studyAi/src/app/util/components/time/timeControls.tsx b/studyAi/src/app/util/components/time/timeControls.tsx new file mode 100644 index 00000000..dee57a16 --- /dev/null +++ b/studyAi/src/app/util/components/time/timeControls.tsx @@ -0,0 +1,50 @@ +"use client"; +import { Button } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowsRotate, faPause } from "@fortawesome/free-solid-svg-icons"; +import TimerIcon from "../../icons/timerIcon"; +const TimeControlsWrapper = ({ + children, + paused, + startTimer, + resetTimer, + stopTimer, +}: { + children: React.ReactNode; + startTimer: () => void; + resetTimer: () => void; + stopTimer: () => void; + paused: boolean; +}) => { + return ( +
+ + +
+ ); +}; +export default TimeControlsWrapper; \ No newline at end of file diff --git a/studyAi/src/app/util/components/time/timer.tsx b/studyAi/src/app/util/components/time/timer.tsx new file mode 100644 index 00000000..7263b4ed --- /dev/null +++ b/studyAi/src/app/util/components/time/timer.tsx @@ -0,0 +1,79 @@ +//we store state in react-sweet state +"use client"; +import formatMilliseconds from "../../parsers/formatMilliseconds"; +import useTimeHook from "./hooks/useTimeHook"; +import TimeControlsWrapper from "./timeControls"; + +const Timer = ({ + initialTimeLeft, + updateTimeAction, + totalTimeGiven, +}: { + updateTimeAction?: () => void; + initialTimeLeft: number; + totalTimeGiven?: number; +}) => { + const { + time, + stopTimer, + setTime, + updateTimeActionIntervalRef, + intervalRef, + mounted, + setPause, + paused, + } = useTimeHook({ + initialTime: initialTimeLeft, + callback: (time) => { + if (updateTimeAction) updateTimeAction(); + }, + }); + const startTimer = () => { + setPause(false); + //we change local state every second, as a balance between performance and accuracy + intervalRef.current = setInterval(() => { + if (!mounted.current) return; + setTime((prevTime) => { + const newTime = prevTime - 1000; + if (newTime > 0) return newTime; + if (newTime <= 0 && intervalRef.current) { + setPause(true); + clearInterval(intervalRef.current); + } + return 0; + }); + }, 1000); + updateTimeActionIntervalRef.current = setInterval( + () => { + if (!mounted.current) return; + //keep this slower occuring action in sync with locally changing one + if (!intervalRef.current && updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + //update below function with time value + if (updateTimeAction) updateTimeAction(); + }, + //we update every 5 second to local state (as updating local storage is a costly computation due to stringification) + initialTimeLeft < 5000 ? initialTimeLeft : 5000 + ); + }; + const resetTimer = () => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (updateTimeActionIntervalRef.current) + clearInterval(updateTimeActionIntervalRef.current); + setTime(totalTimeGiven ? totalTimeGiven : 100000); + }; + return ( +
+ + {formatMilliseconds(time)} + +
+ ); +}; + +export default Timer; diff --git a/studyAi/src/app/util/icons/timerIcon.tsx b/studyAi/src/app/util/icons/timerIcon.tsx new file mode 100644 index 00000000..9cd814b7 --- /dev/null +++ b/studyAi/src/app/util/icons/timerIcon.tsx @@ -0,0 +1,23 @@ +import { SVGProps } from "react"; + +export const TimerIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; +export default TimerIcon; diff --git a/studyAi/src/app/util/parsers/formatMilliseconds.tsx b/studyAi/src/app/util/parsers/formatMilliseconds.tsx new file mode 100644 index 00000000..f9adfc11 --- /dev/null +++ b/studyAi/src/app/util/parsers/formatMilliseconds.tsx @@ -0,0 +1,11 @@ +function formatMilliseconds(milliseconds: number): string { + const totalSeconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + const formattedMinutes = String(minutes).padStart(2, "0"); + const formattedSeconds = String(seconds).padStart(2, "0"); + + return `${formattedMinutes}:${formattedSeconds}`; +} +export default formatMilliseconds; \ No newline at end of file From 5ff08b5fef21b4a20f723ea6185133b77d358bc8 Mon Sep 17 00:00:00 2001 From: Arky Asmal Date: Sun, 22 Oct 2023 03:06:47 -0400 Subject: [PATCH 19/24] modified icon inside timers, so width and height are stable during transitions --- .../question/components/client/navigationBtns.tsx | 4 +++- studyAi/src/app/util/components/time/stopwatch.tsx | 5 +++-- studyAi/src/app/util/components/time/timeControls.tsx | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/studyAi/src/app/library/question/components/client/navigationBtns.tsx b/studyAi/src/app/library/question/components/client/navigationBtns.tsx index 6038ed6d..31d29a8d 100644 --- a/studyAi/src/app/library/question/components/client/navigationBtns.tsx +++ b/studyAi/src/app/library/question/components/client/navigationBtns.tsx @@ -1,5 +1,6 @@ "use client"; +import StopWatch from "@/app/util/components/time/stopwatch"; import Timer from "@/app/util/components/time/timer"; import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -18,7 +19,8 @@ export default function NavigationBtns() { Back - + {/* */} + {/* */} @@ -25,9 +27,10 @@ export default function NavigationBtns() { size={"medium"} variant="outlined" className="flex space-x-2 justify-center items-center h-full" - sx={{ textTransform: "none" }} + sx={{ textTransform: "none", minWidth: "unset" }} + aria-label="Go to next question" > - Next + {windowWidth > 480 && Next}
diff --git a/studyAi/src/app/util/components/navigation/navigationWrapper.tsx b/studyAi/src/app/util/components/navigation/navigationWrapper.tsx index f41cc673..9d52af50 100644 --- a/studyAi/src/app/util/components/navigation/navigationWrapper.tsx +++ b/studyAi/src/app/util/components/navigation/navigationWrapper.tsx @@ -3,8 +3,10 @@ import Navbar from "./client/navbar"; const NavigationWrapper = ({ children, appBars, + usePadding = false, }: { children: React.ReactNode; + usePadding?: boolean; appBars?: { navbar: boolean; footer: boolean; @@ -16,12 +18,19 @@ const NavigationWrapper = ({ <> {appBars.navbar && }
- {children} + {usePadding && ( +
+ {children} +
+ )} + {!usePadding && children}
{appBars.footer &&