Compare commits

..

29 Commits

Author SHA1 Message Date
a77a31d01c table buttons acessibility and label correction 2023-10-01 21:31:27 +02:00
970a948cec get current user profile info to disable edit and delete submission 2023-09-29 16:07:36 +02:00
4dbcc540d6 TableStyle__td padding with rowFooter correction 2023-08-11 13:20:25 +02:00
836ec0aef2 ColumnFilterButton correction and table DOM structure correction 2023-08-11 13:10:56 +02:00
6c4da8c181 disable buttons and leaderboard iterator 2023-08-11 12:28:24 +02:00
aa1c1c3737 edit and delete submission refactor and handle errors 2023-08-04 16:35:15 +02:00
00a9a82bcb edit submission handle 2023-07-31 19:21:20 +02:00
01f82b1365 init editSubmission design and profile data and manage research 2023-07-28 13:32:20 +02:00
afd2361689 separate MobileTableStyles to another file 2023-07-28 12:25:00 +02:00
cfeac6b920 edit submission request init 2023-07-23 11:23:09 +02:00
d26a91b65c init new mobile table 2023-07-21 16:00:05 +02:00
0b3d809e3b refactor and style DeletePopUp 2023-07-14 14:57:11 +02:00
7d7b6d88b9 warning popUp for delete submission 2023-07-14 13:10:20 +02:00
38735c7982 Pager wrong props in Challenges and Leaderboard correction 2023-07-05 22:36:25 +02:00
671d0ed650 test-B evaluations visible in Tables 2023-07-02 19:29:32 +02:00
d645964266 correct popUpMessenge in MyEntries 2023-06-30 14:05:14 +02:00
3610415657 scroll-x for tables 2023-06-30 11:20:55 +02:00
69f65846c2 fix popUpMessageHandler, and popUp message when delete submission impossible 2023-06-28 19:12:06 +02:00
74e0ef19db prototype delete worked 2023-06-14 12:36:22 +02:00
decd4faf22 refactor Leaderboard and new Table component structure 2023-06-14 11:17:09 +02:00
1abe2ddfaf refactor Leaderboard in progress 2023-06-09 17:05:37 +02:00
b8b4c31b45 MyEntries with new table and logic 2023-06-09 15:50:45 +02:00
e5d0e50102 refactor: AllEntries Table replaced by new table and logic 2023-06-09 13:57:11 +02:00
098e865e91 sort columns in New Table complete 2023-06-09 12:42:19 +02:00
3fcd4b4154 New table search 2023-06-09 12:06:28 +02:00
8211ed629c Loading and Paging to new table 2023-06-09 11:40:13 +02:00
5a968ff026 new table structure complete 2023-06-09 11:10:41 +02:00
60ecbd331a NewTable styles 2023-06-07 12:14:38 +02:00
916796ef8e start creating new Table component 2023-06-02 15:49:33 +02:00
67 changed files with 1699 additions and 1270 deletions

View File

@ -14,13 +14,6 @@
"eqeqeq": [ "eqeqeq": [
"error", "error",
"always" "always"
],
"quotes": [
2,
"single",
{
"avoidEscape": true
}
] ]
} }
} }

View File

@ -16,13 +16,13 @@ const App = () => {
const renderApp = React.useCallback(() => { const renderApp = React.useCallback(() => {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<PopUpMessageManager> <BrowserRouter>
<BrowserRouter> <PopUpMessageManager>
<NavigationManager> <NavigationManager>
<RoutingManager /> <RoutingManager />
</NavigationManager> </NavigationManager>
</BrowserRouter> </PopUpMessageManager>
</PopUpMessageManager> </BrowserRouter>
</ThemeProvider> </ThemeProvider>
); );
}, []); }, []);

View File

@ -4,7 +4,7 @@ import LoggedBar from '../../components/navigation/LoggedBar';
import KeyCloakService from '../../services/KeyCloakService'; import KeyCloakService from '../../services/KeyCloakService';
import { CHILDREN_WITH_PROPS, IS_MOBILE } from '../../utils/globals'; import { CHILDREN_WITH_PROPS, IS_MOBILE } from '../../utils/globals';
const NavigationManager = (props) => { const NavigationManager = ({children, popUpMessageHandler}) => {
const [loggedBarVisible, setLoggedBarVisible] = React.useState('100vw'); const [loggedBarVisible, setLoggedBarVisible] = React.useState('100vw');
const [loggedBarHover, setLoggedBarHover] = React.useState(false); const [loggedBarHover, setLoggedBarHover] = React.useState(false);
const [navOptions, setNavOptions] = React.useState(true); const [navOptions, setNavOptions] = React.useState(true);
@ -27,7 +27,7 @@ const NavigationManager = (props) => {
<> <>
<NavBar <NavBar
loggedBarVisibleHandler={loggedBarVisibleHandler} loggedBarVisibleHandler={loggedBarVisibleHandler}
popUpMessageHandler={props.popUpMessageHandler} popUpMessageHandler={popUpMessageHandler}
navOptions={navOptions} navOptions={navOptions}
/> />
{!IS_MOBILE() && ( {!IS_MOBILE() && (
@ -39,7 +39,7 @@ const NavigationManager = (props) => {
username={KeyCloakService.getUsername()} username={KeyCloakService.getUsername()}
/> />
)} )}
{CHILDREN_WITH_PROPS(props.children, { hideNavOptions, showNavOptions })} {CHILDREN_WITH_PROPS(children, { hideNavOptions, showNavOptions, popUpMessageHandler })}
</> </>
); );
}; };

View File

@ -5,11 +5,13 @@ import { CHILDREN_WITH_PROPS } from '../../utils/globals';
const PopUpMessageManager = (props) => { const PopUpMessageManager = (props) => {
const [popUpHeader, setPopUpHeader] = React.useState(''); const [popUpHeader, setPopUpHeader] = React.useState('');
const [popUpMessage, setPopUpMessage] = React.useState(''); const [popUpMessage, setPopUpMessage] = React.useState('');
const [borderColor, setBorderColor] = React.useState(null);
const [confirmPopUpHandler, setConfirmPopUpHandler] = React.useState(null); const [confirmPopUpHandler, setConfirmPopUpHandler] = React.useState(null);
const popUpMessageHandler = (header, message, confirmHandler) => { const popUpMessageHandler = (header, message, confirmHandler=null, borderColor=null) => {
setPopUpHeader(header); setPopUpHeader(header);
setPopUpMessage(message); setPopUpMessage(message);
setBorderColor(borderColor);
if (confirmHandler !== null && confirmHandler !== undefined) { if (confirmHandler !== null && confirmHandler !== undefined) {
setConfirmPopUpHandler(() => confirmHandler()); setConfirmPopUpHandler(() => confirmHandler());
} else { } else {
@ -24,6 +26,7 @@ const PopUpMessageManager = (props) => {
header={popUpHeader} header={popUpHeader}
message={popUpMessage} message={popUpMessage}
confirmHandler={confirmPopUpHandler} confirmHandler={confirmPopUpHandler}
borderColor={borderColor}
popUpMessageHandler={popUpMessageHandler} popUpMessageHandler={popUpMessageHandler}
/> />
); );

View File

@ -16,15 +16,30 @@ const RoutingManager = (props) => {
<Routes> <Routes>
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId`} path={`${CHALLENGE_PAGE}/:challengeId`}
element={<Challenge section={CHALLENGE_SECTIONS.LEADERBOARD} />} element={
<Challenge
section={CHALLENGE_SECTIONS.LEADERBOARD}
popUpMessageHandler={props.popUpMessageHandler}
/>
}
/> />
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId/leaderboard`} path={`${CHALLENGE_PAGE}/:challengeId/leaderboard`}
element={<Challenge section={CHALLENGE_SECTIONS.LEADERBOARD} />} element={
<Challenge
section={CHALLENGE_SECTIONS.LEADERBOARD}
popUpMessageHandler={props.popUpMessageHandler}
/>
}
/> />
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId/allentries`} path={`${CHALLENGE_PAGE}/:challengeId/allentries`}
element={<Challenge section={CHALLENGE_SECTIONS.ALL_ENTRIES} />} element={
<Challenge
section={CHALLENGE_SECTIONS.ALL_ENTRIES}
popUpMessageHandler={props.popUpMessageHandler}
/>
}
/> />
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId/readme`} path={`${CHALLENGE_PAGE}/:challengeId/readme`}
@ -41,13 +56,26 @@ const RoutingManager = (props) => {
/> />
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId/myentries`} path={`${CHALLENGE_PAGE}/:challengeId/myentries`}
element={<Challenge section={CHALLENGE_SECTIONS.MY_ENTRIES} />} element={
<Challenge
section={CHALLENGE_SECTIONS.MY_ENTRIES}
popUpMessageHandler={props.popUpMessageHandler}
/>
}
/> />
<Route <Route
path={`${CHALLENGE_PAGE}/:challengeId/submit`} path={`${CHALLENGE_PAGE}/:challengeId/submit`}
element={<Challenge section={CHALLENGE_SECTIONS.SUBMIT} />} element={
<Challenge
section={CHALLENGE_SECTIONS.SUBMIT}
popUpMessageHandler={props.popUpMessageHandler}
/>
}
/>
<Route
path={CHALLENGES_PAGE}
element={<Challenges popUpMessageHandler={props.popUpMessageHandler} />}
/> />
<Route path={CHALLENGES_PAGE} element={<Challenges />} />
<Route <Route
path={POLICY_PRIVACY_PAGE} path={POLICY_PRIVACY_PAGE}
element={ element={
@ -74,8 +102,18 @@ const RoutingManager = (props) => {
/> />
{KeyCloakService.isLoggedIn() ? ( {KeyCloakService.isLoggedIn() ? (
<> <>
<Route exact path="/" element={<Challenges />} /> <Route
<Route element={<Challenges />} /> exact
path="/"
element={
<Challenges popUpMessageHandler={props.popUpMessageHandler} />
}
/>
<Route
element={
<Challenges popUpMessageHandler={props.popUpMessageHandler} />
}
/>
</> </>
) : ( ) : (
<> <>

View File

@ -38,7 +38,12 @@ const challengeSubmission = (
.then((data) => { .then((data) => {
dispatch({ type: SUBMIT_ACTION.TOGGLE_SUBMISSION_LOADING }); dispatch({ type: SUBMIT_ACTION.TOGGLE_SUBMISSION_LOADING });
const processUrl = API.replace('/api', ''); const processUrl = API.replace('/api', '');
window.location.replace(`${processUrl}/open-view-progress/${data}#form`); if (Number.isInteger(Number(data))) {
console.log(`${processUrl}/open-view-progress/${data}#form`);
window.location.replace(
`${processUrl}/open-view-progress/${data}#form`
);
}
// console.log(data); // console.log(data);
// fetch(`${API}/view-progress-with-web-sockets/${data}`) // fetch(`${API}/view-progress-with-web-sockets/${data}`)

View File

@ -1,17 +1,42 @@
import KeyCloakService from '../services/KeyCloakService'; import KeyCloakService from '../services/KeyCloakService';
import { API } from '../utils/globals'; import { API } from '../utils/globals';
import theme from '../utils/theme';
const deleteSubmission = (submissionId) => { const deleteSubmission = async (
fetch(`${API}/delete-submission/${submissionId}`, { item,
deletedItems,
setDeletedItems,
popUpMessageHandler
) => {
fetch(`${API}/delete-submission/${item.id}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
Authorization: `Bearer ${KeyCloakService.getToken()}`, Authorization: `Bearer ${KeyCloakService.getToken()}`,
}, },
}) })
.then((resp) => resp.json()) .then((resp) => resp.text())
.then((data) => { .then((data) => {
console.log(data); if (data === 'deleted') {
let newDeletedItems = deletedItems.slice();
newDeletedItems.push(item);
setDeletedItems(newDeletedItems);
popUpMessageHandler('Complete', `Submission "${item.id}" deleted`);
} else if (data.includes('<!doctype html>') && data.includes('Login')) {
popUpMessageHandler(
'Error',
'You have to be login in to edit submission!',
null,
theme.colors.red
);
} else {
popUpMessageHandler(
'Error',
"You can't delete this submission!",
null,
theme.colors.red
);
}
}); });
}; };

47
src/api/editSubmission.js Normal file
View File

@ -0,0 +1,47 @@
import KeyCloakService from '../services/KeyCloakService';
import { API } from '../utils/globals';
import theme from '../utils/theme';
const editSubmission = async (
submisssion,
tags,
description,
popUpMessageHandler
) => {
tags = tags.replaceAll(',', '%2C');
fetch(`${API}/edit-submission/${submisssion}/${tags}/${description}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
Authorization: `Bearer ${KeyCloakService.getToken()}`,
},
})
.then((resp) => resp.text())
.then((data) => {
console.log(data);
if (data === 'Submission changed') {
popUpMessageHandler(
'Submission changed!',
`Submission ${submisssion} edited`,
null,
theme.colors.green
);
} else if (data === 'Only owner can edit a submission!') {
popUpMessageHandler('Error', data, null, theme.colors.red);
} else if (data.includes('<!doctype html>') && data.includes('Login')) {
popUpMessageHandler(
'Error',
'You have to be login in to edit submission!',
null,
theme.colors.red
);
} else {
if (data.length > 650) {
data = `${data.slice(0, 650)}...`;
}
popUpMessageHandler('Error', data, null, theme.colors.red);
}
});
};
export default editSubmission;

View File

@ -1,19 +1,62 @@
import { API } from '../utils/globals'; import { API } from '../utils/globals';
import KeyCloakService from '../services/KeyCloakService';
const getChallengeLeaderboard = async ( const getChallengeLeaderboard = (
setDataState, endpoint,
challengeName, challengeName,
setLoading setDataStates,
setLoadingState,
setScoreSorted
) => { ) => {
await fetch(`${API}/leaderboard/${challengeName}`) fetch(`${API}/${endpoint}/${challengeName}`, {
headers: { Authorization: `Bearer ${KeyCloakService.getToken()}` },
})
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
setDataState(data.entries); let item = {};
let result = [];
let initSetScoreSorted = [];
let tests = data.tests;
for (let submission of data.entries) {
for (let evaluation of submission.evaluations) {
item = {
...item,
evaluations: {
...item.evaluations,
[`${evaluation.test.metric}.${evaluation.test.name}`]:
evaluation.score,
},
};
}
for (let test of tests) {
if (!item.evaluations) {
item.evaluations = {};
}
if (!Object.hasOwn(item.evaluations, `${test.metric}.${test.name}`)) {
item = {
...item,
evaluations: {
...item.evaluations,
[`${test.metric}.${test.name}`]: -999999999,
},
};
}
}
item = {
...item.evaluations,
...submission,
};
result.push(item);
item = {};
}
// eslint-disable-next-line no-unused-vars
for (let _ of tests) {
initSetScoreSorted.push(false);
}
for (let setDataState of setDataStates) setDataState(result);
if (setScoreSorted) setScoreSorted(initSetScoreSorted);
if (setLoadingState) setLoadingState(false);
}); });
if (setLoading) {
setLoading(false);
}
}; };
export default getChallengeLeaderboard; export default getChallengeLeaderboard;

View File

@ -1,19 +1,18 @@
import { API } from '../utils/globals'; import { API } from '../utils/globals';
import KeyCloakService from '../services/KeyCloakService'; import KeyCloakService from '../services/KeyCloakService';
const getAllEntries = ( const getEntries = (
endpoint,
challengeName, challengeName,
setDataOriginalState, setDataStates,
setDataState,
setLoadingState, setLoadingState,
setScoreSorted setScoreSorted
) => { ) => {
fetch(`${API}/challenge-all-submissions/${challengeName}`, { fetch(`${API}/${endpoint}/${challengeName}`, {
headers: { Authorization: `Bearer ${KeyCloakService.getToken()}` }, headers: { Authorization: `Bearer ${KeyCloakService.getToken()}` },
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (setDataOriginalState) setDataOriginalState(data);
let item = {}; let item = {};
let result = []; let result = [];
let initSetScoreSorted = []; let initSetScoreSorted = [];
@ -30,37 +29,35 @@ const getAllEntries = (
}; };
} }
for (let test of tests) { for (let test of tests) {
if (item.evaluations) { if (!item.evaluations) {
if ( item.evaluations = {};
!Object.hasOwn(item.evaluations, `${test.metric}.${test.name}`) }
) { if (!Object.hasOwn(item.evaluations, `${test.metric}.${test.name}`)) {
item = { item = {
...item, ...item,
evaluations: { evaluations: {
...item.evaluations, ...item.evaluations,
[`${test.metric}.${test.name}`]: '-1', [`${test.metric}.${test.name}`]: -999999999,
}, },
}; };
}
} }
} }
item = { item = {
...item, ...item.evaluations,
id: submission.id, ...submission,
submitter: submission.submitter,
when: submission.when,
}; };
result.push(item); result.push(item);
item = {}; item = {};
} }
result = result.filter((item) => !item.deleted);
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
for (let _ of tests) { for (let _ of tests) {
initSetScoreSorted.push(false); initSetScoreSorted.push(false);
} }
setDataState(result); for (let setDataState of setDataStates) setDataState(result);
if (setScoreSorted) setScoreSorted(initSetScoreSorted); if (setScoreSorted) setScoreSorted(initSetScoreSorted);
if (setLoadingState) setLoadingState(false); if (setLoadingState) setLoadingState(false);
}); });
}; };
export default getAllEntries; export default getEntries;

View File

@ -1,16 +1,15 @@
import {API} from '../utils/globals'; import { API } from '../utils/globals';
import KeyCloakService from '../services/KeyCloakService'; import KeyCloakService from '../services/KeyCloakService';
const getFullUser = (setDataState, setLoadingState) => { const getFullUser = async (setDataState, setLoadingState) => {
fetch(`${API}/full-user-info`, { fetch(`${API}/full-user-info`, {
headers: {'Authorization': `Bearer ${KeyCloakService.getToken()}`} headers: { Authorization: `Bearer ${KeyCloakService.getToken()}` },
}) })
.then(response => response.json()) .then((response) => response.json())
.then(data => { .then((data) => {
setDataState(data); setDataState(data);
if (setLoadingState) if (setLoadingState) setLoadingState(false);
setLoadingState(false); });
});
}; };
export default getFullUser; export default getFullUser;

View File

@ -1,68 +0,0 @@
import { API } from '../utils/globals';
import KeyCloakService from '../services/KeyCloakService';
const getMyEntries = (
challengeName,
setDataOriginalState,
setDataStateForSearch,
setDataState,
setLoadingState,
setScoreSorted
) => {
fetch(`${API}/challenge-my-submissions/${challengeName}`, {
headers: { Authorization: `Bearer ${KeyCloakService.getToken()}` },
})
.then((response) => response.json())
.then((data) => {
setDataOriginalState(data);
let item = {};
let result = [];
let initSetScoreSorted = [];
let tests = data.tests;
for (let submission of data.submissions) {
for (let evaluation of submission.evaluations) {
item = {
...item,
evaluations: {
...item.evaluations,
[`${evaluation.test.metric}.${evaluation.test.name}`]:
evaluation.score,
},
};
}
for (let test of tests) {
if (item.evaluations) {
if (
!Object.hasOwn(item.evaluations, `${test.metric}.${test.name}`)
) {
item = {
...item,
evaluations: {
...item.evaluations,
[`${test.metric}.${test.name}`]: '-1',
},
};
}
}
}
item = {
...item,
id: submission.id,
submitter: submission.submitter,
when: submission.when,
};
result.push(item);
item = {};
}
// eslint-disable-next-line no-unused-vars
for (let _ of tests) {
initSetScoreSorted.push(false);
}
setScoreSorted(initSetScoreSorted);
setDataStateForSearch(result);
setDataState(result);
setLoadingState(false);
});
};
export default getMyEntries;

View File

@ -1,11 +1,10 @@
import SUBMIT_ACTION from '../pages/Submit/model/SubmitActionEnum';
import { API } from '../utils/globals'; import { API } from '../utils/globals';
const getTags = (dispatch) => { const getTags = (setState) => {
fetch(`${API}/list-tags`) fetch(`${API}/list-tags`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
dispatch({ type: SUBMIT_ACTION.LOAD_TAGS, payload: data }); setState(data);
}); });
}; };

View File

@ -0,0 +1,3 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3571 2.24695C31.9857 2.24695 32.5 2.69695 32.5 3.24695V5.24695C32.5 5.79695 31.9857 6.24695 31.3571 6.24695H1.64286C1.01429 6.24695 0.5 5.79695 0.5 5.24695V3.24695C0.5 2.69695 1.01429 2.24695 1.64286 2.24695H10.2143L10.8857 1.0782C11.1429 0.621948 11.8214 0.246948 12.4071 0.246948H12.4143H20.5786C21.1643 0.246948 21.85 0.621948 22.1143 1.0782L22.7857 2.24695H31.3571ZM4.3 29.4344L2.78571 8.24695H30.2143L28.7 29.4344C28.5857 30.9844 27.0571 32.2469 25.2786 32.2469H7.72143C5.94286 32.2469 4.41429 30.9844 4.3 29.4344Z" fill="#343434"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@ -63,7 +63,7 @@ const Pager = (props) => {
as="a" as="a"
href="#start" href="#start"
src={polygon} src={polygon}
onClick={() => PREVIOUS_PAGE(props.pageNr, props.setPage)} onClick={() => PREVIOUS_PAGE(props.pageNr, props.setPageNr)}
size="cover" size="cover"
backgroundColor={leftArrowVisible()} backgroundColor={leftArrowVisible()}
/> />
@ -76,7 +76,7 @@ const Pager = (props) => {
as="a" as="a"
href="#start" href="#start"
src={polygon} src={polygon}
onClick={() => NEXT_PAGE(props.elements, props.pageNr, props.setPage)} onClick={() => NEXT_PAGE(props.elements, props.pageNr, props.setPageNr)}
size="cover" size="cover"
backgroundColor={rightArrowVisible()} backgroundColor={rightArrowVisible()}
/> />

View File

@ -9,12 +9,12 @@ const PopUpStyle = styled(FlexColumn)`
z-index: 100; z-index: 100;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background-color: ${({ theme }) => theme.colors.dark01}; background-color: ${({ theme, backgroundColor }) =>
backgroundColor ? backgroundColor : theme.colors.dark01};
.PopUpStyle__body { .PopUpStyle__body {
width: ${({ width }) => (width ? width : '60%')}; width: ${({ width }) => (width ? width : '60%')};
height: ${({ height }) => (height ? height : '50%')}; height: ${({ height }) => (height ? height : '50%')};
min-height: ${({ minHeight }) => (minHeight ? minHeight : '50%')};
padding: ${({ padding }) => (padding ? padding : '48px')}; padding: ${({ padding }) => (padding ? padding : '48px')};
margin: ${({ margin }) => (margin ? margin : '0')}; margin: ${({ margin }) => (margin ? margin : '0')};
border-radius: 12px; border-radius: 12px;
@ -32,6 +32,7 @@ const PopUp = (props) => {
return ( return (
<PopUpStyle <PopUpStyle
backgroundColor={props.backgroundColor}
padding={props.padding} padding={props.padding}
width={props.width} width={props.width}
height={props.height} height={props.height}

View File

@ -28,13 +28,13 @@ const PopupMessage = (props) => {
borderRadius="12px" borderRadius="12px"
backgroundColor={theme.colors.white} backgroundColor={theme.colors.white}
padding="56px" padding="56px"
border={`4px solid ${theme.colors.green}`} border={`4px solid ${props.borderColor ? props.borderColor : theme.colors.green}`}
> >
<FlexColumn gap="48px" margin="0 0 48px 0"> <FlexColumn gap="48px" margin="0 0 48px 0">
<H3>{props.header}</H3> <H3>{props.header}</H3>
<Body>{props.message}</Body> <Body>{props.message}</Body>
</FlexColumn> </FlexColumn>
<Button handler={confirmPopUp}>Ok</Button> <Button backgroundColor={props.borderColor ? props.borderColor : theme.colors.green} handler={confirmPopUp}>Ok</Button>
</FlexColumn> </FlexColumn>
</FlexColumn> </FlexColumn>
); );

View File

@ -18,6 +18,7 @@ const SubmitInput = (props) => {
height="36px" height="36px"
border={`1px solid ${theme.colors.dark}`} border={`1px solid ${theme.colors.dark}`}
shadow={theme.shadow} shadow={theme.shadow}
defaultValue={props.defaultValue}
onChange={(e) => props.handler(e.target.value)} onChange={(e) => props.handler(e.target.value)}
padding="4px" padding="4px"
/> />

View File

@ -1,222 +1,79 @@
import React from 'react'; import React from 'react';
import {
Container,
FlexColumn,
FlexRow,
Grid,
} from '../../../utils/containers';
import Media from 'react-media'; import Media from 'react-media';
import theme from '../../../utils/theme'; import theme from '../../../utils/theme';
import { ELEMENTS_PER_PAGE, IS_MOBILE } from '../../../utils/globals'; import MobileTable from './components/MobileTable';
import { Body, Medium } from '../../../utils/fonts'; import DesktopTable from './components/DesktopTable';
import ColumnFilterIcon from '../ColumnFilterIcon'; import EditPopUp from './components/EditPopUp';
// import deleteSubmission from '../../api/deleteSubmission'; import DeletePopUp from './components/DeletePopUp';
import TableStyle from './styles/TableStyle';
import TableLine from './styles/TableLine';
import MobileTableStyle from './styles/MobileTableStyle';
const Table = (props) => { const Table = ({
items,
orderedKeys,
popUpMessageHandler,
sortByUpdate,
profileInfo,
rowFooter = true,
}) => {
const [, updateState] = React.useState(); const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []); const tableUpdate = React.useCallback(() => updateState({}), []);
const [activeIcon, setActiveIcon] = React.useState(null); const [deletedItems, setDeletedItems] = React.useState([]);
const [rotateActiveIcon, setRotateActiveIcon] = React.useState(false); const [deletePopUp, setDeletePopUp] = React.useState(false);
const [editPopUp, setEditPopUp] = React.useState(false);
const metricsRender = (elem) => { const [itemToHandle, setItemToHandle] = React.useState(null);
if (!props.iterableColumnElement) return <></>; const itemsToRender = items.filter((item) => !deletedItems.includes(item));
if (Array.isArray(elem[props.iterableColumnElement.name]))
elem = elem[props.iterableColumnElement.name];
else {
let newElem = [];
for (let metric of props.possibleMetrics) {
if (Object.hasOwn(elem, props.iterableColumnElement.name)) {
if (elem[props.iterableColumnElement.name][metric] === '-1')
newElem.push('N/A');
else newElem.push(elem[props.iterableColumnElement.name][metric]);
} else {
newElem.push('N/A');
}
}
elem = newElem;
}
let indexModificator = 2;
if (props.tableType === 'leaderboard') indexModificator = 4;
if (props.tableType === 'allEntries') indexModificator = 3;
return elem.map((iterableElem, i) => {
return (
<Body
key={`metric-result-${i}`}
as="td"
order={props.iterableColumnElement.order}
textAlign={props.iterableColumnElement.align}
minWidth="88px"
margin="auto 0"
overflowWrap="anywhere"
>
{IS_MOBILE() && (
<Container className="mobile-table-header">
{props.headerElements[indexModificator + i]}
</Container>
)}
{props.iterableColumnElement.format
? props.iterableColumnElement.format(iterableElem)
: iterableElem}
</Body>
);
});
};
const rowRender = (elem) => {
let RowStyle = Body;
console.log(elem);
if (elem.submitter === props.user) RowStyle = Medium;
return props.staticColumnElements.map((elemName, i) => {
return (
<RowStyle
key={`leaderboard-static-elemName-${i}-${elem[elemName.name]}`}
as="td"
order={elemName.order}
textAlign={elemName.align}
margin="auto 0"
minWidth="88px"
overflowWrap="anywhere"
cursor="pointer"
// onClick={props.myEntries && (() => deleteSubmission(elem.id))}
>
{IS_MOBILE() && (
<Container className="mobile-table-header">
{props.headerElements[i]}
</Container>
)}
{elemName.format
? elemName.format(elem[elemName.name])
: elem[elemName.name]}
</RowStyle>
);
});
};
const desktopRender = () => {
const n = (props.pageNr - 1) * (ELEMENTS_PER_PAGE * 2);
let elementsToMap = props.elements.slice(n, n + ELEMENTS_PER_PAGE * 2);
if (elementsToMap.length > 0) {
return (
<TableStyle as="table" margin="32px 0 72px 0" width="100%">
<FlexColumn as="tbody" width="100%">
<Grid
as="tr"
gridGap="20px"
position="relative"
width="100%"
padding="0 6px"
minHeight="44px"
margin="0 0 6px 0"
gridTemplateColumns={props.gridTemplateColumns}
>
{props.headerElements.map((elem, i) => {
return (
<FlexRow
key={`table-header-${i}`}
alignmentX="flex-start"
as="td"
cursor="pointer"
onClick={() => {
if (activeIcon === i) {
let newRotateActiveIcon = !rotateActiveIcon;
setRotateActiveIcon(newRotateActiveIcon);
} else {
setRotateActiveIcon(false);
}
setActiveIcon(i);
props.sortByUpdate(elem, i);
forceUpdate();
}}
>
<Medium
cursor={elem !== '#' ? 'pointer' : ''}
textAlign={elem === 'when' ? 'right' : 'left'}
width={elem === 'when' ? '100%' : 'auto'}
padding="0 4px 0 0"
overflowWrap="anywhere"
minWidth="72px"
>
{elem.replace('.', ' ')}
</Medium>
{elem !== '#' && (
<ColumnFilterIcon
cursor="pointer"
index={i}
active={activeIcon}
rotateIcon={rotateActiveIcon}
/>
)}
</FlexRow>
);
})}
<TableLine
height="2px"
top="calc(100% + 2px)"
as="td"
shadow={theme.shadow}
/>
</Grid>
{elementsToMap.map((elem, index) => {
return (
<Grid
as="tr"
key={`leaderboard-row-${index}`}
backgroundColor={
index % 2 === 1 ? theme.colors.dark01 : 'transparent'
}
gridTemplateColumns={props.gridTemplateColumns}
gridGap="20px"
position="relative"
width="100%"
padding="4px"
minHeight="48px"
>
{rowRender(elem)}
{props.headerElements ? metricsRender(elem) : ''}
</Grid>
);
})}
</FlexColumn>
</TableStyle>
);
}
return <Medium margin="72px 0">No results ;c</Medium>;
};
const mobileRender = () => {
const n = (props.pageNr - 1) * ELEMENTS_PER_PAGE;
let elementsToMap = props.elements.slice(n, n + ELEMENTS_PER_PAGE);
if (elementsToMap.length > 0) {
return (
<MobileTableStyle
as="table"
staticColumnElements={props.staticColumnElements}
headerElements={props.headerElements}
>
<Container as="tbody">
{elementsToMap.map((elem, index) => {
return (
<Grid as="tr" key={`leaderboard-row-${index}`}>
{rowRender(elem)}
{props.headerElements ? metricsRender(elem) : ''}
</Grid>
);
})}
</Container>
</MobileTableStyle>
);
}
return <Medium margin="72px 0">No results ;c</Medium>;
};
return ( return (
<> <>
<Media query={theme.mobile}>{mobileRender()}</Media> <DeletePopUp
<Media query={theme.desktop}>{desktopRender()}</Media> item={itemToHandle}
setDeletePopUp={setDeletePopUp}
deletePopUp={deletePopUp}
setDeletedItems={setDeletedItems}
deletedItems={deletedItems}
popUpMessageHandler={popUpMessageHandler}
/>
<EditPopUp
item={itemToHandle}
setEditPopUp={setEditPopUp}
editPopUp={editPopUp}
popUpMessageHandler={popUpMessageHandler}
/>
<Media query={theme.mobile}>
<MobileTable
elements={itemsToRender}
deletePopUp={deletePopUp}
orderedKeys={orderedKeys}
rowFooter={rowFooter}
deletedItems={deletedItems}
popUpMessageHandler={popUpMessageHandler}
itemToHandle={itemToHandle}
sortByUpdate={sortByUpdate}
profileInfo={profileInfo}
tableUpdate={tableUpdate}
setItemToHandle={setItemToHandle}
setEditPopUp={setEditPopUp}
setDeletePopUp={setDeletePopUp}
setDeletedItems={setDeletedItems}
/>
</Media>
<Media query={theme.desktop}>
<DesktopTable
elements={itemsToRender}
deletePopUp={deletePopUp}
orderedKeys={orderedKeys}
rowFooter={rowFooter}
deletedItems={deletedItems}
popUpMessageHandler={popUpMessageHandler}
itemToHandle={itemToHandle}
sortByUpdate={sortByUpdate}
profileInfo={profileInfo}
tableUpdate={tableUpdate}
setItemToHandle={setItemToHandle}
setEditPopUp={setEditPopUp}
setDeletePopUp={setDeletePopUp}
setDeletedItems={setDeletedItems}
/>
</Media>
</> </>
); );
}; };

View File

@ -0,0 +1,74 @@
import { createPortal } from 'react-dom';
import PopUp from '../../../PopUp';
import Button from '../../../Button';
import { Medium, H3 } from '../../../../../utils/fonts';
import { FlexColumn, FlexRow } from '../../../../../utils/containers';
import theme from '../../../../../utils/theme';
import deleteSubmission from '../../../../../api/deleteSubmission';
const deleteItem = async (
item,
setDeletePopUp,
deletedItems,
setDeletedItems,
popUpMessageHandler
) => {
setDeletePopUp(false);
await deleteSubmission(
item,
deletedItems,
setDeletedItems,
popUpMessageHandler
);
};
const DeletePopUp = ({
deletePopUp,
setDeletePopUp,
item,
deletedItems,
setDeletedItems,
popUpMessageHandler,
}) => {
if (deletePopUp) {
return createPortal(
<PopUp
width="30%"
height="30vh"
padding="32px"
backgroundColor={theme.colors.dark003}
closeHandler={() => setDeletePopUp(false)}
>
<FlexColumn width="100%" height="100%" gap="48px">
<H3>Warning</H3>
<Medium>
Are you sure want to delete submission with id: {item.id}?
</Medium>
<FlexRow gap="48px">
<Button
handler={() =>
deleteItem(
item,
deletedItems,
setDeletedItems,
popUpMessageHandler
)
}
>
Yes
</Button>
<Button
handler={() => setDeletePopUp(false)}
backgroundColor={theme.colors.dark}
>
No
</Button>
</FlexRow>
</FlexColumn>
</PopUp>,
document.body
);
}
};
export default DeletePopUp;

View File

@ -0,0 +1 @@
export { default } from './DeletePopUp';

View File

@ -0,0 +1,48 @@
import React from 'react';
import TableStyle from '../../styles/TableStyle';
import TableHeader from '../TableHeader/TableHeader';
import TableRowItems from '../TableRowItems/TableRowItems';
import TableRowFooter from '../TableRowFooter/TableRowFooter';
import RowsBackgroundStyle from '../../styles/RowsBackgroundStyle';
const DesktopTable = (props) => {
return (
<TableStyle rowFooter={props.rowFooter}>
<tbody>
<TableHeader
orderedKeys={props.orderedKeys}
sortByUpdate={props.sortByUpdate}
tableUpdate={props.tableUpdate}
/>
{props.elements.map((item, i) => {
return (
<tr key={`table-row-${i}`} className="TableStyle__tr">
<TableRowItems
orderedKeys={props.orderedKeys}
item={item}
i={i}
/>
<TableRowFooter
deleteItem={() => {
props.setItemToHandle(item);
props.setDeletePopUp(true);
}}
editItem={() => {
props.setItemToHandle(item);
props.setEditPopUp(true);
}}
rowFooter={props.rowFooter}
profileInfo={props.profileInfo}
item={item}
i={i}
/>
<RowsBackgroundStyle i={i} as="td" />
</tr>
);
})}
</tbody>
</TableStyle>
);
};
export default DesktopTable;

View File

@ -0,0 +1 @@
export { default } from './DesktopTable';

View File

@ -0,0 +1,105 @@
import { createPortal } from 'react-dom';
import PopUp from '../../../PopUp';
import Button from '../../../Button';
import { H3 } from '../../../../../utils/fonts';
import { FlexColumn, FlexRow } from '../../../../../utils/containers';
import theme from '../../../../../utils/theme';
import SubmitInput from '../../../SubmitInput';
import TagsChoose from '../../../../../pages/Submit/components/TagsChoose/TagsChoose';
import editSubmission from '../../../../../api/editSubmission';
import getTags from '../../../../../api/getTags';
import React from 'react';
const editSubmissionHandler = async (
item,
setEditPopUp,
tagsToEdit,
description,
popUpMessageHandler
) => {
setEditPopUp(false);
let tags = '';
if (tagsToEdit) {
tags = tagsToEdit.join(',');
} else {
if (item?.tags) {
tags = item.tags.map((tag) => tag.name).join(',');
}
}
await editSubmission(item.id, tags, description, popUpMessageHandler);
};
const EditPopUp = ({ editPopUp, setEditPopUp, item, popUpMessageHandler }) => {
const [tags, setTags] = React.useState([]);
const [tagsToEdit, setTagsToEdit] = React.useState(item?.tags?.slice());
const [description, setDescription] = React.useState(
item?.description?.slice()
);
React.useMemo(() => {
getTags(setTags);
}, []);
if (editPopUp) {
return createPortal(
<PopUp
width="30%"
height="50vh"
padding="32px"
backgroundColor={theme.colors.dark003}
closeHandler={() => setEditPopUp(false)}
>
<FlexColumn width="100%" height="100%" gap="48px">
<H3>Editing submission</H3>
<SubmitInput
label="Description"
defaultValue={item.description}
handler={(value) => {
setDescription(value);
}}
/>
<TagsChoose
label="Submission tags"
updateTags={(submissionTags, globalTags) => {
setTagsToEdit(submissionTags);
setTags(globalTags);
}}
tags={tags ? tags : []}
submissionTags={tagsToEdit?.length ? tagsToEdit : item.tags}
/>
<FlexRow gap="48px">
<Button
width="100px"
height="32px"
handler={() =>
editSubmissionHandler(
item,
setEditPopUp,
tagsToEdit,
description,
popUpMessageHandler
)
}
>
Confirm
</Button>
<Button
width="100px"
height="32px"
handler={() => {
setTagsToEdit([]);
setEditPopUp(false);
}}
backgroundColor={theme.colors.dark}
>
Cancel
</Button>
</FlexRow>
</FlexColumn>
</PopUp>,
document.body
);
}
};
export default EditPopUp;

View File

@ -0,0 +1 @@
export { default } from './EditPopUp';

View File

@ -0,0 +1,33 @@
import React from 'react';
import TableRowItems from '../TableRowItems/TableRowItems';
import TableRowFooter from '../TableRowFooter/TableRowFooter';
import MobileTableStyle from './MobileTableStyle';
const MobileTable = (props) => {
return (
<MobileTableStyle as="table">
{props.elements.map((item, i) => {
return (
<tr key={`table-row-${i}`} className="TableStyle__tr">
<TableRowItems orderedKeys={props.orderedKeys} item={item} i={i} />
<TableRowFooter
deleteItem={() => {
props.setItemToHandle(item);
props.setDeletePopUp(true);
}}
editItem={() => {
props.setItemToHandle(item);
props.setEditPopUp(true);
}}
rowFooter={props.rowFooter}
item={item}
i={i}
/>
</tr>
);
})}
</MobileTableStyle>
);
};
export default MobileTable;

View File

@ -1,51 +1,43 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Container } from '../../../../utils/containers'; import { Container } from '../../../../../utils/containers';
const MobileTableStyle = styled(Container)` const MobileTableStyle = styled(Container)`
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 32px 0; margin: 32px 0;
tr:nth-of-type(odd) { tr:nth-of-type(odd) {
background: ${({ theme }) => theme.colors.dark03}; background: ${({ theme }) => theme.colors.dark03};
} }
th { th {
background: ${({ theme }) => theme.colors.dark05}; background: ${({ theme }) => theme.colors.dark05};
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
} }
td, td,
th { th {
padding: 6px; padding: 6px;
border: 1px solid ${({ theme }) => theme.colors.white}; border: 1px solid ${({ theme }) => theme.colors.white};
text-align: left; text-align: left;
} }
display: block;
thead, thead,
tbody, tbody,
th, th,
tr,
td { td {
display: block; display: block;
} }
thead tr { thead tr {
position: absolute; position: absolute;
top: -9999px; top: -9999px;
left: -9999px; left: -9999px;
} }
td { td {
border: none; border: none;
border-bottom: 1px solid ${({ theme }) => theme.colors.dark01}; border-bottom: 1px solid ${({ theme }) => theme.colors.dark01};
position: relative; position: relative;
padding-left: 50%; padding-left: 50%;
} }
.mobile-table-header { .mobile-table-header {
font-weight: 400; font-weight: 500;
position: absolute; position: absolute;
top: 6px; top: 6px;
left: 6px; left: 6px;
@ -53,6 +45,22 @@ const MobileTableStyle = styled(Container)`
padding-right: 10px; padding-right: 10px;
white-space: nowrap; white-space: nowrap;
} }
.TableStyle__row-footer {
padding: 0 8px;
min-height: 36px;
justify-content: space-between;
}
.TableStyle__tag {
color: ${({ theme }) => theme.colors.white};
background-color: ${({ theme }) => theme.colors.green08};
padding: 4px;
border-radius: 2px;
font-size: 12px;
font-weight: 600;
}
.TableStyle__tags-container {
gap: 4px;
}
`; `;
export default MobileTableStyle; export default MobileTableStyle;

View File

@ -0,0 +1 @@
export { default } from './MobileTable';

View File

@ -0,0 +1,50 @@
import React from 'react';
import { FlexRow } from '../../../../../utils/containers';
import ColumnFilterIcon from '../../../ColumnFilterIcon';
const TableHeader = (props) => {
const [activeIcon, setActiveIcon] = React.useState(null);
const [rotateActiveIcon, setRotateActiveIcon] = React.useState(false);
return (
<tr className="TableStyle__tr-header">
{props.orderedKeys.map((keyValue, i) => {
return (
<th
key={`table-header-${i}`}
className="TableStyle__th"
onClick={() => {
if (activeIcon === i) {
let newRotateActiveIcon = !rotateActiveIcon;
setRotateActiveIcon(newRotateActiveIcon);
} else {
setRotateActiveIcon(false);
}
setActiveIcon(i);
props.sortByUpdate(keyValue);
props.tableUpdate();
}}
>
<FlexRow as="span" alignmentX="flex-start" gap="8px" width="100%">
{keyValue}
<FlexRow
as="span"
className="TableStyle__sort-button"
column={keyValue}
>
<ColumnFilterIcon
index={i}
active={activeIcon}
rotateIcon={rotateActiveIcon}
/>
</FlexRow>
</FlexRow>
</th>
);
})}
<FlexRow className="TableStyle__line" as="td" />
</tr>
);
};
export default TableHeader;

View File

@ -0,0 +1 @@
export { default } from './TableHeader';

View File

@ -0,0 +1,33 @@
import React from 'react';
import { FlexRow, Svg } from '../../../../../utils/containers';
import theme from '../../../../../utils/theme';
const TableRowButtons = ({ buttons, i, active, buttonAccessMessage }) => {
const getButtonTitle = (defaultTitle) => {
if (buttonAccessMessage === 'default') {
return defaultTitle;
} else return buttonAccessMessage;
};
return (
<FlexRow gap="12px" position='relative'>
{buttons.map((button, j) => {
return (
<Svg
title={getButtonTitle(button.title)}
key={`table-item-button-${i}-${j}`}
onClick={active ? button.handler : null}
src={button.icon}
backgroundColor={active ? theme.colors.dark : theme.colors.dark05}
cursor={active ? 'pointer' : 'auto'}
size="cover"
width="16px"
height="16px"
/>
);
})}
</FlexRow>
);
};
export default TableRowButtons;

View File

@ -0,0 +1 @@
export { default } from './TableRowButtons';

View File

@ -0,0 +1,46 @@
import React from 'react';
import { FlexRow } from '../../../../../utils/containers';
import TableRowTags from '../TableRowTags/TableRowTags';
import TableRowButtons from '../TableRowButtons/TableRowButtons';
import pensilIco from '../../../../../assets/pencil_ico.svg';
import deleteIco from '../../../../../assets/delete_ico.svg';
import KeyCloakService from '../../../../../services/KeyCloakService';
const TableRowFooter = ({ rowFooter, item, i, deleteItem, editItem, profileInfo }) => {
const buttonsActive = () => {
if (!KeyCloakService.isLoggedIn()) return false;
else if (
profileInfo?.preferred_username !== item.submitter &&
profileInfo?.name !== item.submitter
) return false;
return true;
};
const getButtonAccessMessage = () => {
if (!KeyCloakService.isLoggedIn()) {
return "You must be logged in to use this option.";
} else if (profileInfo?.preferred_username !== item.submitter &&
profileInfo?.name !== item.submitter) {
return "You don't have permission to use this option.";
} return "default";
};
if (rowFooter) {
return (
<FlexRow className="TableStyle__row-footer">
<TableRowTags item={item} i={i} />
<TableRowButtons
buttons={[
{ title: "edit", icon: pensilIco, handler: () => editItem() },
{ title: "delete", icon: deleteIco, handler: () => deleteItem() },
]}
active={buttonsActive()}
buttonAccessMessage={getButtonAccessMessage()}
i={i}
/>
</FlexRow>
);
}
};
export default TableRowFooter;

View File

@ -0,0 +1 @@
export { default } from './TableRowFooter';

View File

@ -0,0 +1,37 @@
import React from 'react';
import {
RENDER_WHEN,
RENDER_METRIC_VALUE,
IS_MOBILE,
} from '../../../../../utils/globals';
import { Container } from '../../../../../utils/containers';
const TableRowItems = ({ orderedKeys, item, i }) => {
const renderValue = (keyValue) => {
if (keyValue === 'when') {
return RENDER_WHEN(item[keyValue]);
} else {
return RENDER_METRIC_VALUE(item[keyValue]);
}
};
return (
<>
{orderedKeys.map((keyValue, j) => {
return (
<td key={`table-item-${i}-${j}`} className="TableStyle__td">
{IS_MOBILE() && (
<Container as="span" className="mobile-table-header">
{keyValue}
</Container>
)}
{renderValue(keyValue)}
{keyValue === '#' && i + 1}
</td>
);
})}
</>
);
};
export default TableRowItems;

View File

@ -0,0 +1 @@
export { default } from './TableRowItems';

View File

@ -0,0 +1,12 @@
import renderTags from './renderTags';
import { FlexRow } from '../../../../../utils/containers';
const TableRowTags = ({ item, i }) => {
return (
<FlexRow as="span" className="TableStyle__tags-container">
{renderTags(item.tags)}
</FlexRow>
);
};
export default TableRowTags;

View File

@ -0,0 +1 @@
export { default } from './TableRowTags';

View File

@ -0,0 +1,15 @@
import { FlexRow } from '../../../../../utils/containers';
const renderTags = (tags, i) => {
if (tags && tags.length > 0) {
return tags.map((tag, j) => {
return (
<FlexRow className="TableStyle__tag" key={`submissionTag-${i}-${j}`}>
{tag.name}
</FlexRow>
);
});
}
};
export default renderTags;

View File

@ -0,0 +1,14 @@
import styled from 'styled-components';
import { FlexRow } from '../../../../utils/containers';
const RowsBackgroundStyle = styled(FlexRow)`
width: calc(100% + 12px);
position: absolute;
top: 0;
left: -6px;
height: 100%;
background-color: ${({ theme, i }) =>
i % 2 === 0 ? theme.colors.dark01 : 'transparent'};
`;
export default RowsBackgroundStyle;

View File

@ -1,14 +0,0 @@
import styled from 'styled-components';
import { FlexRow } from '../../../../utils/containers';
const TableLine = styled(FlexRow)`
position: absolute;
top: ${({ top }) => (top ? top : 'auto')};
bottom: ${({ bottom }) => (bottom ? bottom : 'auto')};
left: 0;
width: 100%;
background-color: ${({ theme }) => theme.colors.dark04};
height: ${({ height }) => (height ? height : '1px')};
`;
export default TableLine;

View File

@ -1,8 +1,66 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { FlexColumn } from '../../../../utils/containers';
const TableStyle = styled(FlexColumn)` const TableStyle = styled.table`
overflow-x: ${({ metrics }) => (metrics > 10 ? 'scroll' : 'auto')}; border-collapse: separate;
border-spacing: 12px 0;
width: 100%;
.TableStyle__th {
cursor: pointer;
* {
cursor: pointer;
}
}
.TableStyle__tr-header {
height: 48px;
position: relative;
}
.TableStyle__tr {
position: relative;
height: ${({ rowFooter }) => (rowFooter ? '72px' : 'auto')};
}
.TableStyle__td {
padding: ${({ rowFooter }) => (rowFooter ? '4px 0 32px 0' : '12px 0')};
margin: 0 0 0 2px;
min-width: 80px;
}
.TableStyle_line {
position: absolute;
top: 94%;
bottom: ${({ bottom }) => (bottom ? bottom : 'auto')};
left: -6px;
width: calc(100% + 12px);
background-color: ${({ theme }) => theme.colors.dark04};
height: 3px;
box-shadow: ${({ theme }) => theme.shadow};
}
.TableStyle__tag {
color: ${({ theme }) => theme.colors.white};
background-color: ${({ theme }) => theme.colors.green08};
padding: 4px;
border-radius: 2px;
font-size: 12px;
font-weight: 600;
}
.TableStyle__row-footer {
width: 100%;
justify-content: space-between;
position: absolute;
top: 55%;
left: 0;
z-index: 2;
}
.TableStyle__tags-container {
gap: 4px;
padding: 0 2px;
}
`; `;
export default TableStyle; export default TableStyle;

View File

@ -1,6 +1,12 @@
import React from 'react'; import React from 'react';
import {Container, FlexColumn, FlexRow, Svg, TransBack} from '../../utils/containers'; import {
import {Body, Medium} from '../../utils/fonts'; Container,
FlexColumn,
FlexRow,
Svg,
TransBack,
} from '../../utils/containers';
import { Body, Medium } from '../../utils/fonts';
import theme from '../../utils/theme'; import theme from '../../utils/theme';
import userIco from '../../assets/user_ico.svg'; import userIco from '../../assets/user_ico.svg';
import KeyCloakService from '../../services/KeyCloakService'; import KeyCloakService from '../../services/KeyCloakService';
@ -15,8 +21,8 @@ const LoggedBarStyle = styled(FlexColumn)`
right: 0; right: 0;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
background-color: ${({theme}) => theme.colors.white}; background-color: ${({ theme }) => theme.colors.white};
box-shadow: ${({theme}) => theme.shadow}; box-shadow: ${({ theme }) => theme.shadow};
z-index: 3; z-index: 3;
button { button {
@ -32,11 +38,11 @@ const LoggedBarStyle = styled(FlexColumn)`
&:hover { &:hover {
li { li {
color: ${({theme}) => theme.colors.green}; color: ${({ theme }) => theme.colors.green};
} }
div { div {
background-color: ${({theme}) => theme.colors.green}; background-color: ${({ theme }) => theme.colors.green};
} }
} }
@ -47,37 +53,61 @@ const LoggedBarStyle = styled(FlexColumn)`
`; `;
const LoggedBar = (props) => { const LoggedBar = (props) => {
return ( return (
<TransBack transition='transform' translateX={props.visible} <TransBack
onClick={props.loggedBarVisibleHandler} animTime='0.2s'> transition="transform"
<LoggedBarStyle onMouseEnter={props.loggedBarHoverTrue} translateX={props.visible}
onMouseLeave={props.loggedBarHoverFalse}> onClick={props.loggedBarVisibleHandler}
<FlexRow alignmentX='flex-start' alignmentY='flex-end' animTime="0.2s"
gap='16px' width='100%' padding='12px 16px'> >
<Svg src={userIco} width='32px' height='32px' backgroundColor={theme.colors.dark} size='cover'/> <LoggedBarStyle
<Medium as='p'> onMouseEnter={props.loggedBarHoverTrue}
{props.username} onMouseLeave={props.loggedBarHoverFalse}
</Medium> >
</FlexRow> <FlexRow
<Container width='90%' backgroundColor={theme.colors.dark05} height='1px'/> alignmentX="flex-start"
<FlexColumn as='ul' onClick={() => console.log('profile click')} alignmentY="flex-end"
gap='24px' padding='32px 24px' alignmentX='flex-start'> gap="16px"
<FlexRow as='button' gap='16px'> width="100%"
<Svg width='16px' height='16px' src={userIco} size='cover'/> padding="12px 16px"
<Body as='li'> >
Profile <Svg
</Body> src={userIco}
</FlexRow> width="32px"
<FlexRow as='button' onClick={props.visible === '0' ? KeyCloakService.doLogout : null} gap='16px'> height="32px"
<Svg width='16px' height='16px' src={loginIco} rotate='180deg'/> backgroundColor={theme.colors.dark}
<Body as='li'> size="cover"
Sign out />
</Body> <Medium as="p">{props.username}</Medium>
</FlexRow> </FlexRow>
</FlexColumn> <Container
</LoggedBarStyle> width="90%"
</TransBack> backgroundColor={theme.colors.dark05}
); height="1px"
/>
<FlexColumn
as="ul"
onClick={KeyCloakService.goToProfile}
gap="24px"
padding="32px 24px"
alignmentX="flex-start"
>
<FlexRow as="button" gap="16px">
<Svg width="16px" height="16px" src={userIco} size="cover" />
<Body as="li">Profile</Body>
</FlexRow>
<FlexRow
as="button"
onClick={props.visible === '0' ? KeyCloakService.doLogout : null}
gap="16px"
>
<Svg width="16px" height="16px" src={loginIco} rotate="180deg" />
<Body as="li">Sign out</Body>
</FlexRow>
</FlexColumn>
</LoggedBarStyle>
</TransBack>
);
}; };
export default LoggedBar; export default LoggedBar;

View File

@ -1,243 +1,169 @@
import React from 'react'; import React from 'react';
import theme from '../../utils/theme';
import Media from 'react-media';
import { FlexColumn } from '../../utils/containers'; import { FlexColumn } from '../../utils/containers';
import { H2 } from '../../utils/fonts'; import { H2 } from '../../utils/fonts';
import {
CALC_PAGES,
EVALUATIONS_FORMAT,
RENDER_WHEN,
IS_MOBILE,
} from '../../utils/globals';
import Loading from '../../components/generic/Loading';
import Pager from '../../components/generic/Pager'; import Pager from '../../components/generic/Pager';
import Table from '../../components/generic/Table/Table';
import Search from '../../components/generic/Search'; import Search from '../../components/generic/Search';
import allEntriesSearchQueryHandler from './allEntriesSearchQueryHandler'; import getEntries from '../../api/getEntries';
import getAllEntries from '../../api/getAllEntries'; import Table from '../../components/generic/Table';
import Loading from '../../components/generic/Loading';
import { CALC_PAGES, ELEMENTS_PER_PAGE } from '../../utils/globals';
import searchQueryHandler from './searchHandler';
import orderKeys from './orderKeys';
import KeyCloakService from '../../services/KeyCloakService';
const AllEntries = (props) => { const AllEntries = (props) => {
const [entriesFromApi, setEntriesFromApi] = React.useState([]);
const [entriesAll, setEntriesAll] = React.useState([]); const [entriesAll, setEntriesAll] = React.useState([]);
const [entries, setEntries] = React.useState([]); const [entries, setEntries] = React.useState([]);
const [pageNr, setPageNr] = React.useState(1); const [pageNr, setPageNr] = React.useState(1);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [idSorted, setIdSorted] = React.useState([]);
const [scoresSorted, setScoresSorted] = React.useState([]); const [scoresSorted, setScoresSorted] = React.useState([]);
const [submitterSorted, setSubmitterSorted] = React.useState(false); const [submitterSorted, setSubmitterSorted] = React.useState(false);
const [whenSorted, setWhenSorted] = React.useState(false); const [whenSorted, setWhenSorted] = React.useState(false);
const [profileInfo, setProfileInfo] = React.useState(null);
React.useEffect(() => { const getProfileInfo = () => {
if (props.challengeName) challengeDataRequest(props.challengeName); if (KeyCloakService.isLoggedIn()) {
KeyCloakService.getProfileInfo(setProfileInfo);
} else {
setProfileInfo(false);
}
};
React.useMemo(() => {
if (props.challengeName) {
getEntries(
'challenge-all-submissions',
props.challengeName,
[setEntries, setEntriesAll],
setLoading,
setScoresSorted
);
}
getProfileInfo();
}, [props.challengeName]); }, [props.challengeName]);
const challengeDataRequest = (challengeName) => { const sortByUpdate = React.useCallback(
getAllEntries(challengeName, setEntriesFromApi, setEntriesAll); (elem) => {
getAllEntries( let newEntries = entries.slice();
challengeName, const possibleMetrics = orderKeys(entries[0]).filter(
undefined, (key) => !['id', 'submitter', 'when'].includes(key)
setEntries, );
setLoading, let metricIndex = possibleMetrics.indexOf(elem);
setScoresSorted let newScoresSorted = scoresSorted.slice();
); switch (elem) {
}; case 'id':
if (idSorted) {
const getPossibleMetrics = () => { setIdSorted(false);
let metrics = []; newEntries = newEntries.sort((a, b) =>
if (entriesFromApi.tests) { a.id > b.id ? 1 : b.id > a.id ? -1 : 0
for (let test of entriesFromApi.tests) { );
let myEval = `${test.metric}.${test.name}`; } else {
if (myEval && !metrics.includes(myEval)) { setIdSorted(true);
metrics.push(myEval); newEntries = newEntries.sort((a, b) =>
} a.id < b.id ? 1 : b.id < a.id ? -1 : 0
);
}
break;
case 'submitter':
if (submitterSorted) {
setSubmitterSorted(false);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() < b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() < a.submitter.toLowerCase()
? -1
: 0
);
} else {
setSubmitterSorted(true);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() > b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() > a.submitter.toLowerCase()
? -1
: 0
);
}
break;
case 'when':
if (whenSorted) {
setWhenSorted(false);
newEntries = newEntries.sort((a, b) =>
a.when < b.when ? 1 : b.when < a.when ? -1 : 0
);
} else {
setWhenSorted(true);
newEntries = newEntries.sort((a, b) =>
a.when > b.when ? 1 : b.when > a.when ? -1 : 0
);
}
break;
default:
if (scoresSorted[metricIndex]) {
newEntries = newEntries.sort(
(a, b) => (b ? b[elem] : -1) - (a ? a[elem] : -1)
);
newScoresSorted[metricIndex] = false;
setScoresSorted(newScoresSorted);
} else {
newEntries = newEntries.sort(
(a, b) => (a ? a[elem] : -1) - (b ? b[elem] : -1)
);
newScoresSorted[metricIndex] = true;
setScoresSorted(newScoresSorted);
}
break;
} }
} setEntries(newEntries);
return metrics; },
}; [entries, idSorted, scoresSorted, submitterSorted, whenSorted]
);
const getAllEntriesHeader = () => { const n = (pageNr - 1) * (ELEMENTS_PER_PAGE * 2);
let header = ['#', 'submitter']; const elements = entries.slice(n, n + ELEMENTS_PER_PAGE * 2);
if (IS_MOBILE()) header.push('when');
for (let metric of getPossibleMetrics()) {
header.push(metric);
}
if (!IS_MOBILE()) header.push('when');
return header;
};
const searchQueryHandler = (event) => {
allEntriesSearchQueryHandler(event, entriesAll, setPageNr, setEntries);
};
const sortByUpdate = (elem, i) => {
let newEntries = entries;
switch (elem) {
case '#':
break;
case 'submitter':
if (submitterSorted) {
setSubmitterSorted(false);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() < b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() < a.submitter.toLowerCase()
? -1
: 0
);
} else {
setSubmitterSorted(true);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() > b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() > a.submitter.toLowerCase()
? -1
: 0
);
}
break;
case 'when':
if (whenSorted) {
setWhenSorted(false);
newEntries = newEntries.sort((a, b) =>
a.when < b.when ? 1 : b.when < a.when ? -1 : 0
);
} else {
setWhenSorted(true);
newEntries = newEntries.sort((a, b) =>
a.when > b.when ? 1 : b.when > a.when ? -1 : 0
);
}
break;
default:
// eslint-disable-next-line no-case-declarations
let metricIndex = getPossibleMetrics().indexOf(elem);
// eslint-disable-next-line no-case-declarations
let newScoresSorted = scoresSorted;
if (scoresSorted[metricIndex]) {
newEntries = newEntries.sort(
(a, b) =>
(b.evaluations ? b.evaluations[elem] : -1) -
(a.evaluations ? a.evaluations[elem] : -1)
);
newScoresSorted[metricIndex] = false;
setScoresSorted(newScoresSorted);
} else {
newEntries = newEntries.sort(
(a, b) =>
(a.evaluations ? a.evaluations[elem] : -1) -
(b.evaluations ? b.evaluations[elem] : -1)
);
newScoresSorted[metricIndex] = true;
setScoresSorted(newScoresSorted);
}
break;
}
setEntries(newEntries);
};
const mobileRender = () => {
return (
<FlexColumn padding="24px 12px" width="70%" as="section" id="start">
<H2 as="h2" margin="0 0 12px 0">
All Entries
</H2>
{!loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getAllEntriesHeader()}
possibleMetrics={getPossibleMetrics()}
tableType="allEntries"
gridTemplateColumns={
'1fr ' + '4fr '.repeat(getAllEntriesHeader().length - 1)
}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'submitter', format: null, order: 2, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 5, align: 'right' },
]}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 3,
align: 'left',
}}
pageNr={pageNr}
elements={entries}
sortByUpdate={sortByUpdate}
/>
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="48px"
borderRadius="64px"
pages={CALC_PAGES(entries)}
number={`${pageNr} / ${CALC_PAGES(entries)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
const desktopRender = () => {
return (
<FlexColumn padding="24px" as="section" width="100%" maxWidth="1600px">
<H2 as="h2" margin="0 0 32px 0">
All Entries
</H2>
{!loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getAllEntriesHeader()}
possibleMetrics={getPossibleMetrics()}
gridTemplateColumns={
'1fr 3fr ' + '3fr '.repeat(getPossibleMetrics().length) + ' 3fr'
}
user={props.user}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'submitter', format: null, order: 2, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 5, align: 'right' },
]}
metrics={getPossibleMetrics()}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 3,
align: 'left',
}}
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
sortByUpdate={sortByUpdate}
/>
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="72px"
borderRadius="64px"
pages={CALC_PAGES(entries, 2)}
number={`${pageNr} / ${CALC_PAGES(entries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
return ( return (
<> <FlexColumn
<Media query={theme.mobile}>{mobileRender()}</Media> as="section"
<Media query={theme.desktop}>{desktopRender()}</Media> padding="24px"
</> gap="32px"
width="100%"
maxWidth="1600px"
>
<H2 as="h2">All Entries</H2>
{!loading && (profileInfo !== null) ? (
<>
<Search
searchQueryHandler={(event) =>
searchQueryHandler(event, entriesAll, setPageNr, setEntries)
}
/>
{elements.length > 0 && entries[0] && (
<div style={{ width: '100%', overflowX: 'auto' }}>
<Table
items={elements}
orderedKeys={orderKeys(entries[0])}
sortByUpdate={sortByUpdate}
popUpMessageHandler={props.popUpMessageHandler}
profileInfo={profileInfo}
/>
</div>
)}
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="72px"
borderRadius="64px"
pages={CALC_PAGES(entries, 2)}
number={`${pageNr} / ${CALC_PAGES(entries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
); );
}; };

View File

@ -1,32 +0,0 @@
const allEntriesSearchQueryHandler = (
event,
entriesFromApi,
setPageNr,
setEntries
) => {
let searchQuery = event.target.value;
let submissionsToRender = [];
setPageNr(1);
if (searchQuery === '') setEntries(entriesFromApi);
else {
for (let entry of entriesFromApi) {
const { id, when, submitter } = entry;
console.log(entry);
let evaluations = '';
if (entry.evaluations) {
for (let evaluation of Object.values(entry.evaluations)) {
evaluations += ` ${evaluation}`;
}
}
const str = `${id} ${submitter} ${when.slice(11, 16)} ${when.slice(
0,
10
)} ${evaluations}`;
if (str.toLowerCase().includes(searchQuery.toLowerCase()))
submissionsToRender.push(entry);
}
setEntries(submissionsToRender);
}
};
export default allEntriesSearchQueryHandler;

View File

@ -0,0 +1,27 @@
const orderKeys = (elem) => {
if (elem) {
let result = ['id', 'submitter', 'description'];
const elemKeys = Object.keys(elem);
const dev0keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-0')
.sort();
const dev1keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-1')
.sort();
const testAkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-A')
.sort();
const testBkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-B')
.sort();
result = result.concat(dev0keys);
result = result.concat(dev1keys);
result = result.concat(testAkeys);
result = result.concat(testBkeys);
result.push('when');
return result;
}
return null;
};
export default orderKeys;

View File

@ -0,0 +1,28 @@
const searchQueryHandler = (event, entriesAll, setPageNr, setEntries) => {
let searchQuery = event.target.value;
let submissionsToRender = [];
setPageNr(1);
if (searchQuery === '') setEntries(entriesAll);
else {
for (let entry of entriesAll) {
let { when, tags } = entry;
tags = Object.values(tags)
.map((tag) => tag.name)
.join(' ');
const otherKeys = Object.values(entry)
.join(' ')
.replaceAll(-999999999, 'N/A')
.replaceAll('[object Object]', '')
.replaceAll(',', '');
const str = `${when.slice(11, 16)} ${when.slice(
0,
10
)} ${otherKeys} ${tags}`;
if (str.toLowerCase().includes(searchQuery.toLowerCase()))
submissionsToRender.push(entry);
}
setEntries(submissionsToRender);
}
};
export default searchQueryHandler;

View File

@ -33,7 +33,7 @@ const Challenges = () => {
statusFilterHandle(state.statusFilter, state.challenges, dispatch); statusFilterHandle(state.statusFilter, state.challenges, dispatch);
}, [state.statusFilter, state.challenges]); }, [state.statusFilter, state.challenges]);
const setPage = React.useCallback((value) => { const setPageNr = React.useCallback((value) => {
dispatch({ type: CHALLENGES_ACTION.SET_PAGE, payload: value }); dispatch({ type: CHALLENGES_ACTION.SET_PAGE, payload: value });
}, []); }, []);
@ -73,7 +73,7 @@ const Challenges = () => {
dispatch={dispatch} dispatch={dispatch}
filtersMenuRender={filtersMenuRender} filtersMenuRender={filtersMenuRender}
searchQueryHandler={searchQueryHandler} searchQueryHandler={searchQueryHandler}
setPage={setPage} setPageNr={setPageNr}
filtersMenu={state.filtersMenu} filtersMenu={state.filtersMenu}
loading={state.loading} loading={state.loading}
pageNr={state.pageNr} pageNr={state.pageNr}
@ -85,7 +85,7 @@ const Challenges = () => {
dispatch={dispatch} dispatch={dispatch}
filtersMenuRender={filtersMenuRender} filtersMenuRender={filtersMenuRender}
searchQueryHandler={searchQueryHandler} searchQueryHandler={searchQueryHandler}
setPage={setPage} setPageNr={setPageNr}
filtersMenu={state.filtersMenu} filtersMenu={state.filtersMenu}
loading={state.loading} loading={state.loading}
pageNr={state.pageNr} pageNr={state.pageNr}

View File

@ -35,7 +35,7 @@ const ChallengesDesktop = (props) => {
{!props.loading && ( {!props.loading && (
<Pager <Pager
pageNr={props.pageNr} pageNr={props.pageNr}
setPage={props.setPage} setPageNr={props.setPageNr}
elements={props.challengesFiltered} elements={props.challengesFiltered}
pages={CALC_PAGES(props.challengesFiltered)} pages={CALC_PAGES(props.challengesFiltered)}
width="72px" width="72px"

View File

@ -4,10 +4,10 @@ import { useParams } from 'react-router-dom';
import { H1, Medium } from '../../utils/fonts'; import { H1, Medium } from '../../utils/fonts';
import theme from '../../utils/theme'; import theme from '../../utils/theme';
import MobileChallengeMenu from './components/MobileChallengeMenu'; import MobileChallengeMenu from './components/MobileChallengeMenu';
import Leaderboard from '../Leaderboard/Leaderboard'; import Leaderboard from '../Leaderboard';
import Readme from '../Readme'; import Readme from '../Readme';
import HowTo from '../HowTo/HowTo'; import HowTo from '../HowTo';
import MyEntries from '../MyEntries/MyEntries'; import MyEntries from '../MyEntries';
import Submit from '../Submit'; import Submit from '../Submit';
import Media from 'react-media'; import Media from 'react-media';
import DesktopChallengeMenu from './components/DesktopChallengeMenu'; import DesktopChallengeMenu from './components/DesktopChallengeMenu';
@ -15,18 +15,15 @@ import { CHALLENGE_SECTIONS, RENDER_ICO } from '../../utils/globals';
import textIco from '../../assets/text_ico.svg'; import textIco from '../../assets/text_ico.svg';
import getChallengeInfo from '../../api/getChallengeInfo'; import getChallengeInfo from '../../api/getChallengeInfo';
import Loading from '../../components/generic/Loading'; import Loading from '../../components/generic/Loading';
import getUser from '../../api/getUser'; import AllEntries from '../AllEntries';
import AllEntries from '../AllEntries/AllEntries';
const Challenge = (props) => { const Challenge = (props) => {
const challengeName = useParams().challengeId; const challengeName = useParams().challengeId;
const [challenge, setChallenge] = React.useState([]); const [challenge, setChallenge] = React.useState([]);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [user, setUser] = React.useState('');
React.useEffect(() => { React.useEffect(() => {
getChallengeInfo(setChallenge, setLoading, challengeName); getChallengeInfo(setChallenge, setLoading, challengeName);
getUser(setUser);
}, [challengeName]); }, [challengeName]);
const sectionRender = () => { const sectionRender = () => {
@ -36,12 +33,15 @@ const Challenge = (props) => {
<Leaderboard <Leaderboard
challengeName={challengeName} challengeName={challengeName}
mainMetric={challenge.mainMetric} mainMetric={challenge.mainMetric}
user={user}
/> />
); );
case CHALLENGE_SECTIONS.ALL_ENTRIES: case CHALLENGE_SECTIONS.ALL_ENTRIES:
return ( return (
<AllEntries challengeName={challengeName} setLoading={setLoading} /> <AllEntries
challengeName={challengeName}
setLoading={setLoading}
popUpMessageHandler={props.popUpMessageHandler}
/>
); );
case CHALLENGE_SECTIONS.README: case CHALLENGE_SECTIONS.README:
return ( return (
@ -57,11 +57,15 @@ const Challenge = (props) => {
<HowTo <HowTo
popUpMessageHandler={props.popUpMessageHandler} popUpMessageHandler={props.popUpMessageHandler}
challengeName={challengeName} challengeName={challengeName}
user={user}
/> />
); );
case CHALLENGE_SECTIONS.MY_ENTRIES: case CHALLENGE_SECTIONS.MY_ENTRIES:
return <MyEntries challengeName={challengeName} />; return (
<MyEntries
challengeName={challengeName}
popUpMessageHandler={props.popUpMessageHandler}
/>
);
case CHALLENGE_SECTIONS.SUBMIT: case CHALLENGE_SECTIONS.SUBMIT:
return <Submit challengeName={challengeName} setLoading={setLoading} />; return <Submit challengeName={challengeName} setLoading={setLoading} />;
default: default:

View File

@ -7,17 +7,19 @@ import HowToContent from './components/HowToContent';
const HowTo = (props) => { const HowTo = (props) => {
const [userFullInfo, setUserFullInfo] = React.useState(null); const [userFullInfo, setUserFullInfo] = React.useState(null);
const username = KeyCloakService.getUsername();
React.useEffect(() => { React.useMemo(async () => {
getFullUser(setUserFullInfo); await getFullUser(setUserFullInfo);
setTimeout(() => {
if (!KeyCloakService.isLoggedIn()) { if (!KeyCloakService.isLoggedIn()) {
props.popUpMessageHandler( props.popUpMessageHandler(
'Please log in', 'Please log in',
'To see everything you must log in', 'To see everything you must log in',
() => KeyCloakService.doLogin () => KeyCloakService.doLogin
); );
} }
}, 1000);
}, [props]); }, [props]);
return ( return (
@ -31,7 +33,7 @@ const HowTo = (props) => {
<FlexColumn maxWidth="680px" alignmentX="flex-start" gap="48px"> <FlexColumn maxWidth="680px" alignmentX="flex-start" gap="48px">
<HowToContent <HowToContent
userFullInfo={userFullInfo} userFullInfo={userFullInfo}
user={props.user ? props.user : 'yourID'} user={username ? username : 'yourID'}
challengeName={props.challengeName} challengeName={props.challengeName}
/> />
</FlexColumn> </FlexColumn>

View File

@ -1,20 +1,15 @@
import React from 'react'; import React from 'react';
import Media from 'react-media';
import theme from '../../utils/theme';
import { FlexColumn } from '../../utils/containers'; import { FlexColumn } from '../../utils/containers';
import { H2 } from '../../utils/fonts'; import { H2 } from '../../utils/fonts';
import Table from '../../components/generic/Table/Table'; import Table from '../../components/generic/Table/Table';
import PropsTypes from 'prop-types';
import getChallengeLeaderboard from '../../api/getChallengeLeaderboard'; import getChallengeLeaderboard from '../../api/getChallengeLeaderboard';
import leaderboardSearchQueryHandler from './leaderboardSearchQueryHandler'; import leaderboardSearchQueryHandler from './leaderboardSearchQueryHandler';
import { import { CALC_PAGES } from '../../utils/globals';
CALC_PAGES,
EVALUATIONS_FORMAT,
RENDER_WHEN,
} from '../../utils/globals';
import Search from '../../components/generic/Search'; import Search from '../../components/generic/Search';
import Pager from '../../components/generic/Pager'; import Pager from '../../components/generic/Pager';
import Loading from '../../components/generic/Loading'; import Loading from '../../components/generic/Loading';
import orderKeys from './orderKeys';
import { ELEMENTS_PER_PAGE } from '../../utils/globals';
const Leaderboard = (props) => { const Leaderboard = (props) => {
const [entriesFromApi, setEntriesFromApi] = React.useState([]); const [entriesFromApi, setEntriesFromApi] = React.useState([]);
@ -26,282 +21,186 @@ const Leaderboard = (props) => {
const [entriesSorted, setEntriesSorted] = React.useState(false); const [entriesSorted, setEntriesSorted] = React.useState(false);
const [whenSorted, setWhenSorted] = React.useState(false); const [whenSorted, setWhenSorted] = React.useState(false);
const [scoresSorted, setScoresSorted] = React.useState([]); const [scoresSorted, setScoresSorted] = React.useState([]);
const [idSorted, setIdSorted] = React.useState([]);
React.useEffect(() => { React.useMemo(() => {
challengeDataRequest(props.challengeName); getChallengeLeaderboard(
'leaderboard',
props.challengeName,
[setEntries, setEntriesFromApi],
setLoading,
setScoresSorted
);
}, [props.challengeName]); }, [props.challengeName]);
const challengeDataRequest = (challengeName) => {
getChallengeLeaderboard(setEntriesFromApi, challengeName);
getChallengeLeaderboard(setEntries, challengeName, setLoading);
};
const getMetricIndex = (metricName) => {
let i = 0;
for (let evaluation of entriesFromApi[0].evaluations) {
if (`${evaluation.test.metric}.${evaluation.test.name}` === metricName) {
return i;
}
i++;
}
};
const searchQueryHandler = (event) => { const searchQueryHandler = (event) => {
leaderboardSearchQueryHandler(event, entriesFromApi, setPageNr, setEntries); leaderboardSearchQueryHandler(event, entriesFromApi, setPageNr, setEntries);
}; };
const getPossibleMetrics = () => { const sortByUpdate = React.useCallback(
let metrics = []; (elem) => {
for (let entry of entriesFromApi) { let newEntries = entries.slice();
for (let evaluation of entry.evaluations) { const possibleMetrics = orderKeys(entries[0]).filter(
let metric = evaluation.test.metric; (key) =>
let name = evaluation.test.name; !['id', 'submitter', 'when', 'description', 'times'].includes(key)
if (metric && !metrics.includes(`${metric}.${name}`)) { );
metrics.push(`${metric}.${name}`); let metricIndex = possibleMetrics.indexOf(elem);
} let newScoresSorted = scoresSorted.slice();
switch (elem) {
case 'id':
if (idSorted) {
setIdSorted(false);
newEntries = newEntries.sort((a, b) =>
a.id > b.id ? 1 : b.id > a.id ? -1 : 0
);
} else {
setIdSorted(true);
newEntries = newEntries.sort((a, b) =>
a.id < b.id ? 1 : b.id < a.id ? -1 : 0
);
}
break;
case 'submitter':
if (submitterSorted) {
setSubmitterSorted(false);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() < b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() < a.submitter.toLowerCase()
? -1
: 0
);
} else {
setSubmitterSorted(true);
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() > b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() > a.submitter.toLowerCase()
? -1
: 0
);
}
break;
case 'description':
if (descriptionSorted) {
newEntries = newEntries.sort((a, b) =>
a.description.toLowerCase() < b.description.toLowerCase()
? 1
: b.description.toLowerCase() < a.description.toLowerCase()
? -1
: 0
);
setDescriptionSorted(false);
} else {
newEntries = newEntries.sort((a, b) =>
a.description.toLowerCase() > b.description.toLowerCase()
? 1
: b.description.toLowerCase() > a.description.toLowerCase()
? -1
: 0
);
setDescriptionSorted(true);
}
break;
case 'times':
if (entriesSorted) {
newEntries = newEntries.sort((a, b) =>
a.times > b.times ? 1 : b.times > a.times ? -1 : 0
);
setEntriesSorted(false);
} else {
newEntries = newEntries.sort((a, b) =>
a.times < b.times ? 1 : b.times < a.times ? -1 : 0
);
setEntriesSorted(true);
}
break;
case 'when':
if (whenSorted) {
setWhenSorted(false);
newEntries = newEntries.sort((a, b) =>
a.when < b.when ? 1 : b.when < a.when ? -1 : 0
);
} else {
setWhenSorted(true);
newEntries = newEntries.sort((a, b) =>
a.when > b.when ? 1 : b.when > a.when ? -1 : 0
);
}
break;
default:
if (scoresSorted[metricIndex]) {
newEntries = newEntries.sort(
(a, b) => (b ? b[elem] : -1) - (a ? a[elem] : -1)
);
newScoresSorted[metricIndex] = false;
setScoresSorted(newScoresSorted);
} else {
newEntries = newEntries.sort(
(a, b) => (a ? a[elem] : -1) - (b ? b[elem] : -1)
);
newScoresSorted[metricIndex] = true;
setScoresSorted(newScoresSorted);
}
break;
} }
} setEntries(newEntries);
return metrics; },
}; [
descriptionSorted,
entries,
entriesSorted,
idSorted,
scoresSorted,
submitterSorted,
whenSorted,
]
);
const getLeaderboardHeader = () => { const n = (pageNr - 1) * (ELEMENTS_PER_PAGE * 2);
let header = ['#', 'submitter', 'description']; const elements = entries.slice(n, n + ELEMENTS_PER_PAGE * 2);
for (let metric of getPossibleMetrics()) {
header.push(metric);
}
header.push('entries');
header.push('when');
return header;
};
const getLeaderboardHeaderMobile = () => {
let header = ['#', 'submitter', 'description', 'entries', 'when'];
for (let metric of getPossibleMetrics()) {
header.push(metric);
}
return header;
};
const sortByUpdate = (elem) => {
let metricIndex = 0;
let newEntries = entries;
switch (elem) {
case 'submitter':
if (submitterSorted) {
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() < b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() < a.submitter.toLowerCase()
? -1
: 0
);
setSubmitterSorted(false);
} else {
newEntries = newEntries.sort((a, b) =>
a.submitter.toLowerCase() > b.submitter.toLowerCase()
? 1
: b.submitter.toLowerCase() > a.submitter.toLowerCase()
? -1
: 0
);
setSubmitterSorted(true);
}
break;
case 'description':
if (descriptionSorted) {
newEntries = newEntries.sort((a, b) =>
a.description.toLowerCase() < b.description.toLowerCase()
? 1
: b.description.toLowerCase() < a.description.toLowerCase()
? -1
: 0
);
setDescriptionSorted(false);
} else {
newEntries = newEntries.sort((a, b) =>
a.description.toLowerCase() > b.description.toLowerCase()
? 1
: b.description.toLowerCase() > a.description.toLowerCase()
? -1
: 0
);
setDescriptionSorted(true);
}
break;
case 'entries':
if (entriesSorted) {
newEntries = newEntries.sort((a, b) => b.times - a.times);
setEntriesSorted(false);
} else {
newEntries = newEntries.sort((a, b) => a.times - b.times);
setEntriesSorted(true);
}
break;
case 'when':
if (whenSorted) {
newEntries = newEntries.sort((a, b) =>
a.when < b.when ? 1 : b.when < a.when ? -1 : 0
);
setWhenSorted(false);
} else {
newEntries = newEntries.sort((a, b) =>
a.when > b.when ? 1 : b.when > a.when ? -1 : 0
);
setWhenSorted(true);
}
break;
default:
metricIndex = getMetricIndex(elem);
// eslint-disable-next-line no-case-declarations
let newScoresSorted = scoresSorted;
if (scoresSorted[metricIndex]) {
newEntries = newEntries.sort(
(a, b) =>
b.evaluations[metricIndex].score -
a.evaluations[metricIndex].score
);
newScoresSorted[metricIndex] = false;
setScoresSorted(newScoresSorted);
} else {
newEntries = newEntries.sort(
(a, b) =>
a.evaluations[metricIndex].score -
b.evaluations[metricIndex].score
);
newScoresSorted[metricIndex] = true;
setScoresSorted(newScoresSorted);
}
break;
}
setEntries(newEntries);
};
const mobileRender = () => {
return (
<FlexColumn padding="24px 12px" width="70%" as="section" id="start">
<H2 as="h2" margin="0 0 12px 0">
Leaderboard
</H2>
{!loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getLeaderboardHeaderMobile()}
tableType="leaderboard"
gridTemplateColumns={
entries[0]
? '1fr 2fr 3fr ' +
'2fr '.repeat(entries[0].evaluations.length) +
'1fr 2fr'
: ''
}
user={props.user}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'submitter', format: null, order: 2, align: 'left' },
{ name: 'description', format: null, order: 3, align: 'left' },
{ name: 'times', format: null, order: 4, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 5, align: 'right' },
]}
metrics={getPossibleMetrics()}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 3,
align: 'left',
}}
pageNr={pageNr}
elements={entries}
sortByUpdate={sortByUpdate}
/>
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="48px"
borderRadius="64px"
pages={CALC_PAGES(entries)}
number={`${pageNr} / ${CALC_PAGES(entries)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
const desktopRender = () => {
return (
<FlexColumn padding="24px" as="section" width="100%" maxWidth="1600px">
<H2 as="h2" margin="0 0 32px 0">
Leaderboard
</H2>
{!loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getLeaderboardHeader()}
gridTemplateColumns={
entries[0]
? '1fr 2fr 3fr ' +
'2fr '.repeat(entries[0].evaluations.length) +
'1fr 2fr'
: ''
}
user={props.user}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'submitter', format: null, order: 2, align: 'left' },
{ name: 'description', format: null, order: 3, align: 'left' },
{ name: 'times', format: null, order: 4, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 5, align: 'right' },
]}
metrics={getPossibleMetrics()}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 3,
align: 'left',
}}
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
sortByUpdate={sortByUpdate}
/>
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="72px"
borderRadius="64px"
pages={CALC_PAGES(entries, 2)}
number={`${pageNr} / ${CALC_PAGES(entries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
return ( return (
<> <FlexColumn
<Media query={theme.mobile}>{mobileRender()}</Media> padding="24px"
<Media query={theme.desktop}>{desktopRender()}</Media> gap="32px"
</> as="section"
width="100%"
maxWidth="1600px"
>
<H2 as="h2">Leaderboard</H2>
{!loading ? (
<>
<Search
searchQueryHandler={(event) =>
searchQueryHandler(event, entries, setPageNr, setEntries)
}
/>
{elements.length > 0 && entries[0] && (
<div style={{ width: '100%', overflowX: 'auto' }}>
<Table
items={elements}
orderedKeys={orderKeys(entries[0])}
sortByUpdate={sortByUpdate}
rowFooter={false}
/>
</div>
)}
<Pager
pageNr={pageNr}
elements={entries}
setPageNr={setPageNr}
width="72px"
borderRadius="64px"
pages={CALC_PAGES(entries, 2)}
number={`${pageNr} / ${CALC_PAGES(entries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
); );
}; };
Leaderboard.propsTypes = {
challengeName: PropsTypes.string,
};
Leaderboard.defaultProps = {
challengeName: '',
};
export default Leaderboard; export default Leaderboard;

View File

@ -0,0 +1,27 @@
const orderKeys = (elem) => {
if (elem) {
let result = ['#', 'submitter', 'description'];
const elemKeys = Object.keys(elem);
const dev0keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-0')
.sort();
const dev1keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-1')
.sort();
const testAkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-A')
.sort();
const testBkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-B')
.sort();
result = result.concat(dev0keys);
result = result.concat(dev1keys);
result = result.concat(testAkeys);
result = result.concat(testBkeys);
result.push('times', 'when');
return result;
}
return null;
};
export default orderKeys;

View File

@ -1,220 +1,135 @@
import React from 'react'; import React from 'react';
import { FlexColumn } from '../../utils/containers'; import { FlexColumn } from '../../utils/containers';
import { H2 } from '../../utils/fonts'; import { H2 } from '../../utils/fonts';
import getMyEntries from '../../api/getMyEntries';
import Pager from '../../components/generic/Pager'; import Pager from '../../components/generic/Pager';
import { import { CALC_PAGES } from '../../utils/globals';
CALC_PAGES,
EVALUATIONS_FORMAT,
IS_MOBILE,
RENDER_WHEN,
} from '../../utils/globals';
import Media from 'react-media';
import theme from '../../utils/theme';
import Loading from '../../components/generic/Loading'; import Loading from '../../components/generic/Loading';
import Table from '../../components/generic/Table/Table'; import Table from '../../components/generic/Table/Table';
import myEntriesSearchQueryHandler from './myEntriesSearchQueryHandler';
import Search from '../../components/generic/Search'; import Search from '../../components/generic/Search';
import orderKeys from './orderKeys';
import { ELEMENTS_PER_PAGE } from '../../utils/globals';
import getEntries from '../../api/getEntries';
import searchHandler from './searchHandler';
const MyEntries = (props) => { const MyEntries = (props) => {
const [myEntriesFromAPI, setMyEntriesFromAPI] = React.useState({}); // const [myEntriesFromAPI, setMyEntriesFromAPI] = React.useState({});
const [myEntriesAll, setMyEntriesAll] = React.useState({}); const [myEntriesAll, setMyEntriesAll] = React.useState([]);
const [myEntries, setMyEntries] = React.useState({}); const [myEntries, setMyEntries] = React.useState([]);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [pageNr, setPageNr] = React.useState(1); const [pageNr, setPageNr] = React.useState(1);
const [idSorted, setIdSorted] = React.useState([]);
const [whenSorted, setWhenSorted] = React.useState(false); const [whenSorted, setWhenSorted] = React.useState(false);
const [scoresSorted, setScoresSorted] = React.useState([]); const [scoresSorted, setScoresSorted] = React.useState([]);
React.useEffect(() => { React.useMemo(() => {
challengesRequest(); getEntries(
// eslint-disable-next-line 'challenge-my-submissions',
}, []);
const searchQueryHandler = (event) => {
myEntriesSearchQueryHandler(event, myEntriesAll, setPageNr, setMyEntries);
};
const getPossibleMetrics = () => {
let metrics = [];
for (let test of myEntriesFromAPI.tests) {
let myEval = `${test.metric}.${test.name}`;
if (myEval && !metrics.includes(myEval)) {
metrics.push(myEval);
}
}
return metrics;
};
const getMyEntriesHeader = () => {
let header = ['#'];
if (IS_MOBILE()) header.push('when');
for (let myEval of getPossibleMetrics()) {
header.push(myEval);
}
if (!IS_MOBILE()) header.push('when');
return header;
};
const challengesRequest = () => {
getMyEntries(
props.challengeName, props.challengeName,
setMyEntriesFromAPI, [setMyEntries, setMyEntriesAll],
setMyEntriesAll,
setMyEntries,
setLoading, setLoading,
setScoresSorted setScoresSorted
); );
}; }, [props.challengeName]);
const sortByUpdate = (elem, i) => { const sortByUpdate = React.useCallback(
let newEntries = myEntries; (elem) => {
switch (elem) { let newEntries = myEntries.slice();
case '#': const possibleMetrics = orderKeys(myEntries[0]).filter(
break; (key) => !['id', 'submitter', 'when'].includes(key)
case 'when': );
if (whenSorted) { let metricIndex = possibleMetrics.indexOf(elem);
newEntries = newEntries.sort((a, b) => let newScoresSorted = scoresSorted.slice();
a.when < b.when ? 1 : b.when < a.when ? -1 : 0 switch (elem) {
); case 'id':
setWhenSorted(false); if (idSorted) {
} else { setIdSorted(false);
newEntries = newEntries.sort((a, b) => newEntries = newEntries.sort((a, b) =>
a.when > b.when ? 1 : b.when > a.when ? -1 : 0 a.id > b.id ? 1 : b.id > a.id ? -1 : 0
); );
setWhenSorted(true); } else {
} setIdSorted(true);
break; newEntries = newEntries.sort((a, b) =>
default: a.id < b.id ? 1 : b.id < a.id ? -1 : 0
// eslint-disable-next-line no-case-declarations );
let metricIndex = getPossibleMetrics().indexOf(elem); }
// eslint-disable-next-line no-case-declarations break;
let newScoresSorted = scoresSorted; case 'when':
if (scoresSorted[metricIndex]) { if (whenSorted) {
newEntries = newEntries.sort( setWhenSorted(false);
(a, b) => newEntries = newEntries.sort((a, b) =>
(b.evaluations ? b.evaluations[elem] : -1) - a.when < b.when ? 1 : b.when < a.when ? -1 : 0
(a.evaluations ? a.evaluations[elem] : -1) );
); } else {
newScoresSorted[metricIndex] = false; setWhenSorted(true);
setScoresSorted(newScoresSorted); newEntries = newEntries.sort((a, b) =>
} else { a.when > b.when ? 1 : b.when > a.when ? -1 : 0
newEntries = newEntries.sort( );
(a, b) => }
(a.evaluations ? a.evaluations[elem] : -1) - break;
(b.evaluations ? b.evaluations[elem] : -1) default:
); if (scoresSorted[metricIndex]) {
newScoresSorted[metricIndex] = true; newEntries = newEntries.sort(
setScoresSorted(newScoresSorted); (a, b) => (b ? b[elem] : -1) - (a ? a[elem] : -1)
} );
break; newScoresSorted[metricIndex] = false;
} setScoresSorted(newScoresSorted);
setMyEntries(newEntries); } else {
}; newEntries = newEntries.sort(
(a, b) => (a ? a[elem] : -1) - (b ? b[elem] : -1)
);
newScoresSorted[metricIndex] = true;
setScoresSorted(newScoresSorted);
}
break;
}
setMyEntries(newEntries);
},
[idSorted, myEntries, scoresSorted, whenSorted]
);
const mobileRender = () => { const n = (pageNr - 1) * (ELEMENTS_PER_PAGE * 2);
return ( let elements = myEntries.slice(n, n + ELEMENTS_PER_PAGE * 2);
<FlexColumn padding="24px 12px" width="70%" as="section" id="start">
<H2 as="h2" margin="0 0 12px 0">
My Entries
</H2>
{!loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getMyEntriesHeader()}
possibleMetrics={getPossibleMetrics()}
tableType="myEntries"
gridTemplateColumns={
'1fr ' + '4fr '.repeat(getMyEntriesHeader().length - 1)
}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 3, align: 'right' },
]}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 2,
align: 'left',
}}
pageNr={pageNr}
elements={myEntries}
sortByUpdate={sortByUpdate}
/>
<Pager
pageNr={pageNr}
elements={myEntries}
setPageNr={setPageNr}
width="48px"
borderRadius="64px"
pages={CALC_PAGES(myEntries)}
number={`${pageNr} / ${CALC_PAGES(myEntries)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
const desktopRender = () => {
return (
<FlexColumn padding="24px" as="section" width="100%" maxWidth="1600px">
<FlexColumn padding="24px 12px" width="70%" as="section" id="start">
<H2 as="h2" margin="0 0 32px 0">
My Entries
</H2>
</FlexColumn>
{myEntries && !loading ? (
<>
<Search searchQueryHandler={searchQueryHandler} />
<Table
challengeName={props.challengeName}
headerElements={getMyEntriesHeader()}
possibleMetrics={getPossibleMetrics()}
gridTemplateColumns={
'1fr ' + '3fr '.repeat(getMyEntriesHeader().length - 2) + ' 4fr'
}
staticColumnElements={[
{ name: 'id', format: null, order: 1, align: 'left' },
{ name: 'when', format: RENDER_WHEN, order: 3, align: 'right' },
]}
iterableColumnElement={{
name: 'evaluations',
format: EVALUATIONS_FORMAT,
order: 2,
align: 'left',
}}
pageNr={pageNr}
elements={myEntries}
sortByUpdate={sortByUpdate}
myEntries
/>
<Pager
pageNr={pageNr}
elements={myEntries}
setPageNr={setPageNr}
width="72px"
mobileRender
borderRadius="64px"
pages={CALC_PAGES(myEntries, 2)}
number={`${pageNr} / ${CALC_PAGES(myEntries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
);
};
return ( return (
<> <FlexColumn
<Media query={theme.mobile}>{mobileRender()}</Media> padding="24px"
<Media query={theme.desktop}>{desktopRender()}</Media> gap="32px"
</> as="section"
width="100%"
maxWidth="1600px"
>
<H2 as="h2">My Entries</H2>
{!loading ? (
<>
<Search
searchQueryHandler={(event) =>
searchHandler(event, myEntriesAll, setPageNr, setMyEntries)
}
/>
{elements.length > 0 && myEntries[0] && (
<div style={{ width: '100%', overflowX: 'auto' }}>
<Table
items={elements}
orderedKeys={orderKeys(myEntries[0])}
sortByUpdate={sortByUpdate}
popUpMessageHandler={props.popUpMessageHandler}
/>
</div>
)}
<Pager
pageNr={pageNr}
elements={myEntries}
setPageNr={setPageNr}
width="72px"
borderRadius="64px"
pages={CALC_PAGES(myEntries, 2)}
number={`${pageNr} / ${CALC_PAGES(myEntries, 2)}`}
/>
</>
) : (
<Loading />
)}
</FlexColumn>
); );
}; };

View File

@ -1,33 +0,0 @@
const myEntriesSearchQueryHandler = (
event,
entriesFromApi,
setPageNr,
setEntries
) => {
let searchQuery = event.target.value;
let submissionsToRender = [];
setPageNr(1);
if (searchQuery === '') setEntries(entriesFromApi);
else {
for (let entry of entriesFromApi) {
const { id, when } = entry;
let evaluations = '';
if (entry.evaluations) {
for (let evaluation of Object.values(entry.evaluations)) {
evaluations += ` ${evaluation}`;
}
}
const str = `${id} ${when.slice(11, 16)} ${when.slice(
0,
10
)} ${evaluations}`;
console.log(entry);
console.log(str);
if (str.toLowerCase().includes(searchQuery.toLowerCase()))
submissionsToRender.push(entry);
}
setEntries(submissionsToRender);
}
};
export default myEntriesSearchQueryHandler;

View File

@ -0,0 +1,27 @@
const orderKeys = (elem) => {
if (elem) {
let result = ['id'];
const elemKeys = Object.keys(elem);
const dev0keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-0')
.sort();
const dev1keys = elemKeys
.filter((key) => key.split('.')[1] === 'dev-1')
.sort();
const testAkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-A')
.sort();
const testBkeys = elemKeys
.filter((key) => key.split('.')[1] === 'test-B')
.sort();
result = result.concat(dev0keys);
result = result.concat(dev1keys);
result = result.concat(testAkeys);
result = result.concat(testBkeys);
result.push('when');
return result;
}
return null;
};
export default orderKeys;

View File

@ -0,0 +1,28 @@
const searchQueryHandler = (event, entriesAll, setPageNr, setEntries) => {
let searchQuery = event.target.value;
let submissionsToRender = [];
setPageNr(1);
if (searchQuery === '') setEntries(entriesAll);
else {
for (let entry of entriesAll) {
let { when, tags } = entry;
tags = Object.values(tags)
.map((tag) => tag.name)
.join(' ');
const otherKeys = Object.values(entry)
.join(' ')
.replaceAll(-999999999, 'N/A')
.replaceAll('[object Object]', '')
.replaceAll(',', '');
const str = `${when.slice(11, 16)} ${when.slice(
0,
10
)} ${otherKeys} ${tags}`;
if (str.toLowerCase().includes(searchQuery.toLowerCase()))
submissionsToRender.push(entry);
}
setEntries(submissionsToRender);
}
};
export default searchQueryHandler;

View File

@ -1,65 +1,13 @@
import React from 'react'; import React from 'react';
import { FlexColumn } from '../utils/containers'; import { FlexColumn } from '../../utils/containers';
import { Body, H2 } from '../utils/fonts'; import { H2 } from '../../utils/fonts';
import Media from 'react-media'; import Media from 'react-media';
import theme from '../utils/theme'; import theme from '../../utils/theme';
import getChallengeFullDescription from '../api/getChallengeFullDescription'; import getChallengeFullDescription from '../../api/getChallengeFullDescription';
import styled from 'styled-components'; import InfoList from '../../components/generic/InfoList';
import InfoList from '../components/generic/InfoList'; import Loading from '../../components/generic/Loading';
import Loading from '../components/generic/Loading';
import { marked } from 'marked'; import { marked } from 'marked';
import ReadmeStyle from './ReadmeStyle';
const ReadmeStyle = styled(Body)`
* {
font-weight: inherit;
}
h2 {
font-family: 'Kanit', sans-serif;
margin: 32px 0;
}
h3 {
font-family: 'Kanit', sans-serif;
font-weight: inherit;
font-size: 18px;
line-height: 22px;
margin: 24px 0;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-size: 22px;
line-height: 26px;
}
}
p {
font-family: 'Roboto', sans-serif;
font-weight: 300;
font-size: 14px;
line-height: 20px;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-weight: 400;
font-size: 16px;
line-height: 22px;
}
}
a {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: ${({ theme }) => theme.colors.dark};
text-decoration: none;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-size: 16px;
line-height: 22px;
font-weight: 500;
}
}
`;
const Readme = (props) => { const Readme = (props) => {
const [fullDescription, setFullDescription] = React.useState(''); const [fullDescription, setFullDescription] = React.useState('');
@ -117,8 +65,13 @@ const Readme = (props) => {
const desktopRender = () => { const desktopRender = () => {
return ( return (
<FlexColumn as="section" padding="20px" gap="64px"> <FlexColumn
<FlexColumn gap="32px"> as="section"
className="Readme__section"
padding="20px"
gap="64px"
>
<FlexColumn className="Readme__info" gap="32px">
<H2 as="h2">Info</H2> <H2 as="h2">Info</H2>
<InfoList <InfoList
iconsSize="32px" iconsSize="32px"
@ -126,7 +79,12 @@ const Readme = (props) => {
deadline={props.deadline} deadline={props.deadline}
/> />
</FlexColumn> </FlexColumn>
<FlexColumn alignmentX="flex-start" width="80%" maxWidth="1200px"> <FlexColumn
className="Readme__container"
alignmentX="flex-start"
width="80%"
maxWidth="1200px"
>
<ReadmeStyle <ReadmeStyle
as={fullDescription ? 'section' : 'p'} as={fullDescription ? 'section' : 'p'}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{

View File

@ -0,0 +1,64 @@
import styled from 'styled-components';
const ReadmeStyle = styled.section`
padding: 20px;
gap: 24px;
@media (min-width: ${({ theme }) => theme.overMobile}) {
gap: 64px;
}
/* .ReadmeStyle__content { */
* {
font-weight: inherit;
}
h2 {
font-family: 'Kanit', sans-serif;
margin: 32px 0;
}
h3 {
font-family: 'Kanit', sans-serif;
font-weight: inherit;
font-size: 18px;
line-height: 22px;
margin: 24px 0;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-size: 22px;
line-height: 26px;
}
}
p {
font-family: 'Roboto', sans-serif;
font-weight: 300;
font-size: 14px;
line-height: 20px;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-weight: 400;
font-size: 16px;
line-height: 22px;
}
}
a {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: ${({ theme }) => theme.colors.dark};
text-decoration: none;
@media (min-width: ${({ theme }) => theme.overMobile}) {
font-size: 16px;
line-height: 22px;
font-weight: 500;
}
}
/* } */
`;
export default ReadmeStyle;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { FlexColumn } from '../../../utils/containers';
import { H2 } from '../../../utils/fonts';
import InfoList from '../../../components/generic/InfoList';
import ReadmeStyle from '../ReadmeStyle';
const ReadmeDesktop = (props) => {
return (
<FlexColumn as="section" padding="20px" gap="64px">
<FlexColumn gap="32px">
<H2 as="h2">Info</H2>
<InfoList
iconsSize="32px"
metric={props.metric}
deadline={props.deadline}
/>
</FlexColumn>
<FlexColumn alignmentX="flex-start" width="80%" maxWidth="1200px">
<ReadmeStyle
as={props.fullDescription ? 'section' : 'p'}
dangerouslySetInnerHTML={{
__html: props.fullDescription
? props.parseMarkdownResponse(props.fullDescription)
: props.description,
}}
/>
</FlexColumn>
</FlexColumn>
);
};
export default ReadmeDesktop;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { FlexColumn } from '../../../utils/containers';
import { H2 } from '../../../utils/fonts';
import InfoList from '../../../components/generic/InfoList';
import ReadmeStyle from '../ReadmeStyle';
const ReadmeMobile = (props) => {
return (
<FlexColumn as="section" padding="20px" gap="24px">
<FlexColumn gap="12px" alignmentX="flex-start">
<H2 as="h2">Info</H2>
<InfoList
iconsSize="24px"
metric={props.metric}
deadline={props.deadline}
/>
</FlexColumn>
<FlexColumn alignmentX="flex-start" maxWidth="260px">
<ReadmeStyle
as={props.fullDescription ? 'article' : 'p'}
dangerouslySetInnerHTML={{
__html: props.fullDescription
? props.parseMarkdownResponse(props.fullDescription)
: props.description,
}}
/>
</FlexColumn>
</FlexColumn>
);
};
export default ReadmeMobile;

View File

@ -0,0 +1 @@
export { default } from './Readme';

View File

@ -24,7 +24,7 @@ const Submit = (props) => {
}); });
React.useMemo(() => { React.useMemo(() => {
getTags(dispatch); getTags((data) => dispatch({ type: SUBMIT_ACTION.LOAD_TAGS, payload: data }));
}, []); }, []);
const challengeSubmissionSubmit = React.useCallback(() => { const challengeSubmissionSubmit = React.useCallback(() => {

View File

@ -1,7 +1,6 @@
import SUBMIT_ACTION from './SubmitActionEnum'; import SUBMIT_ACTION from './SubmitActionEnum';
const SubmitReducer = (state, action) => { const SubmitReducer = (state, action) => {
console.log(`SubmitReducer: ${action.type}`);
let newTags = state.tags; let newTags = state.tags;
switch (action.type) { switch (action.type) {
case SUBMIT_ACTION.SET_DESCRIPTION: case SUBMIT_ACTION.SET_DESCRIPTION:

View File

@ -1,5 +1,6 @@
import Keycloak from 'keycloak-js'; import Keycloak from 'keycloak-js';
import { POLICY_PRIVACY_PAGE, ROOT_URL } from '../utils/globals'; import { POLICY_PRIVACY_PAGE, ROOT_URL } from '../utils/globals';
import SESSION_STORAGE from '../utils/sessionStorage';
const _kc = new Keycloak({ const _kc = new Keycloak({
url: process.env.REACT_APP_KC_URL, url: process.env.REACT_APP_KC_URL,
@ -30,14 +31,17 @@ const doLogin = () => {
if (privacyPolicyAccept !== 'accept') { if (privacyPolicyAccept !== 'accept') {
window.location.replace(`${ROOT_URL}${POLICY_PRIVACY_PAGE}/login`); window.location.replace(`${ROOT_URL}${POLICY_PRIVACY_PAGE}/login`);
} else { } else {
sessionStorage.setItem('logout', ''); sessionStorage.setItem(SESSION_STORAGE.LOGOUT, '');
_kc.login(); _kc.login();
} }
}; };
const doLogout = () => { const doLogout = () => {
sessionStorage.clear(); sessionStorage.clear();
sessionStorage.setItem('logout', 'yes'); sessionStorage.setItem(
SESSION_STORAGE.LOGOUT,
SESSION_STORAGE.STATIC_VALUE.YES
);
_kc.logout(); _kc.logout();
}; };
@ -61,6 +65,14 @@ const getUsername = () => _kc.tokenParsed?.preferred_username;
const hasRole = (roles) => roles.some((role) => _kc.hasRealmRole(role)); const hasRole = (roles) => roles.some((role) => _kc.hasRealmRole(role));
const goToProfile = () => {
_kc.accountManagement();
};
const getProfileInfo = async (setProfileInfo) => {
_kc.loadUserInfo().then((response) => setProfileInfo(response));
};
const KeyCloakService = { const KeyCloakService = {
initKeycloak, initKeycloak,
doLogin, doLogin,
@ -71,6 +83,8 @@ const KeyCloakService = {
getUsername, getUsername,
hasRole, hasRole,
doRegister, doRegister,
goToProfile,
getProfileInfo,
}; };
export default KeyCloakService; export default KeyCloakService;

View File

@ -2,9 +2,13 @@ const colors = {
white: '#FCFCFC', white: '#FCFCFC',
green: '#1B998B', green: '#1B998B',
blue: '#4B8FF0', blue: '#4B8FF0',
red: '#FF1B1C',
green03: 'rgba(27, 153, 139, 0.3)', green03: 'rgba(27, 153, 139, 0.3)',
green05: 'rgba(27, 153, 139, 0.5)', green05: 'rgba(27, 153, 139, 0.5)',
green08: 'rgba(27, 153, 139, 0.8)',
dark: '#343434', dark: '#343434',
dark003: 'rgba(52, 52, 52, 0.03)',
dark005: 'rgba(52, 52, 52, 0.05)',
dark01: 'rgba(52, 52, 52, 0.1)', dark01: 'rgba(52, 52, 52, 0.1)',
dark03: 'rgba(52, 52, 52, 0.3)', dark03: 'rgba(52, 52, 52, 0.3)',
dark04: 'rgba(52, 52, 52, 0.4)', dark04: 'rgba(52, 52, 52, 0.4)',

View File

@ -8,7 +8,7 @@ import imageIco from '../assets/image_ico.svg';
import tabularIco from '../assets/tabular_ico.svg'; import tabularIco from '../assets/tabular_ico.svg';
import React from 'react'; import React from 'react';
const ELEMENTS_PER_PAGE = 12; const ELEMENTS_PER_PAGE = 10;
const MINI_DESCRIPTION_LENGTH = 70; const MINI_DESCRIPTION_LENGTH = 70;
const API = process.env.REACT_APP_API; const API = process.env.REACT_APP_API;
const CHALLENGES_PAGE = '/challenges'; const CHALLENGES_PAGE = '/challenges';
@ -111,6 +111,11 @@ const RENDER_WHEN = (when) => {
return `${when.slice(0, 10)} ${when.slice(11, 16)}`; return `${when.slice(0, 10)} ${when.slice(11, 16)}`;
}; };
const RENDER_METRIC_VALUE = (value) => {
if (value <= -999999999) return 'N/A';
else return value;
};
const EVALUATIONS_FORMAT = (evaluate) => { const EVALUATIONS_FORMAT = (evaluate) => {
if (Object.hasOwn(evaluate, 'score')) return evaluate.score.slice(0, 7); if (Object.hasOwn(evaluate, 'score')) return evaluate.score.slice(0, 7);
return evaluate.slice(0, 7); return evaluate.slice(0, 7);
@ -146,6 +151,7 @@ export {
RENDER_ICO, RENDER_ICO,
CALC_PAGES, CALC_PAGES,
RENDER_DEADLINE_TIME, RENDER_DEADLINE_TIME,
RENDER_METRIC_VALUE,
IS_MOBILE, IS_MOBILE,
RENDER_WHEN, RENDER_WHEN,
EVALUATIONS_FORMAT, EVALUATIONS_FORMAT,