Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions services/app/apps/codebattle/assets/css/skeleton.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
34 changes: 30 additions & 4 deletions services/app/apps/codebattle/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -2037,20 +2039,26 @@ a:hover {
display: block;
}

/* Stats Grid - The main data display */
.stats-grid {}

.stat-item {
text-align: center;
border-right: 1px solid #3a3a45;
/* Vertical dividers */
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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const preloadedState = {
presenceList: players,
liveTournaments: [],
completedTournaments: [],
opponents: [],
joinGameModal: {
show: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const LobbyWidget = () => {
seasonTournaments,
// completedTournaments,
presenceList,
opponents,
channel: { online },
} = useSelector(selectors.lobbyDataSelector);

Expand Down Expand Up @@ -191,6 +192,7 @@ const LobbyWidget = () => {
liveTournaments={liveTournaments}
seasonTournaments={seasonTournaments}
user={currentUser}
opponents={opponents}
controls={(
<div className="d-flex flex-column mt-2">
<div className="d-flex w-100">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="d-flex py-2 mx-1 stat-line">
<div className="d-flex align-items-center w-100">
<UserLogo user={user} size="25px" />
<span
title={user?.name}
className={
cn(
'text-white text-truncate ml-2',
{ 'cb-text-skeleton w-100': !user },
)
}
style={{ maxWidth: '70px' }}
>
{user?.name}
</span>
</div>
<div className="d-flex flex-column text-center py-1 w-100">
<span
className={
cn(
'stat-value d-block cb-text-danger',
{ 'd-inline cb-text-skeleton w-25 mx-auto': !user },
)
}
>
{user ? user.rank : ''}
</span>
<span className="stat-label text-uppercase">Place</span>
</div>
<div className="d-flex flex-column text-center py-1 w-100">
<span
className={
cn(
'stat-value d-block cb-text-danger',
{ 'd-inline cb-text-skeleton w-25 mx-auto': !user },
)
}
>
{user ? user.points : ''}
</span>
<span className="stat-label text-uppercase">Points</span>
</div>
</div>
);
};

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 (
<div className="cb-bg-panel cb-rounded mt-2">
<div className="d-flex flex-column">
<div className="cb-bg-highlight-panel text-center cb-rounded-top">
<span className="text-white text-uppercase p-1 pt-2">Closest opponents</span>
</div>
{loading ? (
<>
<OpponentInfo />
<OpponentInfo />
</>
) : opponents.map(id => <OpponentInfo id={id} />)}
</div>
</div>
);
};

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 (
<img
Expand All @@ -53,6 +159,7 @@ const UserLogo = ({ user, size = '70px' }) => {
const SeasonProfilePanel = ({
seasonTournaments = [],
liveTournaments = [],
opponents,
user,
controls,
}) => {
Expand Down Expand Up @@ -133,7 +240,7 @@ const SeasonProfilePanel = ({
</div>
<div className="col-12 col-lg-4 col-md-4 d-flex flex-column my-2 my-lg-0 my-md-0">
<div className="cb-bg-panel cb-rounded">
<div className="text-center p-2 py-3">
<div className="text-center py-2">
<UserLogo user={user} />
<span className="clan-tag mt-2">{user.name}</span>
<span className="h1 clan-title m-0 text-white text-uppercase">
Expand Down Expand Up @@ -175,10 +282,11 @@ const SeasonProfilePanel = ({
</div>
</div>

<div className="d-flex justify-content-center cb-font-size-small py-2 px-3 text-white">
<div className="d-flex justify-content-center cb-font-size-small px-3 py-2 text-white">
<span className="d-block">{contestDatesText}</span>
</div>
</div>
<SeasonOpponents user={user} opponents={opponents} />
{controls}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Achievement from './Achievement';
import Heatmap from './Heatmap';
import UserStatCharts from './UserStatCharts';

function HolipinTags({ name }) {
function HolopinTags({ name }) {
return (
name && (
<div className="row mt-5 mb-md-3 mb-lg-4 mt-lg-0">
Expand Down Expand Up @@ -179,7 +179,7 @@ function UserProfile() {
<Heatmap />
</div>
</div>
<HolipinTags />
<HolopinTags name={user?.githubName} />
</div>
<div
className="tab-pane fade min-h-100"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const initialState = {
completedTournaments: initial.completedTournaments,
seasonProfile: initial.seasonProfile,
presenceList: [],
opponents: [],
newGame: { timeoutSeconds: null },
joinGameModal: {
show: false,
Expand Down Expand Up @@ -129,6 +130,9 @@ const lobby = createSlice({
updateMainChannelState: (state, { payload }) => {
state.mainChannel.online = payload;
},
setOpponents: (state, { payload }) => {
state.opponents = payload.users.map(u => u.id);
},
},
extraReducers: {
[tournamentActions.changeTournamentState]: (state, { payload }) => {
Expand Down
8 changes: 8 additions & 0 deletions services/app/apps/codebattle/lib/codebattle/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading