diff --git a/package.json b/package.json index 0d217ddc..5486a18d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "primereact": "^9.2.3", "react": "18.2.0", "react-chartjs-2": "^5.2.0", + "react-datepicker": "^4.18.0", "react-dom": "18.2.0", "react-firebase-hooks": "^5.1.1", "react-icons": "^4.8.0", @@ -54,6 +55,7 @@ "swr": "^2.1.3", "tailwind-scrollbar-hide": "^1.1.7", "typescript": "4.9.5", + "use-file-picker": "^2.1.0", "uuid": "^9.0.0", "wavesurfer.js": "^6.6.4", "zustand": "^4.3.6" @@ -61,6 +63,7 @@ "devDependencies": { "@types/formidable": "^3.4.0", "@types/lodash": "^4.14.191", + "@types/react-datepicker": "^4.15.1", "@types/uuid": "^9.0.1", "@types/wavesurfer.js": "^6.0.6", "@wixc3/react-board": "^2.2.0", diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3c0abe06..77e9672a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,5 @@ import {User} from "@/interfaces/user"; import Link from "next/link"; -import {Avatar} from "primereact/avatar"; import FocusLayer from "@/components/FocusLayer"; import {preventNavigation} from "@/utils/navigation.disabled"; import {useRouter} from "next/router"; diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx new file mode 100644 index 00000000..4c0e0c46 --- /dev/null +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -0,0 +1,81 @@ +import Button from "@/components/Low/Button"; +import {Type} from "@/interfaces/user"; +import axios from "axios"; +import clsx from "clsx"; +import {capitalize} from "lodash"; +import {useEffect, useState} from "react"; +import {toast} from "react-toastify"; +import ShortUniqueId from "short-unique-id"; +import {useFilePicker} from "use-file-picker"; + +export default function BatchCodeGenerator() { + const [emails, setEmails] = useState([]); + const {openFilePicker, filesContent} = useFilePicker({ + accept: ".txt", + multiple: false, + }); + + useEffect(() => { + if (filesContent.length > 0) { + const file = filesContent[0]; + const emails = file.content + .split("\n") + .filter((x) => new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/).test(x)); + + if (emails.length === 0) { + toast.error("Please upload a .txt file containing e-mails, one per line!"); + return; + } + + setEmails(emails); + } + }, [filesContent]); + + const generateCode = (type: Type) => { + const uid = new ShortUniqueId(); + const codes = emails.map(() => uid.randomUUID(6)); + + axios + .post("/api/code", {type, codes}) + .then(({data, status}) => { + if (data.ok) { + toast.success(`Successfully generated ${capitalize(type)} codes!`, {toastId: "success"}); + return; + } + + if (status === 403) { + toast.error(`You do not have permission to generate ${capitalize(type)} codes!`, {toastId: "forbidden"}); + } + }) + .catch(({response: {status}}) => { + if (status === 403) { + toast.error(`You do not have permission to generate ${capitalize(type)} codes!`, {toastId: "forbidden"}); + return; + } + + toast.error(`Something went wrong, please try again later!`, {toastId: "error"}); + }); + }; + + return ( +
+ + + +
+ + + + +
+
+ ); +} diff --git a/src/pages/(admin)/CodeGenerator.tsx b/src/pages/(admin)/CodeGenerator.tsx index 749f72ce..b1111c92 100644 --- a/src/pages/(admin)/CodeGenerator.tsx +++ b/src/pages/(admin)/CodeGenerator.tsx @@ -15,7 +15,7 @@ export default function CodeGenerator() { const code = uid.randomUUID(6); axios - .post("/api/code", {type, code}) + .post("/api/code", {type, codes: [code]}) .then(({data, status}) => { if (data.ok) { toast.success(`Successfully generated a ${capitalize(type)} code!`, {toastId: "success"}); diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 9469b436..18bf4f00 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -15,6 +15,7 @@ import {BsCheck, BsDash, BsPencil, BsPlus, BsTrash} from "react-icons/bs"; import {toast} from "react-toastify"; import Select from "react-select"; import {uuidv4} from "@firebase/util"; +import {useFilePicker} from "use-file-picker"; const columnHelper = createColumnHelper(); @@ -29,6 +30,41 @@ const CreatePanel = ({user, users, group, onCreate}: CreateDialogProps) => { const [name, setName] = useState(group?.name || undefined); const [admin, setAdmin] = useState(group?.admin || user.id); const [participants, setParticipants] = useState(group?.participants || []); + const {openFilePicker, filesContent} = useFilePicker({ + accept: ".txt", + multiple: false, + }); + + useEffect(() => { + if (filesContent.length > 0) { + const file = filesContent[0]; + const emails = file.content + .toLowerCase() + .split("\n") + .filter((x) => new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/).test(x)); + + if (emails.length === 0) { + toast.error("Please upload a .txt file containing e-mails, one per line!"); + return; + } + + const emailUsers = emails.map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); + const filteredUsers = emailUsers.filter( + (x) => + ((user.type === "developer" || user.type === "owner" || user.type === "admin") && + (x?.type === "student" || x?.type === "teacher")) || + (user.type === "teacher" && x?.type === "student"), + ); + + setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); + toast.success( + user.type !== "teacher" + ? "Added all teachers and students found in the file you've provided!" + : "Added all students found in the file you've provided!", + {toastId: "upload-success"}, + ); + } + }, [filesContent, user.type, users]); return (
@@ -36,28 +72,38 @@ const CreatePanel = ({user, users, group, onCreate}: CreateDialogProps) => {
- ({ + value: x, + label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`, + }))} + placeholder="Participants..." + defaultValue={participants.map((x) => ({ + value: x, + label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`, + }))} + 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", + }), + }} + /> + +