Skip to content

Commit 3bfd3de

Browse files
authored
Merge pull request #2230 from hexlet-codebattle/add_season_opponents_info
add season opponents info
2 parents 2d2db30 + 0f1b02b commit 3bfd3de

File tree

11 files changed

+222
-26
lines changed

11 files changed

+222
-26
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
$item-bg: #15202B;
2+
3+
.cb-text-skeleton {
4+
animation: skeleton-loading 1s linear infinite alternate;
5+
6+
height: 1.2rem;
7+
border-radius: 0.25rem;
8+
background-color: lighten($item-bg, 7%);
9+
}
10+
11+
@keyframes skeleton-loading {
12+
0% {
13+
opacity: .2
14+
}
15+
16+
80%,
17+
100% {
18+
opacity: 1;
19+
}
20+
}

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ $cb-text-color: #999;
44
$cb-border-radius: 0.5rem;
55
$cb-success: #32CD32;
66
$cb-hovered-success: #28a428;
7+
$cb-border-radius: 0.5rem;
78

89
$cb-secondary: #3a3f50;
910
$cb-secondary-focus-background: #3a3f50;
@@ -64,6 +65,7 @@ $cb-grand-slam-bg: rgba(238, 55, 55, 1.0);
6465
@import '~bootstrap/scss/bootstrap';
6566
@import '~nprogress/nprogress.css';
6667
@import 'gamePreview';
68+
@import 'skeleton';
6769
@import 'custom';
6870
@import '~react-contexify/dist/ReactContexify.css';
6971
@import 'react-big-calendar/lib/css/react-big-calendar.css';
@@ -2037,20 +2039,26 @@ a:hover {
20372039
display: block;
20382040
}
20392041

2040-
/* Stats Grid - The main data display */
2041-
.stats-grid {}
2042-
20432042
.stat-item {
20442043
text-align: center;
20452044
border-right: 1px solid #3a3a45;
20462045
/* Vertical dividers */
20472046
padding: 0 5px;
20482047
}
20492048

2049+
.stat-line {
2050+
border-bottom: 1px solid #3a3a45;
2051+
}
2052+
20502053
.stat-item:last-child {
20512054
border-right: none;
20522055
}
20532056

2057+
2058+
.stat-line:last-child {
2059+
border-bottom: none;
2060+
}
2061+
20542062
.stat-value {
20552063
font-size: 20px;
20562064
font-weight: 700;
@@ -2067,10 +2075,28 @@ a:hover {
20672075
}
20682076

20692077
.cb-rounded {
2070-
border-radius: 0.5rem;
2078+
border-radius: $cb-border-radius;
2079+
}
2080+
2081+
.cb-rounded-top {
2082+
border-top-left-radius: $cb-border-radius;
2083+
border-top-right-radius: $cb-border-radius;
2084+
}
2085+
2086+
.cb-rounded-bottom {
2087+
border-top-left-radius: $cb-border-radius;
2088+
border-top-right-radius: $cb-border-radius;
20712089
}
20722090

2091+
.cb-rounded-left {
2092+
border-top-left-radius: $cb-border-radius;
2093+
border-bottom-left-radius: $cb-border-radius;
2094+
}
20732095

2096+
.cb-rounded-right {
2097+
border-top-right-radius: $cb-border-radius;
2098+
border-bottom-right-radius: $cb-border-radius;
2099+
}
20742100

20752101
.cb-bg-panel {
20762102
background-color: $cb-bg-panel;

services/app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const preloadedState = {
129129
presenceList: players,
130130
liveTournaments: [],
131131
completedTournaments: [],
132+
opponents: [],
132133
joinGameModal: {
133134
show: false,
134135
},

services/app/apps/codebattle/assets/js/widgets/middlewares/Users.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export const loadUserStats = dispatch => async user => {
2525
}
2626
};
2727

28+
export const loadUserOpponents = (abortController, onSuccess, onFailure) => {
29+
axios
30+
.get('/api/v1/user/opponents', { signal: abortController.signal })
31+
.then(camelizeKeys)
32+
.then(onSuccess)
33+
.catch(onFailure);
34+
};
35+
2836
export const loadSimpleUserStats = (onSuccess, onFailure) => user => {
2937
axios
3038
.get(`/api/v1/user/${user.id}/simple_stats`)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const LobbyWidget = () => {
9090
seasonTournaments,
9191
// completedTournaments,
9292
presenceList,
93+
opponents,
9394
channel: { online },
9495
} = useSelector(selectors.lobbyDataSelector);
9596

@@ -191,6 +192,7 @@ const LobbyWidget = () => {
191192
liveTournaments={liveTournaments}
192193
seasonTournaments={seasonTournaments}
193194
user={currentUser}
195+
opponents={opponents}
194196
controls={(
195197
<div className="d-flex flex-column mt-2">
196198
<div className="d-flex w-100">

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

Lines changed: 128 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,150 @@
11
import React, { useState, useEffect } from 'react';
22

33
import axios from 'axios';
4+
import cn from 'classnames';
45
import { camelizeKeys } from 'humps';
5-
import { useSelector } from 'react-redux';
6+
import { useDispatch, useSelector } from 'react-redux';
67

8+
import { loadUserOpponents } from '@/middlewares/Users';
79
import {
810
selectDefaultAvatarUrl,
911
currentUserIsAdminSelector,
12+
userByIdSelector,
1013
} from '@/selectors';
1114

1215
import i18n from '../../../i18n';
16+
import { actions } from '../../slices';
1317

1418
import CodebattleLeagueDescription from './CodebattleLeagueDescription';
1519
import TournamentListItem, { activeIcon } from './TournamentListItem';
1620

1721
const contestDatesText = 'Season: Oct 16 - Dec 21';
1822

23+
const OpponentInfo = ({ id }) => {
24+
const user = useSelector(userByIdSelector(id));
25+
26+
return (
27+
<div className="d-flex py-2 mx-1 stat-line">
28+
<div className="d-flex align-items-center w-100">
29+
<UserLogo user={user} size="25px" />
30+
<span
31+
title={user?.name}
32+
className={
33+
cn(
34+
'text-white text-truncate ml-2',
35+
{ 'cb-text-skeleton w-100': !user },
36+
)
37+
}
38+
style={{ maxWidth: '70px' }}
39+
>
40+
{user?.name}
41+
</span>
42+
</div>
43+
<div className="d-flex flex-column text-center py-1 w-100">
44+
<span
45+
className={
46+
cn(
47+
'stat-value d-block cb-text-danger',
48+
{ 'd-inline cb-text-skeleton w-25 mx-auto': !user },
49+
)
50+
}
51+
>
52+
{user ? user.rank : ''}
53+
</span>
54+
<span className="stat-label text-uppercase">Place</span>
55+
</div>
56+
<div className="d-flex flex-column text-center py-1 w-100">
57+
<span
58+
className={
59+
cn(
60+
'stat-value d-block cb-text-danger',
61+
{ 'd-inline cb-text-skeleton w-25 mx-auto': !user },
62+
)
63+
}
64+
>
65+
{user ? user.points : ''}
66+
</span>
67+
<span className="stat-label text-uppercase">Points</span>
68+
</div>
69+
</div>
70+
);
71+
};
72+
73+
const SeasonOpponents = ({ user, opponents }) => {
74+
const dispatch = useDispatch();
75+
const [loading, setLoading] = useState(!!user.points);
76+
77+
useEffect(() => {
78+
if (!user.points) {
79+
const abortController = new AbortController();
80+
81+
const onSuccess = payload => {
82+
if (!abortController.signal.aborted) {
83+
dispatch(actions.setOpponents(payload.data));
84+
dispatch(actions.updateUsers(payload.data));
85+
setLoading(false);
86+
}
87+
};
88+
const onError = () => {
89+
setLoading(false);
90+
};
91+
92+
setLoading(true);
93+
loadUserOpponents(abortController, onSuccess, onError);
94+
95+
return abortController.abort;
96+
}
97+
98+
return () => { };
99+
}, [dispatch, setLoading, user?.points]);
100+
101+
if (!user.points || (!loading && opponents.length === 0)) {
102+
return <></>;
103+
}
104+
105+
return (
106+
<div className="cb-bg-panel cb-rounded mt-2">
107+
<div className="d-flex flex-column">
108+
<div className="cb-bg-highlight-panel text-center cb-rounded-top">
109+
<span className="text-white text-uppercase p-1 pt-2">Closest opponents</span>
110+
</div>
111+
{loading ? (
112+
<>
113+
<OpponentInfo />
114+
<OpponentInfo />
115+
</>
116+
) : opponents.map(id => <OpponentInfo id={id} />)}
117+
</div>
118+
</div>
119+
);
120+
};
121+
19122
const UserLogo = ({ user, size = '70px' }) => {
20123
const [userInfo, setUserInfo] = useState();
21124
const defaultAvatarUrl = useSelector(selectDefaultAvatarUrl);
22-
const avatarUrl = user.avatarUrl || userInfo?.avatarUrl || defaultAvatarUrl;
125+
const avatarUrl = user?.avatarUrl || userInfo?.avatarUrl || defaultAvatarUrl;
23126

24127
useEffect(() => {
25-
const userId = user.id;
26-
const controller = new AbortController();
27-
28-
axios
29-
.get(`/api/v1/user/${userId}/stats`, {
30-
signal: controller.signal,
31-
})
32-
.then(response => {
33-
if (!controller.signal.aborted) {
34-
setUserInfo(camelizeKeys(response.data.user));
35-
}
36-
});
128+
if (user) {
129+
const userId = user.id;
130+
const controller = new AbortController();
131+
132+
axios
133+
.get(`/api/v1/user/${userId}/stats`, {
134+
signal: controller.signal,
135+
})
136+
.then(response => {
137+
if (!controller.signal.aborted) {
138+
setUserInfo(camelizeKeys(response.data.user));
139+
}
140+
});
141+
142+
return controller.abort;
143+
}
37144

38-
return () => {
39-
controller.abort();
40-
};
41-
}, [setUserInfo, user.id]);
145+
return () => { };
146+
// eslint-disable-next-line
147+
}, [setUserInfo, user?.id]);
42148

43149
return (
44150
<img
@@ -53,6 +159,7 @@ const UserLogo = ({ user, size = '70px' }) => {
53159
const SeasonProfilePanel = ({
54160
seasonTournaments = [],
55161
liveTournaments = [],
162+
opponents,
56163
user,
57164
controls,
58165
}) => {
@@ -133,7 +240,7 @@ const SeasonProfilePanel = ({
133240
</div>
134241
<div className="col-12 col-lg-4 col-md-4 d-flex flex-column my-2 my-lg-0 my-md-0">
135242
<div className="cb-bg-panel cb-rounded">
136-
<div className="text-center p-2 py-3">
243+
<div className="text-center py-2">
137244
<UserLogo user={user} />
138245
<span className="clan-tag mt-2">{user.name}</span>
139246
<span className="h1 clan-title m-0 text-white text-uppercase">
@@ -175,10 +282,11 @@ const SeasonProfilePanel = ({
175282
</div>
176283
</div>
177284

178-
<div className="d-flex justify-content-center cb-font-size-small py-2 px-3 text-white">
285+
<div className="d-flex justify-content-center cb-font-size-small px-3 py-2 text-white">
179286
<span className="d-block">{contestDatesText}</span>
180287
</div>
181288
</div>
289+
<SeasonOpponents user={user} opponents={opponents} />
182290
{controls}
183291
</div>
184292
</div>

services/app/apps/codebattle/assets/js/widgets/pages/profile/UserProfile.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Achievement from './Achievement';
1515
import Heatmap from './Heatmap';
1616
import UserStatCharts from './UserStatCharts';
1717

18-
function HolipinTags({ name }) {
18+
function HolopinTags({ name }) {
1919
return (
2020
name && (
2121
<div className="row mt-5 mb-md-3 mb-lg-4 mt-lg-0">
@@ -179,7 +179,7 @@ function UserProfile() {
179179
<Heatmap />
180180
</div>
181181
</div>
182-
<HolipinTags />
182+
<HolopinTags name={user?.githubName} />
183183
</div>
184184
<div
185185
className="tab-pane fade min-h-100"

services/app/apps/codebattle/assets/js/widgets/slices/lobby.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const initialState = {
1616
completedTournaments: initial.completedTournaments,
1717
seasonProfile: initial.seasonProfile,
1818
presenceList: [],
19+
opponents: [],
1920
newGame: { timeoutSeconds: null },
2021
joinGameModal: {
2122
show: false,
@@ -129,6 +130,9 @@ const lobby = createSlice({
129130
updateMainChannelState: (state, { payload }) => {
130131
state.mainChannel.online = payload;
131132
},
133+
setOpponents: (state, { payload }) => {
134+
state.opponents = payload.users.map(u => u.id);
135+
},
132136
},
133137
extraReducers: {
134138
[tournamentActions.changeTournamentState]: (state, { payload }) => {

services/app/apps/codebattle/lib/codebattle/user.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ defmodule Codebattle.User do
188188
|> Repo.all()
189189
end
190190

191+
@spec get_users_by_ranks(list(integer())) :: list(t())
192+
def get_users_by_ranks(ranks) do
193+
__MODULE__
194+
|> where([u], u.rank in ^ranks)
195+
|> order_by([u], {:asc, :rank})
196+
|> Repo.all()
197+
end
198+
191199
def search_users(query) do
192200
__MODULE__
193201
|> where([u], u.is_bot == false)

0 commit comments

Comments
 (0)