diff --git a/services/app/apps/codebattle/assets/css/skeleton.scss b/services/app/apps/codebattle/assets/css/skeleton.scss
new file mode 100644
index 000000000..776c496ed
--- /dev/null
+++ b/services/app/apps/codebattle/assets/css/skeleton.scss
@@ -0,0 +1,20 @@
+$item-bg: #15202B;
+
+.cb-text-skeleton {
+ animation: skeleton-loading 1s linear infinite alternate;
+
+ height: 1.2rem;
+ border-radius: 0.25rem;
+ background-color: lighten($item-bg, 7%);
+}
+
+@keyframes skeleton-loading {
+ 0% {
+ opacity: .2
+ }
+
+ 80%,
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/services/app/apps/codebattle/assets/css/style.scss b/services/app/apps/codebattle/assets/css/style.scss
index 66c54ad59..373f893ba 100644
--- a/services/app/apps/codebattle/assets/css/style.scss
+++ b/services/app/apps/codebattle/assets/css/style.scss
@@ -4,6 +4,7 @@ $cb-text-color: #999;
$cb-border-radius: 0.5rem;
$cb-success: #32CD32;
$cb-hovered-success: #28a428;
+$cb-border-radius: 0.5rem;
$cb-secondary: #3a3f50;
$cb-secondary-focus-background: #3a3f50;
@@ -64,6 +65,7 @@ $cb-grand-slam-bg: rgba(238, 55, 55, 1.0);
@import '~bootstrap/scss/bootstrap';
@import '~nprogress/nprogress.css';
@import 'gamePreview';
+@import 'skeleton';
@import 'custom';
@import '~react-contexify/dist/ReactContexify.css';
@import 'react-big-calendar/lib/css/react-big-calendar.css';
@@ -2037,9 +2039,6 @@ a:hover {
display: block;
}
-/* Stats Grid - The main data display */
-.stats-grid {}
-
.stat-item {
text-align: center;
border-right: 1px solid #3a3a45;
@@ -2047,10 +2046,19 @@ a:hover {
padding: 0 5px;
}
+.stat-line {
+ border-bottom: 1px solid #3a3a45;
+}
+
.stat-item:last-child {
border-right: none;
}
+
+.stat-line:last-child {
+ border-bottom: none;
+}
+
.stat-value {
font-size: 20px;
font-weight: 700;
@@ -2067,10 +2075,28 @@ a:hover {
}
.cb-rounded {
- border-radius: 0.5rem;
+ border-radius: $cb-border-radius;
+}
+
+.cb-rounded-top {
+ border-top-left-radius: $cb-border-radius;
+ border-top-right-radius: $cb-border-radius;
+}
+
+.cb-rounded-bottom {
+ border-top-left-radius: $cb-border-radius;
+ border-top-right-radius: $cb-border-radius;
}
+.cb-rounded-left {
+ border-top-left-radius: $cb-border-radius;
+ border-bottom-left-radius: $cb-border-radius;
+}
+.cb-rounded-right {
+ border-top-right-radius: $cb-border-radius;
+ border-bottom-right-radius: $cb-border-radius;
+}
.cb-bg-panel {
background-color: $cb-bg-panel;
diff --git a/services/app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx b/services/app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx
index 98b38061a..badc977f9 100644
--- a/services/app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx
+++ b/services/app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx
@@ -129,6 +129,7 @@ const preloadedState = {
presenceList: players,
liveTournaments: [],
completedTournaments: [],
+ opponents: [],
joinGameModal: {
show: false,
},
diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Users.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Users.js
index 1304793e2..2a0541d9c 100644
--- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Users.js
+++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Users.js
@@ -25,6 +25,14 @@ export const loadUserStats = dispatch => async user => {
}
};
+export const loadUserOpponents = (abortController, onSuccess, onFailure) => {
+ axios
+ .get('/api/v1/user/opponents', { signal: abortController.signal })
+ .then(camelizeKeys)
+ .then(onSuccess)
+ .catch(onFailure);
+};
+
export const loadSimpleUserStats = (onSuccess, onFailure) => user => {
axios
.get(`/api/v1/user/${user.id}/simple_stats`)
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/lobby/LobbyWidget.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/LobbyWidget.jsx
index bbe1b797e..5a73dcc44 100644
--- a/services/app/apps/codebattle/assets/js/widgets/pages/lobby/LobbyWidget.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/LobbyWidget.jsx
@@ -90,6 +90,7 @@ const LobbyWidget = () => {
seasonTournaments,
// completedTournaments,
presenceList,
+ opponents,
channel: { online },
} = useSelector(selectors.lobbyDataSelector);
@@ -191,6 +192,7 @@ const LobbyWidget = () => {
liveTournaments={liveTournaments}
seasonTournaments={seasonTournaments}
user={currentUser}
+ opponents={opponents}
controls={(
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/lobby/SeasonProfilePanel.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/SeasonProfilePanel.jsx
index 72edb7b7a..63e98c7a6 100644
--- a/services/app/apps/codebattle/assets/js/widgets/pages/lobby/SeasonProfilePanel.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/pages/lobby/SeasonProfilePanel.jsx
@@ -1,44 +1,150 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
+import cn from 'classnames';
import { camelizeKeys } from 'humps';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+import { loadUserOpponents } from '@/middlewares/Users';
import {
selectDefaultAvatarUrl,
currentUserIsAdminSelector,
+ userByIdSelector,
} from '@/selectors';
import i18n from '../../../i18n';
+import { actions } from '../../slices';
import CodebattleLeagueDescription from './CodebattleLeagueDescription';
import TournamentListItem, { activeIcon } from './TournamentListItem';
const contestDatesText = 'Season: Oct 16 - Dec 21';
+const OpponentInfo = ({ id }) => {
+ const user = useSelector(userByIdSelector(id));
+
+ return (
+
+
+
+
+ {user?.name}
+
+
+
+
+ {user ? user.rank : ''}
+
+ Place
+
+
+
+ {user ? user.points : ''}
+
+ Points
+
+
+ );
+};
+
+const SeasonOpponents = ({ user, opponents }) => {
+ const dispatch = useDispatch();
+ const [loading, setLoading] = useState(!!user.points);
+
+ useEffect(() => {
+ if (!user.points) {
+ const abortController = new AbortController();
+
+ const onSuccess = payload => {
+ if (!abortController.signal.aborted) {
+ dispatch(actions.setOpponents(payload.data));
+ dispatch(actions.updateUsers(payload.data));
+ setLoading(false);
+ }
+ };
+ const onError = () => {
+ setLoading(false);
+ };
+
+ setLoading(true);
+ loadUserOpponents(abortController, onSuccess, onError);
+
+ return abortController.abort;
+ }
+
+ return () => { };
+ }, [dispatch, setLoading, user?.points]);
+
+ if (!user.points || (!loading && opponents.length === 0)) {
+ return <>>;
+ }
+
+ return (
+
+
+
+ Closest opponents
+
+ {loading ? (
+ <>
+
+
+ >
+ ) : opponents.map(id =>
)}
+
+
+ );
+};
+
const UserLogo = ({ user, size = '70px' }) => {
const [userInfo, setUserInfo] = useState();
const defaultAvatarUrl = useSelector(selectDefaultAvatarUrl);
- const avatarUrl = user.avatarUrl || userInfo?.avatarUrl || defaultAvatarUrl;
+ const avatarUrl = user?.avatarUrl || userInfo?.avatarUrl || defaultAvatarUrl;
useEffect(() => {
- const userId = user.id;
- const controller = new AbortController();
-
- axios
- .get(`/api/v1/user/${userId}/stats`, {
- signal: controller.signal,
- })
- .then(response => {
- if (!controller.signal.aborted) {
- setUserInfo(camelizeKeys(response.data.user));
- }
- });
+ if (user) {
+ const userId = user.id;
+ const controller = new AbortController();
+
+ axios
+ .get(`/api/v1/user/${userId}/stats`, {
+ signal: controller.signal,
+ })
+ .then(response => {
+ if (!controller.signal.aborted) {
+ setUserInfo(camelizeKeys(response.data.user));
+ }
+ });
+
+ return controller.abort;
+ }
- return () => {
- controller.abort();
- };
- }, [setUserInfo, user.id]);
+ return () => { };
+ // eslint-disable-next-line
+ }, [setUserInfo, user?.id]);
return (
![]()
{
const SeasonProfilePanel = ({
seasonTournaments = [],
liveTournaments = [],
+ opponents,
user,
controls,
}) => {
@@ -133,7 +240,7 @@ const SeasonProfilePanel = ({
-
+
{user.name}
@@ -175,10 +282,11 @@ const SeasonProfilePanel = ({
-
+
{controls}
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/profile/UserProfile.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/profile/UserProfile.jsx
index 7c628c789..b39a1e9be 100644
--- a/services/app/apps/codebattle/assets/js/widgets/pages/profile/UserProfile.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/pages/profile/UserProfile.jsx
@@ -15,7 +15,7 @@ import Achievement from './Achievement';
import Heatmap from './Heatmap';
import UserStatCharts from './UserStatCharts';
-function HolipinTags({ name }) {
+function HolopinTags({ name }) {
return (
name && (
@@ -179,7 +179,7 @@ function UserProfile() {
-
+
{
state.mainChannel.online = payload;
},
+ setOpponents: (state, { payload }) => {
+ state.opponents = payload.users.map(u => u.id);
+ },
},
extraReducers: {
[tournamentActions.changeTournamentState]: (state, { payload }) => {
diff --git a/services/app/apps/codebattle/lib/codebattle/user.ex b/services/app/apps/codebattle/lib/codebattle/user.ex
index 10e3b2c09..5cf03474d 100644
--- a/services/app/apps/codebattle/lib/codebattle/user.ex
+++ b/services/app/apps/codebattle/lib/codebattle/user.ex
@@ -188,6 +188,14 @@ defmodule Codebattle.User do
|> Repo.all()
end
+ @spec get_users_by_ranks(list(integer())) :: list(t())
+ def get_users_by_ranks(ranks) do
+ __MODULE__
+ |> where([u], u.rank in ^ranks)
+ |> order_by([u], {:asc, :rank})
+ |> Repo.all()
+ end
+
def search_users(query) do
__MODULE__
|> where([u], u.is_bot == false)
diff --git a/services/app/apps/codebattle/lib/codebattle_web/controllers/api/v1/user_controller.ex b/services/app/apps/codebattle/lib/codebattle_web/controllers/api/v1/user_controller.ex
index 322be619d..68ad9c072 100644
--- a/services/app/apps/codebattle/lib/codebattle_web/controllers/api/v1/user_controller.ex
+++ b/services/app/apps/codebattle/lib/codebattle_web/controllers/api/v1/user_controller.ex
@@ -55,6 +55,24 @@ defmodule CodebattleWeb.Api.V1.UserController do
json(conn, %{active_game_id: active_game_id, stats: game_stats, user: user})
end
+ def opponents(conn, _) do
+ rank = Map.get(conn.assigns.current_user, :rank, nil)
+
+ if rank == nil do
+ json(conn, %{users: []})
+ else
+ places =
+ if rank == 1 do
+ [2]
+ else
+ [rank + 1, rank - 1]
+ end
+
+ opponents = User.get_users_by_ranks(places)
+ json(conn, %{users: opponents})
+ end
+ end
+
def simple_stats(conn, %{"id" => id}) do
game_stats = Stats.get_game_stats(id)
json(conn, %{stats: game_stats})
diff --git a/services/app/apps/codebattle/lib/codebattle_web/router.ex b/services/app/apps/codebattle/lib/codebattle_web/router.ex
index 806f5471a..938cd262e 100644
--- a/services/app/apps/codebattle/lib/codebattle_web/router.ex
+++ b/services/app/apps/codebattle/lib/codebattle_web/router.ex
@@ -118,6 +118,7 @@ defmodule CodebattleWeb.Router do
get("/:user_id/activity", ActivityController, :show)
get("/game_activity", GameActivityController, :show)
get("/playbook/:id", PlaybookController, :show)
+ get("/user/opponents", UserController, :opponents)
get("/user/:id/stats", UserController, :stats)
get("/user/:id/simple_stats", UserController, :simple_stats)
get("/user/premium_requests", UserController, :premium_requests)