Skip to content

Commit c5cdd1b

Browse files
authored
Merge pull request #2225 from hexlet-codebattle/rewrite_lobby_tournament_list
add_lobby_tournament_card
2 parents 753b1cf + b1a2616 commit c5cdd1b

File tree

18 files changed

+509
-265
lines changed

18 files changed

+509
-265
lines changed

services/app/apps/codebattle/assets/css/style.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2213,7 +2213,7 @@ a.cb-text:hover {
22132213

22142214
.cb-separator {
22152215
width: 100%;
2216-
border: 2px solid gray;
2216+
border: 1px solid gray;
22172217
}
22182218

22192219
// react big calendar for tournaments
@@ -2517,3 +2517,7 @@ a.cb-text:hover {
25172517
.btn-link {
25182518
color: white;
25192519
}
2520+
2521+
.cb-subtle-background {
2522+
background: radial-gradient(circle at 50% 0%, #3a3b40 0%, $cb-bg-panel 100%);
2523+
}

services/app/apps/codebattle/assets/js/widgets/App.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ const rollbarRedux = rollbarMiddleware(rollbar);
5151
const store = configureStore({
5252
reducer: rootReducer,
5353
middleware: getDefaultMiddleware => getDefaultMiddleware({
54-
serializableCheck: { ignoredActions: ['ERROR', PERSIST] },
55-
}).concat(rollbarRedux),
54+
serializableCheck: { ignoredActions: ['ERROR', PERSIST] },
55+
}).concat(rollbarRedux),
5656
});
5757

5858
const persistor = persistStore(store);
@@ -130,7 +130,9 @@ export const Lobby = () => (
130130
<Provider store={store}>
131131
<PersistGate loading={null} persistor={persistor}>
132132
<Suspense>
133-
<LobbyWidget />
133+
<NiceModal.Provider>
134+
<LobbyWidget />
135+
</NiceModal.Provider>
134136
</Suspense>
135137
</PersistGate>
136138
</Provider>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
3+
import dayjs from '../../i18n/dayjs';
4+
5+
const ScheduleNavigationTab = ({
6+
className,
7+
events,
8+
event,
9+
setEvent,
10+
}) => {
11+
const [prev, setPrevEvent] = useState();
12+
const [next, setNextEvent] = useState();
13+
14+
useEffect(() => {
15+
if (event) {
16+
const sortedEvents = events.sort((a, b) => dayjs(a.start).diff(dayjs(b.start)));
17+
const eventIndex = sortedEvents.findIndex(e => e.resourse.id === event.resourse.id);
18+
19+
if (eventIndex === -1) return;
20+
21+
if (eventIndex < 1) {
22+
setPrevEvent(undefined);
23+
} else {
24+
setPrevEvent(sortedEvents[eventIndex - 1]);
25+
}
26+
27+
if (eventIndex > events.length - 2) {
28+
setNextEvent(undefined);
29+
} else {
30+
setNextEvent(sortedEvents[eventIndex + 1]);
31+
}
32+
}
33+
}, [event, events, setPrevEvent, setNextEvent]);
34+
35+
const onClickPrev = useCallback(() => {
36+
setEvent(prev);
37+
}, [setEvent, prev]);
38+
const onClickNext = useCallback(() => {
39+
setEvent(next);
40+
}, [setEvent, next]);
41+
42+
return (
43+
<div className={className}>
44+
<div className="d-flex">
45+
{prev && (
46+
<div
47+
role="button"
48+
onClick={onClickPrev}
49+
onKeyPress={() => { }}
50+
className="btn-link"
51+
tabIndex="0"
52+
>
53+
<span className="pr-2">{'<<'}</span>
54+
{prev.title}
55+
</div>
56+
)}
57+
</div>
58+
<div className="d-flex">
59+
{next && (
60+
<div
61+
role="button"
62+
onClick={onClickNext}
63+
onKeyPress={() => { }}
64+
className="btn-link"
65+
tabIndex="0"
66+
>
67+
{next.title}
68+
<span className="pl-2">{'>>'}</span>
69+
</div>
70+
)}
71+
</div>
72+
</div>
73+
);
74+
};
75+
76+
export default ScheduleNavigationTab;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
3+
import cn from 'classnames';
4+
import capitalize from 'lodash/capitalize';
5+
6+
import { getRankingPoints, getTasksCount, grades } from '@/config/grades';
7+
8+
const getGradeDescriptionClassName = highlight => (
9+
cn(
10+
'd-flex flex-column flex-lg-row flex-md-row flex-sm-row justify-content-between',
11+
{
12+
'text-monospace': highlight,
13+
},
14+
)
15+
);
16+
17+
const GradeInfo = ({ grade, selected }) => (
18+
<div className={getGradeDescriptionClassName(grade === selected)}>
19+
<span className={grade === selected ? 'text-white' : ''}>
20+
{capitalize(grade)}
21+
{grade === selected && '(*)'}
22+
</span>
23+
<span className={cn('pl-3', { 'text-white': grade === selected })}>
24+
[
25+
{getRankingPoints(grade).join(', ')}
26+
]
27+
</span>
28+
</div>
29+
);
30+
31+
const TournamentDescription = ({
32+
className,
33+
tournament,
34+
}) => (
35+
<div className={className}>
36+
{tournament.grade !== grades.open ? (
37+
<>
38+
<span className="text-white">Tournament Highlights:</span>
39+
<div className="d-flex flex-column">
40+
<span>Prizes: Codebattle T-shirt merch for a top-tier of League</span>
41+
<span>{`Challenges: ${getTasksCount(tournament.grade)} unique algorithm problems`}</span>
42+
<span>Impact: Advancing in the Codebattle programmer rankings</span>
43+
</div>
44+
<div className="d-flex justify-content-center w-100">
45+
<div className="card cb-card mt-2">
46+
<div className="card-header text-center">View League Ranking Points System</div>
47+
<div className="card-body">
48+
{[grades.rookie, grades.challenger, grades.pro, grades.elite, grades.masters, grades.grandSlam].map(grade => (
49+
<GradeInfo grade={grade} selected={tournament.grade} />
50+
))}
51+
</div>
52+
</div>
53+
</div>
54+
</>
55+
) : (
56+
<>
57+
<span className="text-white">Tournament Description:</span>
58+
{tournament.description}
59+
</>
60+
)}
61+
</div>
62+
);
63+
64+
export default TournamentDescription;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
3+
import { getRankingPoints, grades } from '@/config/grades';
4+
5+
import dayjs from '../../i18n/dayjs';
6+
7+
import TournamentTimer from './TournamentTimer';
8+
9+
const TournamentPreviewPanel = ({
10+
className,
11+
tournament,
12+
start,
13+
end,
14+
}) => (
15+
<div className={className}>
16+
<div className="d-flex flex-column cb-bg-panel cb-rounded p-3">
17+
<span>{`Start Date: ${dayjs(start).format('MMMM DD, YYYY')}`}</span>
18+
<span>{`Time: ${dayjs(start).format('hh:mm A')} - ${dayjs(end).format('hh:mm A')}`}</span>
19+
{tournament.grade !== grades.open
20+
&& <span>{`First Place Points: ${getRankingPoints(tournament.grade)[0]} Ranking Points`}</span>}
21+
<span><TournamentTimer date={start} label="Starts in: " /></span>
22+
</div>
23+
</div>
24+
);
25+
26+
export default TournamentPreviewPanel;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
import dayjs from '../../i18n/dayjs';
4+
5+
const TournamentTimer = ({ date = new Date(), label, children }) => {
6+
const [duration, setDuration] = useState(0);
7+
const [stoped, setStoped] = useState(0);
8+
9+
useEffect(() => {
10+
if (stoped) {
11+
return () => { };
12+
}
13+
14+
const interval = setInterval(() => {
15+
setDuration(dayjs(date).diff(dayjs()));
16+
}, 100);
17+
18+
return () => {
19+
clearInterval(interval);
20+
};
21+
}, [date, stoped, setDuration]);
22+
23+
if (stoped || duration > 1000 * 60 * 60 * 24) {
24+
return <>{children}</>;
25+
}
26+
27+
if (duration < 0) {
28+
setStoped(true);
29+
return <>{children}</>;
30+
}
31+
32+
return (
33+
<>
34+
{label}
35+
{' '}
36+
<span className="text-monospace">{dayjs.duration(duration).format('HH:mm:ss')}</span>
37+
</>
38+
);
39+
};
40+
41+
export default TournamentTimer;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const grades = {
2+
open: 'open',
3+
rookie: 'rookie',
4+
challenger: 'challenger',
5+
pro: 'pro',
6+
elite: 'elite',
7+
masters: 'masters',
8+
grandSlam: 'grand_slam',
9+
};
10+
11+
export const getRankingPoints = grade => {
12+
switch (grade) {
13+
case grades.rookie: return [8, 4, 2];
14+
case grades.challenger: return [16, 8, 4, 2];
15+
case grades.pro: return [128, 64, 32, 16, 8, 4, 2];
16+
case grades.elite: return [256, 128, 64, 32, 16, 8, 4, 2];
17+
case grades.masters: return [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2];
18+
case grades.grandSlam: return [2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2];
19+
default: return [0];
20+
}
21+
};
22+
23+
export const getTasksCount = grade => {
24+
switch (grade) {
25+
case grades.rookie: return 4;
26+
case grades.challenger: return 6;
27+
case grades.pro: return 8;
28+
case grades.elite: return 10;
29+
case grades.masters: return 12;
30+
case grades.grandSlam: return 14;
31+
default: return 0;
32+
}
33+
};

services/app/apps/codebattle/assets/js/widgets/config/modalCodes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const modalCodes = {
99
awardModal: 'award_modal',
1010
eventStageModal: 'event_stage_modal',
1111
calendarEventModal: 'calendar_event_modal',
12+
tournamentModal: 'tournament_modal',
1213
};
1314

1415
export default modalCodes;

services/app/apps/codebattle/assets/js/widgets/pages/game/InfoWidget.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ function InfoWidget({ viewMode }) {
161161
const task = useSelector(isTestingRoom ? builderTaskSelector : gameTaskSelector);
162162
const { outputData, canShowOutput } = usePlayerOutputForInfoPanel(viewMode, roomMachineState);
163163

164-
if (task.type === 'css') {
164+
if (task?.type === 'css') {
165165
return (
166166
<CssBattleInfoWidget
167167
viewMode={viewMode}

services/app/apps/codebattle/assets/js/widgets/pages/lobby/LobbyWidget.jsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import Gon from 'gon';
77
import Modal from 'react-bootstrap/Modal';
88
import { useDispatch, useSelector } from 'react-redux';
99

10-
import * as lobbyMiddlewares from '../../middlewares/Lobby';
11-
import * as selectors from '../../selectors';
12-
import { actions } from '../../slices';
13-
import { getLobbyUrl, makeGameUrl } from '../../utils/urlBuilders';
10+
import * as lobbyMiddlewares from '@/middlewares/Lobby';
11+
import * as selectors from '@/selectors';
12+
import { actions } from '@/slices';
13+
import { getLobbyUrl, makeGameUrl } from '@/utils/urlBuilders';
14+
import useLobbyModals from '@/utils/useLobbyModals';
1415

1516
import ActiveGames from './ActiveGames';
1617
import Announcement from './Announcement';
@@ -147,6 +148,8 @@ const LobbyWidget = () => {
147148
// eslint-disable-next-line react-hooks/exhaustive-deps
148149
}, []);
149150

151+
useLobbyModals();
152+
150153
return (
151154
<div>
152155
<Modal

0 commit comments

Comments
 (0)