the browser-facing portion of osu!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add daily ranking page

nanaya 31a34324 b6a30a0d

+219 -13
+7 -2
app/Http/Controllers/Multiplayer/RoomsController.php
··· 6 6 namespace App\Http\Controllers\Multiplayer; 7 7 8 8 use App\Exceptions\InvariantException; 9 - use App\Http\Controllers\Controller as BaseController; 9 + use App\Http\Controllers\Controller; 10 + use App\Http\Controllers\Ranking\DailyChallengeController; 10 11 use App\Models\Model; 11 12 use App\Models\Multiplayer\Room; 12 13 use App\Transformers\Multiplayer\RoomTransformer; 13 14 14 - class RoomsController extends BaseController 15 + class RoomsController extends Controller 15 16 { 16 17 public function __construct() 17 18 { ··· 175 176 'recent_participants', 176 177 ] 177 178 ); 179 + } 180 + 181 + if ($room->category === 'daily_challenge') { 182 + return ujs_redirect(route('daily-challenge.show', DailyChallengeController::roomId($room))); 178 183 } 179 184 180 185 $playlistItemsQuery = $room->playlist();
+70
app/Http/Controllers/Ranking/DailyChallengeController.php
··· 1 + <?php 2 + 3 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4 + // See the LICENCE file in the repository root for full licence text. 5 + 6 + declare(strict_types=1); 7 + 8 + namespace App\Http\Controllers\Ranking; 9 + 10 + use App\Http\Controllers\Controller; 11 + use App\Models\Multiplayer\Room; 12 + use Carbon\CarbonImmutable; 13 + use Carbon\Exceptions\InvalidFormatException; 14 + 15 + class DailyChallengeController extends Controller 16 + { 17 + private const string DATE_FORMAT = 'Y-m-d'; 18 + 19 + public static function roomId(Room $room): string 20 + { 21 + return $room->starts_at->format(static::DATE_FORMAT); 22 + } 23 + 24 + public function index() 25 + { 26 + $room = Room::dailyChallenges()->last() ?? abort(404); 27 + 28 + return ujs_redirect(route('daily-challenge.show', ['daily_challenge' => static::roomId($room)])); 29 + } 30 + 31 + public function show(string $dateString) 32 + { 33 + try { 34 + $date = CarbonImmutable::createFromFormat(static::DATE_FORMAT, $dateString); 35 + } catch (InvalidFormatException) { 36 + abort(404, 'invalid date'); 37 + } 38 + 39 + $room = Room::dailyChallengeFor($date) ?? abort(404); 40 + $playlist = $room->playlist[0]; 41 + 42 + $currentRoomOption = [ 43 + 'id' => $dateString, 44 + 'text' => $dateString, 45 + ]; 46 + $roomOptions = Room::dailyChallenges() 47 + ->orderBy('id') 48 + ->get() 49 + ->map(static::roomId(...)) 50 + ->map(fn (string $roomName): array => [ 51 + 'id' => $roomName, 52 + 'text' => $roomName, 53 + ]); 54 + 55 + $scores = $room->topScores()->paginate(); 56 + 57 + $userScore = ($currentUser = \Auth::user()) === null 58 + ? null 59 + : $room->topScores()->whereBelongsTo($currentUser)->first(); 60 + 61 + return ext_view('rankings.daily_challenge', compact( 62 + 'currentRoomOption', 63 + 'playlist', 64 + 'room', 65 + 'roomOptions', 66 + 'scores', 67 + 'userScore', 68 + )); 69 + } 70 + }
+11 -1
app/Http/Controllers/RankingController.php
··· 33 33 const RANKING_TYPES = ['performance', 'charts', 'score', 'country']; 34 34 const SPOTLIGHT_TYPES = ['charts']; 35 35 // in display order 36 - const TYPES = ['performance', 'score', 'country', 'multiplayer', 'seasons', 'charts', 'kudosu']; 36 + const TYPES = [ 37 + 'performance', 38 + 'score', 39 + 'country', 40 + 'multiplayer', 41 + 'daily_challenge', 42 + 'seasons', 43 + 'charts', 44 + 'kudosu', 45 + ]; 37 46 38 47 public function __construct() 39 48 { ··· 114 123 ): string { 115 124 return match ($type) { 116 125 'country' => route('rankings', ['mode' => $rulesetName, 'type' => $type]), 126 + 'daily_challenge' => route('daily-challenge.index'), 117 127 'kudosu' => route('rankings.kudosu'), 118 128 'multiplayer' => route('multiplayer.rooms.show', ['room' => 'latest']), 119 129 'seasons' => route('seasons.show', ['season' => 'latest']),
+6 -1
app/Models/Multiplayer/Room.php
··· 226 226 public function macroDailyChallengeFor(): \Closure 227 227 { 228 228 return fn (Builder $query, CarbonImmutable $date): ?static 229 - => static::where('category', 'daily_challenge') 229 + => static::dailyChallenges() 230 230 ->whereBetween('starts_at', [$date->startOfDay(), $date->endOfDay()]) 231 231 ->last(); 232 232 } ··· 263 263 ->where(function ($q) { 264 264 $q->where('ends_at', '>', Carbon::now())->orWhereNull('ends_at'); 265 265 }); 266 + } 267 + 268 + public function scopeDailyChallenges(Builder $query): Builder 269 + { 270 + return $query->where('category', 'daily_challenge'); 266 271 } 267 272 268 273 public function scopeEnded($query)
+3
app/Singletons/RouteSection.php
··· 132 132 'passport' => [ 133 133 '_' => 'user', 134 134 ], 135 + 'ranking' => [ 136 + '_' => 'rankings', 137 + ], 135 138 'store' => [ 136 139 '_' => 'store', 137 140 ],
+1
app/helpers.php
··· 917 917 'main.artist_tracks_controller._' => 'main.artists_controller._', 918 918 'main.store_controller._' => 'store._', 919 919 'multiplayer.rooms_controller._' => 'main.ranking_controller._', 920 + 'ranking.daily_challenge_controller._' => 'main.ranking_controller._', 920 921 default => $controllerKey, 921 922 }; 922 923 $namespaceKey = "{$currentRoute['namespace']}._";
+4
resources/css/bem/rankings-beatmapsets.less
··· 9 9 gap: 10px; 10 10 grid-template-columns: 1fr 1fr; 11 11 12 + &--daily-challenge { 13 + margin-top: 0; 14 + } 15 + 12 16 &--single { 13 17 grid-template-columns: 1fr; 14 18 }
+4 -1
resources/js/components/basic-select-options.tsx
··· 5 5 import SelectOptionJson from 'interfaces/select-option-json'; 6 6 import { route } from 'laroute'; 7 7 import * as React from 'react'; 8 + import { fail } from 'utils/fail'; 8 9 import { navigate } from 'utils/turbolinks'; 9 10 10 11 interface Props { 11 12 currentItem: SelectOptionJson; 12 13 items: SelectOptionJson[]; 13 - type: 'judge_results' | 'multiplayer' | 'seasons'; 14 + type: 'daily_challenge' | 'judge_results' | 'multiplayer' | 'seasons'; 14 15 } 15 16 16 17 export default class BasicSelectOptions extends React.PureComponent<Props> { ··· 32 33 33 34 private href(id: number | null) { 34 35 switch (this.props.type) { 36 + case 'daily_challenge': 37 + return route('daily-challenge.show', { daily_challenge: id ?? fail('missing id parameter') }); 35 38 case 'judge_results': 36 39 return route('contest-entries.judge-results', { contest_entry: id ?? 0 }); 37 40 case 'multiplayer':
+1 -4
resources/js/core/spoilerbox.ts
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + import { fail } from 'utils/fail'; 4 5 import { htmlElementOrNull } from 'utils/html'; 5 - 6 - function fail(message: string): never { 7 - throw new Error(message); 8 - } 9 6 10 7 function expand(e: JQuery.ClickEvent) { 11 8 e.stopPropagation();
+6
resources/js/utils/fail.ts
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + export function fail(message: string): never { 5 + throw new Error(message); 6 + }
+7
resources/lang/en/rankings.php
··· 9 9 'title' => 'Country', 10 10 ], 11 11 12 + 'daily_challenge' => [ 13 + 'beatmap' => 'Difficulty', 14 + 'percentile_10' => '10th Percentile Score', 15 + 'percentile_50' => '50th Percentile Score', 16 + ], 17 + 12 18 'filter' => [ 13 19 'title' => 'Show', 14 20 ··· 30 36 'type' => [ 31 37 'charts' => 'spotlights (old)', 32 38 'country' => 'country', 39 + 'daily_challenge' => 'daily challenge', 33 40 'kudosu' => 'kudosu', 34 41 'multiplayer' => 'multiplayer', 35 42 'performance' => 'performance',
+4 -2
resources/views/rankings/_beatmapsets.blade.php
··· 3 3 See the LICENCE file in the repository root for full licence text. 4 4 --}} 5 5 6 - <div class="{{ class_with_modifiers('rankings-beatmapsets', ['single' => count($beatmapsets) === 1]) }}"> 6 + <div class="{{ class_with_modifiers('rankings-beatmapsets', $modifiers ?? null, ['single' => count($beatmapsets) === 1]) }}"> 7 7 @foreach ($beatmapsets as $beatmapset) 8 8 <div 9 9 class="js-react--beatmapset-panel u-contents" 10 10 data-beatmapset-panel="{{ json_encode(['beatmapset' => json_item($beatmapset, 'Beatmapset', ['beatmaps'])]) }}" 11 - ></div> 11 + > 12 + <div class="beatmapset-panel beatmapset-panel--size-normal"></div> 13 + </div> 12 14 @endforeach 13 15 </div>
+79
resources/views/rankings/daily_challenge.blade.php
··· 1 + {{-- 2 + Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 3 + See the LICENCE file in the repository root for full licence text. 4 + --}} 5 + @extends('rankings.index', [ 6 + 'hasMode' => false, 7 + 'hasPager' => true, 8 + 'type' => 'daily_challenge', 9 + 'titlePrepend' => osu_trans('rankings.type.daily_challenge').': '.$currentRoomOption['text'], 10 + ]) 11 + 12 + @php 13 + $percentile = $playlist->scorePercentile(); 14 + @endphp 15 + @section('ranking-header') 16 + <div class="osu-page osu-page--ranking-info"> 17 + <div class="js-react--basic-select-options"> 18 + <div class="select-options select-options--spotlight"> 19 + <div class="select-options__select"> 20 + <span class="select-options__option"> 21 + {{ $currentRoomOption['text'] }} 22 + </span> 23 + </div> 24 + </div> 25 + </div> 26 + 27 + <script id="json-basic-select-options" type="application/json"> 28 + {!! json_encode([ 29 + 'currentItem' => $currentRoomOption, 30 + 'items' => $roomOptions, 31 + 'type' => 'daily_challenge', 32 + ]) !!} 33 + </script> 34 + 35 + <div class="grid-items grid-items--ranking-info-bar"> 36 + <div class="counter-box counter-box--ranking"> 37 + <div class="counter-box__title"> 38 + {{ osu_trans('rankings.daily_challenge.beatmap') }} 39 + </div> 40 + <div class="counter-box__count"> 41 + <span class="fal fa-extra-mode-{{ $playlist->beatmap->mode }}"></span> 42 + {{ $playlist->beatmap->version }} 43 + </div> 44 + </div> 45 + <div class="counter-box counter-box--ranking"> 46 + <div class="counter-box__title"> 47 + {{ osu_trans('rankings.spotlight.participants') }} 48 + </div> 49 + <div class="counter-box__count"> 50 + {{ i18n_number_format($scores->total()) }} 51 + </div> 52 + </div> 53 + <div class="counter-box counter-box--ranking"> 54 + <div class="counter-box__title"> 55 + {{ osu_trans('rankings.daily_challenge.percentile_10') }} 56 + </div> 57 + <div class="counter-box__count"> 58 + {{ i18n_number_format($percentile['10p']) }} 59 + </div> 60 + </div> 61 + <div class="counter-box counter-box--ranking"> 62 + <div class="counter-box__title"> 63 + {{ osu_trans('rankings.daily_challenge.percentile_50') }} 64 + </div> 65 + <div class="counter-box__count"> 66 + {{ i18n_number_format($percentile['50p']) }} 67 + </div> 68 + </div> 69 + </div> 70 + </div> 71 + @endsection 72 + 73 + @section('scores-header') 74 + @include('rankings._beatmapsets', ['beatmapsets' => [$playlist->beatmap->beatmapset], 'modifiers' => 'daily-challenge']) 75 + @endsection 76 + 77 + @section('scores') 78 + @include('multiplayer.rooms._rankings_table') 79 + @endsection
+7 -2
resources/views/rankings/index.blade.php
··· 31 31 'links' => $links, 32 32 'theme' => 'rankings', 33 33 ]]) 34 - @slot('linksAppend') 34 + @slot('contentAppend') 35 35 @if($hasMode) 36 36 @include('rankings._mode_selector') 37 37 @endif 38 + @endslot 38 39 40 + @slot('linksAppend') 39 41 @yield('additionalHeaderLinks') 40 42 @endslot 41 43 @endcomponent ··· 43 45 @yield('ranking-header') 44 46 45 47 @if ($hasScores) 46 - <div class="osu-page osu-page--generic" id="scores"> 48 + <div class="osu-page osu-page--generic"> 49 + @yield('scores-header') 50 + 51 + <div id="scores"></div> 47 52 @if ($hasPager) 48 53 @include('objects._pagination_v2', [ 49 54 'object' => $scores
+1
routes/web.php
··· 281 281 }); 282 282 283 283 Route::get('rankings/kudosu', 'RankingController@kudosu')->name('rankings.kudosu'); 284 + Route::resource('rankings/daily-challenge', 'Ranking\DailyChallengeController', ['only' => ['index', 'show']]); 284 285 Route::get('rankings/{mode?}/{type?}', 'RankingController@index')->name('rankings'); 285 286 286 287 Route::resource('reports', 'ReportsController', ['only' => ['store']]);
+8
tests/Browser/SanityTest.php
··· 5 5 6 6 namespace Tests\Browser; 7 7 8 + use App\Http\Controllers\Ranking\DailyChallengeController; 8 9 use App\Libraries\Session; 9 10 use App\Libraries\SessionVerification; 10 11 use App\Models\Artist; ··· 36 37 use App\Models\Language; 37 38 use App\Models\LegacyMatch; 38 39 use App\Models\LoginAttempt; 40 + use App\Models\Multiplayer\PlaylistItem; 39 41 use App\Models\Multiplayer\Room; 40 42 use App\Models\NewsPost; 41 43 use App\Models\Notification; ··· 269 271 self::$scaffolding['score'] = Score\Best\Osu::factory()->withReplay()->create(); 270 272 271 273 self::$scaffolding['room'] = Room::factory()->create(['category' => 'spotlight']); 274 + 275 + self::$scaffolding['daily_challenge_room'] = Room::factory()->create(['category' => 'daily_challenge']); 276 + PlaylistItem::factory()->create(['room_id' => self::$scaffolding['daily_challenge_room']]); 272 277 } 273 278 274 279 private static function filterLog(array $log) ··· 444 449 ], 445 450 'changelog.show' => [ 446 451 'changelog' => self::$scaffolding['build']->version, 452 + ], 453 + 'daily-challenge.show' => [ 454 + 'daily_challenge' => DailyChallengeController::roomId(self::$scaffolding['daily_challenge_room']), 447 455 ], 448 456 'scores.download-legacy' => [ 449 457 'rulesetOrScore' => static::$scaffolding['score']->getMode(),