diff --git a/components/Judging/TeamMembersSelector.js b/components/Judging/TeamMembersSelector.js
new file mode 100644
index 00000000..9fc8a964
--- /dev/null
+++ b/components/Judging/TeamMembersSelector.js
@@ -0,0 +1,313 @@
+// teamMembers should be array of object:
+//
+// [
+// {
+// discord,
+// email,
+// id,
+// name
+// },
+// ...
+// ]
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import TextField from '../TextField';
+import { getAllApplicants } from '../../utility/firebase';
+
+const InputArea = styled.div`
+ background: white;
+ padding: 0.5rem;
+ border: 1px solid #eeeeee;
+ display: flex;
+ gap: 0.5rem;
+ position: relative;
+ min-height: calc(1rem + calc(1rem * 1.2));
+`;
+
+const Member = styled.div`
+ background: #2d2937;
+ color: white;
+ padding: 0.5rem 0.7rem;
+ border-radius: 5px;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+`;
+
+const AddText = styled.span`
+ margin: 0.5rem 0.3rem;
+ cursor: pointer;
+ font-weight: 600;
+ position: absolute;
+ top: 0.5rem;
+ right: 10px;
+`;
+
+const AddPanelOverlay = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ ${(p) => (p.isAdding ? 'display: flex;' : 'display: none;')}
+ align-items: flex-end;
+ justify-content: center;
+`;
+
+const AddPanel = styled.div`
+ transition: all 0.5s linear;
+ background: #f8f8f8;
+ border-style: none solid solid solid;
+ border-width: 1px;
+ border-color: #eeeeee;
+ box-sizing: border-box;
+ border-radius: 8px 8px 0 0;
+`;
+
+const AddTitle = styled.h4`
+ line-height: 150%;
+ margin: 0;
+`;
+
+const HackerOptions = styled.div`
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ ::-webkit-scrollbar {
+ width: 5px;
+ }
+ ::-webkit-scrollbar-track {
+ background: #f8f8f8;
+ }
+ ::-webkit-scrollbar-thumb {
+ background: #eeeeee;
+ border-radius: 9001px;
+ }
+`;
+
+const EmptyOptions = styled.div`
+ font-size: 0.9rem;
+ color: #777777;
+`;
+
+const SelectableHacker = styled.div`
+ line-height: 150%;
+ box-sizing: border-box;
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+ width: 100%;
+
+ :hover {
+ background: #eeeeee;
+ }
+`;
+
+const SmallText = styled.span`
+ font-size: 0.8rem;
+ color: #777777;
+`;
+
+const TeamMembersSelector = (props) => {
+ const { value, updateValue, fnNoOverflow } = props;
+ // onChange => ('teamMembers', e)
+
+ // const copyValues = () => {
+ // const duplicate = [];
+ // value.forEach((item) => {
+ // duplicate.push(item);
+ // });
+ // return duplicate;
+ // };
+
+ const [members, setMembers] = useState(value);
+ const [renderMembers, setRenderMembers] = useState([]);
+ const [trigger, setTrigger] = useState(false);
+ const [isAdding, setIsAdding] = useState(false);
+ const [hackerSearch, setHackerSearch] = useState('');
+ const [applicants, setApplicants] = useState([]);
+
+ const handleAddHackerToTeam = (hacker) => {
+ setHackerSearch(''); // doesn't reset... maybe something to do with onChangeCustomValue
+ const currentMembers = members;
+ members.push(hacker);
+ setMembers(currentMembers);
+ setTrigger(!trigger);
+ setIsAdding(false);
+ };
+
+ const handleDeleteMember = (index) => {
+ const currentMembers = members;
+ currentMembers.splice(index, 1);
+ setMembers(currentMembers);
+ setTrigger(!trigger);
+ };
+
+ const DeleteIcon = ({ fn }) => {
+ return (
+
+
+
+ );
+ };
+
+ useEffect(() => {
+ const renderArray = [];
+
+ if (members) {
+ members.forEach((item, index) => {
+ renderArray.push(
+
+ {item.name}
+ handleDeleteMember(index)} />
+
+ );
+ });
+ }
+
+ setRenderMembers(renderArray);
+
+ // Save
+ updateValue('teamMembers', members, true);
+ }, [trigger]);
+
+ useEffect(() => {
+ if (isAdding) {
+ document
+ .getElementById('project-modal')
+ ?.scrollTo(0, document.getElementById('project-modal')?.scrollHeight);
+ fnNoOverflow(true);
+ } else {
+ fnNoOverflow(false);
+ }
+ }, [isAdding]);
+
+ useEffect(() => {
+ getAllApplicants(setApplicants);
+ }, []);
+
+ return (
+ <>
+
+
+ {renderMembers}
+ setIsAdding(true)}>Add Member
+
+
+
+
+ {/* Head */}
+
+
+ {/* Title + Cancel */}
+
Add Team Member
+
setIsAdding(false)} />
+
+ {/* Search */}
+
setHackerSearch(e.target.value)}
+ />
+
+
+ {/* Searched Options */}
+
+ {/* OPTIMIZATION TODO: debounce search + pull from firebase where applicant is accepted (if possible) */}
+ {hackerSearch ? (
+ applicants?.map((applicant, index) => {
+ const name = `${applicant.basicInfo.firstName} ${applicant.basicInfo.lastName}`;
+ const { email } = applicant.basicInfo;
+ if (
+ (name.toLowerCase().includes(hackerSearch.toLowerCase()) ||
+ hackerSearch === '*') &&
+ applicant.status.applicationStatus === 'accepted' &&
+ applicant.status.attending &&
+ (members
+ ? !(
+ members?.filter((e) => e.id === applicant._id).length >
+ 0
+ )
+ : true)
+ ) {
+ return (
+
+ handleAddHackerToTeam({
+ discord: '-#-',
+ email,
+ id: applicant._id,
+ name,
+ })
+ }
+ >
+ {name} {email}
+
+ );
+ }
+ return <>>;
+ })
+ ) : (
+ Begin typing to search
+ )}
+
+
+
+ >
+ );
+};
+
+export default TeamMembersSelector;
diff --git a/components/modal.js b/components/modal.js
index 3bbd6117..ad013f7a 100644
--- a/components/modal.js
+++ b/components/modal.js
@@ -138,7 +138,7 @@ const ModalBackground = styled.div`
width: 740px;
max-width: 100%;
max-height: 70%;
- overflow-y: auto;
+ ${(p) => (p.noOverflow ? 'overflow-y: hidden;' : 'overflow-y: auto;')}
`;
const ButtonContainer = styled.div`
@@ -239,6 +239,7 @@ export default function Modal({
lastModified,
children,
modalTitle,
+ noOverflow,
}) {
if (!isOpen) {
return null;
@@ -277,7 +278,11 @@ export default function Modal({
return (
<>
{isOpen && }
-
+
{(modalTitle && getTitle(modalTitle)) || getTitle(modalAction)}
{children}
diff --git a/components/projectsCard.js b/components/projectsCard.js
index 2b64ee83..e0e575ca 100644
--- a/components/projectsCard.js
+++ b/components/projectsCard.js
@@ -79,7 +79,11 @@ export default ({
Link
- {project.teamMembers?.toString()}
+
+ {project.teamMembers.map((item, index) => (
+ {item.name}
+ ))}
+