From cb0f52fcd0b191f6a8c59c6c9dd1769d87dc8c39 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 10 Dec 2023 18:20:06 -0800 Subject: [PATCH 01/55] adding some basic elements to portal --- src/newStyles/components.less | 1 - .../CompetitionPortalPage/index.less | 31 ++++++ .../CompetitionPortalPage/index.tsx | 97 +++++++++++++++++-- 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/newStyles/components.less b/src/newStyles/components.less index 7a44630..2ddc081 100644 --- a/src/newStyles/components.less +++ b/src/newStyles/components.less @@ -53,7 +53,6 @@ // A wrapper for most sections with content .generic-section { padding: 3rem @padding-base2; - margin: 0 auto; /* This will center the container horizontally */ box-sizing: border-box; /* Include padding and borders within the width */ } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index a292be7..2dfdfa9 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -2,15 +2,46 @@ @import '../../../newStyles/components.less'; @import '../../../newStyles/index.less'; + .CompetitionPortalPage { .noContainer(); + + // prevents layout shifts caused by scrollbar + overflow-y: hidden; + background: @white; + + #portalHeader{ .generic-section(); .constrained-bounds(); margin-top: 4rem; } + #portalStatsContent { + + + p#noTeamMessage { + margin-top: 2rem; + text-align: left; + background: @gray; + border-radius: @radius-base4; + padding: @padding-base3; + max-width: @sm; + + } + + } + + #portalTabContent { + .generic-section(); + .constrained-bounds(); + + .ant-tabs-ink-bar { + display: none; + } + } + } \ No newline at end of file diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index da78e21..2578036 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,22 +1,87 @@ import React, { useContext, useEffect } from "react"; -import { message } from "antd"; +import { List, Tabs, message } from "antd"; import { Layout, Space, Button } from 'antd'; import UserContext from "../../../UserContext"; import { useHistory } from 'react-router-dom'; +import { + getTeamInfo, + createTeam, + getCompetitionUser, + } from '../../../actions/teams/utils'; + import './index.less'; +import path from 'path'; + import DefaultLayout from "../../../components/layouts/default"; const { Content } = Layout; + +const FindTeamsTab = (): React.ReactNode => { + return ( + +

Find Teams

+ +
+ ); + }; + + +const LeaderBoardTab = (): React.ReactNode => { + return ( + +

Leaderboard

+ +
+ ); + }; + + + const MyTeamTab = (): React.ReactNode => { + return ( + +

My Team

+
+ ); + }; + + + function CompetitionPortalPage () { const history = useHistory(); const { user } = useContext(UserContext); + const tabItems= [ + { + label:

Leaderboard

, + key: '1', + children: LeaderBoardTab(), + }, + { + label:

Find Teams

, + key: '2', + children: FindTeamsTab(), + }, + { + label:

My Team

, + key: '3', + children: MyTeamTab(), + } + ]; + useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); - // history.replace(path.join(window.location.pathname, '../../../login')); + history.replace(path.join(window.location.pathname, '../../../login')); + } + }, []); + + + useEffect(() => { + if (!user.loggedIn) { + message.info('You need to login to upload predictions and participate'); + history.replace(path.join(window.location.pathname, '../../../login')); } }, []); @@ -25,10 +90,30 @@ function CompetitionPortalPage () { -
-

Hello, {user.username}

-

Welcome the the AI Portal

-
+
+

Hello, {user.username}

+

Welcome the the AI Portal

+
+ +
+

Uh oh! You’re not in a team yet. Ask your friends to share their invite code, then navigate to Find Teams below to join their group!

+ +
+
+ + + + + + + + {/* If the user is not part of a team yet, then */} +
From 7f14cd6b909a0420366ca310116454d5486dc17e Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 10 Dec 2023 22:24:50 -0800 Subject: [PATCH 02/55] experimented with fetching competition data --- .../CompetitionPortalPage/index.less | 12 +++ .../CompetitionPortalPage/index.tsx | 87 +++++++++++++++---- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 2dfdfa9..0126da7 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -43,5 +43,17 @@ } + .teamPreviewCard { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background: @gray; + padding: @padding-base3 @padding-base2; + border-radius: @radius-base4; + } + + } \ No newline at end of file diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 2578036..762a50c 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,5 @@ -import React, { useContext, useEffect } from "react"; -import { List, Tabs, message } from "antd"; +import React, { useContext, useEffect, useState } from "react"; +import { Avatar, List, Tabs, message } from "antd"; import { Layout, Space, Button } from 'antd'; import UserContext from "../../../UserContext"; import { useHistory } from 'react-router-dom'; @@ -7,21 +7,49 @@ import { getTeamInfo, createTeam, getCompetitionUser, + getTeams, } from '../../../actions/teams/utils'; import './index.less'; import path from 'path'; import DefaultLayout from "../../../components/layouts/default"; +import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; +import { all } from "axios"; const { Content } = Layout; +const teamCard = (team: any): React.ReactNode => { + return ( +
+

{team.teamName}

+

{team.teamMembers.length} members

+ {/* {team.teamMembers.map((member: string, index: number) => ( +

{member}

+ ))} */} +
+ ); +}; -const FindTeamsTab = (): React.ReactNode => { + +const FindTeamsTab = (data: Array): React.ReactNode => { + const [position, setPosition] = useState('bottom'); + const [align, setAlign] = useState('center'); + return ( -

Find Teams

+ ( + + {teamCard(item)} + + + )} + />
); @@ -35,7 +63,7 @@ const LeaderBoardTab = (): React.ReactNode => { ); - }; +}; const MyTeamTab = (): React.ReactNode => { @@ -51,6 +79,8 @@ const LeaderBoardTab = (): React.ReactNode => { function CompetitionPortalPage () { const history = useHistory(); const { user } = useContext(UserContext); + const [allTeams, setAllTeams] = useState>([]); + const tabItems= [ { @@ -61,29 +91,36 @@ function CompetitionPortalPage () { { label:

Find Teams

, key: '2', - children: FindTeamsTab(), + children: FindTeamsTab(allTeams), }, { label:

My Team

, key: '3', children: MyTeamTab(), } - ]; + ]; useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); history.replace(path.join(window.location.pathname, '../../../login')); } - }, []); - - useEffect(() => { - if (!user.loggedIn) { - message.info('You need to login to upload predictions and participate'); - history.replace(path.join(window.location.pathname, '../../../login')); + else { + + getTeams("TestCompetition2").then(res => { + if(res.data) { + setAllTeams(Array.from(res.data)) + console.log(Array.from(res.data)); + } + }) + .catch(error => { + console.log(error); + }); } + }, []); + return ( @@ -101,18 +138,32 @@ function CompetitionPortalPage () { - - - - {/* If the user is not part of a team yet, then */} Leaderboard

, + key: '1', + children: LeaderBoardTab(), + }, + { + label:

Find Teams

, + key: '2', + children: FindTeamsTab(allTeams), + }, + { + label:

My Team

, + key: '3', + children: MyTeamTab(), + } + ] + } >
From 876587c58db7bec7a261af68611f78efe174f618 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 10 Dec 2023 22:33:01 -0800 Subject: [PATCH 03/55] tweaked some styles --- .../Competitions/CompetitionPortalPage/index.less | 3 ++- .../Competitions/CompetitionPortalPage/index.tsx | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 0126da7..ee0df2c 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -24,10 +24,11 @@ p#noTeamMessage { margin-top: 2rem; text-align: left; - background: @gray; + background: @black; border-radius: @radius-base4; padding: @padding-base3; max-width: @sm; + color: @white; } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 762a50c..391c603 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -23,19 +23,26 @@ const { Content } = Layout; const teamCard = (team: any): React.ReactNode => { return (
-

{team.teamName}

+

{team.teamName}

{team.teamMembers.length} members

{/* {team.teamMembers.map((member: string, index: number) => (

{member}

))} */} + + + {/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} +
); }; const FindTeamsTab = (data: Array): React.ReactNode => { - const [position, setPosition] = useState('bottom'); - const [align, setAlign] = useState('center'); + const [position] = useState('bottom'); + const [align] = useState('center'); return ( From 9f4b63abe00b167d8e02d0150fcabe7d880f2451 Mon Sep 17 00:00:00 2001 From: William Kim Date: Thu, 14 Dec 2023 12:06:14 -0800 Subject: [PATCH 04/55] added search bar to find teams tab --- .../CompetitionPortalPage/index.less | 7 ++ .../CompetitionPortalPage/index.tsx | 74 ++++++++++++++++--- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index ee0df2c..d61fbc6 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -43,6 +43,13 @@ } } + #findTeamsContainer { + AutoComplete#teamSearchBar { + font-size: 1.2rem; + width: 100%; + } + } + .teamPreviewCard { width: 100%; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 391c603..8eb67e5 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; -import { Avatar, List, Tabs, message } from "antd"; -import { Layout, Space, Button } from 'antd'; +import { AutoComplete, Avatar, List, Tabs, message } from "antd"; +import { Layout, Space, Button, Input } from 'antd'; import UserContext from "../../../UserContext"; import { useHistory } from 'react-router-dom'; import { @@ -41,26 +41,74 @@ const teamCard = (team: any): React.ReactNode => { const FindTeamsTab = (data: Array): React.ReactNode => { + const [position] = useState('bottom'); const [align] = useState('center'); - + const [searchTerm, setSearchTerm] = useState(""); + + // dropdown options for search bar + const [options, setOptions] = useState>(data); + + // data for the list + + + useEffect(() => { + if(data) { + setOptions(data); + } + }, [data]) + + const handleSearch = (value: string) => { + setSearchTerm(value); + + // Reset options back to the original data if the value is an empty string + if (value === "") { + setOptions(data); + } + } + + + const handleSelect = (value: string) => { + + const filteredOptions = data.filter((item: any) => + item.teamName.toLowerCase().includes(value.toLowerCase()) + ); + + setOptions(filteredOptions) + + } + return ( + handleSearch(text)} + onSelect = {handleSelect} + options={options.map((item: any) => ({ value: item.teamName }))} + filterOption = {(inputValue, option) => + option?.value.toLowerCase().includes(inputValue.toLowerCase()) + } + size = "large" + style = {{width: "100%", fontSize: 1.4}} + > + + + + ( {teamCard(item)} - )} /> ); - }; +}; const LeaderBoardTab = (): React.ReactNode => { @@ -89,6 +137,7 @@ function CompetitionPortalPage () { const [allTeams, setAllTeams] = useState>([]); + {/** Might save for later when we need to edit the tab Items */} const tabItems= [ { label:

Leaderboard

, @@ -107,6 +156,8 @@ function CompetitionPortalPage () { } ]; + + useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); @@ -114,7 +165,8 @@ function CompetitionPortalPage () { } else { - + // Grab all the teams for the current competitions + // Note: I am using the test competition name from mongoDB getTeams("TestCompetition2").then(res => { if(res.data) { setAllTeams(Array.from(res.data)) @@ -139,15 +191,17 @@ function CompetitionPortalPage () {

Welcome the the AI Portal

+ {/** This section will display the stats for the user's team otherwise shows default message telling them to find a team */}
-

Uh oh! You’re not in a team yet. Ask your friends to share their invite code, then navigate to Find Teams below to join their group!

- +

+ Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, + then navigate to Find Teams below to join their group! +

- {/* If the user is not part of a team yet, then */} Date: Thu, 14 Dec 2023 12:09:51 -0800 Subject: [PATCH 05/55] tweaked filter functionality --- src/pages/Competitions/CompetitionPortalPage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 8eb67e5..b221b4c 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -86,7 +86,7 @@ const FindTeamsTab = (data: Array): React.ReactNode => { onSelect = {handleSelect} options={options.map((item: any) => ({ value: item.teamName }))} filterOption = {(inputValue, option) => - option?.value.toLowerCase().includes(inputValue.toLowerCase()) + option?.value.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1 } size = "large" style = {{width: "100%", fontSize: 1.4}} From b08bfcbbd502b7bac6172a90375dba99dd072357 Mon Sep 17 00:00:00 2001 From: William Kim Date: Thu, 14 Dec 2023 13:34:56 -0800 Subject: [PATCH 06/55] added comments --- .../CompetitionPortalPage/index.less | 5 ++- .../CompetitionPortalPage/index.tsx | 32 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index d61fbc6..ad90879 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -6,7 +6,6 @@ .CompetitionPortalPage { .noContainer(); - // prevents layout shifts caused by scrollbar overflow-y: hidden; background: @white; @@ -15,6 +14,8 @@ #portalHeader{ .generic-section(); .constrained-bounds(); + max-width: 1200px !important; + margin-top: 4rem; } @@ -37,6 +38,8 @@ #portalTabContent { .generic-section(); .constrained-bounds(); + max-width: 1200px !important; + .ant-tabs-ink-bar { display: none; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index b221b4c..4bcd8e3 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -42,25 +42,22 @@ const teamCard = (team: any): React.ReactNode => { const FindTeamsTab = (data: Array): React.ReactNode => { + // constants to align the pagination options for the teams list const [position] = useState('bottom'); const [align] = useState('center'); - const [searchTerm, setSearchTerm] = useState(""); // dropdown options for search bar const [options, setOptions] = useState>(data); - // data for the list - - + // Initialize the teams data once that data defined useEffect(() => { if(data) { setOptions(data); } }, [data]) - const handleSearch = (value: string) => { - setSearchTerm(value); + const handleSearch = (value: string) => { // Reset options back to the original data if the value is an empty string if (value === "") { setOptions(data); @@ -70,12 +67,11 @@ const FindTeamsTab = (data: Array): React.ReactNode => { const handleSelect = (value: string) => { + // filter the list items const filteredOptions = data.filter((item: any) => - item.teamName.toLowerCase().includes(value.toLowerCase()) + item.teamName.toUpperCase().includes(value.toUpperCase()) ); - setOptions(filteredOptions) - } return ( @@ -84,17 +80,21 @@ const FindTeamsTab = (data: Array): React.ReactNode => { id = "teamSearchBar" onSearch={(text) => handleSearch(text)} onSelect = {handleSelect} + + // list of all possible options for dropdown options={options.map((item: any) => ({ value: item.teamName }))} + + // filterOption to handle filtered dropdown items filterOption = {(inputValue, option) => - option?.value.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1 + option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 } size = "large" style = {{width: "100%", fontSize: 1.4}} > - + {/** List to preview all the teams based on the user's query */} { return (

Leaderboard

-
); }; @@ -165,8 +164,9 @@ function CompetitionPortalPage () { } else { - // Grab all the teams for the current competitions - // Note: I am using the test competition name from mongoDB + /* Grab all the teams for the current competitions + * Note: I am using the test competition 2 name from mongoDB + */ getTeams("TestCompetition2").then(res => { if(res.data) { setAllTeams(Array.from(res.data)) @@ -183,7 +183,6 @@ function CompetitionPortalPage () { return ( -
@@ -201,6 +200,7 @@ function CompetitionPortalPage () { + - - ); From a4cd26db3170ed596ab921c5aab7c485c6d18a76 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 14:26:13 -0800 Subject: [PATCH 07/55] added functionality to join and leave teams --- src/actions/teams/utils.ts | 48 ++++- src/components/TeamCard/index.tsx | 138 +++++++++++++ .../CompetitionPortalPage/index.tsx | 183 +++++++++++------- 3 files changed, 300 insertions(+), 69 deletions(-) create mode 100644 src/components/TeamCard/index.tsx diff --git a/src/actions/teams/utils.ts b/src/actions/teams/utils.ts index 5e2f4a3..1420045 100644 --- a/src/actions/teams/utils.ts +++ b/src/actions/teams/utils.ts @@ -142,7 +142,8 @@ export const addToTeam = async ( let body = { username, teamName, - code, + competitionName, + code }; return new Promise((resolve, reject) => { @@ -155,7 +156,7 @@ export const addToTeam = async ( headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', - }, + } } ) .then((res: AxiosResponse) => { @@ -168,3 +169,46 @@ export const addToTeam = async ( }); }); }; + + + +export const leaveTeam = async( + competitionName: string, + username: string, + teamName: string +): Promise => { + let token = getToken(COOKIE_NAME); + + return new Promise((resolve, reject) => { + axios + .delete( + process.env.REACT_APP_API + + `/v1/competitions/teams/${competitionName}/remove-member`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + data: { + username: username, + teamName: teamName, + } + + + } + + + ) + .then((res: AxiosResponse) => { + resolve(res); + }) + .catch((error) => { + message.error(error.response.data); + reject(error); + }); + }); + + + +}; + + diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx new file mode 100644 index 0000000..256e65b --- /dev/null +++ b/src/components/TeamCard/index.tsx @@ -0,0 +1,138 @@ +import { Modal, Col, Button, Form, Input, message } from "antd"; +import { useState } from "react"; +import { User } from "../../UserContext"; +import { addToTeam, leaveTeam } from '../../actions/teams/utils'; + +import React from "react"; +import { useForm } from "react-hook-form"; +import { error } from "console"; + + +const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user:User, compUser: any, fetchTeamCallback: () => void }) => { + // Modal state to trigger a team's details + const [isModalOpen, setIsModalOpen] = useState(false); + const [code, setCode] = useState(""); + const [confirmLoading, setConfirmLoading] = useState(false); + + + const onSubmit = () => { + setConfirmLoading(true); + + console.log("Comp user", compUser) + if(compUser.competitionTeam !== null) { + if(compUser.competitionTeam.teamName !== team.teamName) { + message.info("Cannot join another team if you're already in one") + setConfirmLoading(false); + return; + } + } + + addToTeam(team.competitionName, user.username, team.teamName, code).then( + () => { + message.success('Joined team!'); + setConfirmLoading(false); + setIsModalOpen(false); + + // trigger a refresh to refetch all the team data + fetchTeamCallback(); + } + ) + .catch((error) => { + console.log(error); + }); + setConfirmLoading(false); + + }; + + const onLeaveTeam = () => { + leaveTeam(team.competitionName, user.username, team.teamName).then( (res) => { + message.success(res.data.msg); + setIsModalOpen(false); + + fetchTeamCallback(); + }) + .catch((error) => ( + message.error(error) + )); + } + + + + // Modal props + const showModal = () => { + setIsModalOpen(true); + }; + const handleCancel = () => { + setIsModalOpen(false); + }; + + + return ( + <> + {team.teamName}} + footer = { + + <> + {team.teamMembers.includes(user.username) && ( + + )} + + {!team.teamMembers.includes(user.username) && ( + + + )} + + + } + > + + +
+

Members

+ {team.teamMembers.map((member: string, index: number) => ( +

{member}

+ ))} +
+ + {!team.teamMembers.includes(user.username) && ( + + setCode(e.target.value)} + placeholder="Code" + /> + )} + + +
+ + {/* If user is in not in team, show option to join team */} +
+

{team.teamName}

+ +

{team.teamMembers.length} members

+ {team.teamMembers.includes(user.username) && (

your team

)} +
+ + {/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} + +
+ + + ); + }; + + export default TeamCard; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 4bcd8e3..323cc86 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from "react"; -import { AutoComplete, Avatar, List, Tabs, message } from "antd"; -import { Layout, Space, Button, Input } from 'antd'; -import UserContext from "../../../UserContext"; +import { AutoComplete, Avatar, Col, Form, List, Tabs, message } from "antd"; +import { Layout, Space, Button, Input, Modal } from 'antd'; +import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; import { getTeamInfo, @@ -9,38 +9,20 @@ import { getCompetitionUser, getTeams, } from '../../../actions/teams/utils'; - +import TeamCard from '../../../components/TeamCard/index'; import './index.less'; import path from 'path'; - import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; -import { all } from "axios"; - +import { registerCompetitionUser } from "../../../actions/competition"; +import { useForm } from "react-hook-form"; const { Content } = Layout; -const teamCard = (team: any): React.ReactNode => { - return ( -
-

{team.teamName}

-

{team.teamMembers.length} members

- {/* {team.teamMembers.map((member: string, index: number) => ( -

{member}

- ))} */} - - - {/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} - -
- ); -}; - - -const FindTeamsTab = (data: Array): React.ReactNode => { +const FindTeamsTab = ( + { data, user, compUser, registered, fetchTeams }: + { data: Object[], user: User, compUser: any, registered: Boolean, fetchTeams: () => void } +) => { // constants to align the pagination options for the teams list const [position] = useState('bottom'); @@ -49,12 +31,13 @@ const FindTeamsTab = (data: Array): React.ReactNode => { // dropdown options for search bar const [options, setOptions] = useState>(data); + // Initialize the teams data once that data defined useEffect(() => { if(data) { setOptions(data); } - }, [data]) + }, [data, registered]) const handleSearch = (value: string) => { @@ -99,9 +82,9 @@ const FindTeamsTab = (data: Array): React.ReactNode => { split = {false} pagination={{ position, align}} dataSource={options} - renderItem={(item: any) => ( - - {teamCard(item)} + renderItem={(team: any) => ( + + {} )} /> @@ -111,7 +94,7 @@ const FindTeamsTab = (data: Array): React.ReactNode => { }; -const LeaderBoardTab = (): React.ReactNode => { +const LeaderBoardTab = () => { return (

Leaderboard

@@ -120,7 +103,7 @@ const LeaderBoardTab = (): React.ReactNode => { }; - const MyTeamTab = (): React.ReactNode => { + const MyTeamTab = () => { return (

My Team

@@ -133,29 +116,56 @@ const LeaderBoardTab = (): React.ReactNode => { function CompetitionPortalPage () { const history = useHistory(); const { user } = useContext(UserContext); + const [compUser, setCompUser] = useState({}); + const [allTeams, setAllTeams] = useState>([]); + const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key + const [isRegistered, setIsRegistered] = useState(false); + const competitionName = "TestCompetition2"; - {/** Might save for later when we need to edit the tab Items */} - const tabItems= [ - { - label:

Leaderboard

, - key: '1', - children: LeaderBoardTab(), - }, - { - label:

Find Teams

, - key: '2', - children: FindTeamsTab(allTeams), - }, - { - label:

My Team

, - key: '3', - children: MyTeamTab(), - } - ]; + // callback function to trigger manual tab focus from elsewhere in the UI + const switchActiveTab = (key: string) => { + setActiveTab(key); + } + + const [isModalOpen, setIsModalOpen] = useState(false); + const { handleSubmit } = useForm(); + + // Modal props + const showModal = () => { + setIsModalOpen(true); + }; + const handleCancel = () => { + setIsModalOpen(false); + }; + + // Function to directly fetch all the teams + const fetchTeams = () => { + + getCompetitionUser(competitionName, user.username).then((res) => { + console.log(res.data) + if(!res.data.registered) { + message.info("you are not registered!"); + showModal(); + } + else { + setCompUser(res.data); + console.log(compUser) + getTeams(competitionName).then(res => { + if(res.data) { + setAllTeams(Array.from(res.data)) + console.log(Array.from(res.data)); + } + }) + .catch(error => { + console.log(error); + }); + } + }) + } useEffect(() => { if (!user.loggedIn) { @@ -164,25 +174,58 @@ function CompetitionPortalPage () { } else { - /* Grab all the teams for the current competitions - * Note: I am using the test competition 2 name from mongoDB - */ - getTeams("TestCompetition2").then(res => { - if(res.data) { - setAllTeams(Array.from(res.data)) - console.log(Array.from(res.data)); - } - }) - .catch(error => { - console.log(error); - }); + fetchTeams(); + } - }, []); + }, [user]); + + + const onSubmit = () => { + registerCompetitionUser(competitionName, user.username).then((res) => { + if (res.data.msg == "Success") { + window.location.reload(); + setIsRegistered(true); + } + else { + message.info(res.data.msg); + } + }) + + } return ( + + {/** Register Modal */} + Register} + footer = {null} + > +

+ Looks like we don't have you registered for {competitionName} yet. Click register below to get started. The page will + reload once we confirm your registration. Otherwise, feel free to leave this page. +

+ + + +
+ +
@@ -206,6 +249,7 @@ function CompetitionPortalPage () { size="small" animated={true} tabPosition="top" + activeKey={activeTab} onChange={(key) => setActiveTab(key)} items={ [ { @@ -216,13 +260,18 @@ function CompetitionPortalPage () { { label:

Find Teams

, key: '2', - children: FindTeamsTab(allTeams), + children: }, { label:

My Team

, key: '3', children: MyTeamTab(), - } + }, ] } > From 5470a33228d278f785abae2f68d6cb7518dedd40 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 20:18:34 -0800 Subject: [PATCH 08/55] user can make a new team --- src/components/TeamCard/index.less | 51 ++++++++++++++ src/components/TeamCard/index.tsx | 38 +++++----- .../CompetitionPortalPage/index.less | 23 ++++--- .../CompetitionPortalPage/index.tsx | 69 ++++++++++++++++--- 4 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 src/components/TeamCard/index.less diff --git a/src/components/TeamCard/index.less b/src/components/TeamCard/index.less new file mode 100644 index 0000000..4ac35d5 --- /dev/null +++ b/src/components/TeamCard/index.less @@ -0,0 +1,51 @@ +@import '../../styles/base.less'; +@import '../../newStyles/variables.less'; +@import '../../newStyles/components.less'; + + +#teamModalContent { + + section#teamMembersContainer { + background: @gray; + padding: @padding-base2; + border-radius: @radius-base4; + margin-top: 2rem; + } + + +} + +#joinCodeInput { + max-width: 300px; + margin-right: 1rem; +} + +div.teamPreviewCard { + + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background: @gray; + padding: @padding-base2; + border-radius: @radius-base4; + box-shadow: @bx3; + + + h3 { + max-width: 300px; + font-weight: @semi-bold; + } + +} + +#teamViewButton { + .button-black(); + +} + + +#teamJoinButton { + .button-black-square(); +} \ No newline at end of file diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index 256e65b..5dbf49a 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -2,7 +2,7 @@ import { Modal, Col, Button, Form, Input, message } from "antd"; import { useState } from "react"; import { User } from "../../UserContext"; import { addToTeam, leaveTeam } from '../../actions/teams/utils'; - +import './index.less'; import React from "react"; import { useForm } from "react-hook-form"; import { error } from "console"; @@ -86,10 +86,21 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user )} {!team.teamMembers.includes(user.username) && ( - - + <> + + setCode(e.target.value)} + placeholder="Code" + /> + + + + )} @@ -97,36 +108,27 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user > -
-

Members

+
{team.teamMembers.map((member: string, index: number) => (

{member}

))}
- {!team.teamMembers.includes(user.username) && ( - - setCode(e.target.value)} - placeholder="Code" - /> - )} + {/* If user is in not in team, show option to join team */}
-

{team.teamName}

+

{team.teamName}

{team.teamMembers.length} members

{team.teamMembers.includes(user.username) && (

your team

)}
{/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} -
diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index ad90879..184ad21 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -19,6 +19,9 @@ margin-top: 4rem; } + + + #portalStatsContent { @@ -35,6 +38,8 @@ } + + #portalTabContent { .generic-section(); .constrained-bounds(); @@ -53,18 +58,14 @@ } } +} - .teamPreviewCard { - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - background: @gray; - padding: @padding-base3 @padding-base2; - border-radius: @radius-base4; +#myTeamContainer { + #teamNameInput { + max-width: 300px; } - - + #makeTeamButton { + margin-top: 1rem; + } } \ No newline at end of file diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 323cc86..a3a9361 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { AutoComplete, Avatar, Col, Form, List, Tabs, message } from "antd"; +import { AutoComplete, Avatar, Col, Drawer, Form, List, Row, Skeleton, Tabs, message } from "antd"; import { Layout, Space, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; @@ -15,7 +15,6 @@ import path from 'path'; import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; import { registerCompetitionUser } from "../../../actions/competition"; -import { useForm } from "react-hook-form"; const { Content } = Layout; @@ -31,7 +30,6 @@ const FindTeamsTab = ( // dropdown options for search bar const [options, setOptions] = useState>(data); - // Initialize the teams data once that data defined useEffect(() => { if(data) { @@ -94,7 +92,7 @@ const FindTeamsTab = ( }; -const LeaderBoardTab = () => { +const LeaderBoardTab = ( ) => { return (

Leaderboard

@@ -103,10 +101,63 @@ const LeaderBoardTab = () => { }; - const MyTeamTab = () => { + const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsCallback: () => void}) => { + + const [isLoading, setIsLoading] = useState(false); + const [newTeamName, setNewTeamName] = useState(""); + useEffect(() => { + + }, []); + + const handleClick = () => { + + if(newTeamName.length == 0) { + message.info('Name cannot be empty'); + return; + } + + setIsLoading(true); + createTeam(compUser.competitionName, compUser.username, newTeamName).then((res) => { + message.success('Successfully made a new team!'); + fetchTeamsCallback(); + + }) + .catch((error) => { + message.error(error.message); + }); + setIsLoading(false); + + } + return ( -

My Team

+ {compUser.competitionTeam == null && ( + +
+ setNewTeamName(e.target.value)} + > +
+ +
+ + ) } + + {compUser.competitionTeam !== null && ( +
+

{compUser.competitionTeam.teamName}

+
+ )}
); }; @@ -131,7 +182,6 @@ function CompetitionPortalPage () { } const [isModalOpen, setIsModalOpen] = useState(false); - const { handleSubmit } = useForm(); // Modal props const showModal = () => { @@ -270,13 +320,16 @@ function CompetitionPortalPage () { { label:

My Team

, key: '3', - children: MyTeamTab(), + children: , }, ] } >
+ ); From 98a249f22243b9ce8d1df8a5833241c56bdbf5ac Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 20:45:24 -0800 Subject: [PATCH 09/55] setting up stats preview when user changes teams --- .../CompetitionPortalPage/index.tsx | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index a3a9361..2de8974 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -91,6 +91,9 @@ const FindTeamsTab = ( ); }; + + + const LeaderBoardTab = ( ) => { return ( @@ -101,6 +104,10 @@ const LeaderBoardTab = ( ) => { }; + + + + const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsCallback: () => void}) => { const [isLoading, setIsLoading] = useState(false); @@ -120,12 +127,12 @@ const LeaderBoardTab = ( ) => { createTeam(compUser.competitionName, compUser.username, newTeamName).then((res) => { message.success('Successfully made a new team!'); fetchTeamsCallback(); + console.log(compUser) }) .catch((error) => { message.error(error.message); }); - setIsLoading(false); } @@ -173,6 +180,8 @@ function CompetitionPortalPage () { const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key const [isRegistered, setIsRegistered] = useState(false); + const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); + const [teamInfo, setTeamInfo] = useState({}); const competitionName = "TestCompetition2"; @@ -225,13 +234,29 @@ function CompetitionPortalPage () { else { fetchTeams(); - } - }, [user]); + }, [user]); + + // only grab team info when user is in a team + useEffect(() => { + if(Object.keys(compUser).length !== 0){ + + if(compUser.competitionTeam != null){ + console.log(compUser) + setIsLoadingTeamInfo(true); + getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { + setTeamInfo(res.data); + setIsLoadingTeamInfo(false); + + }) + } + } + + }, [compUser]) - const onSubmit = () => { + const onSubmit = () => { registerCompetitionUser(competitionName, user.username).then((res) => { if (res.data.msg == "Success") { window.location.reload(); @@ -284,11 +309,28 @@ function CompetitionPortalPage () {
{/** This section will display the stats for the user's team otherwise shows default message telling them to find a team */} +
-

- Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, - then navigate to Find Teams below to join their group! -

+ {compUser.competitionTeam == null ? ( +

+ Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, + then navigate to Find Teams below to join their group! +

+ ) + : + <> + {isLoadingTeamInfo ? ( + + ): +
+

Team Name: {teamInfo.teamName}

+
+ } + + + + } +
From ec9f7e8b5eacf886537244e5858330109826f84c Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 22:10:48 -0800 Subject: [PATCH 10/55] fixed team card UI --- src/components/TeamCard/index.less | 18 +++++-- src/components/TeamCard/index.tsx | 12 ++--- .../CompetitionPortalPage/index.tsx | 50 +++++++++++-------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/components/TeamCard/index.less b/src/components/TeamCard/index.less index 4ac35d5..eec9f3b 100644 --- a/src/components/TeamCard/index.less +++ b/src/components/TeamCard/index.less @@ -23,10 +23,9 @@ div.teamPreviewCard { width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; + display: grid; align-items: center; + grid-template-columns: 1fr 1fr 1fr; background: @gray; padding: @padding-base2; border-radius: @radius-base4; @@ -38,10 +37,23 @@ div.teamPreviewCard { font-weight: @semi-bold; } + span:first-of-type{ + text-align: center; + } + + span:last-child { + width: 100%; + display:flex; + justify-content: end; + } + + } #teamViewButton { .button-black(); + width:fit-content; + } diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index 5dbf49a..0f27468 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -113,9 +113,6 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user

{member}

))}
- - - @@ -128,9 +125,12 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user {/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} - + + + + diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 2de8974..d6ea94c 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -134,6 +134,9 @@ const LeaderBoardTab = ( ) => { message.error(error.message); }); + setIsLoading(false); + + } return ( @@ -180,6 +183,8 @@ function CompetitionPortalPage () { const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key const [isRegistered, setIsRegistered] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); const [teamInfo, setTeamInfo] = useState({}); const competitionName = "TestCompetition2"; @@ -190,7 +195,6 @@ function CompetitionPortalPage () { setActiveTab(key); } - const [isModalOpen, setIsModalOpen] = useState(false); // Modal props const showModal = () => { @@ -221,7 +225,6 @@ function CompetitionPortalPage () { .catch(error => { console.log(error); }); - } }) } @@ -240,19 +243,22 @@ function CompetitionPortalPage () { // only grab team info when user is in a team useEffect(() => { - if(Object.keys(compUser).length !== 0){ + setIsLoadingTeamInfo(true); + if(Object.keys(compUser).length !== 0){ if(compUser.competitionTeam != null){ console.log(compUser) - setIsLoadingTeamInfo(true); getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { setTeamInfo(res.data); - setIsLoadingTeamInfo(false); }) } + setIsLoadingTeamInfo(false); + + } - + + }, [compUser]) @@ -311,26 +317,26 @@ function CompetitionPortalPage () { {/** This section will display the stats for the user's team otherwise shows default message telling them to find a team */}
- {compUser.competitionTeam == null ? ( -

- Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, - then navigate to Find Teams below to join their group! -

- ) - : - <> + {isLoadingTeamInfo ? ( ): -
-

Team Name: {teamInfo.teamName}

-
- } - - - - } + ( <> + {compUser.competitionTeam == null ? ( +

+ Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, + then navigate to Find Teams below to join their group! +

+ ) + : +
+

Team Name: {teamInfo.teamName}

+
+ } + + )} +
From 386b10e3193dfb0413609ee38729793f229003ca Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 23:05:49 -0800 Subject: [PATCH 11/55] added team profile picture --- src/components/TeamCard/index.less | 16 ++++++- src/components/TeamCard/index.tsx | 2 +- .../CompetitionPortalPage/index.less | 12 +++++- .../CompetitionPortalPage/index.tsx | 43 +++++++++++++++++-- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/components/TeamCard/index.less b/src/components/TeamCard/index.less index eec9f3b..4bdbeb4 100644 --- a/src/components/TeamCard/index.less +++ b/src/components/TeamCard/index.less @@ -25,7 +25,7 @@ div.teamPreviewCard { width: 100%; display: grid; align-items: center; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1.5fr 1fr 1fr; background: @gray; padding: @padding-base2; border-radius: @radius-base4; @@ -38,7 +38,19 @@ div.teamPreviewCard { } span:first-of-type{ - text-align: center; + width: 70%; + display: flex; + align-items: center; + p:nth-child(2){ + margin-left: 8px; + display: inline; + background: rgb(33, 94, 247); + width: fit-content; + padding: 6px 20px; + border-radius: @radius-base6; + color: white; + } + } span:last-child { diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index 0f27468..076c854 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -121,7 +121,7 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user

{team.teamName}

{team.teamMembers.length} members

- {team.teamMembers.includes(user.username) && (

your team

)} + {team.teamMembers.includes(user.username) && (

you

)}
{/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 184ad21..d84e105 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -52,7 +52,9 @@ } #findTeamsContainer { - AutoComplete#teamSearchBar { + height: 100vh; + + #teamSearchBar { font-size: 1.2rem; width: 100%; } @@ -61,6 +63,7 @@ } #myTeamContainer { + height: 100vh; #teamNameInput { max-width: 300px; } @@ -68,4 +71,11 @@ #makeTeamButton { margin-top: 1rem; } + + #teamAffix { + box-sizing: border-box; + box-shadow: @bx2; + border-radius: @radius-base4; + padding: @padding-base3; + } } \ No newline at end of file diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index d6ea94c..99ac1ba 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { AutoComplete, Avatar, Col, Drawer, Form, List, Row, Skeleton, Tabs, message } from "antd"; +import { Affix, AutoComplete, Avatar, Col, Drawer, Form, List, Row, Skeleton, Tabs, message } from "antd"; import { Layout, Space, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; @@ -15,6 +15,7 @@ import path from 'path'; import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; import { registerCompetitionUser } from "../../../actions/competition"; +import { genColor } from "../../../utils/colors"; const { Content } = Layout; @@ -112,10 +113,33 @@ const LeaderBoardTab = ( ) => { const [isLoading, setIsLoading] = useState(false); const [newTeamName, setNewTeamName] = useState(""); + useEffect(() => { }, []); + + + const generateTeamPicture = () => { + + const color1 = genColor(compUser.competitionTeam.teamName); + const color2 = genColor(`${compUser.competitionTeam.teamName}_additional_seed`); + + return ( +
+
+ ) + } + const handleClick = () => { if(newTeamName.length == 0) { @@ -135,8 +159,6 @@ const LeaderBoardTab = ( ) => { }); setIsLoading(false); - - } return ( @@ -160,12 +182,25 @@ const LeaderBoardTab = ( ) => { Make Team + ) } {compUser.competitionTeam !== null && (
-

{compUser.competitionTeam.teamName}

+ + {generateTeamPicture()} +

{compUser.competitionTeam.teamName}

+ +
+ + +
+ +

Members

+

Description

+
+
)}
From 0687bf6322183a73e9c02213048113a12cdeaa37 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sun, 17 Dec 2023 23:26:45 -0800 Subject: [PATCH 12/55] tweaking team card preview --- src/components/TeamCard/index.less | 3 ++- src/components/TeamCard/index.tsx | 8 ++++---- src/pages/Competitions/CompetitionPortalPage/index.tsx | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/TeamCard/index.less b/src/components/TeamCard/index.less index 4bdbeb4..733434c 100644 --- a/src/components/TeamCard/index.less +++ b/src/components/TeamCard/index.less @@ -37,10 +37,11 @@ div.teamPreviewCard { font-weight: @semi-bold; } - span:first-of-type{ + span:first-of-type { width: 70%; display: flex; align-items: center; + p:nth-child(2){ margin-left: 8px; display: inline; diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index 076c854..5affe13 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -1,4 +1,4 @@ -import { Modal, Col, Button, Form, Input, message } from "antd"; +import { Modal, Col, Button, Form, Input, message, Badge } from "antd"; import { useState } from "react"; import { User } from "../../UserContext"; import { addToTeam, leaveTeam } from '../../actions/teams/utils'; @@ -117,11 +117,11 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user {/* If user is in not in team, show option to join team */} -
+

{team.teamName}

-

{team.teamMembers.length} members

- {team.teamMembers.includes(user.username) && (

you

)} +

{team.teamMembers.length} members

{/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 99ac1ba..fb7ffe2 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -188,17 +188,18 @@ const LeaderBoardTab = ( ) => { {compUser.competitionTeam !== null && (
- + {generateTeamPicture()} +

{compUser.competitionTeam.teamName}

+
-

Members

-

Description

+

Team Members

From 9978ddf319841d6c56122f088d212c7533b8554f Mon Sep 17 00:00:00 2001 From: William Kim Date: Mon, 18 Dec 2023 20:58:27 -0800 Subject: [PATCH 13/55] saving changes --- src/pages/Competitions/CompetitionPortalPage/index.less | 2 +- src/pages/Competitions/CompetitionPortalPage/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index d84e105..32bf0ea 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -43,6 +43,7 @@ #portalTabContent { .generic-section(); .constrained-bounds(); + height: 100vh; max-width: 1200px !important; @@ -63,7 +64,6 @@ } #myTeamContainer { - height: 100vh; #teamNameInput { max-width: 300px; } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index fb7ffe2..d573a28 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -199,7 +199,7 @@ const LeaderBoardTab = ( ) => {
-

Team Members

+

Team Members

From 6b31e8bbbc2fdc4c07ea6b2d35c6d335a335c6db Mon Sep 17 00:00:00 2001 From: William Kim Date: Tue, 19 Dec 2023 22:28:55 -0800 Subject: [PATCH 14/55] refined team preview card UI --- src/components/TeamCard/index.less | 50 ++++++++++------- src/components/TeamCard/index.tsx | 53 +++++++++++++------ .../CompetitionPortalPage/index.tsx | 6 +-- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/components/TeamCard/index.less b/src/components/TeamCard/index.less index 733434c..f8b85f4 100644 --- a/src/components/TeamCard/index.less +++ b/src/components/TeamCard/index.less @@ -25,49 +25,61 @@ div.teamPreviewCard { width: 100%; display: grid; align-items: center; - grid-template-columns: 1.5fr 1fr 1fr; + grid-template-columns: 2fr 1fr 1fr; background: @gray; padding: @padding-base2; border-radius: @radius-base4; box-shadow: @bx3; + cursor: pointer; - h3 { - max-width: 300px; - font-weight: @semi-bold; + + span:first-child { + display: flex; + align-items: center; + h4 { + font-weight: @semi-bold; + } } - span:first-of-type { - width: 70%; + span:nth-of-type(2) { display: flex; align-items: center; + text-align: center; - p:nth-child(2){ - margin-left: 8px; - display: inline; - background: rgb(33, 94, 247); - width: fit-content; - padding: 6px 20px; - border-radius: @radius-base6; - color: white; + p{ + font-weight: @semi-bold; } - } - span:last-child { - width: 100%; + span:last-of-type { display:flex; justify-content: end; } + @media screen and (max-width: 480px){ + span#teamViewButtonSpan{ + display: none; + } + + span:nth-of-type(2) { + justify-content: end; + } + } +} +@media screen and (max-width: 480px){ + div.teamPreviewCard { + grid-template-columns: 1fr 1fr; + } + } + + #teamViewButton { .button-black(); width:fit-content; - - } diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index 5affe13..f844f26 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -1,11 +1,16 @@ -import { Modal, Col, Button, Form, Input, message, Badge } from "antd"; +import { Modal, Col, Button, Form, Input, message, Badge, Avatar } from "antd"; import { useState } from "react"; import { User } from "../../UserContext"; import { addToTeam, leaveTeam } from '../../actions/teams/utils'; +import { BsPeopleFill } from "react-icons/bs"; +import { IoMdPerson } from "react-icons/io"; + import './index.less'; import React from "react"; + import { useForm } from "react-hook-form"; import { error } from "console"; +import { genColor } from "../../utils/colors"; const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user:User, compUser: any, fetchTeamCallback: () => void }) => { @@ -65,8 +70,10 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user const handleCancel = () => { setIsModalOpen(false); }; - - + + const color1 = genColor(team.teamName); + const color2 = genColor(`${team.teamName}_additional_seed`); + return ( <> setCode(e.target.value)} placeholder="Code" /> - - )} - } >
- {team.teamMembers.map((member: string, index: number) => ( -

{member}

- ))} + + {team.teamMembers.length !== 0 ? + team.teamMembers.map((member: string, index: number) => ( +

{member}

+ )) + + :

no members

}
{/* If user is in not in team, show option to join team */} -
-

{team.teamName}

+
showModal()}> -

{team.teamMembers.length} members

+
+
+

{team.teamName}

+
+ + + {/*

{team.teamMembers.length} members

*/} + + } /> +
{/** Clicking the button should open a modal to display team details and the option to join if user isn't part of team yet */} - + -
diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index d573a28..f19a4ad 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -70,8 +70,8 @@ const FindTeamsTab = ( filterOption = {(inputValue, option) => option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 } - size = "large" - style = {{width: "100%", fontSize: 1.4}} + size="large" + style = {{width: "100%"}} > @@ -79,7 +79,7 @@ const FindTeamsTab = ( {/** List to preview all the teams based on the user's query */} ( From 0fe840166746c5f99693d3000d60018851f1a07d Mon Sep 17 00:00:00 2001 From: William Kim Date: Wed, 20 Dec 2023 18:14:52 -0800 Subject: [PATCH 15/55] added leaderboard --- src/actions/competition.ts | 9 + src/components/TeamCard/index.tsx | 13 +- src/newStyles/components.less | 4 + .../CompetitionPortalPage/index.less | 36 +++- .../CompetitionPortalPage/index.tsx | 198 +++++++++++++----- 5 files changed, 193 insertions(+), 67 deletions(-) diff --git a/src/actions/competition.ts b/src/actions/competition.ts index 01ebc8e..9d0107d 100644 --- a/src/actions/competition.ts +++ b/src/actions/competition.ts @@ -10,6 +10,15 @@ export type PastCompetition = { route: string; }; +export interface CompetitionData { + rank: number; + team: string; + users: string[]; + score: number; + submitHistory: Array; + last: Date; +} + export const uploadSubmission = async ( file: File | undefined, tagsSelected: string[], diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index f844f26..c327239 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -13,7 +13,10 @@ import { error } from "console"; import { genColor } from "../../utils/colors"; -const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user:User, compUser: any, fetchTeamCallback: () => void }) => { +const TeamCard = ( + { team, user, compUser, fetchTeamCallback, updateRankings }: + { team: any, user:User, compUser: any, fetchTeamCallback: () => void, updateRankings: () => void } +) => { // Modal state to trigger a team's details const [isModalOpen, setIsModalOpen] = useState(false); const [code, setCode] = useState(""); @@ -40,6 +43,9 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user // trigger a refresh to refetch all the team data fetchTeamCallback(); + + // update the leaderboard with the new team + updateRankings(); } ) .catch((error) => { @@ -55,14 +61,13 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user setIsModalOpen(false); fetchTeamCallback(); + updateRankings(); }) .catch((error) => ( message.error(error) )); } - - // Modal props const showModal = () => { setIsModalOpen(true); @@ -144,7 +149,7 @@ const TeamCard = ({ team, user, compUser, fetchTeamCallback }: { team: any, user {/*

{team.teamMembers.length} members

*/} - } /> + } />
diff --git a/src/newStyles/components.less b/src/newStyles/components.less index 2ddc081..50c2d73 100644 --- a/src/newStyles/components.less +++ b/src/newStyles/components.less @@ -47,6 +47,10 @@ @media screen and (max-width: @sm){ width: 100%; } + + @media screen and (min-width: 769px){ + width: 80%; + } } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 32bf0ea..343abe3 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -5,9 +5,9 @@ .CompetitionPortalPage { .noContainer(); + height: 100vh; // prevents layout shifts caused by scrollbar - overflow-y: hidden; background: @white; @@ -15,7 +15,6 @@ .generic-section(); .constrained-bounds(); max-width: 1200px !important; - margin-top: 4rem; } @@ -24,7 +23,6 @@ #portalStatsContent { - p#noTeamMessage { margin-top: 2rem; text-align: left; @@ -44,25 +42,40 @@ .generic-section(); .constrained-bounds(); height: 100vh; - max-width: 1200px !important; - + max-width: 1200px !important; .ant-tabs-ink-bar { display: none; } } + #findTeamsContainer { - height: 100vh; - #teamSearchBar { - font-size: 1.2rem; - width: 100%; - } + + Input { + line-height: 2.5rem ; + font-size: @font3; + } } } + +#leaderBoardContainer { + + section { + display: inline-flex; + width: 100%; + justify-content: space-between; + margin-bottom: 1rem; + + Button { + .button-black-square(); + } + } +} + #myTeamContainer { #teamNameInput { max-width: 300px; @@ -78,4 +91,5 @@ border-radius: @radius-base4; padding: @padding-base3; } -} \ No newline at end of file +} + diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index f19a4ad..da97f13 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; -import { Affix, AutoComplete, Avatar, Col, Drawer, Form, List, Row, Skeleton, Tabs, message } from "antd"; -import { Layout, Space, Button, Input, Modal } from 'antd'; +import { Affix, AutoComplete, Avatar, Col, Drawer, List, Row, Skeleton, Tabs, message } from "antd"; +import { Layout, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; import { @@ -14,14 +14,15 @@ import './index.less'; import path from 'path'; import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; -import { registerCompetitionUser } from "../../../actions/competition"; +import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser } from "../../../actions/competition"; import { genColor } from "../../../utils/colors"; +import Table, { ColumnsType } from "antd/es/table"; const { Content } = Layout; const FindTeamsTab = ( - { data, user, compUser, registered, fetchTeams }: - { data: Object[], user: User, compUser: any, registered: Boolean, fetchTeams: () => void } + { data, user, compUser, registered, fetchTeams, updateRankings }: + { data: Object[], user: User, compUser: any, registered: Boolean, fetchTeams: () => void, updateRankings: () => void } ) => { // constants to align the pagination options for the teams list @@ -38,7 +39,6 @@ const FindTeamsTab = ( } }, [data, registered]) - const handleSearch = (value: string) => { // Reset options back to the original data if the value is an empty string if (value === "") { @@ -46,7 +46,6 @@ const FindTeamsTab = ( } } - const handleSelect = (value: string) => { // filter the list items @@ -57,7 +56,7 @@ const FindTeamsTab = ( } return ( - + handleSearch(text)} @@ -73,7 +72,7 @@ const FindTeamsTab = ( size="large" style = {{width: "100%"}} > - + {/** List to preview all the teams based on the user's query */} @@ -83,7 +82,7 @@ const FindTeamsTab = ( dataSource={options} renderItem={(team: any) => ( - {} + {} )} /> @@ -92,14 +91,79 @@ const FindTeamsTab = ( ); }; +const LeaderBoardTab = ( + {rankData, lastRefresh, updateRankingsCallback, isLoading}: + { + rankData: any, + lastRefresh: Date | null, + updateRankingsCallback: () => void, + isLoading: boolean + } +) => { + const columns: ColumnsType = [ + { + title: 'Rank', + dataIndex: 'rank', + sorter: (a, b) => a.rank - b.rank, + defaultSortOrder: 'ascend', + }, + { + title: 'Team', + dataIndex: 'team', + sorter: (a, b) => a.team.length - b.team.length, + render(value, record, index) { + const color1 = genColor(record.team); + const color2 = genColor(`${record.team}_abcs`); + return ( + +
+ {value.length > 28 ? ( + {value.substring(0, 28)}... + ) : ( + {value.substring(0, 28)} + )} +
+ ); + }, + }, + { + title: 'Score', + dataIndex: 'score', + sorter: (a, b) => a.score - b.score, + }, + ]; + + return ( + +
+

+ (Table last refreshed{': '} + {lastRefresh ? lastRefresh.toLocaleString() : ''}) +

+ +
- - -const LeaderBoardTab = ( ) => { - return ( - -

Leaderboard

+ + ); }; @@ -109,7 +173,7 @@ const LeaderBoardTab = ( ) => { - const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsCallback: () => void}) => { +const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsCallback: () => void}) => { const [isLoading, setIsLoading] = useState(false); const [newTeamName, setNewTeamName] = useState(""); @@ -162,7 +226,7 @@ const LeaderBoardTab = ( ) => { } return ( - + {compUser.competitionTeam == null && (
@@ -211,28 +275,39 @@ const LeaderBoardTab = ( ) => { function CompetitionPortalPage () { + + const competitionName = "TestCompetition2"; + const history = useHistory(); const { user } = useContext(UserContext); const [compUser, setCompUser] = useState({}); + const [isRegistered, setIsRegistered] = useState(false); const [allTeams, setAllTeams] = useState>([]); - const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key - const [isRegistered, setIsRegistered] = useState(false); + const [teamInfo, setTeamInfo] = useState({}); + + const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); + const [isLoadingLeaderBoard, setIsLoadingLeaderBoard] = useState(false); + const [lastRefresh, setLastRefresh] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key - const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); - const [teamInfo, setTeamInfo] = useState({}); - const competitionName = "TestCompetition2"; + const [rankData, setRankData] = useState([]); + // meta data for current competition + const [meta, setMeta] = useState<{ + competitionName: string; + description: string; + startDate: string; + endDate: string; + submissionsEnabled: boolean; + } | null>(null); - // callback function to trigger manual tab focus from elsewhere in the UI - const switchActiveTab = (key: string) => { - setActiveTab(key); - } - // Modal props + // Modal callback to show modal to register user for competition const showModal = () => { setIsModalOpen(true); }; @@ -251,11 +326,9 @@ function CompetitionPortalPage () { } else { setCompUser(res.data); - console.log(compUser) getTeams(competitionName).then(res => { if(res.data) { setAllTeams(Array.from(res.data)) - console.log(Array.from(res.data)); } }) .catch(error => { @@ -265,6 +338,34 @@ function CompetitionPortalPage () { }) } + const getCompMetaData = () => { + getMetaData(competitionName).then((res) => { + setMeta(res.data); + }); + } + + + const updateRankings = () => { + + setIsLoadingLeaderBoard(true); + getLeaderboard(competitionName).then((res) => { + + let newData = res.data.map((d: any, index: number) => { + return { + rank: index + 1, + team: d.teamName, + score: d.bestScore, + submitHistory: d.submitHistory, + }; + }); + setLastRefresh(new Date()); + setRankData(newData); + setIsLoadingLeaderBoard(false); + }); + + }; + + useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); @@ -273,10 +374,13 @@ function CompetitionPortalPage () { else { fetchTeams(); + getCompMetaData(); + updateRankings(); } }, [user]); + // only grab team info when user is in a team useEffect(() => { setIsLoadingTeamInfo(true); @@ -286,15 +390,10 @@ function CompetitionPortalPage () { console.log(compUser) getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { setTeamInfo(res.data); - }) } setIsLoadingTeamInfo(false); - - } - - }, [compUser]) @@ -308,10 +407,9 @@ function CompetitionPortalPage () { message.info(res.data.msg); } }) + } - } - return ( @@ -350,34 +448,27 @@ function CompetitionPortalPage () {

Welcome the the AI Portal

- {/** This section will display the stats for the user's team otherwise shows default message telling them to find a team */} - +
{isLoadingTeamInfo ? ( ): ( <> - {compUser.competitionTeam == null ? ( + {compUser.competitionTeam == null ?

Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, then navigate to Find Teams below to join their group!

- ) + : -
-

Team Name: {teamInfo.teamName}

-
+

Team Name: {teamInfo.teamName}

} )} - -
- - Leaderboard

, key: '1', - children: LeaderBoardTab(), + children: }, { label:

Find Teams

, @@ -399,7 +494,8 @@ function CompetitionPortalPage () { user = {user} compUser={compUser} registered = {isRegistered} - fetchTeams={fetchTeams}/> + fetchTeams={fetchTeams} + updateRankings={updateRankings}/> }, { label:

My Team

, @@ -416,8 +512,6 @@ function CompetitionPortalPage () { ); - } - export default CompetitionPortalPage; From dae6ea3522a7b9673f1e54d7737632c0ad530478 Mon Sep 17 00:00:00 2001 From: William Kim Date: Wed, 20 Dec 2023 18:41:47 -0800 Subject: [PATCH 16/55] tweaked leaderboard refresh text --- src/pages/Competitions/CompetitionPortalPage/index.less | 7 ++++--- src/pages/Competitions/CompetitionPortalPage/index.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 343abe3..390842b 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -51,7 +51,7 @@ #findTeamsContainer { - + margin-bottom: 5rem; Input { line-height: 2.5rem ; @@ -69,7 +69,7 @@ width: 100%; justify-content: space-between; margin-bottom: 1rem; - + Button { .button-black-square(); } @@ -77,6 +77,8 @@ } #myTeamContainer { + + #teamNameInput { max-width: 300px; } @@ -86,7 +88,6 @@ } #teamAffix { - box-sizing: border-box; box-shadow: @bx2; border-radius: @radius-base4; padding: @padding-base3; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index da97f13..2577c9f 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -147,9 +147,9 @@ const LeaderBoardTab = (
-

- (Table last refreshed{': '} - {lastRefresh ? lastRefresh.toLocaleString() : ''}) +

+ Last refreshed{': '} + {lastRefresh ? lastRefresh.toLocaleString() : ''}

- -
); @@ -183,7 +186,6 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC }, []); - const generateTeamPicture = () => { const color1 = genColor(compUser.competitionTeam.teamName); @@ -277,24 +279,37 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC function CompetitionPortalPage () { const competitionName = "TestCompetition2"; - const history = useHistory(); + + // User profile data const { user } = useContext(UserContext); const [compUser, setCompUser] = useState({}); const [isRegistered, setIsRegistered] = useState(false); + // array for all teams const [allTeams, setAllTeams] = useState>([]); - const [teamInfo, setTeamInfo] = useState({}); + // Competition Team Data + const [teamInfo, setTeamInfo] = useState({}); const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); - const [isLoadingLeaderBoard, setIsLoadingLeaderBoard] = useState(false); - const [lastRefresh, setLastRefresh] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key - const [rankData, setRankData] = useState([]); + // Rank data for all teams + const [rankingsData, setRankingsData] = useState([]); + + // Rank data for user's current team + const [userRankData, setUserRankData] = useState({rank: 0, + team: "", + score: 0, + submitHistory: []} + ); + + // Loading states for leaderboard data fetching status + const [isLoadingLeaderBoard, setIsLoadingLeaderBoard] = useState(false); + const [lastRefresh, setLastRefresh] = useState(null); + // meta data for current competition const [meta, setMeta] = useState<{ @@ -349,8 +364,21 @@ function CompetitionPortalPage () { setIsLoadingLeaderBoard(true); getLeaderboard(competitionName).then((res) => { - - let newData = res.data.map((d: any, index: number) => { + console.log(res.data) + + + let newData = res.data.sort((a: any, b: any) => b.bestScore - a.bestScore) // Sort by bestScore in descending order + .map((d: any, index: number) => { + + if(d.teamName === teamInfo.teamName) { + setUserRankData( { + rank: index + 1, + team: d.teamName, + score: d.bestScore, + submitHistory: d.submitHistory, + }); + } + return { rank: index + 1, team: d.teamName, @@ -358,11 +386,11 @@ function CompetitionPortalPage () { submitHistory: d.submitHistory, }; }); + setLastRefresh(new Date()); - setRankData(newData); + setRankingsData(newData); setIsLoadingLeaderBoard(false); }); - }; @@ -375,12 +403,12 @@ function CompetitionPortalPage () { else { fetchTeams(); getCompMetaData(); - updateRankings(); } }, [user]); + // only grab team info when user is in a team useEffect(() => { setIsLoadingTeamInfo(true); @@ -462,7 +490,22 @@ function CompetitionPortalPage () {

: -

Team Name: {teamInfo.teamName}

+
+
+

Your Total Submissions

+
+ +
+

Team Score

+

{userRankData.score}

+
+ +
+

Ranking

+

{userRankData.rank}

+
+ +
} )} @@ -481,7 +524,7 @@ function CompetitionPortalPage () { label:

Leaderboard

, key: '1', children: From 24ca16c465cf5de181a224ccdcbfa6e6fbae417e Mon Sep 17 00:00:00 2001 From: William Kim Date: Wed, 20 Dec 2023 22:51:23 -0800 Subject: [PATCH 18/55] tweaked portal stats header UI --- .../CompetitionPortalPage/index.less | 7 +--- .../CompetitionPortalPage/index.tsx | 42 ++++++++++--------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index cf28d7d..9617e75 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -24,12 +24,9 @@ #portalStatsContent { width: 100%; margin-top: 2rem; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - p#noTeamMessage { - margin-top: 2rem; + + section#noTeamMessage { min-height: 100px; text-align: left; align-items: center; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 38586a3..7e9b6aa 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -17,6 +17,7 @@ import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Paginati import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser } from "../../../actions/competition"; import { genColor } from "../../../utils/colors"; import Table, { ColumnsType } from "antd/es/table"; +import MainFooter from "../../../components/MainFooter"; const { Content } = Layout; @@ -270,6 +271,7 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC )} + ); }; @@ -484,28 +486,30 @@ function CompetitionPortalPage () { ): ( <> {compUser.competitionTeam == null ? -

+

+

Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, then navigate to Find Teams below to join their group! -

- +

+
+ : -
-
-

Your Total Submissions

-
- -
-

Team Score

-

{userRankData.score}

-
- -
-

Ranking

-

{userRankData.rank}

-
- -
+
+
+

Your Total Submissions

+
+ +
+

Team Score

+

{userRankData.score}

+
+ +
+

Ranking

+

{userRankData.rank}

+
+ +
} )} From f6e4832235d15bae06fd05787d84538193a786e4 Mon Sep 17 00:00:00 2001 From: William Kim Date: Fri, 22 Dec 2023 10:44:04 -0800 Subject: [PATCH 19/55] team ranks stats load properly now --- src/components/TeamCard/index.tsx | 4 +- .../CompetitionPortalPage/index.tsx | 49 ++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index c327239..ba28bf1 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -45,7 +45,7 @@ const TeamCard = ( fetchTeamCallback(); // update the leaderboard with the new team - updateRankings(); + // updateRankings(); } ) .catch((error) => { @@ -61,7 +61,7 @@ const TeamCard = ( setIsModalOpen(false); fetchTeamCallback(); - updateRankings(); + // updateRankings(); }) .catch((error) => ( message.error(error) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 7e9b6aa..fe9e252 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -18,6 +18,7 @@ import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompeti import { genColor } from "../../../utils/colors"; import Table, { ColumnsType } from "antd/es/table"; import MainFooter from "../../../components/MainFooter"; +import { AxiosResponse } from "axios"; const { Content } = Layout; @@ -145,9 +146,7 @@ const LeaderBoardTab = ( }, ]; - useEffect(() => { - updateRankingsCallback(); - }, []); + return ( @@ -323,7 +322,6 @@ function CompetitionPortalPage () { } | null>(null); - // Modal callback to show modal to register user for competition const showModal = () => { setIsModalOpen(true); @@ -336,13 +334,13 @@ function CompetitionPortalPage () { const fetchTeams = () => { getCompetitionUser(competitionName, user.username).then((res) => { - console.log(res.data) if(!res.data.registered) { message.info("you are not registered!"); showModal(); } else { setCompUser(res.data); + getTeams(competitionName).then(res => { if(res.data) { setAllTeams(Array.from(res.data)) @@ -355,20 +353,24 @@ function CompetitionPortalPage () { }) } - const getCompMetaData = () => { + // Function to get compeitition meta data + const getCompMetaData = async () => { + getMetaData(competitionName).then((res) => { setMeta(res.data); - }); + }) } + // Function to get ranking of user's team and all teams + function updateRankings() { - const updateRankings = () => { + console.log("updating ranks for team: ", teamInfo.teamName); setIsLoadingLeaderBoard(true); + getLeaderboard(competitionName).then((res) => { console.log(res.data) - let newData = res.data.sort((a: any, b: any) => b.bestScore - a.bestScore) // Sort by bestScore in descending order .map((d: any, index: number) => { @@ -379,6 +381,8 @@ function CompetitionPortalPage () { score: d.bestScore, submitHistory: d.submitHistory, }); + + console.log("user rank data: ", userRankData); } return { @@ -392,10 +396,13 @@ function CompetitionPortalPage () { setLastRefresh(new Date()); setRankingsData(newData); setIsLoadingLeaderBoard(false); + setIsLoadingTeamInfo(false); + }); }; + // If user is not logged in, navigate to auth useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); @@ -410,7 +417,6 @@ function CompetitionPortalPage () { }, [user]); - // only grab team info when user is in a team useEffect(() => { setIsLoadingTeamInfo(true); @@ -422,11 +428,31 @@ function CompetitionPortalPage () { setTeamInfo(res.data); }) } + else { + setIsLoadingTeamInfo(false); + } + } + else { setIsLoadingTeamInfo(false); } + + + }, [compUser]) - + + + // Get the updated rankings based on the state of the user's membership to a team + useEffect(() => { + + updateRankings(); + + + }, [teamInfo]); + + + + // When user registers for compeititons, allow access to portal const onSubmit = () => { registerCompetitionUser(competitionName, user.username).then((res) => { if (res.data.msg == "Success") { @@ -501,6 +527,7 @@ function CompetitionPortalPage () {

Team Score

+ {}

{userRankData.score}

From 315d5a5b01b22a2b99c204ae4d1f08eb62bad009 Mon Sep 17 00:00:00 2001 From: William Kim Date: Fri, 22 Dec 2023 17:59:47 -0800 Subject: [PATCH 20/55] tweaking some aync logic and UI --- .../CompetitionPortalPage/index.tsx | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index fe9e252..c4ffdba 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -292,7 +292,7 @@ function CompetitionPortalPage () { // Competition Team Data const [teamInfo, setTeamInfo] = useState({}); - const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(false); + const [isLoadingTeamInfo, setIsLoadingTeamInfo] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [activeTab, setActiveTab] = useState('1'); // Set the default active tab key @@ -330,17 +330,27 @@ function CompetitionPortalPage () { setIsModalOpen(false); }; - // Function to directly fetch all the teams + /** + * Fetches the comp user first to handle cases where the user + * may have joined or left a team prior to this, so we need to get their most up to date + * competition profile data. + * More importantly, we need to check if the user is registered before we fetch + * any other data. + **/ const fetchTeams = () => { getCompetitionUser(competitionName, user.username).then((res) => { if(!res.data.registered) { message.info("you are not registered!"); + + // Expose the register modal. When user registers, the page will reload showModal(); } - else { + else { + // update the compeition user state setCompUser(res.data); + // fetch all the teams getTeams(competitionName).then(res => { if(res.data) { setAllTeams(Array.from(res.data)) @@ -364,7 +374,7 @@ function CompetitionPortalPage () { // Function to get ranking of user's team and all teams function updateRankings() { - console.log("updating ranks for team: ", teamInfo.teamName); + // console.log("updating ranks for team: ", teamInfo.teamName); setIsLoadingLeaderBoard(true); @@ -374,16 +384,19 @@ function CompetitionPortalPage () { let newData = res.data.sort((a: any, b: any) => b.bestScore - a.bestScore) // Sort by bestScore in descending order .map((d: any, index: number) => { - if(d.teamName === teamInfo.teamName) { - setUserRankData( { - rank: index + 1, - team: d.teamName, - score: d.bestScore, - submitHistory: d.submitHistory, - }); - - console.log("user rank data: ", userRankData); + if(teamInfo != null) { + if(d.teamName === teamInfo.teamName) { + setUserRankData( { + rank: index + 1, + team: d.teamName, + score: d.bestScore, + submitHistory: d.submitHistory, + }); + + console.log("user rank data: ", userRankData); + } } + return { rank: index + 1, @@ -402,7 +415,10 @@ function CompetitionPortalPage () { }; - // If user is not logged in, navigate to auth + /** + * On first page load, check if user is logged in. + * Otherwise, fetch all the teams and competition metadata + */ useEffect(() => { if (!user.loggedIn) { message.info('You need to login to upload predictions and participate'); @@ -410,17 +426,28 @@ function CompetitionPortalPage () { } else { + // Fetch teams will set the comp user state and grab all teams + setIsLoadingTeamInfo(true); + fetchTeams(); getCompMetaData(); } - }, [user]); + }, []); - // only grab team info when user is in a team + /** + * Whenever the user calls fetchTeams(), the compUser state + * will usually change. This useEffect will capture + * those state transitions and check the comp user + * details. Here we use the compUser's team property + * to update the current teamInfo (Object) + * + */ useEffect(() => { setIsLoadingTeamInfo(true); + // If comp user is in a team, grab the team information if(Object.keys(compUser).length !== 0){ if(compUser.competitionTeam != null){ console.log(compUser) @@ -429,25 +456,26 @@ function CompetitionPortalPage () { }) } else { + setTeamInfo(null) setIsLoadingTeamInfo(false); } } else { + setTeamInfo(null) setIsLoadingTeamInfo(false); } - - }, [compUser]) - // Get the updated rankings based on the state of the user's membership to a team + /** + * Gets the updated rankings on first page load and + * based on the state of the user's membership to a team + */ useEffect(() => { - - updateRankings(); + updateRankings(); - }, [teamInfo]); From 164ce061ce0fde19da3eecf6ed55385ed68681ae Mon Sep 17 00:00:00 2001 From: William Kim Date: Sat, 23 Dec 2023 00:29:10 -0800 Subject: [PATCH 21/55] added more stats to header --- .../CompetitionPortalPage/index.less | 117 ++++++++++++++---- .../CompetitionPortalPage/index.tsx | 84 +++++++++++-- 2 files changed, 169 insertions(+), 32 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 9617e75..3e724d3 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -16,62 +16,137 @@ .constrained-bounds(); max-width: 1200px !important; margin-top: 4rem; + + span { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + Button { + .button-black-square(); + display: flex; + justify-content: center; + align-items: center; + } + } + + #portalBanner { + .gradient(); + border-radius: @radius-base4; + padding: @padding-base2; + margin-top: 1rem; + box-sizing: border-box; + + p { + color: white; + } + } } #portalStatsContent { + width: 100%; + min-height: 120px; margin-top: 2rem; section#noTeamMessage { - min-height: 100px; + height: 108px; text-align: left; - align-items: center; - background: @black; - border-radius: @radius-base4; + align-items: start; padding: @padding-base2; max-width: @sm; - color: @white; + color: @black; } - section { - display: inline-flex; + + #portalStatsRow { + display: flex; + flex-wrap: wrap; + width: 100%; + justify-content: space-between; } + .portalStatsBox { - min-height: 100px; - width: 100%; + background-color: @light-gray; + box-shadow: @bx3; + box-sizing: border-box; + height: 140px; padding: @padding-base2; border-radius: @radius-base4; display: flex; flex-direction: column; + transition: background-color 0.2s ease-in; + margin-bottom: 2rem; - h3, p { - color: white; - } - } + flex: 1 0 20%; + min-width: 0; /* Ensure items can shrink */ + box-sizing: border-box; + p { + color: @black; + } - .portalStatsBox:first-child { - background-color: @p3; + p:nth-of-type(2) { + font-size: 2.8rem; + font-weight: @semi-bold; + margin-top: 1rem; + } } - .portalStatsBox:not(:first-child) { - background-color: @light-gray; - h3, p { - color: black; + @media screen and (max-width: @md) { + .portalStatsBox { + flex: 1 0 45%; + margin-bottom: 2rem; } + } + .portalStatsBox:not(:last-child) { margin-right: 2rem; } - - } + .portalStatsBox:last-child { + background: none; + box-shadow: none; + padding: 0; + margin-left: 2rem; + p { + margin-bottom: 2rem; + font-weight: @semi-bold; + } + + #scoreHistoryChart { + display: flex; + flex-direction: row; + align-items: flex-end; + + .scoreBar { + border-radius: @radius-base1; + background-color: darkgray; + width: 2rem; + transition: all 0.2s ease-in; + + } + .scoreBar:hover{ + background-color: @mainred; + } + + + .scoreBar:not(:last-child) { + margin-right: 1rem; + } + } + } + + + } #portalTabContent { .generic-section(); diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index c4ffdba..ef88d7f 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { Affix, AutoComplete, Avatar, Col, Drawer, List, Row, Skeleton, Tabs, message } from "antd"; +import { Affix, AutoComplete, Avatar, Col, Drawer, List, Row, Skeleton, Tabs, Tooltip, message } from "antd"; import { Layout, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; @@ -16,9 +16,11 @@ import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser } from "../../../actions/competition"; import { genColor } from "../../../utils/colors"; +import { IoHelp } from "react-icons/io5"; import Table, { ColumnsType } from "antd/es/table"; import MainFooter from "../../../components/MainFooter"; import { AxiosResponse } from "axios"; + const { Content } = Layout; @@ -312,6 +314,9 @@ function CompetitionPortalPage () { const [lastRefresh, setLastRefresh] = useState(null); + const [barHeights, setBarHeights] = useState([]); + const [scale, setScale] = useState(1); + // meta data for current competition const [meta, setMeta] = useState<{ competitionName: string; @@ -436,6 +441,18 @@ function CompetitionPortalPage () { }, []); + + /** + * Function to directly get the udpated team information without any + * use effect dependencies. Useful whenever the user makes bot + * submissions + */ + const updateTeamInformation = () => { + getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { + setTeamInfo(res.data); + }) + } + /** * Whenever the user calls fetchTeams(), the compUser state * will usually change. This useEffect will capture @@ -451,9 +468,7 @@ function CompetitionPortalPage () { if(Object.keys(compUser).length !== 0){ if(compUser.competitionTeam != null){ console.log(compUser) - getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { - setTeamInfo(res.data); - }) + updateTeamInformation(); } else { setTeamInfo(null) @@ -475,7 +490,30 @@ function CompetitionPortalPage () { */ useEffect(() => { updateRankings(); - + + if(teamInfo !== null) { + console.log("setting bar chart") + // update the score history chart here + if(teamInfo.scoreHistory) { + let scores = teamInfo.scoreHistory.slice(-7); + scores = scores.map(Number); + setBarHeights(scores); + + // Find the maximum score + const maxScore = Math.max(...scores); + + // Set your desired maximum height for the bars + const maxBarHeight = 92; + + // Calculate the scaling factor + const scalingFactor = maxBarHeight / maxScore; + setScale(scalingFactor); + console.log(teamInfo.scoreHistory); + + } + + } + }, [teamInfo]); @@ -528,8 +566,15 @@ function CompetitionPortalPage () {
-

Hello, {user.username}

-

Welcome the the AI Portal

+ +

Hello, {user.username}

+ + +
+
+

Welcome the the AI Portal for {competitionName}

+
+
@@ -550,20 +595,37 @@ function CompetitionPortalPage () { :
-

Your Total Submissions

+

Your Total Submissions

-

Team Score

- {} +

Best Score

+

{userRankData.score}

-

Ranking

+

Ranking

{userRankData.rank}

+
+

Score History

+
+ {barHeights.map((height:number, index:any) => ( + +
+ +
+ ))} +
+ +
+
} From c05e4321d06b2a6b680771eb1c592acf43eba083 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sat, 23 Dec 2023 10:42:52 -0800 Subject: [PATCH 22/55] added icons to stats --- .../CompetitionPortalPage/index.less | 18 ++++++++---- .../CompetitionPortalPage/index.tsx | 29 +++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 3e724d3..11748a2 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -74,8 +74,7 @@ .portalStatsBox { background-color: @light-gray; box-shadow: @bx3; - box-sizing: border-box; - height: 140px; + min-height: 140px; padding: @padding-base2; border-radius: @radius-base4; display: flex; @@ -87,11 +86,20 @@ min-width: 0; /* Ensure items can shrink */ box-sizing: border-box; - p { - color: @black; + span { + display: flex; + justify-content: start; + flex-direction: row; + align-items: center; + flex-wrap: wrap; + + p { + color: @text-gray3; + } + } - p:nth-of-type(2) { + p.stat { font-size: 2.8rem; font-weight: @semi-bold; margin-top: 1rem; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index ef88d7f..0a7bd58 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -17,9 +17,14 @@ import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Paginati import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser } from "../../../actions/competition"; import { genColor } from "../../../utils/colors"; import { IoHelp } from "react-icons/io5"; +import { BsPatchCheckFill } from "react-icons/bs"; +import { FaCheck, FaStar } from "react-icons/fa"; +import { GrScorecard } from "react-icons/gr"; + import Table, { ColumnsType } from "antd/es/table"; import MainFooter from "../../../components/MainFooter"; import { AxiosResponse } from "axios"; +import { BiStats } from "react-icons/bi"; const { Content } = Layout; @@ -595,18 +600,30 @@ function CompetitionPortalPage () { :
-

Your Total Submissions

+ + +

Your Submissions

+
+ +

0

+
-

Best Score

- -

{userRankData.score}

+ + +

Best Score

+
+ +

{userRankData.score}

-

Ranking

-

{userRankData.rank}

+ + +

Ranking

+
+

{userRankData.rank}

From 511862cc2a4404013d41150710a436aa4a315812 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sat, 23 Dec 2023 11:09:39 -0800 Subject: [PATCH 23/55] changed icon colors --- .../CompetitionPortalPage/index.less | 22 ++++++++++++++++--- .../CompetitionPortalPage/index.tsx | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 11748a2..6ac41de 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -79,11 +79,12 @@ border-radius: @radius-base4; display: flex; flex-direction: column; + justify-content: space-between; transition: background-color 0.2s ease-in; margin-bottom: 2rem; flex: 1 0 20%; - min-width: 0; /* Ensure items can shrink */ + min-width: 100px; /* Ensure items can shrink */ box-sizing: border-box; span { @@ -106,13 +107,22 @@ } } + @media screen and (max-width: @md) { .portalStatsBox { flex: 1 0 45%; margin-bottom: 2rem; } - } + + + @media screen and (max-width: @lg) { + span p { + width: 100%; + } + } + + .portalStatsBox:not(:last-child) { @@ -137,7 +147,7 @@ .scoreBar { border-radius: @radius-base1; - background-color: darkgray; + background-color: @b2; width: 2rem; transition: all 0.2s ease-in; @@ -195,6 +205,12 @@ margin-right: 2rem; } + @media screen and (max-width: @sm) { + p#lastRefreshedText { + display: none; + } + } + Button { .button-black-square(); } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 0a7bd58..fa55f58 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -601,7 +601,7 @@ function CompetitionPortalPage () {
- +

Your Submissions

@@ -611,7 +611,7 @@ function CompetitionPortalPage () {
- +

Best Score

From e6138fa96455f22755219d02c6924d8d47b79b12 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sat, 23 Dec 2023 14:22:57 -0800 Subject: [PATCH 24/55] added statistic to score history --- .../CompetitionPortalPage/index.less | 12 ++++++--- .../CompetitionPortalPage/index.tsx | 26 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 6ac41de..4d1254c 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -135,9 +135,15 @@ padding: 0; margin-left: 2rem; - p { - margin-bottom: 2rem; - font-weight: @semi-bold; + span { + display: inline-flex; + flex-direction: row; + margin-bottom: 1rem; + p { + font-weight: @semi-bold; + width: 100%; + } + } #scoreHistoryChart { diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index fa55f58..62e85c5 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; -import { Affix, AutoComplete, Avatar, Col, Drawer, List, Row, Skeleton, Tabs, Tooltip, message } from "antd"; +import { Affix, AutoComplete, Statistic, Drawer, List, Skeleton, Tabs, Tooltip, message } from "antd"; +import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; import { Layout, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; @@ -320,6 +321,7 @@ function CompetitionPortalPage () { const [barHeights, setBarHeights] = useState([]); + const [scoreHistoryPercentage, setScoreHistoryPercentage] = useState(0); const [scale, setScale] = useState(1); // meta data for current competition @@ -506,6 +508,12 @@ function CompetitionPortalPage () { // Find the maximum score const maxScore = Math.max(...scores); + + // Find relative growth of scores + let lastTwo = scores.slice(-2); + const diff = lastTwo[1] - lastTwo[0]; + const percent = diff / lastTwo[0]; + setScoreHistoryPercentage(percent); // Set your desired maximum height for the bars const maxBarHeight = 92; @@ -573,7 +581,10 @@ function CompetitionPortalPage () {

Hello, {user.username}

- + + + +
@@ -627,7 +638,16 @@ function CompetitionPortalPage () {
-

Score History

+ +

Score History

+ : } + suffix="%" + /> +
{barHeights.map((height:number, index:any) => ( From f8586d6deee3cf3b0bc13122aecfb91324bb2aa1 Mon Sep 17 00:00:00 2001 From: William Kim Date: Sat, 23 Dec 2023 14:58:41 -0800 Subject: [PATCH 25/55] updated score history bar chart --- .../CompetitionPortalPage/index.tsx | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 62e85c5..de4888d 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { Affix, AutoComplete, Statistic, Drawer, List, Skeleton, Tabs, Tooltip, message } from "antd"; +import { Affix, AutoComplete, Statistic, Drawer, List, Skeleton, Tabs, Tooltip, message, Empty } from "antd"; import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; import { Layout, Button, Input, Modal } from 'antd'; import UserContext, { User } from "../../../UserContext"; @@ -640,25 +640,43 @@ function CompetitionPortalPage () {

Score History

- : } - suffix="%" - /> + {scoreHistoryPercentage ? + : } + suffix="%" + /> + : + <> + }
- {barHeights.map((height:number, index:any) => ( - + {barHeights.length > 0 ? + <> + {barHeights.map((height:number, index:any) => ( + +
+ +
+ ))} + {Array.from({ length: 7 - barHeights.length }, (index : any) => (
-
- ))} + ))} + + : + + }
From ee88aa66947a973745395b689127f6915bddd8d4 Mon Sep 17 00:00:00 2001 From: jenetic Date: Sun, 24 Dec 2023 02:30:22 -0800 Subject: [PATCH 26/55] Add basic layout for my team tab --- package-lock.json | 9 +-- package.json | 3 +- .../CompetitionPortalPage/index.less | 61 ++++++++++++++++++- .../CompetitionPortalPage/index.tsx | 61 +++++++++++++++---- 4 files changed, 117 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82f957d..f2cd6f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,8 @@ "react-intersection-observer": "^9.5.2", "serve": "^14.2.0", "style-loader": "^3.3.3", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "webpack": "^5.89.0" }, "engines": { "npm": ">=9.8.1 <10.0.0" @@ -19079,9 +19080,9 @@ } }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", diff --git a/package.json b/package.json index fd9931a..868e868 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-intersection-observer": "^9.5.2", "serve": "^14.2.0", "style-loader": "^3.3.3", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "webpack": "^5.89.0" } } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 4d1254c..9945fd7 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -225,6 +225,14 @@ #myTeamContainer { + section { + display: grid; + grid-template-columns: 3fr 2fr; + } + + .mainHeader { + margin: 2rem auto; + } #teamNameInput { max-width: 300px; @@ -234,10 +242,61 @@ margin-top: 1rem; } - #teamAffix { + #teamScoreOverview { box-shadow: @bx2; border-radius: @radius-base4; padding: @padding-base3; + background-color: @black; + + .statHeader { + color: gray; + margin-bottom: 0.6rem;; + } + + .stat { + font-size: 2.5rem; + color: @white; + } + + .score { + font-size: 3.5rem; + color: @white; + } + + #teamScoreSpecifics { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + margin-top: 2rem; + } + } + + #teamAffix { + margin-left: 2rem; + + .teamMemberName { + font-weight: bold; + } + + #teamMembersHeader { + display: flex; + justify-content: space-between; + // background-color: aqua; + + .heading { + margin: 0.6rem; + // background-color: greenyellow; + } + } + + #teamMember { + display: flex; + margin: 1.5rem 1rem; + } + } + + #teamHeader { + display: flex; + margin-bottom: 2rem; } } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index de4888d..dd59076 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -262,20 +262,59 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC {compUser.competitionTeam !== null && (
- - {generateTeamPicture()} -
-

{compUser.competitionTeam.teamName}

-
- -
+
+
+ {generateTeamPicture()} +
+

{compUser.competitionTeam.teamName}

+

nth place

+
+
+ +
+

score

+

0

+
+
+

Sigma

+

0.78

+
+
+

Mu

+

0.78

+
+
+

MSE

+

0.78

+
+
+
+
+

Upload File

+ + + +

Submission Log

+ {/*

fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds jdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj

*/} +
- + {/* */}
- -

Team Members

+
+

Team Members

+ +
+ {compUser.competitionTeam.teamMembers.map((member: string, index: number) => ( +
+ {generateTeamPicture()} +
+

{member}

+

0 submissions

+
+
+ ))}
-
+ {/*
*/}
)} From 91f121bbe4f20edc8c0b8e72c79aab83d748fd7a Mon Sep 17 00:00:00 2001 From: jenetic Date: Wed, 27 Dec 2023 15:04:16 -0800 Subject: [PATCH 27/55] Add antd drag-to-upload component --- .../CompetitionPortalPage/index.less | 5 + .../CompetitionPortalPage/index.tsx | 624 +++++++++--------- 2 files changed, 331 insertions(+), 298 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 9945fd7..5cfcfb6 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -234,6 +234,10 @@ margin: 2rem auto; } + .submitButton { + margin-top: 1rem; + } + #teamNameInput { max-width: 300px; } @@ -297,6 +301,7 @@ #teamHeader { display: flex; margin-bottom: 2rem; + align-items: center; } } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index dd59076..4133d71 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,7 +1,8 @@ import React, { useContext, useEffect, useState } from "react"; import { Affix, AutoComplete, Statistic, Drawer, List, Skeleton, Tabs, Tooltip, message, Empty } from "antd"; -import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; -import { Layout, Button, Input, Modal } from 'antd'; +import { ArrowDownOutlined, ArrowUpOutlined, PlusOutlined, InboxOutlined } from '@ant-design/icons'; +import { Form, Layout, Button, Input, Modal, Upload } from 'antd'; +import type { UploadProps } from 'antd'; import UserContext, { User } from "../../../UserContext"; import { useHistory } from 'react-router-dom'; import { @@ -9,7 +10,7 @@ import { createTeam, getCompetitionUser, getTeams, - } from '../../../actions/teams/utils'; +} from '../../../actions/teams/utils'; import TeamCard from '../../../components/TeamCard/index'; import './index.less'; import path from 'path'; @@ -31,9 +32,9 @@ const { Content } = Layout; const FindTeamsTab = ( - { data, user, compUser, registered, fetchTeams, updateRankings }: - { data: Object[], user: User, compUser: any, registered: Boolean, fetchTeams: () => void, updateRankings: () => void } -) => { + { data, user, compUser, registered, fetchTeams, updateRankings }: + { data: Object[], user: User, compUser: any, registered: Boolean, fetchTeams: () => void, updateRankings: () => void } +) => { // constants to align the pagination options for the teams list const [position] = useState('bottom'); @@ -44,7 +45,7 @@ const FindTeamsTab = ( // Initialize the teams data once that data defined useEffect(() => { - if(data) { + if (data) { setOptions(data); } }, [data, registered]) @@ -52,7 +53,7 @@ const FindTeamsTab = ( const handleSearch = (value: string) => { // Reset options back to the original data if the value is an empty string if (value === "") { - setOptions(data); + setOptions(data); } } @@ -66,101 +67,101 @@ const FindTeamsTab = ( } return ( - - handleSearch(text)} - onSelect = {handleSelect} - - // list of all possible options for dropdown - options={options.map((item: any) => ({ value: item.teamName }))} - - // filterOption to handle filtered dropdown items - filterOption = {(inputValue, option) => - option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 - } - size="large" - style = {{width: "100%"}} - > - - - - {/** List to preview all the teams based on the user's query */} - ( - - {} - - )} - /> + + handleSearch(text)} + onSelect={handleSelect} + + // list of all possible options for dropdown + options={options.map((item: any) => ({ value: item.teamName }))} + + // filterOption to handle filtered dropdown items + filterOption={(inputValue, option) => + option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + } + size="large" + style={{ width: "100%" }} + > + + + + {/** List to preview all the teams based on the user's query */} + ( + + {} + + )} + /> - + ); }; const LeaderBoardTab = ( - {rankData, lastRefresh, updateRankingsCallback, isLoading}: - { - rankData: any, - lastRefresh: Date | null, - updateRankingsCallback: () => void, - isLoading: boolean - } + { rankData, lastRefresh, updateRankingsCallback, isLoading }: + { + rankData: any, + lastRefresh: Date | null, + updateRankingsCallback: () => void, + isLoading: boolean + } ) => { const columns: ColumnsType = [ { - title: 'Rank', - dataIndex: 'rank', - sorter: (a, b) => b.score - a.score, - defaultSortOrder: 'ascend', + title: 'Rank', + dataIndex: 'rank', + sorter: (a, b) => b.score - a.score, + defaultSortOrder: 'ascend', }, { - title: 'Team', - dataIndex: 'team', - sorter: (a, b) => a.team.length - b.team.length, - render(value, record, index) { - const color1 = genColor(record.team); - const color2 = genColor(`${record.team}_additional_seed`); - - return ( - -
- {value.length > 28 ? ( - {value.substring(0, 28)}... - ) : ( - {value.substring(0, 28)} - )} -
- ); - }, + title: 'Team', + dataIndex: 'team', + sorter: (a, b) => a.team.length - b.team.length, + render(value, record, index) { + const color1 = genColor(record.team); + const color2 = genColor(`${record.team}_additional_seed`); + + return ( + +
+ {value.length > 28 ? ( + {value.substring(0, 28)}... + ) : ( + {value.substring(0, 28)} + )} +
+ ); + }, }, { - title: 'Score', - dataIndex: 'score', - sorter: (a, b) => a.score - b.score, + title: 'Score', + dataIndex: 'score', + sorter: (a, b) => a.score - b.score, }, ]; - + return ( - +
-

+

Last refreshed{': '} {lastRefresh ? lastRefresh.toLocaleString() : ''}

@@ -170,12 +171,12 @@ const LeaderBoardTab = ( onClick={() => { updateRankingsCallback(); }} - > - Refresh - + > + Refresh +
- + ); }; @@ -184,15 +185,33 @@ const LeaderBoardTab = ( -const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsCallback: () => void}) => { +const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeamsCallback: () => void }) => { - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [newTeamName, setNewTeamName] = useState(""); - - useEffect(() => { - - }, []); + // Upload submission + const { Dragger } = Upload; + const uploadProps: UploadProps = { + name: 'file', + multiple: false, + // TODO: replace placeholder link with actual file uploading logic + action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188', + onChange(info) { + const { status } = info.file; + if (status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (status === 'done') { + message.success(`${info.file.name} file uploaded successfully.`); + } else if (status === 'error') { + message.error(`${info.file.name} file upload failed.`); + } + }, + onDrop(e) { + console.log('Dropped files', e.dataTransfer.files); + }, + }; const generateTeamPicture = () => { @@ -200,23 +219,23 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC const color2 = genColor(`${compUser.competitionTeam.teamName}_additional_seed`); return ( -
+
) } const handleClick = () => { - if(newTeamName.length == 0) { + if (newTeamName.length == 0) { message.info('Name cannot be empty'); return; } @@ -226,86 +245,93 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC message.success('Successfully made a new team!'); fetchTeamsCallback(); console.log(compUser) - + }) - .catch((error) => { - message.error(error.message); - }); + .catch((error) => { + message.error(error.message); + }); setIsLoading(false); } return ( - - {compUser.competitionTeam == null && ( - -
- setNewTeamName(e.target.value)} + + {compUser.competitionTeam == null && ( + +
+ setNewTeamName(e.target.value)} > -
- -
- - - ) } +
+ +
+ )} - {compUser.competitionTeam !== null && ( -
-
-
+ {compUser.competitionTeam !== null && ( +
+
+
{generateTeamPicture()} -
-

{compUser.competitionTeam.teamName}

-

nth place

-
+

{compUser.competitionTeam.teamName}

- -
-

score

-

0

-
+ +
+

score

+

0

+
-

Sigma

-

0.78

+

Sigma

+

0.78

-

Mu

-

0.78

+

Mu

+

0.78

-

MSE

-

0.78

+

MSE

+

0.78

+

Upload Submission

+
-

Upload File

- - + +

+ +

+

Click or drag file to this area to upload

+
+ +

Submission Log

- {/*

fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds jdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfdsjdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj fsdfdsfds dfsdf sfh jkkdjhk jdshfk kkjkj

*/} -
- - {/* */} -
-
-

Team Members

- + +
+ + {/* */} +
+
+

Team Members

+
{compUser.competitionTeam.teamMembers.map((member: string, index: number) => ( -
+
{generateTeamPicture()}

{member}

@@ -314,19 +340,19 @@ const MyTeamTab = ( { compUser, fetchTeamsCallback}: {compUser: any, fetchTeamsC
))}
- {/* */} -
- )} - - + {/* */} +
+ )} + +
); - }; +}; + - -function CompetitionPortalPage () { +function CompetitionPortalPage() { - const competitionName = "TestCompetition2"; + const competitionName = "TestCompetition2"; const history = useHistory(); // User profile data @@ -348,10 +374,12 @@ function CompetitionPortalPage () { const [rankingsData, setRankingsData] = useState([]); // Rank data for user's current team - const [userRankData, setUserRankData] = useState({rank: 0, + const [userRankData, setUserRankData] = useState({ + rank: 0, team: "", score: 0, - submitHistory: []} + submitHistory: [] + } ); // Loading states for leaderboard data fetching status @@ -373,8 +401,8 @@ function CompetitionPortalPage () { } | null>(null); - // Modal callback to show modal to register user for competition - const showModal = () => { + // Modal callback to show modal to register user for competition + const showModal = () => { setIsModalOpen(true); }; const handleCancel = () => { @@ -389,33 +417,33 @@ function CompetitionPortalPage () { * any other data. **/ const fetchTeams = () => { - - getCompetitionUser(competitionName, user.username).then((res) => { - if(!res.data.registered) { + + getCompetitionUser(competitionName, user.username).then((res) => { + if (!res.data.registered) { message.info("you are not registered!"); // Expose the register modal. When user registers, the page will reload showModal(); } - else { + else { // update the compeition user state setCompUser(res.data); // fetch all the teams getTeams(competitionName).then(res => { - if(res.data) { + if (res.data) { setAllTeams(Array.from(res.data)) } }) - .catch(error => { - console.log(error); - }); + .catch(error => { + console.log(error); + }); } }) } // Function to get compeitition meta data - const getCompMetaData = async () => { + const getCompMetaData = async () => { getMetaData(competitionName).then((res) => { setMeta(res.data); @@ -431,31 +459,31 @@ function CompetitionPortalPage () { getLeaderboard(competitionName).then((res) => { console.log(res.data) - + let newData = res.data.sort((a: any, b: any) => b.bestScore - a.bestScore) // Sort by bestScore in descending order - .map((d: any, index: number) => { - - if(teamInfo != null) { - if(d.teamName === teamInfo.teamName) { - setUserRankData( { - rank: index + 1, - team: d.teamName, - score: d.bestScore, - submitHistory: d.submitHistory, - }); - - console.log("user rank data: ", userRankData); + .map((d: any, index: number) => { + + if (teamInfo != null) { + if (d.teamName === teamInfo.teamName) { + setUserRankData({ + rank: index + 1, + team: d.teamName, + score: d.bestScore, + submitHistory: d.submitHistory, + }); + + console.log("user rank data: ", userRankData); + } } - } - - - return { - rank: index + 1, - team: d.teamName, - score: d.bestScore, - submitHistory: d.submitHistory, - }; - }); + + + return { + rank: index + 1, + team: d.teamName, + score: d.bestScore, + submitHistory: d.submitHistory, + }; + }); setLastRefresh(new Date()); setRankingsData(newData); @@ -463,7 +491,7 @@ function CompetitionPortalPage () { setIsLoadingTeamInfo(false); }); - }; + }; /** @@ -472,8 +500,8 @@ function CompetitionPortalPage () { */ useEffect(() => { if (!user.loggedIn) { - message.info('You need to login to upload predictions and participate'); - history.replace(path.join(window.location.pathname, '../../../login')); + message.info('You need to login to upload predictions and participate'); + history.replace(path.join(window.location.pathname, '../../../login')); } else { @@ -483,7 +511,7 @@ function CompetitionPortalPage () { fetchTeams(); getCompMetaData(); } - + }, []); @@ -496,7 +524,7 @@ function CompetitionPortalPage () { const updateTeamInformation = () => { getTeamInfo(competitionName, compUser.competitionTeam.teamName).then((res) => { setTeamInfo(res.data); - }) + }) } /** @@ -511,8 +539,8 @@ function CompetitionPortalPage () { setIsLoadingTeamInfo(true); // If comp user is in a team, grab the team information - if(Object.keys(compUser).length !== 0){ - if(compUser.competitionTeam != null){ + if (Object.keys(compUser).length !== 0) { + if (compUser.competitionTeam != null) { console.log(compUser) updateTeamInformation(); } @@ -525,7 +553,7 @@ function CompetitionPortalPage () { setTeamInfo(null) setIsLoadingTeamInfo(false); } - + }, [compUser]) @@ -537,14 +565,14 @@ function CompetitionPortalPage () { useEffect(() => { updateRankings(); - if(teamInfo !== null) { + if (teamInfo !== null) { console.log("setting bar chart") // update the score history chart here - if(teamInfo.scoreHistory) { + if (teamInfo.scoreHistory) { let scores = teamInfo.scoreHistory.slice(-7); scores = scores.map(Number); setBarHeights(scores); - + // Find the maximum score const maxScore = Math.max(...scores); @@ -553,26 +581,26 @@ function CompetitionPortalPage () { const diff = lastTwo[1] - lastTwo[0]; const percent = diff / lastTwo[0]; setScoreHistoryPercentage(percent); - - // Set your desired maximum height for the bars + + // Set your desired maximum height for the bars const maxBarHeight = 92; - + // Calculate the scaling factor const scalingFactor = maxBarHeight / maxScore; setScale(scalingFactor); console.log(teamInfo.scoreHistory); } - + } - + }, [teamInfo]); // When user registers for compeititons, allow access to portal const onSubmit = () => { - registerCompetitionUser(competitionName, user.username).then((res) => { + registerCompetitionUser(competitionName, user.username).then((res) => { if (res.data.msg == "Success") { window.location.reload(); setIsRegistered(true); @@ -589,21 +617,21 @@ function CompetitionPortalPage () { {/** Register Modal */} Register} - footer = {null} - > + className="registerUserModal" + width={800} + centered + closeIcon={false} + open={isModalOpen} + maskClosable={false} + onCancel={handleCancel} + title={

Register

} + footer={null} + >

- Looks like we don't have you registered for {competitionName} yet. Click register below to get started. The page will + Looks like we don't have you registered for {competitionName} yet. Click register below to get started. The page will reload once we confirm your registration. Otherwise, feel free to leave this page.

- @@ -612,122 +640,122 @@ function CompetitionPortalPage () { history.push(path.join(history.location.pathname, '../competitions')); }} >Go Back -
+ - +

Hello, {user.username}

- +
-
+

Welcome the the AI Portal for {competitionName}

- +
- -
- {isLoadingTeamInfo ? ( +
+ + {isLoadingTeamInfo ? ( - ): - ( <> - {compUser.competitionTeam == null ? -
+ ) : + (<> + {compUser.competitionTeam == null ? +

- Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, - then navigate to Find Teams below to join their group! + Uh oh! You’re not in a team yet. Either make your own team or ask your friends to share their invite code, + then navigate to Find Teams below to join their group!

- - : -
-
+ + : +
+
- +

Your Submissions

-

0

- +

0

+
-
+
- +

Best Score

- -

{userRankData.score}

+ +

{userRankData.score}

-
+
- +

Ranking

-

{userRankData.rank}

+

{userRankData.rank}

-
+

Score History

- {scoreHistoryPercentage ? + {scoreHistoryPercentage ? : } + valueStyle={{ color: scoreHistoryPercentage < 0 ? '#cf1322' : '#3f8600', fontSize: "1.2rem", display: "inline" }} + prefix={scoreHistoryPercentage < 0 ? : } suffix="%" /> : - <> + <> }
-
+
{barHeights.length > 0 ? <> - {barHeights.map((height:number, index:any) => ( - + {barHeights.map((height: number, index: any) => ( + +
+ +
+ ))} + {Array.from({ length: 7 - barHeights.length }, (index: any) => (
-
- ))} - {Array.from({ length: 7 - barHeights.length }, (index : any) => ( -
- - ))} + ))} : }
- +
} - )} + )}
- + Leaderboard

, key: '1', - children: + children: }, { label:

Find Teams

, key: '2', - children: + updateRankings={updateRankings} /> }, { label:

My Team

, key: '3', - children: , }, ] From f3d00d8a432cd513c99f18bd614552f18c4f134c Mon Sep 17 00:00:00 2001 From: jenetic Date: Wed, 27 Dec 2023 15:39:01 -0800 Subject: [PATCH 28/55] Add avatars for team members in my team tab using DiceBear --- package-lock.json | 410 ++++++++++++++++++ package.json | 2 + .../CompetitionPortalPage/index.tsx | 59 ++- 3 files changed, 458 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2cd6f7..116de0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "@acmucsd/acm-ai-site", "version": "2.0.0", "dependencies": { + "@dicebear/collection": "^7.0.1", + "@dicebear/core": "^7.0.1", "@types/aos": "^3.0.3", "@types/chart.js": "^2.9.27", "aos": "^3.0.0-beta.6", @@ -2569,6 +2571,395 @@ "node": ">=10" } }, + "node_modules/@dicebear/adventurer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-7.0.1.tgz", + "integrity": "sha512-eqbHHAQO8HjG8YNMl8xgklxphC7HvfDtqVr1rkJWP98e7r2AdQpu0cPYIOZPV4uv9gxl1ncaErQjdjvIvFRGiA==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/adventurer-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-7.0.1.tgz", + "integrity": "sha512-dZfyaUFS8qQv7Lv+OXNTHVkercDCh+VqGSJU8jIf3FFbtFbFF79FXZJwJ8V3+pr0xKcZWa8i+8hXLtU3gqZ18g==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/avataaars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-7.0.1.tgz", + "integrity": "sha512-U7JJLDFJsbVyQl3j1SqtTxi5h+I5JXL8CGfwAOPtQTnk/tKQFXM9WF/zdHegtxbxYAxQaYJtyprdwTJHx5ELnw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/avataaars-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-7.0.1.tgz", + "integrity": "sha512-e3XwK3xup4ifJ/BUNjR5rcrw9982SC75UTJlPsKuuOM/Lwx3MtUe3+dqeDSyYbrC7KoWespX70oDZK1+2dBQFw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/big-ears": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-7.0.1.tgz", + "integrity": "sha512-ITI0IQCwdn5s5/kUrNdO488TQvZdiCljnzKpqbQ1hqfsxZ0C+eZs+cudZ0bqLftYxM+WBvmaJwrh3pXNAz1h+w==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/big-ears-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-7.0.1.tgz", + "integrity": "sha512-2QK9HVmApoGFLi3ONW9mh0Tk/PPyHx9rvzUvcT5H/mb80ooBqIVMPYYq4rVlGVP6wAtsNHdoxzzlKja0DG+vvQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/big-smile": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-7.0.1.tgz", + "integrity": "sha512-hVAhUMZ0LUhMFvtmUDR8GU7v2ufl5pOcVPiVSC3oV8nyywFp7s1ZqYGhi6rBCEG3qsMR54JfMFWkjV88j4Yrmg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/bottts": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-7.0.1.tgz", + "integrity": "sha512-k0adSvnT9+gFDO7/Cmts9TM3CSWYrZrxZe1WpELjTvwe4QOqdn3LgrYR9JXU/2hRz3GaXtP02SHNd85CkadYVw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/bottts-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-7.0.1.tgz", + "integrity": "sha512-1T1NEKAEvqyGlUprkO1Q1btITZnMBiCP5YeCy6wYyM7qJsPVDSySsjASJ1j/+IZFi8ePgWReFIbigFiHdo7iLA==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/collection": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-7.0.1.tgz", + "integrity": "sha512-Y5xzjU2hiklYUkqsSr5VBpVPG++iUUTm1UDJLPM+iXg3lMF3PQuifXoaAxcuoBvvnKfJKNHf5wP1Bq6nRUl4NA==", + "dependencies": { + "@dicebear/adventurer": "7.0.1", + "@dicebear/adventurer-neutral": "7.0.1", + "@dicebear/avataaars": "7.0.1", + "@dicebear/avataaars-neutral": "7.0.1", + "@dicebear/big-ears": "7.0.1", + "@dicebear/big-ears-neutral": "7.0.1", + "@dicebear/big-smile": "7.0.1", + "@dicebear/bottts": "7.0.1", + "@dicebear/bottts-neutral": "7.0.1", + "@dicebear/croodles": "7.0.1", + "@dicebear/croodles-neutral": "7.0.1", + "@dicebear/fun-emoji": "7.0.1", + "@dicebear/icons": "7.0.1", + "@dicebear/identicon": "7.0.1", + "@dicebear/initials": "7.0.1", + "@dicebear/lorelei": "7.0.1", + "@dicebear/lorelei-neutral": "7.0.1", + "@dicebear/micah": "7.0.1", + "@dicebear/miniavs": "7.0.1", + "@dicebear/notionists": "7.0.1", + "@dicebear/notionists-neutral": "7.0.1", + "@dicebear/open-peeps": "7.0.1", + "@dicebear/personas": "7.0.1", + "@dicebear/pixel-art": "7.0.1", + "@dicebear/pixel-art-neutral": "7.0.1", + "@dicebear/rings": "7.0.1", + "@dicebear/shapes": "7.0.1", + "@dicebear/thumbs": "7.0.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/converter": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/converter/-/converter-7.0.1.tgz", + "integrity": "sha512-CEIF6ZKi1FAE9kW10FvuPUjA6HLi+LcuB/GRFct/Bv28llzTel9xwbmfOEa1aIM8Nnp8BuT4U7tBIytksf+ptw==", + "dependencies": { + "@types/json-schema": "^7.0.11", + "tmp-promise": "^3.0.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@resvg/resvg-js": "^2.4.1", + "exiftool-vendored": "^22.0.0", + "sharp": "^0.32.1" + }, + "peerDependenciesMeta": { + "@resvg/resvg-js": { + "optional": true + }, + "exiftool-vendored": { + "optional": true + }, + "sharp": { + "optional": true + } + } + }, + "node_modules/@dicebear/core": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-7.0.1.tgz", + "integrity": "sha512-jaJG693c+myLocgG3kKXdHa+WJ+S6OcD31SEr9Oby7hhOzALQYD+LcJ15oBWwI7SLHJcGPYTOLyx2eDr8YhXCQ==", + "dependencies": { + "@dicebear/converter": "7.0.1", + "@types/json-schema": "^7.0.11" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@dicebear/croodles": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-7.0.1.tgz", + "integrity": "sha512-uauBTUvKFvsiaT+LWYKCEboEeOJy2Pk055nsdczi13UgHHfj+Qvy0/ky/uzYn+WC/1gewqQ6w/yS1WfpgPtIpg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/croodles-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-7.0.1.tgz", + "integrity": "sha512-u09YylowZcbSAVyKJ4I8BCo1ehluqg3onYCclx++8mOWcEo+XGsGKIeN7osayaflNY/qtA9Jt2JsPgiS8KpQ5A==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/fun-emoji": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-7.0.1.tgz", + "integrity": "sha512-oJj5sb4rakro4e0lZTCkcKkiClHxDWr6+NWTwoU5L1HYRkXV6ngk4s7xSdOrYBQpYjLhdu+Lpx1VHYNpLUu2vg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/icons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-7.0.1.tgz", + "integrity": "sha512-juHS4feScGCz4YdiwjxR60RJ2G7Z6W+tdUqNHN9ufMvY/FpJTfrQvzvrJfJfc84QZwIrqI/96WV3JIBEIO2AwQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/identicon": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-7.0.1.tgz", + "integrity": "sha512-9W9pqqhvpMsZmOkjuLwlw0iift56A3VFq7eNpJPB1mm6gytfqgxozgOVLDFgug9VXgUVI2Jrk/XnXGIFVIeVQA==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/initials": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-7.0.1.tgz", + "integrity": "sha512-zCI6fky4odM5ezl/GlhcSdnu+oNfmBbIghFB5NzgB/wV5nHmw2okONRC+Mgmxv8P8EpFb9z5hEOnh8xwW8htow==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/lorelei": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-7.0.1.tgz", + "integrity": "sha512-3pyI2JF70PlqZUZEs5pVxmQWDJ2/bWmGG/iFtwsEh9HivtF8Zon4Er0NrsEoiKDvScyY4VGwl4LyUBc8JvNb9w==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/lorelei-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-7.0.1.tgz", + "integrity": "sha512-4XaqE5v1dhE4TYrKSGG/VNUFqA31ADlqOnr6bd27E5MnaJLlY8ZAm3sue7EI9kEJ/i5KYov+Q4uS7JNDA5+cag==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/micah": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-7.0.1.tgz", + "integrity": "sha512-zHnEewRaREZGNTqnlZiSoha/wNFxEsVQ3E5QYpe9KB3rcLW4CVUgFAHjb449vniG6NfsAWzyAkOfhy4N6Zzw0g==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/miniavs": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-7.0.1.tgz", + "integrity": "sha512-v0n2JT0N1I7vAGoi4NQ98IKtn4JgjwD2Gkqq7l5QAy0jzl1v289FfTng0cOrthroMGBQ5jPS0wUyI0TluoFZRw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/notionists": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-7.0.1.tgz", + "integrity": "sha512-uEYBywouoUmvWtWARyeqAoQWX1DpvKL33dVxZ5K/ulYd/nXu9WHeFCPaP4tqE5II1XPS4khwneimFN6F1HA5NQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/notionists-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-7.0.1.tgz", + "integrity": "sha512-jRA7u2UU1I9EXzqBZL3vwI/V7pdDT60yB3bBjyD5J4TznT7bMwt7qEm1eV31U37mn3H+LTFiPD9/4G6whiU3nQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/open-peeps": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-7.0.1.tgz", + "integrity": "sha512-z1gXzd7XXLzSZpOrDPZmnJDXySCUEKmunRdRuWBSRrfIcVkgStZM0y8uuSrs3LpR8U2xcNJN9yO2wNRRWKmFEw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/personas": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-7.0.1.tgz", + "integrity": "sha512-6/nsrN7JIlMqdH7UwhrACVoCEM3IVHkpMq2I0A1JbhmYp240TI8kM5xYSF0KRdOyAPbyDH/TEB8Uld4LKE+3wQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/pixel-art": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-7.0.1.tgz", + "integrity": "sha512-9f17Ze4533CbHp23E+gRSSZdCUAB5/PieRq6/ZtVOnPI/PfglhhKMKSxQIm/H267gE2Y+VVhHpUTwGlbAgh1Lg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/pixel-art-neutral": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-7.0.1.tgz", + "integrity": "sha512-+9RS0ohGDbPu+W2eGGk3LyzvFbM5qsuhCQR4qO7YIcvmODyNFPJ7eW9g/MHFVPLQXq60SCEUF5CEKY0xs4baUQ==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/rings": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-7.0.1.tgz", + "integrity": "sha512-6wsLE4kbkBGeaaEA/afIV0eNYYfIVXo60XgApJA7JdcwyvdTa9LE5Wcp2VBEsZYXdsT9Ml7BC4er/QyMqCayUw==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/shapes": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-7.0.1.tgz", + "integrity": "sha512-/ol+SazDlJYYe5pYaqKcnYDBjux+2Ny57hIrkHhonV0z4ny3Pq6c4Lq+hN3MnTBpKJszCXLrSP3uCbSQpjnkOg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, + "node_modules/@dicebear/thumbs": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-7.0.1.tgz", + "integrity": "sha512-eQYVJ8NN9buPfbd2Va0fY8sHRq9n1d7FJt/dL9xwimRGlpWh9lqS6gcHazuSHhSgnRHsHLANEiyboIcyhWh2Hg==", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^7.0.0" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -18391,6 +18782,25 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 868e868..65448b6 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "@acmucsd/acm-ai-site", "version": "2.0.0", "dependencies": { + "@dicebear/collection": "^7.0.1", + "@dicebear/core": "^7.0.1", "@types/aos": "^3.0.3", "@types/chart.js": "^2.9.27", "aos": "^3.0.0-beta.6", diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 4133d71..b3569f6 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -28,6 +28,9 @@ import MainFooter from "../../../components/MainFooter"; import { AxiosResponse } from "axios"; import { BiStats } from "react-icons/bi"; +import { createAvatar } from '@dicebear/core'; +import { botttsNeutral } from '@dicebear/collection'; + const { Content } = Layout; @@ -198,20 +201,51 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams // TODO: replace placeholder link with actual file uploading logic action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188', onChange(info) { - const { status } = info.file; - if (status !== 'uploading') { - console.log(info.file, info.fileList); - } - if (status === 'done') { - message.success(`${info.file.name} file uploaded successfully.`); - } else if (status === 'error') { - message.error(`${info.file.name} file upload failed.`); - } + const { status } = info.file; + if (status !== 'uploading') { + console.log(info.file, info.fileList); + } + if (status === 'done') { + message.success(`${info.file.name} file uploaded successfully.`); + } else if (status === 'error') { + message.error(`${info.file.name} file upload failed.`); + } }, onDrop(e) { console.log('Dropped files', e.dataTransfer.files); }, - }; + }; + + const TeamMemberAvatar: React.FC<{ username: string }> = ({ username }) => { + const [avatarUrl, setAvatarUrl] = useState(''); + useEffect(() => { + // Generate avatar based on username using DiceBear + const svg = createAvatar(botttsNeutral, { + seed: username, + radius: 50, + backgroundType: ["gradientLinear"] + }); + + // Convert the SVG string to a data URL + const dataUrl = `data:image/svg+xml;base64,${btoa(svg.toString())}`; + + // Set the avatar URL + setAvatarUrl(dataUrl); + }, [username]); + return ( + {`Avatar + ); + } const generateTeamPicture = () => { @@ -321,7 +355,6 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams

Submission Log

-
{/* */} @@ -332,8 +365,8 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams {compUser.competitionTeam.teamMembers.map((member: string, index: number) => (
- {generateTeamPicture()} -
+ +

{member}

0 submissions

From cb6fd449dd7d3e492b8a3841a420907dc8a7bce3 Mon Sep 17 00:00:00 2001 From: jenetic Date: Wed, 27 Dec 2023 16:06:37 -0800 Subject: [PATCH 29/55] Add invite code modal to 'invite' button on sidebar --- .../CompetitionPortalPage/index.less | 5 +++- .../CompetitionPortalPage/index.tsx | 24 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 5cfcfb6..f098a4e 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -281,6 +281,10 @@ font-weight: bold; } + .inviteCode { + background-color: red; + } + #teamMembersHeader { display: flex; justify-content: space-between; @@ -297,7 +301,6 @@ margin: 1.5rem 1rem; } } - #teamHeader { display: flex; margin-bottom: 2rem; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index b3569f6..c7b3d10 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -191,7 +191,17 @@ const LeaderBoardTab = ( const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeamsCallback: () => void }) => { const [isLoading, setIsLoading] = useState(false); - const [newTeamName, setNewTeamName] = useState(""); + const [newTeamName, setNewTeamName] = useState(""); + const [isInviteModalVisible, setIsInviteModalVisible] = useState(false); + + // Invite button modal + const showInviteModal = () => { + setIsInviteModalVisible(true); + }; + + const handleInviteModalClose = () => { + setIsInviteModalVisible(false); + }; // Upload submission const { Dragger } = Upload; @@ -361,7 +371,17 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams

Team Members

- + + +

Share your Invite Code to your friend. Make sure to tell them your team name as well!

+ {/* TODO: For some reason, I couldn't get the CSS to show up when I put it in the CSS file */} +

+ {compUser.competitionTeam.joinCode} +

+
{compUser.competitionTeam.teamMembers.map((member: string, index: number) => (
From 8f0d642bdf57f1c344cc392d46164ce81e112a8a Mon Sep 17 00:00:00 2001 From: jenetic Date: Wed, 27 Dec 2023 16:30:40 -0800 Subject: [PATCH 30/55] Turn my team tab into 1 column instead of 2 on small screens --- .../CompetitionPortalPage/index.less | 68 +++++++++++-------- .../CompetitionPortalPage/index.tsx | 2 +- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index f098a4e..359e4f6 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -11,7 +11,7 @@ background: @white; - #portalHeader{ + #portalHeader { .generic-section(); .constrained-bounds(); max-width: 1200px !important; @@ -48,7 +48,7 @@ #portalStatsContent { - + width: 100%; min-height: 120px; margin-top: 2rem; @@ -75,7 +75,7 @@ background-color: @light-gray; box-shadow: @bx3; min-height: 140px; - padding: @padding-base2; + padding: @padding-base2; border-radius: @radius-base4; display: flex; flex-direction: column; @@ -84,7 +84,8 @@ margin-bottom: 2rem; flex: 1 0 20%; - min-width: 100px; /* Ensure items can shrink */ + min-width: 100px; + /* Ensure items can shrink */ box-sizing: border-box; span { @@ -93,7 +94,7 @@ flex-direction: row; align-items: center; flex-wrap: wrap; - + p { color: @text-gray3; } @@ -104,7 +105,7 @@ font-size: 2.8rem; font-weight: @semi-bold; margin-top: 1rem; - } + } } @@ -114,17 +115,17 @@ margin-bottom: 2rem; } } - + @media screen and (max-width: @lg) { - span p { - width: 100%; - } + span p { + width: 100%; + } } - - - + + + .portalStatsBox:not(:last-child) { margin-right: 2rem; } @@ -135,41 +136,43 @@ padding: 0; margin-left: 2rem; - span { + span { display: inline-flex; flex-direction: row; margin-bottom: 1rem; + p { font-weight: @semi-bold; width: 100%; } - + } #scoreHistoryChart { display: flex; flex-direction: row; align-items: flex-end; - + .scoreBar { border-radius: @radius-base1; background-color: @b2; width: 2rem; transition: all 0.2s ease-in; - + } - .scoreBar:hover{ + + .scoreBar:hover { background-color: @mainred; } - + .scoreBar:not(:last-child) { margin-right: 1rem; } } } - + } #portalTabContent { @@ -178,6 +181,7 @@ height: 100vh; max-width: 1200px !important; + .ant-tabs-ink-bar { display: none; } @@ -188,7 +192,7 @@ margin-bottom: 5rem; Input { - line-height: 2.5rem ; + line-height: 2.5rem; font-size: @font3; } } @@ -254,7 +258,8 @@ .statHeader { color: gray; - margin-bottom: 0.6rem;; + margin-bottom: 0.6rem; + ; } .stat { @@ -288,12 +293,7 @@ #teamMembersHeader { display: flex; justify-content: space-between; - // background-color: aqua; - - .heading { - margin: 0.6rem; - // background-color: greenyellow; - } + align-items: center; } #teamMember { @@ -301,10 +301,20 @@ margin: 1.5rem 1rem; } } + #teamHeader { display: flex; margin-bottom: 2rem; align-items: center; } -} + @media screen and (max-width: @sm) { + section { + display: block + } + + #teamAffix { + margin-left: 0; + } + } +} \ No newline at end of file diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index c7b3d10..a51045e 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -371,7 +371,7 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams

Team Members

- +

Share your Invite Code to your friend. Make sure to tell them your team name as well!

{/* TODO: For some reason, I couldn't get the CSS to show up when I put it in the CSS file */} From 8e7152cad1c2e7cc471a1716ec4089d2b2bc4d5d Mon Sep 17 00:00:00 2001 From: jenetic Date: Wed, 27 Dec 2023 16:56:20 -0800 Subject: [PATCH 31/55] Add leave button to my team tab --- src/components/TeamCard/index.tsx | 2 +- .../CompetitionPortalPage/index.less | 7 ++ .../CompetitionPortalPage/index.tsx | 66 ++++++++++++++----- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/components/TeamCard/index.tsx b/src/components/TeamCard/index.tsx index ba28bf1..a9ff088 100644 --- a/src/components/TeamCard/index.tsx +++ b/src/components/TeamCard/index.tsx @@ -57,7 +57,7 @@ const TeamCard = ( const onLeaveTeam = () => { leaveTeam(team.competitionName, user.username, team.teamName).then( (res) => { - message.success(res.data.msg); + message.success('Successfully left team.'); setIsModalOpen(false); fetchTeamCallback(); diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index 359e4f6..bfbf6a3 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -242,6 +242,13 @@ margin-top: 1rem; } + #teamNameWrapper { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + #teamNameInput { max-width: 300px; } diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index a51045e..302fece 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -10,6 +10,7 @@ import { createTeam, getCompetitionUser, getTeams, + leaveTeam } from '../../../actions/teams/utils'; import TeamCard from '../../../components/TeamCard/index'; import './index.less'; @@ -193,6 +194,16 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams const [isLoading, setIsLoading] = useState(false); const [newTeamName, setNewTeamName] = useState(""); const [isInviteModalVisible, setIsInviteModalVisible] = useState(false); + const [isLeaveModalVisible, setIsLeaveModalVisible] = useState(false); + + // Leave button modal + const showLeaveModal = () => { + setIsLeaveModalVisible(true); + }; + + const handleLeaveModalClose = () => { + setIsLeaveModalVisible(false); + }; // Invite button modal const showInviteModal = () => { @@ -222,7 +233,7 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams } }, onDrop(e) { - console.log('Dropped files', e.dataTransfer.files); + console.log('Dropped files', e.dataTransfer.files); }, }; @@ -231,20 +242,20 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams useEffect(() => { // Generate avatar based on username using DiceBear const svg = createAvatar(botttsNeutral, { - seed: username, - radius: 50, - backgroundType: ["gradientLinear"] + seed: username, + radius: 50, + backgroundType: ["gradientLinear"] }); - + // Convert the SVG string to a data URL const dataUrl = `data:image/svg+xml;base64,${btoa(svg.toString())}`; - + // Set the avatar URL setAvatarUrl(dataUrl); }, [username]); return ( {`Avatar ); } @@ -277,6 +288,17 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams ) } + const handleLeaveTeam = () => { + leaveTeam(compUser.competitionName, compUser.username, compUser.competitionTeam.teamName).then((res) => { + message.success('Successfully left team.'); + handleLeaveModalClose(); + fetchTeamsCallback(); + }) + .catch((error) => ( + message.error(error) + )); + } + const handleClick = () => { if (newTeamName.length == 0) { @@ -325,8 +347,20 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams
- {generateTeamPicture()} -

{compUser.competitionTeam.teamName}

+
{generateTeamPicture()}
+
+

{compUser.competitionTeam.teamName}

+ + + +
@@ -348,30 +382,30 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams

Upload Submission

- +

- +

Click or drag file to this area to upload

- +

Submission Log

{/* */}
-

Team Members

- +

Members

+

Share your Invite Code to your friend. Make sure to tell them your team name as well!

{/* TODO: For some reason, I couldn't get the CSS to show up when I put it in the CSS file */} From 451150f8cd2297177dc0c2e93b059b1a08c61c18 Mon Sep 17 00:00:00 2001 From: William Kim Date: Fri, 29 Dec 2023 10:22:00 -0800 Subject: [PATCH 32/55] polished my team ui --- package-lock.json | 7 +- package.json | 1 + .../CompetitionPortalPage/CountDownTimer.tsx | 70 +++++++ .../CompetitionPortalPage/index.less | 115 ++++++++--- .../CompetitionPortalPage/index.tsx | 187 +++++++++++++----- 5 files changed, 299 insertions(+), 81 deletions(-) create mode 100644 src/pages/Competitions/CompetitionPortalPage/CountDownTimer.tsx diff --git a/package-lock.json b/package-lock.json index 116de0c..8973a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "chart.js": "^2.9.4", "cors": "^2.8.5", "express": "^4.17.1", + "moment": "^2.30.1", "path-browserify": "^1.0.1", "postcss-loader": "^7.3.3", "prettier": "^2.1.2", @@ -13211,9 +13212,9 @@ } }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { "node": "*" } diff --git a/package.json b/package.json index 65448b6..3070284 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "chart.js": "^2.9.4", "cors": "^2.8.5", "express": "^4.17.1", + "moment": "^2.30.1", "path-browserify": "^1.0.1", "postcss-loader": "^7.3.3", "prettier": "^2.1.2", diff --git a/src/pages/Competitions/CompetitionPortalPage/CountDownTimer.tsx b/src/pages/Competitions/CompetitionPortalPage/CountDownTimer.tsx new file mode 100644 index 0000000..60b2837 --- /dev/null +++ b/src/pages/Competitions/CompetitionPortalPage/CountDownTimer.tsx @@ -0,0 +1,70 @@ + +import React, { useState, useEffect } from 'react'; +import moment from 'moment'; + +const CountdownTimer = ({ endDate }: {endDate: any}) => { + +const calculateTimeRemaining = () => { + const now = moment(); + const endDateTime = moment(endDate); + const duration = moment.duration(endDateTime.diff(now)); + + return { + days: duration.days(), + hours: duration.hours(), + minutes: duration.minutes(), + seconds: duration.seconds(), + }; + }; + const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining()); + + + const formatTimeRemaining = () => { + const { days, hours, minutes, seconds } = timeRemaining; + + if (days > 0) { + return `Closes in ${days} day${days !== 1 ? 's' : ''}${hours > 0 ? ` ${hours} hours` : ''}${ + minutes > 0 ? ` ${minutes} minutes` : '' + }`; + } else if (hours > 0) { + return `Closes in ${hours} hour${hours !== 1 ? 's' : ''}${minutes > 0 ? ` ${minutes} minutes` : ''}`; + } else { + return `Closes in ${minutes} minute${minutes !== 1 ? 's' : ''}${seconds > 0 ? ` ${seconds} seconds` : ''}`; + } + }; + + +useEffect(() => { + let intervalId: string | number | NodeJS.Timeout | undefined; + + const updateInterval = () => { + const { minutes } = timeRemaining; + + if (minutes > 1) { + // Use a longer interval when there's more than 1 minute remaining + return 60 * 1000; // 1 minute + } else { + // Switch to a shorter interval when there's 1 minute or less remaining + return 1000; // 1 second + } + }; + + const updateTimer = () => { + setTimeRemaining(calculateTimeRemaining()); + }; + + intervalId = setInterval(updateTimer, updateInterval()); + + return () => { + clearInterval(intervalId); + }; +}, [endDate]); + + + + return ( +

{formatTimeRemaining()}

+ ); +}; + +export default CountdownTimer; diff --git a/src/pages/Competitions/CompetitionPortalPage/index.less b/src/pages/Competitions/CompetitionPortalPage/index.less index bfbf6a3..14d4354 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.less +++ b/src/pages/Competitions/CompetitionPortalPage/index.less @@ -7,10 +7,6 @@ .noContainer(); height: 100vh; - // prevents layout shifts caused by scrollbar - background: @white; - - #portalHeader { .generic-section(); .constrained-bounds(); @@ -72,7 +68,7 @@ } .portalStatsBox { - background-color: @light-gray; + background-color: white; box-shadow: @bx3; min-height: 140px; padding: @padding-base2; @@ -81,7 +77,6 @@ flex-direction: column; justify-content: space-between; transition: background-color 0.2s ease-in; - margin-bottom: 2rem; flex: 1 0 20%; min-width: 100px; @@ -155,10 +150,10 @@ .scoreBar { border-radius: @radius-base1; - background-color: @b2; + background-color: @black; width: 2rem; transition: all 0.2s ease-in; - + cursor: pointer; } .scoreBar:hover { @@ -179,12 +174,13 @@ .generic-section(); .constrained-bounds(); height: 100vh; - max-width: 1200px !important; .ant-tabs-ink-bar { display: none; } + + } @@ -238,8 +234,17 @@ margin: 2rem auto; } - .submitButton { + #submitFileButton { + margin-top: 1rem; + .button-black-square(); + } + + #makeTeamButton { margin-top: 1rem; + width: fit-content; + } + #leaveTeamButton { + border: none; } #teamNameWrapper { @@ -247,22 +252,29 @@ justify-content: space-between; align-items: center; width: 100%; + + #rankingTag { + border-radius: @radius-base4; + background-color: rgb(18, 113, 255); + padding: 4px 8px; + width: fit-content; + color: white; + transform: translateY(4px); + box-sizing: border-box; + font-weight: @semi-bold; + } } #teamNameInput { max-width: 300px; } - #makeTeamButton { - margin-top: 1rem; - } - + #teamScoreOverview { box-shadow: @bx2; border-radius: @radius-base4; padding: @padding-base3; - background-color: @black; - + background-color: rgb(21, 19, 19); .statHeader { color: gray; margin-bottom: 0.6rem; @@ -286,36 +298,75 @@ } } - #teamAffix { - margin-left: 2rem; + #teamHeader { + display: flex; + margin-bottom: 2rem; + align-items: center; - .teamMemberName { - font-weight: bold; + h3 { + font-weight: @semi-bold; } + } - .inviteCode { - background-color: red; - } + #uploadFileSection, #submissionLogSection { + display: block; + margin-top: 5rem; + } + + #submissionCountDown { + background-color: @light-gray; + border-radius: @radius-base3; + padding: 10px; + display: inline-block; + margin-left: 1rem; + } + + #teamAffix { + margin-top: 1rem; + height: fit-content; + box-sizing: border-box; + padding: @padding-base3; + background-color: @light-gray; + margin-left: 2rem; + border-radius: @radius-base4; #teamMembersHeader { display: flex; justify-content: space-between; align-items: center; + + + #inviteButton { + .button-black-square(); + } + + .inviteCode { + background-color: red; + } + + } - #teamMember { + div.teamMember { display: flex; - margin: 1.5rem 1rem; + padding: 8px; + background: none; + border-radius: @radius-base4; + margin: 1rem 0rem 1rem 0; + transition: 0.2s ease-in; + + .teamMemberName { + font-weight: bold; + } } - } - #teamHeader { - display: flex; - margin-bottom: 2rem; - align-items: center; + div.teamMember:hover { + background-color: white !important; + cursor: pointer; + } } - @media screen and (max-width: @sm) { + @media screen and (max-width: @md) { section { display: block } @@ -324,4 +375,4 @@ margin-left: 0; } } -} \ No newline at end of file +} diff --git a/src/pages/Competitions/CompetitionPortalPage/index.tsx b/src/pages/Competitions/CompetitionPortalPage/index.tsx index 302fece..884d089 100644 --- a/src/pages/Competitions/CompetitionPortalPage/index.tsx +++ b/src/pages/Competitions/CompetitionPortalPage/index.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { Affix, AutoComplete, Statistic, Drawer, List, Skeleton, Tabs, Tooltip, message, Empty } from "antd"; import { ArrowDownOutlined, ArrowUpOutlined, PlusOutlined, InboxOutlined } from '@ant-design/icons'; import { Form, Layout, Button, Input, Modal, Upload } from 'antd'; @@ -15,22 +15,22 @@ import { import TeamCard from '../../../components/TeamCard/index'; import './index.less'; import path from 'path'; +import moment from 'moment'; import DefaultLayout from "../../../components/layouts/default"; import { PaginationPosition, PaginationAlign } from "antd/es/pagination/Pagination"; -import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser } from "../../../actions/competition"; +import { CompetitionData, getLeaderboard, getMetaData, getRanks, registerCompetitionUser, uploadSubmission } from "../../../actions/competition"; import { genColor } from "../../../utils/colors"; import { IoHelp } from "react-icons/io5"; -import { BsPatchCheckFill } from "react-icons/bs"; +import { IoEllipsisVertical , IoPersonAdd} from "react-icons/io5"; import { FaCheck, FaStar } from "react-icons/fa"; -import { GrScorecard } from "react-icons/gr"; - import Table, { ColumnsType } from "antd/es/table"; -import MainFooter from "../../../components/MainFooter"; import { AxiosResponse } from "axios"; import { BiStats } from "react-icons/bi"; import { createAvatar } from '@dicebear/core'; -import { botttsNeutral } from '@dicebear/collection'; +import { botttsNeutral, identicon } from '@dicebear/collection'; +import CountdownTimer from "./CountDownTimer"; +import TextArea from "antd/es/input/TextArea"; const { Content } = Layout; @@ -189,13 +189,19 @@ const LeaderBoardTab = ( -const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeamsCallback: () => void }) => { +const MyTeamTab = ({ isLoadingTeamInfo, compUser, rankData, metaData , fetchTeamsCallback}: { isLoadingTeamInfo: boolean, compUser: any, rankData: any, metaData: any, fetchTeamsCallback: () => void }) => { const [isLoading, setIsLoading] = useState(false); const [newTeamName, setNewTeamName] = useState(""); const [isInviteModalVisible, setIsInviteModalVisible] = useState(false); const [isLeaveModalVisible, setIsLeaveModalVisible] = useState(false); + + const [submissionFile, setFile] = useState(); + const [desc, setDesc] = useState(''); + const [tags, setTags] = useState>([]); // not being used for now + const [uploading, setUploading] = useState(false); + // Leave button modal const showLeaveModal = () => { setIsLeaveModalVisible(true); @@ -220,7 +226,7 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams name: 'file', multiple: false, // TODO: replace placeholder link with actual file uploading logic - action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188', + // action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188', onChange(info) { const { status } = info.file; if (status !== 'uploading') { @@ -228,6 +234,7 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams } if (status === 'done') { message.success(`${info.file.name} file uploaded successfully.`); + setFile(info.file) } else if (status === 'error') { message.error(`${info.file.name} file upload failed.`); } @@ -237,9 +244,33 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams }, }; + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setUploading(true); + uploadSubmission( + submissionFile, + tags, + desc, + compUser.competitionName, + compUser.username as string + ) + .then((res) => { + message.success('Submission Uploaded Succesfully'); + }) + .catch((err) => { + message.error(`${err}`); + }) + .finally(() => { + setUploading(false); + }); + }; + const TeamMemberAvatar: React.FC<{ username: string }> = ({ username }) => { const [avatarUrl, setAvatarUrl] = useState(''); + const [loadingImage, setLoadingImage] = useState(false); + useEffect(() => { + setLoadingImage(true); // Generate avatar based on username using DiceBear const svg = createAvatar(botttsNeutral, { seed: username, @@ -252,20 +283,28 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams // Set the avatar URL setAvatarUrl(dataUrl); + + setLoadingImage(false); }, [username]); + return ( - + {loadingImage ? ( + + ) : ( + {`Avatar - ); + }} + alt={`Avatar for ${username}`} + /> + )} + + ) } const generateTeamPicture = () => { @@ -288,6 +327,14 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams ) } + + function getOrdinal(number: any) { + const suffixes = ['th', 'st', 'nd', 'rd']; + const v = number % 100; + + return number + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]); + } + const handleLeaveTeam = () => { leaveTeam(compUser.competitionName, compUser.username, compUser.competitionTeam.teamName).then((res) => { message.success('Successfully left team.'); @@ -321,6 +368,10 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams } return ( + <> + {isLoadingTeamInfo ? + + : {compUser.competitionTeam == null && ( @@ -346,12 +397,18 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams {compUser.competitionTeam !== null && (
+
-
{generateTeamPicture()}
+ {generateTeamPicture()}
-

{compUser.competitionTeam.teamName}

- +
+

{compUser.competitionTeam.teamName}

+
{getOrdinal(rankData.rank)} place
+
+ +
+ {/** TODO: Lowkey don't know what stats would work best here as everything besides the score is not finalized */}

score

0

@@ -381,32 +439,62 @@ const MyTeamTab = ({ compUser, fetchTeamsCallback }: { compUser: any, fetchTeams
-

Upload Submission

-
- + +

Upload Submission

+