]> git.localhorst.tv Git - alttp.git/commitdiff
user nicknames
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 25 Mar 2022 12:03:53 +0000 (13:03 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 25 Mar 2022 12:03:53 +0000 (13:03 +0100)
12 files changed:
app/Http/Controllers/UserController.php
app/Policies/UserPolicy.php
database/migrations/2022_03_25_113858_user_nickname.php [new file with mode: 0644]
resources/js/components/users/Box.js
resources/js/components/users/EditNicknameButton.js [new file with mode: 0644]
resources/js/components/users/EditNicknameDialog.js [new file with mode: 0644]
resources/js/components/users/EditNicknameForm.js [new file with mode: 0644]
resources/js/components/users/Profile.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
routes/api.php

index 692c45862feb0e21186862476cf9ff5a76e31bc8..da3ebde1bf113bd18e2b56b6fee72ab23a155fd3 100644 (file)
@@ -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);
 
index 67bc561b8c2b2d922f6b40944e43863d2a88b6ce..b11e3a812ff8c52596b2756e9fab007584c8bdd9 100644 (file)
@@ -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 (file)
index 0000000..753c742
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::table('users', function(Blueprint $table) {
+                       $table->string('nickname')->nullable()->default(null);
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::table('users', function(Blueprint $table) {
+                       $table->dropColumn('nickname');
+               });
+       }
+};
index d7ee3af9944ac3c736678f11d40ec0ad1c91cbb8..bc4673ee9e12d818c209ced29906346bd3ff8f22 100644 (file)
@@ -20,7 +20,7 @@ const Box = ({ discriminator, user }) => {
                variant="link"
        >
                <img alt="" src={getAvatarUrl(user)} />
-               <span>{user.username}</span>
+               <span>{discriminator || !user.nickname ? user.username : user.nickname}</span>
                {discriminator ?
                        <span className="text-muted">
                                {'#'}
@@ -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 (file)
index 0000000..5366f24
--- /dev/null
@@ -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 <>
+                       <EditNicknameDialog
+                               onHide={() => setShowDialog(false)}
+                               show={showDialog}
+                               user={user}
+                       />
+                       <Button
+                               onClick={() => setShowDialog(true)}
+                               title={i18n.t('button.edit')}
+                               variant="outline-secondary"
+                       >
+                               <Icon.EDIT title="" />
+                       </Button>
+               </>;
+       }
+       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 (file)
index 0000000..dbc9cbf
--- /dev/null
@@ -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,
+}) =>
+<Modal className="edit-stream-link-dialog" onHide={onHide} show={show}>
+       <Modal.Header closeButton>
+               <Modal.Title>
+                       {i18n.t('users.editNickname')}
+               </Modal.Title>
+       </Modal.Header>
+       <EditNicknameForm
+               onCancel={onHide}
+               user={user}
+       />
+</Modal>;
+
+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 (file)
index 0000000..3f6a5bb
--- /dev/null
@@ -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,
+}) =>
+<Form noValidate onSubmit={handleSubmit}>
+       <Modal.Body>
+               <Row>
+                       <Form.Group as={Col} controlId="user.nickname">
+                               <Form.Label>{i18n.t('users.nickname')}</Form.Label>
+                               <Form.Control
+                                       isInvalid={!!(touched.nickname && errors.nickname)}
+                                       name="nickname"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       placeholder={user.username}
+                                       type="text"
+                                       value={values.nickname || ''}
+                               />
+                               {touched.nickname && errors.nickname ?
+                                       <Form.Control.Feedback type="invalid">
+                                               {i18n.t(errors.nickname)}
+                                       </Form.Control.Feedback>
+                               : null}
+                       </Form.Group>
+               </Row>
+       </Modal.Body>
+       <Modal.Footer>
+               {onCancel ?
+                       <Button onClick={onCancel} variant="secondary">
+                               {i18n.t('button.cancel')}
+                       </Button>
+               : null}
+               <Button type="submit" variant="primary">
+                       {i18n.t('button.save')}
+               </Button>
+       </Modal.Footer>
+</Form>;
+
+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));
index e2fe2e790be5e3739a52da8e266d0212b8937ac8..a8a4e95143d40891dc631835249403d6621644b8 100644 (file)
@@ -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 }) => <Container>
-       <h1>{user.username}</h1>
+       <h1>
+               {user.nickname || user.username}
+               {' '}
+               <EditNicknameButton user={user} />
+       </h1>
        <Row>
                <Col md={6} className="mb-5">
                        <h2>{i18n.t('users.discordTag')}</h2>
@@ -62,6 +67,7 @@ const Profile = ({ user }) => <Container>
 
 Profile.propTypes = {
        user: PropTypes.shape({
+               nickname: PropTypes.string,
                participation: PropTypes.arrayOf(PropTypes.shape({
                })),
                round_first_count: PropTypes.number,
index 32662944a90412639878392d3e32663f6e3e59fe..3df65d6461dfbd94d27c5caf1a7f7f6e8ea488ee 100644 (file)
@@ -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);
index 7f2c713adec21faf7d69bf14c06582258e203227..5e97b4f7bafa2475b9abb6c34198cbe7d1f5b678 100644 (file)
@@ -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',
index e7907437eabc78742bf096ad16af5a7db3a687c0..76328bf925933aed571b5b3ebc56e2de52a79cb3 100644 (file)
@@ -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',
index 46ec7d04ce4bd6b56bd0d1df39b381f701741fdf..6f6ef2471780b6d494c43e72aeda6c47e2021306 100644 (file)
@@ -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');