diff --git a/app/Http/Controllers/TeamsController.php b/app/Http/Controllers/TeamsController.php index c58a9311823..228e76d6959 100644 --- a/app/Http/Controllers/TeamsController.php +++ b/app/Http/Controllers/TeamsController.php @@ -7,7 +7,10 @@ namespace App\Http\Controllers; +use App\Exceptions\InvariantException; +use App\Models\Beatmap; use App\Models\Team; +use App\Models\User; use App\Transformers\UserCompactTransformer; use Symfony\Component\HttpFoundation\Response; @@ -27,6 +30,11 @@ public static function pageLinks(string $current, Team $team): array 'title' => osu_trans('teams.header_links.show'), 'url' => route('teams.show', ['team' => $team->getKey()]), ], + [ + 'active' => $current === 'leaderboard', + 'title' => osu_trans('teams.header_links.leaderboard'), + 'url' => route('teams.leaderboard', ['team' => $team->getKey()]), + ], ]; if (priv_check('TeamUpdate', $team)->can()) { @@ -66,6 +74,28 @@ public function edit(string $id): Response return ext_view('teams.edit', compact('team')); } + public function leaderboard(string $id, ?string $ruleset = null): Response + { + $team = Team::findOrFail($id); + $ruleset ??= Beatmap::modeStr($team->default_ruleset_id); + $statisticsRelationName = User::statisticsRelationName($ruleset); + if ($statisticsRelationName === null) { + throw new InvariantException(osu_trans('beatmaps.invalid_ruleset')); + } + $leaderboard = $team + ->members + ->loadMissing("user.{$statisticsRelationName}") + ->map(fn ($member) => + ( + $member->user->$statisticsRelationName + ?? $member->user->$statisticsRelationName()->make() + )->setRelation('user', $member->user)) + ->sortByDesc(['rank_score', 'total_score']) + ->values(); + + return ext_view('teams.leaderboard', compact('leaderboard', 'ruleset', 'team')); + } + public function part(string $id): Response { $team = Team::findOrFail($id); diff --git a/app/helpers.php b/app/helpers.php index 70a7408b80f..f5b8aebe16d 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1299,6 +1299,10 @@ function i18n_date_auto(DateTimeInterface $date, string $skeleton): string function i18n_number_format($number, $style = null, $pattern = null, $precision = null, $locale = null) { + if ($number === null) { + return null; + } + if ($style === null && $pattern === null && $precision === null) { static $formatters = []; $locale ??= App::getLocale(); diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index c54c04ab27b..13422180340 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -387,6 +387,8 @@ @import "bem/team-info-entries"; @import "bem/team-info-entry"; @import "bem/team-members"; +@import "bem/team-members-leaderboard"; +@import "bem/team-members-leaderboard-item"; @import "bem/team-members-manage"; @import "bem/team-settings"; @import "bem/team-settings-description-preview"; diff --git a/resources/css/bem/team-members-leaderboard-item.less b/resources/css/bem/team-members-leaderboard-item.less new file mode 100644 index 00000000000..48612b62e19 --- /dev/null +++ b/resources/css/bem/team-members-leaderboard-item.less @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.team-members-leaderboard-item { + --gutter: 10px; + align-items: center; + background: hsl(var(--hsl-b3)); + border-radius: @border-radius--large; + display: grid; + font-size: @font-size--title-small; + gap: 2px 10px; + grid-column: 1 / -1; + grid-template-columns: subgrid; + padding: 4px var(--gutter); + + &:hover { + background: hsl(var(--hsl-b2)); + } + + @media @desktop { + --gutter: 20px; + } + + &__avatar { + .default-border-radius(); + align-items: center; + display: flex; + overflow: hidden; + width: 40px; + } + + &__number { + font-size: @font-size--title-small-3; + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + gap: var(--gutter); + + @media @desktop { + display: block; + padding: 0 var(--gutter); + grid-column: initial; + } + } + + &__number-title { + color: hsl(var(--hsl-c2)); + font-size: @font-size--normal; + } + + &__numbers { + display: grid; + grid-template-columns: auto 1fr; + grid-column: 2 / -1; + + @media @desktop { + display: contents; + } + } + + &__rank { + text-align: end; + } + + &__username { + display: flex; + align-items: center; + width: max-content; + gap: 10px; + } +} diff --git a/resources/css/bem/team-members-leaderboard.less b/resources/css/bem/team-members-leaderboard.less new file mode 100644 index 00000000000..cb3656da75f --- /dev/null +++ b/resources/css/bem/team-members-leaderboard.less @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.team-members-leaderboard { + display: grid; + margin: 0; + padding: 0; + list-style: none; + gap: 2px; + grid-template-columns: auto 1fr; + + @media @desktop { + grid-template-columns: auto 1fr auto auto auto; + } +} diff --git a/resources/lang/en/beatmaps.php b/resources/lang/en/beatmaps.php index cd7245bd195..9ed40d3fa81 100644 --- a/resources/lang/en/beatmaps.php +++ b/resources/lang/en/beatmaps.php @@ -4,6 +4,8 @@ // See the LICENCE file in the repository root for full licence text. return [ + 'invalid_ruleset' => 'Invalid ruleset specified.', + 'change_owner' => [ 'too_many' => 'Too many guest mappers.', ], diff --git a/resources/lang/en/page_title.php b/resources/lang/en/page_title.php index 9c0db2fdbc3..91b4fdda190 100644 --- a/resources/lang/en/page_title.php +++ b/resources/lang/en/page_title.php @@ -110,6 +110,7 @@ 'teams_controller' => [ '_' => 'teams', 'edit' => 'team settings', + 'leaderboard' => 'team leaderboard', 'show' => 'team info', ], 'tournaments_controller' => [ diff --git a/resources/lang/en/teams.php b/resources/lang/en/teams.php index d7785e75068..33bf29d5ea3 100644 --- a/resources/lang/en/teams.php +++ b/resources/lang/en/teams.php @@ -59,6 +59,7 @@ 'header_links' => [ 'edit' => 'settings', + 'leaderboard' => 'leaderboard', 'show' => 'info', 'members' => [ @@ -66,6 +67,12 @@ ], ], + 'leaderboard' => [ + 'global_rank' => 'Global Rank', + 'performance' => 'Performance', + 'total_score' => 'Total Score', + ], + 'members' => [ 'destroy' => [ 'success' => 'Team member removed', @@ -118,8 +125,8 @@ ], 'sections' => [ - 'members' => 'Members', 'info' => 'Info', + 'members' => 'Members', ], ], ]; diff --git a/resources/views/teams/_members_leaderboard.blade.php b/resources/views/teams/_members_leaderboard.blade.php new file mode 100644 index 00000000000..7c46ac40d10 --- /dev/null +++ b/resources/views/teams/_members_leaderboard.blade.php @@ -0,0 +1,54 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} +
    + @foreach ($leaderboard as $i => $stats) +
  • +
    + #{{ i18n_number_format($i + 1) }} +
    + +
    +
    +
    + {{ osu_trans('teams.leaderboard.total_score') }} +
    +
    + {{ i18n_number_format($stats->total_score) }} +
    +
    +
    +
    + {{ osu_trans('teams.leaderboard.performance') }} +
    +
    + {{ i18n_number_format($stats->pp()) ?? '-' }} +
    +
    +
    +
    + {{ osu_trans('teams.leaderboard.global_rank') }} +
    +
    + {{ i18n_number_format($stats->globalRank()) ?? '-' }} +
    +
    +
    + @endforeach +
diff --git a/resources/views/teams/leaderboard.blade.php b/resources/views/teams/leaderboard.blade.php new file mode 100644 index 00000000000..1aceba31ae0 --- /dev/null +++ b/resources/views/teams/leaderboard.blade.php @@ -0,0 +1,30 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} +@extends('master', [ + 'titlePrepend' => $team->name, +]) + +@section('content') + @component('layout._page_header_v4', ['params' => [ + 'backgroundImage' => $team->header()->url(), + 'links' => App\Http\Controllers\TeamsController::pageLinks('leaderboard', $team), + 'theme' => 'team', + ]]) + @slot('linksAppend') + @include('objects._ruleset_selector', [ + 'currentRuleset' => $ruleset, + 'urlFn' => fn ($r) => route('teams.leaderboard', ['team' => $team->getKey(), 'ruleset' => $r]), + ]) + @endslot + @endcomponent + +
+ +
+@endsection diff --git a/routes/web.php b/routes/web.php index 1ea6fc3985d..2f4f258282a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -300,6 +300,7 @@ Route::resource('applications', 'Teams\ApplicationsController', ['only' => ['destroy', 'store']]); Route::post('applications/{application}/accept', 'Teams\ApplicationsController@accept')->name('applications.accept'); Route::post('applications/{application}/reject', 'Teams\ApplicationsController@reject')->name('applications.reject'); + Route::get('leaderboard/{ruleset?}', 'TeamsController@leaderboard')->name('leaderboard'); Route::post('part', 'TeamsController@part')->name('part'); Route::resource('members', 'Teams\MembersController', ['only' => ['destroy', 'index']]); });