Implemented a simple page to view the currently registered users
This commit is contained in:
@@ -110,13 +110,20 @@ export default function FillBlanks({id, allowRepetition, prompt, solutions, text
|
||||
setCurrentBlankId(undefined);
|
||||
}}
|
||||
/>
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span className="text-lg font-medium text-center px-48">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
<>
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function WriteBlanks({id, prompt, maxWords, solutions, text, onNe
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
{text.split("\\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
|
||||
@@ -35,10 +35,9 @@ export default function Navbar({profilePicture}: Props) {
|
||||
</label>
|
||||
<ul tabIndex={0} className="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<a className="justify-between">
|
||||
<Link href="/profile" className="justify-between">
|
||||
Profile
|
||||
<span className="badge">New</span>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a>Settings</a>
|
||||
|
||||
@@ -17,9 +17,7 @@ export default function ProfileCard({user, className}: Props) {
|
||||
<img src={user.profilePicture} alt="Profile picture" className="rounded-full" />
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-neutral-600 font-bold text-2xl">
|
||||
{user.name.first} {user.name.last}
|
||||
</span>
|
||||
<span className="text-neutral-600 font-bold text-2xl">{user.name}</span>
|
||||
<LevelLabel experience={user.experience} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function FillBlanksSolutions({prompt, solutions, text, userSoluti
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
{text.split("\\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function WriteBlanksSolutions({
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-medium text-center px-48">{prompt}</span>
|
||||
<span>
|
||||
{text.split("\n").map((line) => (
|
||||
{text.split("\\n").map((line) => (
|
||||
<>
|
||||
{renderLines(line)}
|
||||
<br />
|
||||
|
||||
@@ -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
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 overflow-auto">
|
||||
<p className="text-sm text-gray-500">
|
||||
{content.split("\n").map((line, index) => (
|
||||
{content.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
@@ -115,17 +117,13 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
|
||||
</span>
|
||||
<span className="self-end text-sm">You will be allowed to read the text while doing the exercises</span>
|
||||
</div>
|
||||
<div className="bg-gray-300 rounded-xl p-4 flex flex-col gap-4 items-center w-full overflow-auto">
|
||||
<span className="text-xl font-semibold">{exam.text.title}</span>
|
||||
<span>
|
||||
{exam.text.content.split("\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<span>{line}</span>
|
||||
<br />
|
||||
</Fragment>
|
||||
<Panel header={exam.text.title}>
|
||||
<p>
|
||||
{exam.text.content.split("\\n").map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</p>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
||||
<div className="flex flex-col max-w-2xl gap-2">
|
||||
<span>{exam.text.info}</span>
|
||||
<span className="font-bold ml-8">
|
||||
{exam.text.prompt.split("\n").map((line, index) => (
|
||||
{exam.text.prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<span>{line}</span>
|
||||
<br />
|
||||
|
||||
19
src/hooks/useUsers.tsx
Normal file
19
src/hooks/useUsers.tsx
Normal file
@@ -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<User[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.get<User[]>("/api/users/list")
|
||||
.then((response) => setUsers(response.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return {users, isLoading, isError};
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
26
src/pages/api/users/list.ts
Normal file
26
src/pages/api/users/list.ts
Normal file
@@ -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(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export default function Page({user}: {user: User}) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||
<main className="w-full h-screen flex flex-col items-center bg-white text-black">
|
||||
<ToastContainer />
|
||||
<Navbar profilePicture={user.profilePicture} />
|
||||
{renderScreen()}
|
||||
|
||||
@@ -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() {
|
||||
<span className="p-inputgroup-addon">
|
||||
<i className="pi pi-star"></i>
|
||||
</span>
|
||||
<InputText
|
||||
<Password
|
||||
placeholder="Password..."
|
||||
type="password"
|
||||
required
|
||||
feedback={false}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
|
||||
49
src/pages/profile.tsx
Normal file
49
src/pages/profile.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>IELTS GPT | Profile</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100">
|
||||
<Navbar profilePicture={user.profilePicture} />
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-4 relative">
|
||||
<section className="bg-white drop-shadow-xl p-4 rounded-xl w-96 flex flex-col items-center">
|
||||
<Avatar image={user.profilePicture} size="xlarge" shape="circle" />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
103
src/pages/users.tsx
Normal file
103
src/pages/users.tsx
Normal file
@@ -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 (
|
||||
<Dropdown
|
||||
value={options.value}
|
||||
options={userTypes.map((x) => _.capitalize(x))}
|
||||
onChange={(e) => options.filterApplyCallback(e.value)}
|
||||
placeholder="Select One"
|
||||
className="p-column-filter"
|
||||
showClear
|
||||
style={{minWidth: "12rem"}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>IELTS GPT | Profile</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100">
|
||||
<Navbar profilePicture={user.profilePicture} />
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-4 relative">
|
||||
<DataTable
|
||||
dataKey="id"
|
||||
filters={filters}
|
||||
filterDisplay="row"
|
||||
className="w-full h-full"
|
||||
loading={isLoading}
|
||||
value={users}
|
||||
removableSort
|
||||
stripedRows
|
||||
paginator
|
||||
rows={5}
|
||||
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||
tableStyle={{minWidth: "50rem"}}>
|
||||
<Column field="id" sortable header="ID" />
|
||||
<Column field="name" sortable header="Name" filter filterPlaceholder="Search by name" />
|
||||
<Column field="experience" sortable header="Experience" />
|
||||
<Column field="experience" sortable header="Level" body={(data: User) => levelCalculator(data.experience).currentLevel} />
|
||||
<Column
|
||||
field="type"
|
||||
showFilterMenu={false}
|
||||
filter
|
||||
filterElement={typeRowFilterTemplate}
|
||||
sortable
|
||||
header="Type"
|
||||
body={(data: User) => _.capitalize(data.type)}
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
201
src/resources/permissions.ts
Normal file
201
src/resources/permissions.ts
Normal file
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user