From 82b95e7542824bcdf4f1b245559cb1c93f4eafdd Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 25 Mar 2022 13:03:53 +0100 Subject: [PATCH] user nicknames --- app/Http/Controllers/UserController.php | 15 +++ app/Policies/UserPolicy.php | 12 ++ .../2022_03_25_113858_user_nickname.php | 32 ++++++ resources/js/components/users/Box.js | 3 +- .../js/components/users/EditNicknameButton.js | 41 +++++++ .../js/components/users/EditNicknameDialog.js | 33 ++++++ .../js/components/users/EditNicknameForm.js | 105 ++++++++++++++++++ resources/js/components/users/Profile.js | 8 +- resources/js/helpers/permissions.js | 3 + resources/js/i18n/de.js | 4 + resources/js/i18n/en.js | 4 + routes/api.php | 1 + 12 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2022_03_25_113858_user_nickname.php create mode 100644 resources/js/components/users/EditNicknameButton.js create mode 100644 resources/js/components/users/EditNicknameDialog.js create mode 100644 resources/js/components/users/EditNicknameForm.js diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 692c458..da3ebde 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -23,6 +23,21 @@ class UserController extends Controller return $user->toJson(); } + public function setNickname(Request $request, User $user) { + $this->authorize('setNickname', $user); + + $validatedData = $request->validate([ + 'nickname' => 'string', + ]); + + $user->nickname = $validatedData['nickname']; + $user->update(); + + UserChanged::dispatch($user); + + return $user->toJson(); + } + public function setStreamLink(Request $request, User $user) { $this->authorize('setStreamLink', $user); diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php index 67bc561..b11e3a8 100644 --- a/app/Policies/UserPolicy.php +++ b/app/Policies/UserPolicy.php @@ -91,6 +91,18 @@ class UserPolicy return false; } + /** + * Determine whether the user change the stream link of the model. + * + * @param \App\Models\User $user + * @param \App\Models\User $model + * @return \Illuminate\Auth\Access\Response|bool + */ + public function setNickname(User $user, User $model) + { + return $user->role == 'admin' || $user->id == $model->id; + } + /** * Determine whether the user change the stream link of the model. * diff --git a/database/migrations/2022_03_25_113858_user_nickname.php b/database/migrations/2022_03_25_113858_user_nickname.php new file mode 100644 index 0000000..753c742 --- /dev/null +++ b/database/migrations/2022_03_25_113858_user_nickname.php @@ -0,0 +1,32 @@ +string('nickname')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function(Blueprint $table) { + $table->dropColumn('nickname'); + }); + } +}; diff --git a/resources/js/components/users/Box.js b/resources/js/components/users/Box.js index d7ee3af..bc4673e 100644 --- a/resources/js/components/users/Box.js +++ b/resources/js/components/users/Box.js @@ -20,7 +20,7 @@ const Box = ({ discriminator, user }) => { variant="link" > - {user.username} + {discriminator || !user.nickname ? user.username : user.nickname} {discriminator ? {'#'} @@ -35,6 +35,7 @@ Box.propTypes = { user: PropTypes.shape({ discriminator: PropTypes.string, id: PropTypes.string, + nickname: PropTypes.string, username: PropTypes.string, }), }; diff --git a/resources/js/components/users/EditNicknameButton.js b/resources/js/components/users/EditNicknameButton.js new file mode 100644 index 0000000..5366f24 --- /dev/null +++ b/resources/js/components/users/EditNicknameButton.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import { Button } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; + +import EditNicknameDialog from './EditNicknameDialog'; +import Icon from '../common/Icon'; +import { mayEditNickname } from '../../helpers/permissions'; +import { withUser } from '../../helpers/UserContext'; +import i18n from '../../i18n'; + +const EditNicknameButton = ({ authUser, user }) => { + const [showDialog, setShowDialog] = useState(false); + + if (mayEditNickname(authUser, user)) { + return <> + setShowDialog(false)} + show={showDialog} + user={user} + /> + + ; + } + return null; +}; + +EditNicknameButton.propTypes = { + authUser: PropTypes.shape({ + }), + user: PropTypes.shape({ + }), +}; + +export default withTranslation()(withUser(EditNicknameButton, 'authUser')); diff --git a/resources/js/components/users/EditNicknameDialog.js b/resources/js/components/users/EditNicknameDialog.js new file mode 100644 index 0000000..dbc9cbf --- /dev/null +++ b/resources/js/components/users/EditNicknameDialog.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; + +import EditNicknameForm from './EditNicknameForm'; +import i18n from '../../i18n'; + +const EditNicknameDialog = ({ + onHide, + show, + user, +}) => + + + + {i18n.t('users.editNickname')} + + + +; + +EditNicknameDialog.propTypes = { + onHide: PropTypes.func, + show: PropTypes.bool, + user: PropTypes.shape({ + }), +}; + +export default withTranslation()(EditNicknameDialog); diff --git a/resources/js/components/users/EditNicknameForm.js b/resources/js/components/users/EditNicknameForm.js new file mode 100644 index 0000000..3f6a5bb --- /dev/null +++ b/resources/js/components/users/EditNicknameForm.js @@ -0,0 +1,105 @@ +import axios from 'axios'; +import { withFormik } from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Col, Form, Modal, Row } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; +import toastr from 'toastr'; + +import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik'; +import i18n from '../../i18n'; +import yup from '../../schema/yup'; + +const EditStreamLinkForm = ({ + errors, + handleBlur, + handleChange, + handleSubmit, + onCancel, + touched, + user, + values, +}) => +
+ + + + {i18n.t('users.nickname')} + + {touched.nickname && errors.nickname ? + + {i18n.t(errors.nickname)} + + : null} + + + + + {onCancel ? + + : null} + + +
; + +EditStreamLinkForm.propTypes = { + errors: PropTypes.shape({ + nickname: PropTypes.string, + }), + handleBlur: PropTypes.func, + handleChange: PropTypes.func, + handleSubmit: PropTypes.func, + onCancel: PropTypes.func, + touched: PropTypes.shape({ + nickname: PropTypes.bool, + }), + user: PropTypes.shape({ + username: PropTypes.bool, + }), + values: PropTypes.shape({ + nickname: PropTypes.string, + }), +}; + +export default withFormik({ + displayName: 'SeedForm', + enableReinitialize: true, + handleSubmit: async (values, actions) => { + const { user_id, nickname } = values; + const { setErrors } = actions; + const { onCancel } = actions.props; + try { + await axios.post(`/api/users/${user_id}/setNickname`, { + nickname, + }); + toastr.success(i18n.t('users.setNicknameSuccess')); + if (onCancel) { + onCancel(); + } + } catch (e) { + toastr.error(i18n.t('users.setNicknameError')); + if (e.response && e.response.data && e.response.data.errors) { + setErrors(laravelErrorsToFormik(e.response.data.errors)); + } + } + }, + mapPropsToValues: ({ user }) => ({ + user_id: user.id, + nickname: user.nickname || '', + }), + validationSchema: yup.object().shape({ + nickname: yup.string(), + }), +})(withTranslation()(EditStreamLinkForm)); diff --git a/resources/js/components/users/Profile.js b/resources/js/components/users/Profile.js index e2fe2e7..a8a4e95 100644 --- a/resources/js/components/users/Profile.js +++ b/resources/js/components/users/Profile.js @@ -5,13 +5,18 @@ import { withTranslation } from 'react-i18next'; import Box from './Box'; import Records from './Records'; +import EditNicknameButton from './EditNicknameButton'; import EditStreamLinkButton from './EditStreamLinkButton'; import Participation from './Participation'; import Icon from '../common/Icon'; import i18n from '../../i18n'; const Profile = ({ user }) => -

{user.username}

+

+ {user.nickname || user.username} + {' '} + +

{i18n.t('users.discordTag')}

@@ -62,6 +67,7 @@ const Profile = ({ user }) => Profile.propTypes = { user: PropTypes.shape({ + nickname: PropTypes.string, participation: PropTypes.arrayOf(PropTypes.shape({ })), round_first_count: PropTypes.number, diff --git a/resources/js/helpers/permissions.js b/resources/js/helpers/permissions.js index 3266294..3df65d6 100644 --- a/resources/js/helpers/permissions.js +++ b/resources/js/helpers/permissions.js @@ -44,5 +44,8 @@ export const maySeeResults = (user, tournament, round) => // Users +export const mayEditNickname = (user, subject) => + isAdmin(user) || isSameUser(user, subject); + export const mayEditStreamLink = (user, subject) => isAdmin(user) || isSameUser(user, subject); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 7f2c713..5e97b4f 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -177,10 +177,14 @@ export default { }, users: { discordTag: 'Discord Tag', + editNickname: 'Name bearbeiten', editStreamLink: 'Stream Link bearbeiten', + nickname: 'Name', noStream: 'Kein Stream gesetzt', participationEmpty: 'Hat noch an keinen Turnieren teilgenommen.', roundRecords: 'Renn-Platzierungen', + setNicknameError: 'Konnte Namen nicht speichern', + setNicknameSuccess: 'Name geändert', setStreamLinkError: 'Konnte Stream Link nicht speichern', setStreamLinkSuccess: 'Stream Link gespeichert', stream: 'Stream', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index e790743..76328bf 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -177,10 +177,14 @@ export default { }, users: { discordTag: 'Discord tag', + editNickname: 'Edit name', editStreamLink: 'Edit stream link', + nickname: 'Name', noStream: 'No stream set', participationEmpty: 'Has not participated in any tourneys yet.', roundRecords: 'Race records', + setNicknameError: 'Could not save name', + setNicknameSuccess: 'Name changed', setStreamLinkError: 'Could not save stream link', setStreamLinkSuccess: 'Stream link saved', stream: 'Stream', diff --git a/routes/api.php b/routes/api.php index 46ec7d0..6f6ef24 100644 --- a/routes/api.php +++ b/routes/api.php @@ -31,4 +31,5 @@ Route::get('tournaments/{id}', 'App\Http\Controllers\TournamentController@single Route::get('users/{id}', 'App\Http\Controllers\UserController@single'); Route::post('users/set-language', 'App\Http\Controllers\UserController@setLanguage'); +Route::post('users/{user}/setNickname', 'App\Http\Controllers\UserController@setNickname'); Route::post('users/{user}/setStreamLink', 'App\Http\Controllers\UserController@setStreamLink'); -- 2.39.2