From f88db929f424d603d1d3c1e700762aacc193559f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 14 Apr 2023 12:34:56 +0100 Subject: [PATCH] Implemented a simple page to view the currently registered users --- src/components/Exercises/FillBlanks.tsx | 15 +- src/components/Exercises/WriteBlanks.tsx | 2 +- src/components/Navbar.tsx | 5 +- src/components/ProfileCard.tsx | 4 +- src/components/Solutions/FillBlanks.tsx | 2 +- src/components/Solutions/WriteBlanks.tsx | 2 +- src/exams/Reading.tsx | 20 +-- src/exams/Writing.tsx | 2 +- src/hooks/useUsers.tsx | 19 +++ src/interfaces/user.ts | 9 +- src/pages/api/users/list.ts | 26 +++ src/pages/exam/index.tsx | 2 +- src/pages/login.tsx | 4 +- src/pages/profile.tsx | 49 ++++++ src/pages/users.tsx | 103 ++++++++++++ src/resources/permissions.ts | 201 +++++++++++++++++++++++ 16 files changed, 433 insertions(+), 32 deletions(-) create mode 100644 src/hooks/useUsers.tsx create mode 100644 src/pages/api/users/list.ts create mode 100644 src/pages/profile.tsx create mode 100644 src/pages/users.tsx create mode 100644 src/resources/permissions.ts diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index 74f114a3..6172b32b 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -110,13 +110,20 @@ export default function FillBlanks({id, allowRepetition, prompt, solutions, text setCurrentBlankId(undefined); }} /> - {prompt} + + {prompt.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} +
- {text.split("\n").map((line) => ( - <> + {text.split("\\n").map((line, index) => ( + {renderLines(line)}
- +
))}
diff --git a/src/components/Exercises/WriteBlanks.tsx b/src/components/Exercises/WriteBlanks.tsx index ab286d2b..ea893bdc 100644 --- a/src/components/Exercises/WriteBlanks.tsx +++ b/src/components/Exercises/WriteBlanks.tsx @@ -79,7 +79,7 @@ export default function WriteBlanks({id, prompt, maxWords, solutions, text, onNe
{prompt} - {text.split("\n").map((line) => ( + {text.split("\\n").map((line) => ( <> {renderLines(line)}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index cabaad95..581c6733 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -35,10 +35,9 @@ export default function Navbar({profilePicture}: Props) {
  • - + Profile - New - +
  • Settings diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index e7ebe28c..40d97eb6 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -17,9 +17,7 @@ export default function ProfileCard({user, className}: Props) { Profile picture
- - {user.name.first} {user.name.last} - + {user.name}
diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index ca0142c5..86fafc1d 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -47,7 +47,7 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
{prompt} - {text.split("\n").map((line) => ( + {text.split("\\n").map((line) => ( <> {renderLines(line)}
diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx index d3ecddd0..4dc3647a 100644 --- a/src/components/Solutions/WriteBlanks.tsx +++ b/src/components/Solutions/WriteBlanks.tsx @@ -82,7 +82,7 @@ export default function WriteBlanksSolutions({
{prompt} - {text.split("\n").map((line) => ( + {text.split("\\n").map((line) => ( <> {renderLines(line)}
diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 502c0b7c..f57d4d5b 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -7,6 +7,8 @@ import {infoButtonStyle} from "@/constants/buttonStyles"; import {Dialog, Transition} from "@headlessui/react"; import {renderExercise} from "@/components/Exercises"; import {renderSolution} from "@/components/Solutions"; +import {Panel} from "primereact/panel"; +import {Steps} from "primereact/steps"; interface Props { exam: ReadingExam; @@ -45,7 +47,7 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s

- {content.split("\n").map((line, index) => ( + {content.split("\\n").map((line, index) => ( {line}
@@ -115,17 +117,13 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) You will be allowed to read the text while doing the exercises

-
- {exam.text.title} - - {exam.text.content.split("\n").map((line, index) => ( - - {line} -
-
+ +

+ {exam.text.content.split("\\n").map((line, index) => ( +

{line}

))} -
-
+

+ ); diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index 8241d490..6b6cbde2 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -50,7 +50,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
{exam.text.info} - {exam.text.prompt.split("\n").map((line, index) => ( + {exam.text.prompt.split("\\n").map((line, index) => ( {line}
diff --git a/src/hooks/useUsers.tsx b/src/hooks/useUsers.tsx new file mode 100644 index 00000000..231aa0f2 --- /dev/null +++ b/src/hooks/useUsers.tsx @@ -0,0 +1,19 @@ +import {User} from "@/interfaces/user"; +import axios from "axios"; +import {useEffect, useState} from "react"; + +export default function useUsers() { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + useEffect(() => { + setIsLoading(true); + axios + .get("/api/users/list") + .then((response) => setUsers(response.data)) + .finally(() => setIsLoading(false)); + }, []); + + return {users, isLoading, isError}; +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 37c786f9..54e00842 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,11 +1,10 @@ export interface User { email: string; - name: Name; + name: string; profilePicture: string; id: string; experience: number; + type: Type; } -interface Name { - first: string; - last: string; -} + +export type Type = "student" | "teacher" | "admin" | "owner" | "developer"; diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts new file mode 100644 index 00000000..095e4722 --- /dev/null +++ b/src/pages/api/users/list.ts @@ -0,0 +1,26 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {app} from "@/firebase"; +import {getFirestore, collection, getDocs} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const snapshot = await getDocs(collection(db, "users")); + + res.status(200).json( + snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })), + ); +} diff --git a/src/pages/exam/index.tsx b/src/pages/exam/index.tsx index e61782b9..dae93891 100644 --- a/src/pages/exam/index.tsx +++ b/src/pages/exam/index.tsx @@ -133,7 +133,7 @@ export default function Page({user}: {user: User}) { -
+
{renderScreen()} diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 0dfdbaa2..43e02115 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -6,6 +6,7 @@ import Head from "next/head"; import useUser from "@/hooks/useUser"; import {InputText} from "primereact/inputtext"; import {Button} from "primereact/button"; +import {Password} from "primereact/password"; export default function Login() { const [email, setEmail] = useState(""); @@ -65,10 +66,11 @@ export default function Login() { - setPassword(e.target.value)} autoComplete="current-password" diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx new file mode 100644 index 00000000..d652f6a1 --- /dev/null +++ b/src/pages/profile.tsx @@ -0,0 +1,49 @@ +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {User} from "@/interfaces/user"; +import Head from "next/head"; +import Navbar from "@/components/Navbar"; +import {Avatar} from "primereact/avatar"; + +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; + + if (!user) { + res.setHeader("location", "/login"); + res.statusCode = 302; + res.end(); + return { + props: { + user: null, + }, + }; + } + + return { + props: {user: req.session.user}, + }; +}, sessionOptions); + +export default function Profile({user}: {user: User}) { + return ( + <> + + IELTS GPT | Profile + + + + +
+ +
+
+ +
+
+
+ + ); +} diff --git a/src/pages/users.tsx b/src/pages/users.tsx new file mode 100644 index 00000000..f7befcb6 --- /dev/null +++ b/src/pages/users.tsx @@ -0,0 +1,103 @@ +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {Type, User} from "@/interfaces/user"; +import Head from "next/head"; +import Navbar from "@/components/Navbar"; +import {Avatar} from "primereact/avatar"; +import {useEffect, useState} from "react"; +import {FilterMatchMode, FilterOperator} from "primereact/api"; +import useUsers from "@/hooks/useUsers"; +import {DataTable} from "primereact/datatable"; +import {Column} from "primereact/column"; +import _ from "lodash"; +import {levelCalculator} from "@/resources/level"; +import {Dropdown} from "primereact/dropdown"; + +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; + + if (!user) { + res.setHeader("location", "/login"); + res.statusCode = 302; + res.end(); + return { + props: { + user: null, + }, + }; + } + + return { + props: {user: req.session.user}, + }; +}, sessionOptions); + +export default function Users({user}: {user: User}) { + const {users, isLoading} = useUsers(); + const [filters] = useState({ + name: {value: null, matchMode: FilterMatchMode.CONTAINS}, + type: {value: null, matchMode: FilterMatchMode.EQUALS}, + }); + + const userTypes: Type[] = ["admin", "developer", "owner", "student", "teacher"]; + + const typeRowFilterTemplate = (options: any) => { + return ( + _.capitalize(x))} + onChange={(e) => options.filterApplyCallback(e.value)} + placeholder="Select One" + className="p-column-filter" + showClear + style={{minWidth: "12rem"}} + /> + ); + }; + + return ( + <> + + IELTS GPT | Profile + + + + +
+ +
+ + + + + levelCalculator(data.experience).currentLevel} /> + _.capitalize(data.type)} + /> + +
+
+ + ); +} diff --git a/src/resources/permissions.ts b/src/resources/permissions.ts new file mode 100644 index 00000000..b816ddbf --- /dev/null +++ b/src/resources/permissions.ts @@ -0,0 +1,201 @@ +import {Type, User} from "@/interfaces/user"; + +interface Permissions { + createUser: {[key in Type]: boolean}; + deleteUser: {[key in Type]: boolean}; + manageUser: {[key in Type]: boolean}; + viewUsers: {[key in Type]: boolean}; + viewStats: boolean; + viewUserStats: boolean; + viewClassStats: boolean; + createClass: boolean; + manageClass: boolean; + deleteClass: boolean; +} + +const permissions: {[key in Type]: Permissions} = { + student: { + createUser: { + admin: false, + developer: false, + owner: false, + student: false, + teacher: false, + }, + deleteUser: { + admin: false, + developer: false, + owner: false, + student: false, + teacher: false, + }, + manageUser: { + admin: false, + developer: false, + owner: false, + student: false, + teacher: false, + }, + viewUsers: { + admin: false, + developer: false, + owner: false, + student: false, + teacher: false, + }, + createClass: false, + deleteClass: false, + manageClass: false, + viewStats: true, + viewUserStats: false, + viewClassStats: false, + }, + teacher: { + createUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: false, + }, + deleteUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: false, + }, + manageUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: false, + }, + viewUsers: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: false, + }, + createClass: true, + deleteClass: true, + manageClass: true, + viewStats: true, + viewUserStats: true, + viewClassStats: true, + }, + admin: { + createUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: true, + }, + deleteUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: true, + }, + manageUser: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: true, + }, + viewUsers: { + admin: false, + developer: false, + owner: false, + student: true, + teacher: true, + }, + createClass: true, + deleteClass: true, + manageClass: true, + viewStats: true, + viewUserStats: true, + viewClassStats: true, + }, + owner: { + createUser: { + admin: true, + developer: false, + owner: false, + student: true, + teacher: true, + }, + deleteUser: { + admin: true, + developer: false, + owner: false, + student: true, + teacher: true, + }, + manageUser: { + admin: true, + developer: false, + owner: false, + student: true, + teacher: true, + }, + viewUsers: { + admin: true, + developer: false, + owner: false, + student: true, + teacher: true, + }, + createClass: true, + deleteClass: true, + manageClass: true, + viewStats: true, + viewUserStats: true, + viewClassStats: true, + }, + developer: { + createUser: { + admin: true, + developer: true, + owner: true, + student: true, + teacher: true, + }, + deleteUser: { + admin: true, + developer: true, + owner: true, + student: true, + teacher: true, + }, + manageUser: { + admin: true, + developer: true, + owner: true, + student: true, + teacher: true, + }, + viewUsers: { + admin: true, + developer: true, + owner: true, + student: true, + teacher: true, + }, + createClass: true, + deleteClass: true, + manageClass: true, + viewStats: true, + viewUserStats: true, + viewClassStats: true, + }, +}; + +export default function getPermissions(user: User) { + return permissions[user.type]; +}