diff --git a/app/Http/Controllers/TeamsController.php b/app/Http/Controllers/TeamsController.php index ee853c5b74c..0792c39db0b 100644 --- a/app/Http/Controllers/TeamsController.php +++ b/app/Http/Controllers/TeamsController.php @@ -13,6 +13,12 @@ class TeamsController extends Controller { + public function __construct() + { + parent::__construct(); + $this->middleware('auth', ['only' => ['part']]); + } + public function edit(string $id): Response { $team = Team::findOrFail($id); @@ -21,6 +27,17 @@ public function edit(string $id): Response return ext_view('teams.edit', compact('team')); } + public function part(string $id): Response + { + $team = Team::findOrFail($id); + priv_check('TeamPart', $team)->ensureCan(); + + $team->members()->findOrFail(\Auth::user()->getKey())->delete(); + \Session::flash('popup', osu_trans('teams.part.ok')); + + return ujs_redirect(route('teams.show', ['team' => $team])); + } + public function show(string $id): Response { $team = Team diff --git a/app/Singletons/OsuAuthorize.php b/app/Singletons/OsuAuthorize.php index 27c14d000e0..de5e189746b 100644 --- a/app/Singletons/OsuAuthorize.php +++ b/app/Singletons/OsuAuthorize.php @@ -48,9 +48,10 @@ public static function alwaysCheck($ability) $set ??= new Ds\Set([ 'ContestJudge', - 'IsOwnClient', 'IsNotOAuth', + 'IsOwnClient', 'IsSpecialScope', + 'TeamPart', 'UserUpdateEmail', ]); @@ -1904,6 +1905,22 @@ public function checkScorePin(?User $user, ScoreBest|Solo\Score $score): string return 'ok'; } + public function checkTeamPart(?User $user, Team $team): ?string + { + $this->ensureLoggedIn($user); + + $prefix = 'team.part.'; + + if ($team->leader_id === $user->getKey()) { + return $prefix.'is_leader'; + } + if ($team->getKey() !== $user?->team?->getKey()) { + return $prefix.'not_member'; + } + + return 'ok'; + } + public function checkTeamUpdate(?User $user, Team $team): ?string { $this->ensureLoggedIn($user); diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index 20a241d8000..c54c04ab27b 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -383,6 +383,7 @@ @import "bem/supporter-promo"; @import "bem/supporter-quote"; @import "bem/supporter-status"; +@import "bem/team-action-button"; @import "bem/team-info-entries"; @import "bem/team-info-entry"; @import "bem/team-members"; diff --git a/resources/css/bem/profile-detail-bar.less b/resources/css/bem/profile-detail-bar.less index 8524856f16d..49a0cd83b63 100644 --- a/resources/css/bem/profile-detail-bar.less +++ b/resources/css/bem/profile-detail-bar.less @@ -19,6 +19,10 @@ --padding: @gutter-v2-desktop; } + &--team { + justify-content: end; + } + &__level { margin-left: auto; display: flex; diff --git a/resources/css/bem/team-action-button.less b/resources/css/bem/team-action-button.less new file mode 100644 index 00000000000..bb0712b5cc3 --- /dev/null +++ b/resources/css/bem/team-action-button.less @@ -0,0 +1,31 @@ +// 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-action-button { + .reset-input(); + .link-plain(); + .link-white(); + .center-content(); + .default-text-shadow(); + display: flex; + width: auto; + height: 100%; + padding: 10px 30px; + border-radius: 10000px; + text-align: center; + font-weight: bold; + font-size: @font-size--title-small; + background: var(--bg); + + --bg: hsl(var(--hsl-b5)); + --bg-hover: hsl(var(--hsl-b6)); + + &:hover { + background: var(--bg-hover); + } + + &--part { + --bg: hsl(var(--hsl-red-3)); + --bg-hover: hsl(var(--hsl-red-2)); + } +} diff --git a/resources/lang/en/authorization.php b/resources/lang/en/authorization.php index 046abae50ea..ba061ef6ccf 100644 --- a/resources/lang/en/authorization.php +++ b/resources/lang/en/authorization.php @@ -191,6 +191,13 @@ ], ], + 'team' => [ + 'part' => [ + 'is_leader' => "Team leader can't leave the team.", + 'not_member' => 'Not a member of the team.', + ], + ], + 'user' => [ 'page' => [ 'edit' => [ diff --git a/resources/lang/en/teams.php b/resources/lang/en/teams.php index 463c321fd04..ae637b63d52 100644 --- a/resources/lang/en/teams.php +++ b/resources/lang/en/teams.php @@ -59,9 +59,14 @@ ], ], ], + + 'part' => [ + 'ok' => 'Left the team ;_;', + ], + 'show' => [ 'bar' => [ - 'settings' => 'Settings', + 'part' => 'Leave Team', ], 'info' => [ diff --git a/resources/views/teams/show.blade.php b/resources/views/teams/show.blade.php index 7e8bd0e786c..ed4c2d6e2ce 100644 --- a/resources/views/teams/show.blade.php +++ b/resources/views/teams/show.blade.php @@ -13,6 +13,11 @@ $teamMembers['member'] ??= []; $teamMembers['leader'] ??= $toJson([$team->members()->make(['user_id' => $team->leader_id])->userOrDeleted()]); $headerUrl = $team->header()->url(); + + $buttons = new Ds\Set(); + if (priv_check('TeamPart', $team)->can()) { + $buttons->add('part'); + } @endphp @extends('master', [ @@ -69,6 +74,21 @@ class="btn-circle btn-circle--page-toggle" + @if (!$buttons->isEmpty()) +
+ @if ($buttons->contains('part')) +
+ +
+ @endif +
+ @endif
diff --git a/routes/web.php b/routes/web.php index a94e8853fa3..bdfac527ab9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -296,8 +296,9 @@ Route::post('user-cover-presets/batch-activate', 'UserCoverPresetsController@batchActivate')->name('user-cover-presets.batch-activate'); Route::resource('user-cover-presets', 'UserCoverPresetsController', ['only' => ['index', 'store', 'update']]); - Route::group(['as' => 'teams.', 'prefix' => 'teams/{team}', 'namespace' => 'Teams'], function () { - Route::resource('members', 'MembersController', ['only' => ['destroy', 'index']]); + Route::group(['as' => 'teams.', 'prefix' => 'teams/{team}'], function () { + Route::post('part', 'TeamsController@part')->name('part'); + Route::resource('members', 'Teams\MembersController', ['only' => ['destroy', 'index']]); }); Route::resource('teams', 'TeamsController', ['only' => ['edit', 'show', 'update']]);