From 55a9417bddbb89005d58bdb70d0d97d2590aea9f Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Mon, 27 Oct 2025 16:23:37 +0300 Subject: [PATCH 1/3] add season opponents info --- .../apps/codebattle/assets/css/skeleton.scss | 20 +++ .../app/apps/codebattle/assets/css/style.scss | 34 +++- .../assets/js/widgets/middlewares/Users.js | 8 + .../js/widgets/pages/lobby/LobbyWidget.jsx | 2 + .../pages/lobby/SeasonProfilePanel.jsx | 148 +++++++++++++++--- .../js/widgets/pages/profile/UserProfile.jsx | 4 +- .../assets/js/widgets/slices/lobby.js | 4 + .../apps/codebattle/lib/codebattle/user.ex | 8 + .../controllers/api/v1/user_controller.ex | 18 +++ .../codebattle/lib/codebattle_web/router.ex | 1 + 10 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 services/app/apps/codebattle/assets/css/skeleton.scss 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/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..0006fff21 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 = ({
-
+
{contestDatesText}
+ {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) From 7a7112d1523ca8d2fa1e84c380fdd9a8cdcde664 Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Mon, 27 Oct 2025 16:31:55 +0300 Subject: [PATCH 2/3] fix test --- .../app/apps/codebattle/assets/js/__tests__/LobbyWidget.test.jsx | 1 + 1 file changed, 1 insertion(+) 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, }, From 0f1b02bef8ce2abca935bdce577d7a167130cac7 Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Mon, 27 Oct 2025 16:34:16 +0300 Subject: [PATCH 3/3] fixes --- .../assets/js/widgets/pages/lobby/SeasonProfilePanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0006fff21..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 @@ -98,7 +98,7 @@ const SeasonOpponents = ({ user, opponents }) => { return () => { }; }, [dispatch, setLoading, user?.points]); - if (user.points || (!loading && opponents.length === 0)) { + if (!user.points || (!loading && opponents.length === 0)) { return <>; }