180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
import { useEffect, useState } from 'react';
|
||
import { Link } from 'react-router';
|
||
import { Spinner } from 'react-bootstrap';
|
||
import { signedGet, isSuccess } from '@/api/client';
|
||
import { asArray, rankRoleId, rankRoleName, rankRolePillLabel, userInitials } from '@/views/dashboard/helpers';
|
||
import EmptyChart from '@/views/dashboard/EmptyChart';
|
||
|
||
function podiumUsersFromRankRole(rankRole) {
|
||
return asArray(rankRole?.users)
|
||
.slice(0, 3)
|
||
.map((row) => {
|
||
const info = row.user || {};
|
||
const name = String(info.name || '').trim();
|
||
return {
|
||
name,
|
||
initials: userInitials(name),
|
||
avatar: info.avatar || '',
|
||
score: Number.isFinite(Number(row.score)) ? Number(row.score) : 0,
|
||
};
|
||
});
|
||
}
|
||
|
||
function Podium({ users }) {
|
||
const slots = [users[1] || null, users[0] || null, users[2] || null];
|
||
const ranks = [2, 1, 3];
|
||
return (
|
||
<div className="mlb-podium-row">
|
||
{slots.map((user, index) => {
|
||
const rank = ranks[index];
|
||
return (
|
||
<div key={rank} className={`mlb-col ${rank === 1 ? 'is-first' : ''} ${user ? '' : 'is-empty'}`}>
|
||
{user ? (
|
||
<>
|
||
<div className="mlb-avatar-wrap">
|
||
{rank === 1 ? (
|
||
<span className="mlb-crown" aria-hidden="true">
|
||
<svg width="26" height="18" viewBox="0 0 26 18" fill="none">
|
||
<path d="M3 15.5L1 5l6.2 4L13 1l5.8 8L25 5l-2 10.5H3z" fill="#F0C94A" />
|
||
<path d="M4 15.5h18v2H4v-2z" fill="#E3B341" />
|
||
</svg>
|
||
</span>
|
||
) : null}
|
||
<div className={`mlb-avatar rank-${rank}`}>
|
||
{user.avatar && !String(user.avatar).includes('ui-avatars.com') ? (
|
||
<img src={user.avatar} alt="" />
|
||
) : (
|
||
user.initials
|
||
)}
|
||
</div>
|
||
<span className={`mlb-badge rank-${rank}`}>{rank}</span>
|
||
</div>
|
||
<div className="mlb-name" title={user.name}>
|
||
{user.name}
|
||
</div>
|
||
<div className={`mlb-bar rank-${rank}`}>{user.score}</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="mlb-name">-</div>
|
||
<div className={`mlb-bar rank-${rank}`}>-</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LeaderboardCard({ item }) {
|
||
const [activeIndex, setActiveIndex] = useState(0);
|
||
const pills = item.pills || [];
|
||
const active = pills[activeIndex] || pills[0];
|
||
const users = active?.podium_users || item.podium_users || [];
|
||
const detailsUrl = active?.url || item.details_url || '#';
|
||
|
||
return (
|
||
<div className="col-6">
|
||
<article className="mlb-card">
|
||
<div className="mlb-card-head">
|
||
<h3 className="mlb-card-title">{item.title}</h3>
|
||
<div className="mlb-card-meta">
|
||
<p className="mlb-card-date">
|
||
<span>{item.interval_label || '—'}</span>
|
||
</p>
|
||
<Link className="mlb-card-details" to={detailsUrl}>
|
||
View details
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
<div className="mlb-podium">
|
||
<Podium users={users} />
|
||
</div>
|
||
{pills.length ? (
|
||
<div className="mlb-pills">
|
||
{pills.map((pill, index) => (
|
||
<button
|
||
key={pill.label}
|
||
type="button"
|
||
className={`mlb-pill ${index === activeIndex ? 'is-active' : ''}`}
|
||
onClick={() => setActiveIndex(index)}
|
||
>
|
||
{pill.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</article>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function MilestoneTeamLeaderboard({ widget }) {
|
||
const [items, setItems] = useState(null);
|
||
|
||
useEffect(() => {
|
||
const load = async () => {
|
||
const response = await signedGet('reports', 'roles_leaderboard/list', {}, { timeout: 120000 });
|
||
if (!isSuccess(response)) {
|
||
setItems([]);
|
||
return;
|
||
}
|
||
let leaderboards = response.data;
|
||
if (leaderboards?.items) leaderboards = leaderboards.items;
|
||
leaderboards = asArray(leaderboards);
|
||
const mapped = leaderboards.map((board) => {
|
||
const rankRoles = asArray(board.rank_roles);
|
||
const pills = rankRoles.map((rankRole, index) => {
|
||
const limit = Number(rankRole.limit) > 0 ? Number(rankRole.limit) : 10;
|
||
const roleId = rankRoleId(rankRole);
|
||
return {
|
||
label: rankRolePillLabel(rankRole.title, limit, rankRoleName(rankRole)),
|
||
url: `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}&role=${encodeURIComponent(roleId)}`,
|
||
podium_users: podiumUsersFromRankRole(rankRole),
|
||
active: index === 0,
|
||
};
|
||
});
|
||
const start = board.validity_start || '';
|
||
const end = board.validity_end || '';
|
||
return {
|
||
id: board.id,
|
||
title: board.name || widget.title || 'Leaderboard',
|
||
interval_label: `${start}${start && end ? ' – ' : ''}${end}`.trim(),
|
||
details_url: pills[0]?.url || `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}`,
|
||
podium_users: pills[0]?.podium_users || [],
|
||
pills,
|
||
};
|
||
});
|
||
setItems(mapped);
|
||
};
|
||
load();
|
||
}, [widget.title]);
|
||
|
||
return (
|
||
<div className="mb-3">
|
||
<div className="row">
|
||
<div className="col-12">
|
||
<div className="page-title-box">
|
||
<h4 className="page-title">{widget.title || 'Leaderboard'}</h4>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{items === null ? (
|
||
<div className="text-center py-4">
|
||
<Spinner animation="border" />
|
||
<p className="text-muted mt-2 mb-0">Loading leaderboards...</p>
|
||
</div>
|
||
) : items.length === 0 ? (
|
||
<EmptyChart type="radialBar" label="No data" />
|
||
) : (
|
||
<div className="row">
|
||
{items.map((item) => (
|
||
<LeaderboardCard key={item.id} item={item} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|