diff --git a/services/app/apps/codebattle/assets/css/style.scss b/services/app/apps/codebattle/assets/css/style.scss index 38230dd5b..1e9e2c1cb 100644 --- a/services/app/apps/codebattle/assets/css/style.scss +++ b/services/app/apps/codebattle/assets/css/style.scss @@ -2213,7 +2213,7 @@ a.cb-text:hover { .cb-separator { width: 100%; - border: 2px solid gray; + border: 1px solid gray; } // react big calendar for tournaments @@ -2517,3 +2517,7 @@ a.cb-text:hover { .btn-link { color: white; } + +.cb-subtle-background { + background: radial-gradient(circle at 50% 0%, #3a3b40 0%, $cb-bg-panel 100%); +} diff --git a/services/app/apps/codebattle/assets/js/widgets/App.jsx b/services/app/apps/codebattle/assets/js/widgets/App.jsx index e034331b3..59f415a7d 100755 --- a/services/app/apps/codebattle/assets/js/widgets/App.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/App.jsx @@ -51,8 +51,8 @@ const rollbarRedux = rollbarMiddleware(rollbar); const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - serializableCheck: { ignoredActions: ['ERROR', PERSIST] }, - }).concat(rollbarRedux), + serializableCheck: { ignoredActions: ['ERROR', PERSIST] }, + }).concat(rollbarRedux), }); const persistor = persistStore(store); @@ -130,7 +130,9 @@ export const Lobby = () => ( - + + + diff --git a/services/app/apps/codebattle/assets/js/widgets/components/ScheduleNavigationBar.jsx b/services/app/apps/codebattle/assets/js/widgets/components/ScheduleNavigationBar.jsx new file mode 100644 index 000000000..e741274bb --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/ScheduleNavigationBar.jsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +import dayjs from '../../i18n/dayjs'; + +const ScheduleNavigationTab = ({ + className, + events, + event, + setEvent, +}) => { + const [prev, setPrevEvent] = useState(); + const [next, setNextEvent] = useState(); + + useEffect(() => { + if (event) { + const sortedEvents = events.sort((a, b) => dayjs(a.start).diff(dayjs(b.start))); + const eventIndex = sortedEvents.findIndex(e => e.resourse.id === event.resourse.id); + + if (eventIndex === -1) return; + + if (eventIndex < 1) { + setPrevEvent(undefined); + } else { + setPrevEvent(sortedEvents[eventIndex - 1]); + } + + if (eventIndex > events.length - 2) { + setNextEvent(undefined); + } else { + setNextEvent(sortedEvents[eventIndex + 1]); + } + } + }, [event, events, setPrevEvent, setNextEvent]); + + const onClickPrev = useCallback(() => { + setEvent(prev); + }, [setEvent, prev]); + const onClickNext = useCallback(() => { + setEvent(next); + }, [setEvent, next]); + + return ( +
+
+ {prev && ( +
{ }} + className="btn-link" + tabIndex="0" + > + {'<<'} + {prev.title} +
+ )} +
+
+ {next && ( +
{ }} + className="btn-link" + tabIndex="0" + > + {next.title} + {'>>'} +
+ )} +
+
+ ); +}; + +export default ScheduleNavigationTab; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/TournamentDescription.jsx b/services/app/apps/codebattle/assets/js/widgets/components/TournamentDescription.jsx new file mode 100644 index 000000000..ef965adb3 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/TournamentDescription.jsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import cn from 'classnames'; +import capitalize from 'lodash/capitalize'; + +import { getRankingPoints, getTasksCount, grades } from '@/config/grades'; + +const getGradeDescriptionClassName = highlight => ( + cn( + 'd-flex flex-column flex-lg-row flex-md-row flex-sm-row justify-content-between', + { + 'text-monospace': highlight, + }, + ) +); + +const GradeInfo = ({ grade, selected }) => ( +
+ + {capitalize(grade)} + {grade === selected && '(*)'} + + + [ + {getRankingPoints(grade).join(', ')} + ] + +
+); + +const TournamentDescription = ({ + className, + tournament, +}) => ( +
+ {tournament.grade !== grades.open ? ( + <> + Tournament Highlights: +
+ Prizes: Codebattle T-shirt merch for a top-tier of League + {`Challenges: ${getTasksCount(tournament.grade)} unique algorithm problems`} + Impact: Advancing in the Codebattle programmer rankings +
+
+
+
View League Ranking Points System
+
+ {[grades.rookie, grades.challenger, grades.pro, grades.elite, grades.masters, grades.grandSlam].map(grade => ( + + ))} +
+
+
+ + ) : ( + <> + Tournament Description: + {tournament.description} + + )} +
+); + +export default TournamentDescription; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/TournamentPreviewPanel.jsx b/services/app/apps/codebattle/assets/js/widgets/components/TournamentPreviewPanel.jsx new file mode 100644 index 000000000..886b26746 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/TournamentPreviewPanel.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { getRankingPoints, grades } from '@/config/grades'; + +import dayjs from '../../i18n/dayjs'; + +import TournamentTimer from './TournamentTimer'; + +const TournamentPreviewPanel = ({ + className, + tournament, + start, + end, +}) => ( +
+
+ {`Start Date: ${dayjs(start).format('MMMM DD, YYYY')}`} + {`Time: ${dayjs(start).format('hh:mm A')} - ${dayjs(end).format('hh:mm A')}`} + {tournament.grade !== grades.open + && {`First Place Points: ${getRankingPoints(tournament.grade)[0]} Ranking Points`}} + +
+
+); + +export default TournamentPreviewPanel; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/TournamentTimer.jsx b/services/app/apps/codebattle/assets/js/widgets/components/TournamentTimer.jsx new file mode 100644 index 000000000..66d8e928b --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/TournamentTimer.jsx @@ -0,0 +1,41 @@ +import React, { useState, useEffect } from 'react'; + +import dayjs from '../../i18n/dayjs'; + +const TournamentTimer = ({ date = new Date(), label, children }) => { + const [duration, setDuration] = useState(0); + const [stoped, setStoped] = useState(0); + + useEffect(() => { + if (stoped) { + return () => { }; + } + + const interval = setInterval(() => { + setDuration(dayjs(date).diff(dayjs())); + }, 100); + + return () => { + clearInterval(interval); + }; + }, [date, stoped, setDuration]); + + if (stoped || duration > 1000 * 60 * 60 * 24) { + return <>{children}; + } + + if (duration < 0) { + setStoped(true); + return <>{children}; + } + + return ( + <> + {label} + {' '} + {dayjs.duration(duration).format('HH:mm:ss')} + + ); +}; + +export default TournamentTimer; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/grades.js b/services/app/apps/codebattle/assets/js/widgets/config/grades.js new file mode 100644 index 000000000..5285b2c60 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/config/grades.js @@ -0,0 +1,33 @@ +export const grades = { + open: 'open', + rookie: 'rookie', + challenger: 'challenger', + pro: 'pro', + elite: 'elite', + masters: 'masters', + grandSlam: 'grand_slam', +}; + +export const getRankingPoints = grade => { + switch (grade) { + case grades.rookie: return [8, 4, 2]; + case grades.challenger: return [16, 8, 4, 2]; + case grades.pro: return [128, 64, 32, 16, 8, 4, 2]; + case grades.elite: return [256, 128, 64, 32, 16, 8, 4, 2]; + case grades.masters: return [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2]; + case grades.grandSlam: return [2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2]; + default: return [0]; + } +}; + +export const getTasksCount = grade => { + switch (grade) { + case grades.rookie: return 4; + case grades.challenger: return 6; + case grades.pro: return 8; + case grades.elite: return 10; + case grades.masters: return 12; + case grades.grandSlam: return 14; + default: return 0; + } +}; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/modalCodes.js b/services/app/apps/codebattle/assets/js/widgets/config/modalCodes.js index 8defb3212..cd214c81e 100644 --- a/services/app/apps/codebattle/assets/js/widgets/config/modalCodes.js +++ b/services/app/apps/codebattle/assets/js/widgets/config/modalCodes.js @@ -9,6 +9,7 @@ const modalCodes = { awardModal: 'award_modal', eventStageModal: 'event_stage_modal', calendarEventModal: 'calendar_event_modal', + tournamentModal: 'tournament_modal', }; export default modalCodes; diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/InfoWidget.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/InfoWidget.jsx index af6ecf73f..57a870df6 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/InfoWidget.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/InfoWidget.jsx @@ -161,7 +161,7 @@ function InfoWidget({ viewMode }) { const task = useSelector(isTestingRoom ? builderTaskSelector : gameTaskSelector); const { outputData, canShowOutput } = usePlayerOutputForInfoPanel(viewMode, roomMachineState); - if (task.type === 'css') { + if (task?.type === 'css') { return ( { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useLobbyModals(); + return (
{seasonTournaments?.length || liveTournaments?.length ? ( -
+
{liveTournaments?.length !== 0 && ( <>
@@ -72,14 +72,16 @@ const SeasonProfilePanel = ({ Live Tournaments
- {liveTournaments.map(tournament => ( - - ))} +
+ {liveTournaments.map(tournament => ( + + ))} +
)} {seasonTournaments?.length !== 0 && ( @@ -89,10 +91,7 @@ const SeasonProfilePanel = ({ Upcoming Tournaments
-
+
{seasonTournaments.map(tournament => ( { + switch (grade) { + case grades.open: return 'MMM D, YYYY [at] '; + default: return '[at] h:mma'; + } +}; + const getActionText = tournament => { switch (tournament.state) { - case tournamentStates.upcoming: - return 'Check Upcoming'; case tournamentStates.waitingParticipants: return 'Join'; case tournamentStates.active: @@ -27,18 +36,46 @@ const getActionText = tournament => { case tournamentStates.canceled: return 'Show'; case tournamentStates.finished: - return 'View results'; + return 'Results'; default: return 'Show'; } }; -const TournamentAction = ({ tournament, isAdmin = false }) => { - if (!isAdmin && tournament.state === tournamentStates.upcoming) { - return <>; +const TournamentTitle = ({ tournament }) => { + if (tournament.grade === grades.open) { + return ( + + {tournament.name} + + ); } - const className = cn('btn text-nowrap rounded', { + const words = tournament.name.split(' '); + const subtitle = words[words.length - 1]; + words.pop(); + const title = words.join(' '); + + return ( +
+ + {title} + + {subtitle} +
+ ); +}; + +const TournamentAction = ({ tournament, isAdmin = false }) => { + const infoClassName = 'btn btn-outline-secondary cb-btn-outline-secondary mx-2 px-3 cb-rounded border-0'; + + const actionClassName = cn('btn text-nowrap px-2 cb-rounded', { 'btn-secondary cb-btn-secondary': [ tournamentStates.finished, tournamentStates.canceled, @@ -49,18 +86,32 @@ const TournamentAction = ({ tournament, isAdmin = false }) => { tournamentStates.waitingParticipants, ].includes(tournament.state), }); + const text = getActionText(tournament); + const openTournamentInfo = () => { + NiceModal.show(modalCodes.tournamentModal, { tournament }); + }; + return ( ); @@ -90,45 +141,49 @@ export const upcomingIcon = ( ); const TournamentListItem = ({ tournament, icon, isAdmin = false }) => ( -
-
-
- {icon || getIconForGrade(tournament.grade)} +
+
+
+
+ {icon || getIconForGrade(tournament.grade)} +
+
-
- - {tournament.name} - - - {tournament.state !== 'upcoming' && ( - - - {mapTournamentTitleByState[tournament.state]} +
+
+
+ {tournament.grade !== grades.open && ( + + + {getRankingPoints(tournament.grade)[0]} + Ranking Points )} - {tournamentStates.canceled !== tournament.state - && tournament.state !== 'upcoming' && ( - <> - + + {tournament.state !== 'upcoming' && ( + + + {mapTournamentTitleByState[tournament.state]} + + )} + {tournamentStates.canceled !== tournament.state + && tournament.state !== 'upcoming' && ( + {tournament.playersCount} - - )} + )} + {showStartsAt(tournament.state) && ( <> - + - {dayjs(tournament.startsAt).format('MMMM D, YYYY [at] h:mma')} + + {dayjs(tournament.startsAt).format(getDateFormat(tournament.grade))} + )} @@ -136,16 +191,14 @@ const TournamentListItem = ({ tournament, icon, isAdmin = false }) => ( <> - {dayjs(tournament.lastRoundEndedAt).format( - 'MMMM D, YYYY [at] h:mma', - )} + {dayjs(tournament.lastRoundEndedAt).format(getDateFormat(tournament.grade))} )} - +
+
-
); diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/lobby/TournamentModal.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/TournamentModal.jsx new file mode 100644 index 000000000..06017ad2c --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/TournamentModal.jsx @@ -0,0 +1,81 @@ +import React, { memo } from 'react'; + +import NiceModal, { useModal } from '@ebay/nice-modal-react'; +import cn from 'classnames'; +import i18n from 'i18next'; +import Button from 'react-bootstrap/Button'; +import Modal from 'react-bootstrap/Modal'; +import { useSelector } from 'react-redux'; + +import TournamentDescription from '@/components/TournamentDescription'; +import TournamentPreviewPanel from '@/components/TournamentPreviewPanel'; +import { grades } from '@/config/grades'; +import ModalCodes from '@/config/modalCodes'; +import { currentUserIsAdminSelector } from '@/selectors'; + +import dayjs from '../../../i18n/dayjs'; + +export const TournamentModal = NiceModal.create(({ tournament }) => { + const isAdmin = useSelector(currentUserIsAdminSelector); + + const modal = useModal(ModalCodes.tournamentModal); + + const isUpcoming = tournament?.grade === 'upcoming'; + const start = dayjs(tournament.startsAt).toDate(); + const end = dayjs(tournament.startsAt).add(1, 'hour').toDate(); + + if (!tournament) { + return <>; + } + + return ( + + + + {tournament.grade !== grades.open && Codebattle League 2025} + {i18n.t('Tournament: %{name}', { name: tournament.name })} + + + +
+ + +
+
+ + {tournament.id && ( + + {i18n.t('Open Tournament')} + + )} + + +
+ ); +}); + +export default memo(TournamentModal); diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/schedule/EventModal.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/schedule/EventModal.jsx index ccf789d85..c5d7c98aa 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/schedule/EventModal.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/schedule/EventModal.jsx @@ -1,100 +1,23 @@ import React, { - memo, useCallback, useEffect, useState, + memo, useCallback, useState, } from 'react'; import NiceModal, { useModal } from '@ebay/nice-modal-react'; import cn from 'classnames'; import i18n from 'i18next'; -import capitalize from 'lodash/capitalize'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { useSelector } from 'react-redux'; +import ScheduleNavigationTab from '@/components/ScheduleNavigationBar'; +import TournamentDescription from '@/components/TournamentDescription'; +import TournamentPreviewPanel from '@/components/TournamentPreviewPanel'; +import { grades } from '@/config/grades'; +import ModalCodes from '@/config/modalCodes'; import { currentUserIsAdminSelector } from '@/selectors'; -import dayjs from '../../../i18n/dayjs'; -import ModalCodes from '../../config/modalCodes'; - -import { grades } from './TournamentSchedule'; - -const getRankingPoints = grade => { - switch (grade) { - case grades.rookie: return [8, 4, 2]; - case grades.challenger: return [16, 8, 4, 2]; - case grades.pro: return [128, 64, 32, 16, 8, 4, 2]; - case grades.elite: return [256, 128, 64, 32, 16, 8, 4, 2]; - case grades.masters: return [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2]; - case grades.grandSlam: return [2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2]; - default: return [0]; - } -}; - -const getTasksCount = grade => { - switch (grade) { - case grades.rookie: return 4; - case grades.challenger: return 6; - case grades.pro: return 8; - case grades.elite: return 10; - case grades.masters: return 12; - case grades.grandSlam: return 14; - default: return 0; - } -}; - -const GradeInfo = ({ grade, selected }) => ( -
- - {capitalize(grade)} - {grade === selected && '(*)'} - - - [ - {getRankingPoints(grade).join(', ')} - ] - -
-); - -const Timer = ({ date = new Date(), label }) => { - const [duration, setDuration] = useState(0); - const [stoped, setStoped] = useState(0); - - useEffect(() => { - if (stoped) { - return () => { }; - } - - const interval = setInterval(() => { - setDuration(dayjs(date).diff(dayjs())); - }, 100); - - return () => { - clearInterval(interval); - }; - }, [date, stoped, setDuration]); - - if (stoped || duration > 1000 * 60 * 60 * 24) { - return <>; - } - - if (duration < 0) { - setStoped(true); - return <>; - } - - return ( - <> - {label} - {' '} - {dayjs.duration(duration).format('HH:mm:ss')} - - ); -}; - export const EventModal = NiceModal.create(({ event: selectedEvent, events, clearEvent }) => { const [currentEvent, setCurrentEvent] = useState(); - const [prevEvent, setPrevEvent] = useState(); - const [nextEvent, setNextEvent] = useState(); const isAdmin = useSelector(currentUserIsAdminSelector); @@ -102,34 +25,6 @@ export const EventModal = NiceModal.create(({ event: selectedEvent, events, clea const event = currentEvent || selectedEvent; const isUpcoming = event?.resourse?.grade === 'upcoming'; - - useEffect(() => { - if (event) { - const sortedEvents = events.sort((a, b) => dayjs(a.start).diff(dayjs(b.start))); - const eventIndex = sortedEvents.findIndex(e => e.resourse.id === event.resourse.id); - - if (eventIndex === -1) return; - - if (eventIndex < 1) { - setPrevEvent(undefined); - } else { - setPrevEvent(sortedEvents[eventIndex - 1]); - } - - if (eventIndex > events.length - 2) { - setNextEvent(undefined); - } else { - setNextEvent(sortedEvents[eventIndex + 1]); - } - } - }, [event, events, setPrevEvent, setNextEvent]); - - const clickPrev = useCallback(() => { - setCurrentEvent(prevEvent); - }, [setCurrentEvent, prevEvent]); - const clickNext = useCallback(() => { - setCurrentEvent(nextEvent); - }, [setCurrentEvent, nextEvent]); const handleClose = useCallback(() => { modal.hide(); clearEvent(); @@ -150,72 +45,22 @@ export const EventModal = NiceModal.create(({ event: selectedEvent, events, clea
-
-
- {prevEvent && ( -
{ }} - className="btn-link" - tabIndex="0" - > - {'<<'} - {prevEvent.title} -
- )} -
-
- {nextEvent && ( -
{ }} - className="btn-link" - tabIndex="0" - > - {nextEvent.title} - {'>>'} -
- )} -
-
-
-
- {`Start Date: ${dayjs(event.start).format('MMMM DD, YYYY')}`} - {`Time: ${dayjs(event.start).format('hh:mm A')} - ${dayjs(event.end).format('hh:mm A')}`} - {event.resourse.grade !== grades.open - && {`First Place Points: ${getRankingPoints(event.resourse.grade)[0]} Ranking Points`}} - -
-
-
- {event.resourse.grade !== grades.open ? ( - <> - Tournament Highlights: -
- Prizes: Codebattle T-shirt merch for a top-tier of League - {`Challenges: ${getTasksCount(event.resourse.grade)} unique algorithm problems`} - Impact: Advancing in the Codebattle programmer rankings -
-
-
-
View League Ranking Points System
-
- {[grades.rookie, grades.challenger, grades.pro, grades.elite, grades.masters, grades.grandSlam].map(grade => ( - - ))} -
-
-
- - ) : ( - <> - Tournament Description: - {event.resourse.description} - - )} -
+ + +
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/schedule/ScheduleLegend.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/schedule/ScheduleLegend.jsx index 2bb106bd3..4bfb6a135 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/schedule/ScheduleLegend.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/schedule/ScheduleLegend.jsx @@ -12,14 +12,20 @@ export const states = { }; const sectionBtnClassName = cn( - 'btn btn-secondary border-0 cb-btn-secondary cb-rounded mx-2', + 'btn btn-secondary cb-btn-secondary cb-rounded w-100 m-2', ); const ScheduleLegend = ({ onChangeContext, loading, context }) => { const isAdmin = useSelector(currentUserIsAdminSelector); return ( -
+