diff --git a/components/judges/AllTeamsTab/AllTeamsForm.tsx b/components/judges/AllTeamsTab/AllTeamsForm.tsx new file mode 100644 index 00000000..c0e67d9c --- /dev/null +++ b/components/judges/AllTeamsTab/AllTeamsForm.tsx @@ -0,0 +1,148 @@ +import { FilterConfirmProps, FilterValue, SorterResult } from 'antd/es/table/interface'; +import { useContext, useRef, useState } from 'react'; +import { TeamData } from '../../../types/database'; +import { Button, Input, InputRef, Table } from 'antd'; +import { getAccentColor, ThemeContext } from '../../../theme/themeProvider'; +import { SearchOutlined } from '@ant-design/icons'; +import Highlighter from 'react-highlight-words'; +import Link from 'next/link'; + +interface AllTeamsProps { + teamsData: TeamData[]; + teamId: string; + handleTeamChange: (teamId: string) => void; +} + +export const AllTeamsForm = ({ teamsData, teamId, handleTeamChange }: AllTeamsProps) => { + const [filteredInfo, setFilteredInfo] = useState>({}); + const [sortedInfo, setSortedInfo] = useState>({}); + const [searchedColumn, setSearchedColumn] = useState(''); + const [searchText, setSearchText] = useState(''); + const searchInput = useRef(null); + const { accentColor, baseTheme } = useContext(ThemeContext); + + const handleChange = (pagination: any, filters: any, sorter: any) => { + setSortedInfo(sorter as SorterResult); + setFilteredInfo(filters); + }; + + const handleReset = (clearFilters: () => void) => { + clearFilters(); + setSearchText(''); + }; + + const getColumnSearchProps = (dataIndex: string) => ({ + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + }: { + setSelectedKeys: (selectedKeys: React.Key[]) => void; + selectedKeys: React.Key[]; + confirm: (param?: FilterConfirmProps) => void; + clearFilters: () => void; + }) => ( +
+ { + setSelectedKeys(e.target.value ? [e.target.value] : []); + confirm({ closeDropdown: false }); + setSearchText(e.target.value); + setSearchedColumn(dataIndex); + }} + onPressEnter={() => confirm({ closeDropdown: true })} + style={{ marginBottom: 8, display: 'block' }} + /> + +
+ ), + filterIcon: (filtered: boolean) => , + onFilter: (value: string | number | boolean, record: any): boolean => { + const recordValue = dataIndex in record ? record[dataIndex] : record.application?.[dataIndex]; + if (recordValue === undefined || recordValue === null) { + return false; + } + return recordValue.toString().toLowerCase().includes(value.toString().toLowerCase()); + }, + filteredValue: + (dataIndex in filteredInfo ? filteredInfo[dataIndex] : filteredInfo['application.' + dataIndex]) || null, + onFilterDropdownOpenChange: (open: boolean) => { + if (open) { + setTimeout(() => searchInput.current?.select(), 100); + } + }, + render: (text: string) => + searchedColumn === dataIndex ? ( + + ) : ( + text + ), + }); + + const columns = [ + { + title: 'Table', + dataIndex: 'locationNum', + key: 'locationNum', + width: '20%', + sorter: (a: any, b: any) => a.locationNum - b.locationNum, + sortOrder: sortedInfo.columnKey === 'locationNum' ? sortedInfo.order : null, + }, + { + title: 'Team', + dataIndex: 'name', + key: 'name', + width: '40%', + ...getColumnSearchProps('name'), + }, + { + title: 'Devpost', + dataIndex: 'devpost', + key: 'devpost', + width: '40%', + render: (link: URL) => { + return ( + <> + + + {link} + + + + ); + }, + }, + ]; + + return ( + <> + ({ + onClick: () => handleTeamChange(teamId !== String(record._id) ? String(record._id) : ''), + })} + /> + + ); +}; diff --git a/components/judges/AllTeamsTab/AllTeamsTab.tsx b/components/judges/AllTeamsTab/AllTeamsTab.tsx index 971b66bb..5ca7280c 100644 --- a/components/judges/AllTeamsTab/AllTeamsTab.tsx +++ b/components/judges/AllTeamsTab/AllTeamsTab.tsx @@ -1,14 +1,42 @@ -import useSWR from 'swr'; +import useSWR, { useSWRConfig } from 'swr'; import { ResponseError, TeamData } from '../../../types/database'; -import { Button, Input, InputRef, Table } from 'antd'; -import Link from 'next/link'; -import { getAccentColor, ThemeContext } from '../../../theme/themeProvider'; -import { useContext, useRef, useState } from 'react'; -import { FilterConfirmProps, FilterValue, SorterResult } from 'antd/es/table/interface'; -import { SearchOutlined } from '@ant-design/icons'; -import Highlighter from 'react-highlight-words'; +import { AllTeamsForm } from './AllTeamsForm'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { JudgingFormFields } from '../../../types/client'; +import JudgingForm from '../AssignedTab/JudgingForm'; +import { ScopedMutator } from 'swr/dist/types'; +import { handleSubmitFailure, handleSubmitSuccess } from '../../../lib/helpers'; + +async function handleSubmit( + formData: JudgingFormFields, + mutate: ScopedMutator, + teamId: string, + isNewForm: boolean, + setIsNewForm: React.Dispatch> +) { + const res = await fetch(`/api/judging-form?id=${teamId}`, { + method: isNewForm ? 'POST' : 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }); + + if (res.ok) { + mutate('/api/teams'); + mutate('/api/judging-form'); + handleSubmitSuccess(`Successfully ${isNewForm ? 'submitted' : 'updated'}!`); + setIsNewForm(false); + } else { + handleSubmitFailure(await res.text()); + } +} export const AllTeamsTab = () => { + const [teamID, setTeamID] = useState(''); + const [isNewForm, setIsNewForm] = useState(false); + const { mutate } = useSWRConfig(); + // Get data for all teams const { data: teamsData, error: teamsError } = useSWR('/api/teams-all', async url => { const res = await fetch(url, { method: 'GET' }); @@ -20,137 +48,69 @@ export const AllTeamsTab = () => { return (await res.json()) as TeamData[]; }); - const [filteredInfo, setFilteredInfo] = useState>({}); - const [sortedInfo, setSortedInfo] = useState>({}); - const [searchedColumn, setSearchedColumn] = useState(''); - const [searchText, setSearchText] = useState(''); - const searchInput = useRef(null); - const { accentColor, baseTheme } = useContext(ThemeContext); + // Get data for form component, formData will be false if teamId is not yet set. + const { data: formData, error: formError } = useSWR( + () => (teamID ? ['/api/judging-form', teamID] : null), + async (url: any, id: any) => { + const res = await fetch(`${url}?id=${id}`, { method: 'GET' }); + if (!res.ok) { + if (res.status === 404) { + const emptyJudgeForm: JudgingFormFields = { + technicalAbility: 0, + creativity: 0, + utility: 0, + presentation: 0, + wowFactor: 0, + comments: '', + feedback: '', + }; + setIsNewForm(true); + return emptyJudgeForm; + } + const error = new Error('Failed to get form information.') as ResponseError; + error.status = res.status; + throw error; + } + setIsNewForm(false); + return (await res.json()) as JudgingFormFields; + } + ); const teams = teamsData?.map(x => ({ ...x, key: x._id })) || ([] as TeamData[]); + const currentTeam = teams.find(team => String(team._id) === teamID); - const handleChange = (pagination: any, filters: any, sorter: any) => { - setSortedInfo(sorter as SorterResult); - setFilteredInfo(filters); + const handleTeamChange: Dispatch> = e => { + setTeamID(e); + setTimeout(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }, 200); }; - const handleReset = (clearFilters: () => void) => { - clearFilters(); - setSearchText(''); - }; - - const getColumnSearchProps = (dataIndex: string) => ({ - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - }: { - setSelectedKeys: (selectedKeys: React.Key[]) => void; - selectedKeys: React.Key[]; - confirm: (param?: FilterConfirmProps) => void; - clearFilters: () => void; - }) => ( -
- { - setSelectedKeys(e.target.value ? [e.target.value] : []); - confirm({ closeDropdown: false }); - setSearchText(e.target.value); - setSearchedColumn(dataIndex); - }} - onPressEnter={() => confirm({ closeDropdown: true })} - style={{ marginBottom: 8, display: 'block' }} - /> - -
- ), - filterIcon: (filtered: boolean) => , - onFilter: (value: string | number | boolean, record: any): boolean => { - const recordValue = dataIndex in record ? record[dataIndex] : record.application?.[dataIndex]; - if (recordValue === undefined || recordValue === null) { - return false; - } - return recordValue.toString().toLowerCase().includes(value.toString().toLowerCase()); - }, - filteredValue: - (dataIndex in filteredInfo ? filteredInfo[dataIndex] : filteredInfo['application.' + dataIndex]) || null, - onFilterDropdownOpenChange: (open: boolean) => { - if (open) { - setTimeout(() => searchInput.current?.select(), 100); - } - }, - render: (text: string) => - searchedColumn === dataIndex ? ( - - ) : ( - text - ), - }); - - const columns = [ - { - title: 'Table', - dataIndex: 'locationNum', - key: 'locationNum', - width: '20%', - sorter: (a: any, b: any) => a.locationNum - b.locationNum, - sortOrder: sortedInfo.columnKey === 'locationNum' ? sortedInfo.order : null, - }, - { - title: 'Team', - dataIndex: 'name', - key: 'name', - width: '40%', - ...getColumnSearchProps('name'), - }, - { - title: 'Devpost', - dataIndex: 'devpost', - key: 'devpost', - width: '40%', - render: (link: URL) => { - return ( - <> - - - {link} - - - - ); - }, - }, - ]; - return ( <> {teamsError ? ( -
{teamsError ? (teamsError as ResponseError).message : 'Failed to get data.'}
+
{(teamsError as ResponseError).message || 'Failed to get data.'}
) : ( -
+ <> + + {formData && ( + <> +

+ {currentTeam?.name} +

+ handleSubmit(formData, mutate, teamID, isNewForm, setIsNewForm)} + /> + + )} + )} ); diff --git a/models/team.ts b/models/team.ts index 22a568a8..5de204db 100644 --- a/models/team.ts +++ b/models/team.ts @@ -14,7 +14,7 @@ const TeamSchema = new Schema( }, joinCode: { type: String, - required: true, + required: false, unique: true, }, devpost: { @@ -23,18 +23,18 @@ const TeamSchema = new Schema( members: { type: [Schema.Types.ObjectId], ref: 'User', - required: true, + required: false, validate: { // check team size validator: (arr: Array) => arr.length <= 4, message: 'Max team size is 4 members.', }, }, - scores: { type: [Schema.Types.ObjectId], ref: 'Scores' }, + scores: { type: [Schema.Types.ObjectId], ref: 'Scores', required: false }, locationNum: { type: Number, reqiured: false }, }, { - timestamps: true, + timestamps: false, } ); diff --git a/pages/api/judging-form.ts b/pages/api/judging-form.ts index 45b5e0a0..d28b3915 100644 --- a/pages/api/judging-form.ts +++ b/pages/api/judging-form.ts @@ -59,7 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< // update the scores for the team const team = await Team.findById(teamID); team.scores = scores.id; - await log(judgeID, `Submitted scores for team ${team.name} (join code ${team.joinCode})`); + await log(judgeID, `Submitted scores for team ${team.name}`); await team.save(); return res.status(201).send(scores); } @@ -77,7 +77,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ); const team = await Team.findById(teamID); - await log(judgeID, `Update scores for team ${team.name} (join code ${team.joinCode})`); + await log(judgeID, `Update scores for team ${team.name}`); if (scores) { return res.status(200).send(scores); } else { diff --git a/types/database.ts b/types/database.ts index e9cfef32..267993d2 100644 --- a/types/database.ts +++ b/types/database.ts @@ -71,10 +71,10 @@ export interface PreAddData { export interface TeamData { _id: mongoose.Schema.Types.ObjectId; name: string; - joinCode: string; + joinCode?: string; devpost: string; - members: mongoose.Schema.Types.ObjectId[]; - scores: mongoose.Schema.Types.ObjectId[]; + members?: mongoose.Schema.Types.ObjectId[]; + scores?: mongoose.Schema.Types.ObjectId[]; locationNum?: number; createdAt: Date; }