Added the ability to create groups
This commit is contained in:
@@ -99,7 +99,7 @@ export default function ExamList() {
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="bg-white rounded-lg shadow py-2" key={row.id}>
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
||||
232
src/pages/(admin)/Lists/GroupList.tsx
Normal file
232
src/pages/(admin)/Lists/GroupList.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import Input from "@/components/Low/Input";
|
||||
import useExams from "@/hooks/useExams";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {Module} from "@/interfaces";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {Group, Type, User} from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {Combobox, Dialog, Disclosure, Transition} from "@headlessui/react";
|
||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {BsCheck, BsDash, BsPlus, BsTrash, BsUpload} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import Select from "react-select";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
|
||||
const CLASSES: {[key in Module]: string} = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<Group>();
|
||||
|
||||
interface CreateDialogProps {
|
||||
user: User;
|
||||
users: User[];
|
||||
onCreate: (group: Group) => void;
|
||||
}
|
||||
|
||||
const CreatePanel = ({user, users, onCreate}: CreateDialogProps) => {
|
||||
const [name, setName] = useState<string>();
|
||||
const [isSelfAdmin, setIsSelfAdmin] = useState(true);
|
||||
const [admin, setAdmin] = useState<string>(user.id);
|
||||
const [participants, setParticipants] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 mt-4 w-full px-4 py-2">
|
||||
<div className="flex flex-col gap-8">
|
||||
<Input name="name" type="text" label="Name" onChange={setName} required />
|
||||
{!isSelfAdmin && user.type === "developer" && (
|
||||
<Select
|
||||
placeholder="Administrator"
|
||||
options={users
|
||||
.filter((x) => x.type === "teacher" || x.type === "admin")
|
||||
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||
isSearchable
|
||||
defaultValue={{value: user.id, label: `${user.email} - ${user.name}`}}
|
||||
onChange={(value) => (value ? setAdmin(value.value) : setAdmin(user.id))}
|
||||
/>
|
||||
)}
|
||||
{user.type === "developer" && (
|
||||
<Checkbox isChecked={isSelfAdmin} onChange={setIsSelfAdmin}>
|
||||
I am the group's administrator
|
||||
</Checkbox>
|
||||
)}
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Participants</label>
|
||||
<Select
|
||||
placeholder="Participants..."
|
||||
options={users
|
||||
.filter((x) => (user.type === "teacher" ? x.type === "student" : x.type === "student" || x.type === "teacher"))
|
||||
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||
isMulti
|
||||
isSearchable
|
||||
styles={{
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "999px",
|
||||
padding: "1rem 1.5rem",
|
||||
zIndex: "40",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full max-w-[200px] self-end"
|
||||
disabled={!name}
|
||||
onClick={() => {
|
||||
onCreate({name: name!, admin, participants, id: uuidv4()});
|
||||
}}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function GroupList({user}: {user: User}) {
|
||||
const {users} = useUsers();
|
||||
const {groups, reload} = useGroups(user.type === "admin" || user.type === "teacher" ? user.id : undefined);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const createGroup = (group: Group) => {
|
||||
return axios
|
||||
.post<{ok: boolean}>("/api/groups", group)
|
||||
.then(() => {
|
||||
toast.success(`Group "${group.name}" created successfully`);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
return false;
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const deleteGroup = (group: Group) => {
|
||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||
|
||||
axios
|
||||
.delete<{ok: boolean}>(`/api/groups/${group.id}`)
|
||||
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
|
||||
.catch(() => toast.error("Something went wrong, please try again later!"))
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("admin", {
|
||||
header: "Admin",
|
||||
cell: (info) => (
|
||||
<div className="tooltip" data-tip={capitalize(users.find((x) => x.id === info.getValue())?.type)}>
|
||||
{users.find((x) => x.id === info.getValue())?.name}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("participants", {
|
||||
header: "Participants",
|
||||
cell: (info) =>
|
||||
info
|
||||
.getValue()
|
||||
.map((x) => users.find((y) => y.id === x)?.name)
|
||||
.join(", "),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Group}}) => {
|
||||
return (
|
||||
<>
|
||||
{(user.type === "developer" || user.type === "owner" || user.id === row.original.admin) && (
|
||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteGroup(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: groups,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="py-4" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Disclosure>
|
||||
{({open, close}) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={clsx(
|
||||
"w-full px-4 py-2 bg-mti-purple-ultralight/40 flex gap-2 items-center justify-center rounded-lg",
|
||||
"transition duration-300 ease-in-out",
|
||||
"hover:bg-mti-purple-ultralight cursor-pointer",
|
||||
)}>
|
||||
{!open ? <BsPlus className="w-6 h-6" /> : <BsDash className="w-6 h-6" />}
|
||||
|
||||
<span>{!open ? "Create group" : "Cancel"}</span>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel>
|
||||
<CreatePanel
|
||||
user={user}
|
||||
users={users}
|
||||
onCreate={(group) => {
|
||||
createGroup(group).then((result) => {
|
||||
if (result) close();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {User} from "@/interfaces/user";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import ExamList from "./ExamList";
|
||||
import GroupList from "./GroupList";
|
||||
import UserList from "./UserList";
|
||||
|
||||
export default function Lists({user}: {user: User}) {
|
||||
@@ -30,6 +31,17 @@ export default function Lists({user}: {user: User}) {
|
||||
}>
|
||||
Exam List
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||
"transition duration-300 ease-in-out",
|
||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||
)
|
||||
}>
|
||||
Group List
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-2">
|
||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow">
|
||||
@@ -38,6 +50,9 @@ export default function Lists({user}: {user: User}) {
|
||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow">
|
||||
<ExamList />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow">
|
||||
<GroupList user={user} />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user