]> git.localhorst.tv Git - alttp.git/commitdiff
allow users to set their stream link
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 20 Mar 2022 14:50:36 +0000 (15:50 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 20 Mar 2022 14:50:36 +0000 (15:50 +0100)
19 files changed:
app/Events/UserChanged.php [new file with mode: 0644]
app/Http/Controllers/UserController.php
app/Policies/UserPolicy.php [new file with mode: 0644]
resources/js/components/App.js
resources/js/components/pages/Tournament.js
resources/js/components/pages/User.js [new file with mode: 0644]
resources/js/components/results/ReportForm.js
resources/js/components/rounds/SeedForm.js
resources/js/components/users/EditStreamLinkButton.js [new file with mode: 0644]
resources/js/components/users/EditStreamLinkDialog.js [new file with mode: 0644]
resources/js/components/users/EditStreamLinkForm.js [new file with mode: 0644]
resources/js/components/users/Profile.js [new file with mode: 0644]
resources/js/helpers/Participant.js
resources/js/helpers/Tournament.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/common.scss
routes/api.php

diff --git a/app/Events/UserChanged.php b/app/Events/UserChanged.php
new file mode 100644 (file)
index 0000000..9009c44
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Events;
+
+use App\Models\User;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class UserChanged implements ShouldBroadcast
+{
+       use Dispatchable, InteractsWithSockets, SerializesModels;
+
+       /**
+        * Create a new event instance.
+        *
+        * @return void
+        */
+       public function __construct(User $user)
+       {
+               $this->user = $user;
+       }
+
+       /**
+        * Get the channels the event should broadcast on.
+        *
+        * @return \Illuminate\Broadcasting\Channel|array
+        */
+       public function broadcastOn()
+       {
+               return [
+                       new Channel('App.Control'),
+                       new PrivateChannel('App.Models.User.'.$this->user->id),
+               ];
+       }
+
+       public $user;
+
+}
index 58d725d383b4aee9e5d8111fa329cee103b836a1..c0ab73d9b442a30ff340375e97d4747b0e5e9a5f 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace App\Http\Controllers;
 
+use App\Events\UserChanged;
+use App\Models\User;
 use Illuminate\Http\Request;
 
 class UserController extends Controller
@@ -21,4 +23,25 @@ class UserController extends Controller
                return $user->toJson();
        }
 
+       public function setStreamLink(Request $request, User $user) {
+               $this->authorize('setStreamLink', $user);
+
+               $validatedData = $request->validate([
+                       'stream_link' => 'required|url',
+               ]);
+
+               $user->stream_link = $validatedData['stream_link'];
+               $user->update();
+
+               UserChanged::dispatch($user);
+
+               return $user->toJson();
+       }
+
+       public function single(Request $request, $id) {
+               $user = User::findOrFail($id);
+               $this->authorize('view', $user);
+               return $user->toJson();
+       }
+
 }
diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php
new file mode 100644 (file)
index 0000000..67bc561
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Policies;
+
+use App\Models\User;
+use Illuminate\Auth\Access\HandlesAuthorization;
+
+class UserPolicy
+{
+       use HandlesAuthorization;
+
+       /**
+        * Determine whether the user can view any models.
+        *
+        * @param  \App\Models\User  $user
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function viewAny(User $user)
+       {
+               return true;
+       }
+
+       /**
+        * Determine whether the user can view the model.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\User  $model
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function view(User $user, User $model)
+       {
+               return true;
+       }
+
+       /**
+        * Determine whether the user can create models.
+        *
+        * @param  \App\Models\User  $user
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function create(User $user)
+       {
+               return false;
+       }
+
+       /**
+        * Determine whether the user can update the model.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\User  $model
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function update(User $user, User $model)
+       {
+               return $user->id === $model->id;
+       }
+
+       /**
+        * Determine whether the user can delete the model.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\User  $model
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function delete(User $user, User $model)
+       {
+               return false;
+       }
+
+       /**
+        * Determine whether the user can restore the model.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\User  $model
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function restore(User $user, User $model)
+       {
+               return false;
+       }
+
+       /**
+        * Determine whether the user can permanently delete the model.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\User  $model
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function forceDelete(User $user, User $model)
+       {
+               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 setStreamLink(User $user, User $model)
+       {
+               return $user->role == 'admin' || $user->id == $model->id;
+       }
+
+}
index 0199b931c43a81776c8d6188726107538509e74d..d914d21b0e4a6e9abf573e06d430d263ee5a5f2d 100644 (file)
@@ -4,6 +4,7 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
 
 import Header from './common/Header';
 import Tournament from './pages/Tournament';
+import User from './pages/User';
 import UserContext from '../helpers/UserContext';
 
 const App = () => {
@@ -51,6 +52,7 @@ const App = () => {
                        <Header doLogout={doLogout} />
                        <Routes>
                                <Route path="tournaments/:id" element={<Tournament />} />
+                               <Route path="users/:id" element={<User />} />
                                <Route path="*" element={<Navigate to="/tournaments/1" />} />
                        </Routes>
                </UserContext.Provider>
index 11dc388c67d60c3832a977a7265a93033b7010ee..6e32227ab2d04d330b8025dc16744fdbbee5d606 100644 (file)
@@ -7,7 +7,7 @@ import ErrorMessage from '../common/ErrorMessage';
 import Loading from '../common/Loading';
 import NotFound from '../pages/NotFound';
 import Detail from '../tournament/Detail';
-import { patchResult, patchRound, sortParticipants } from '../../helpers/Tournament';
+import { patchResult, patchRound, patchUser, sortParticipants } from '../../helpers/Tournament';
 
 const Tournament = () => {
        const params = useParams();
@@ -63,6 +63,20 @@ const Tournament = () => {
                };
        }, [id]);
 
+       useEffect(() => {
+               const cb = (e) => {
+                       if (e.user) {
+                               setTournament(tournament => patchUser(tournament, e.user));
+                       }
+               };
+               window.Echo.channel('App.Control')
+                       .listen('UserChanged', cb);
+               return () => {
+                       window.Echo.channel('App.Control')
+                               .stopListening('UserChanged', cb);
+               };
+       }, []);
+
        if (loading) {
                return <Loading />;
        }
diff --git a/resources/js/components/pages/User.js b/resources/js/components/pages/User.js
new file mode 100644 (file)
index 0000000..8dfdba4
--- /dev/null
@@ -0,0 +1,66 @@
+import axios from 'axios';
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+import ErrorBoundary from '../common/ErrorBoundary';
+import ErrorMessage from '../common/ErrorMessage';
+import Loading from '../common/Loading';
+import NotFound from '../pages/NotFound';
+import Profile from '../users/Profile';
+
+const User = () => {
+       const params = useParams();
+       const { id } = params;
+
+       const [error, setError] = useState(null);
+       const [loading, setLoading] = useState(true);
+       const [user, setUser] = useState(null);
+
+       useEffect(() => {
+               setLoading(true);
+               axios
+                       .get(`/api/users/${id}`)
+                       .then(response => {
+                               setError(null);
+                               setLoading(false);
+                               setUser(response.data);
+                       })
+                       .catch(error => {
+                               setError(error);
+                               setLoading(false);
+                               setUser(null);
+                       });
+       }, [id]);
+
+       useEffect(() => {
+               const cb = (e) => {
+                       if (e.user) {
+                               setUser(user => e.user.id === user.id ? { ...user, ...e.user } : user);
+                       }
+               };
+               window.Echo.channel('App.Control')
+                       .listen('UserChanged', cb);
+               return () => {
+                       window.Echo.channel('App.Control')
+                               .stopListening('UserChanged', cb);
+               };
+       }, []);
+
+       if (loading) {
+               return <Loading />;
+       }
+
+       if (error) {
+               return <ErrorMessage error={error} />;
+       }
+
+       if (!user) {
+               return <NotFound />;
+       }
+
+       return <ErrorBoundary>
+               <Profile user={user} />
+       </ErrorBoundary>;
+};
+
+export default User;
index e7d1d9dec2566eb6afca44578138e0d7179a7128..943aac39ff059c016bef1f49e9fff978fc75d60e 100644 (file)
@@ -7,10 +7,10 @@ import { withTranslation } from 'react-i18next';
 import toastr from 'toastr';
 
 import LargeCheck from '../common/LargeCheck';
-import i18n from '../../i18n';
 import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
 import { findResult } from '../../helpers/Participant';
 import { formatTime, parseTime } from '../../helpers/Result';
+import i18n from '../../i18n';
 import yup from '../../schema/yup';
 
 const ReportForm = ({
@@ -114,7 +114,7 @@ export default withFormik({
                                onCancel();
                        }
                } catch (e) {
-                       toastr.success(i18n.t('results.reportError'));
+                       toastr.error(i18n.t('results.reportError'));
                        if (e.response && e.response.data && e.response.data.errors) {
                                setErrors(laravelErrorsToFormik(e.response.data.errors));
                        }
index e21da2e4c364ffa6fb1ed41286f87fbc1696e12a..3cff560e93760b0898c784475c254b1fa7d2d236 100644 (file)
@@ -4,11 +4,13 @@ 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 ReportForm = ({
+const SeedForm = ({
        errors,
        handleBlur,
        handleChange,
@@ -51,7 +53,7 @@ const ReportForm = ({
        </Modal.Footer>
 </Form>;
 
-ReportForm.propTypes = {
+SeedForm.propTypes = {
        errors: PropTypes.shape({
                seed: PropTypes.string,
        }),
@@ -72,12 +74,21 @@ export default withFormik({
        enableReinitialize: true,
        handleSubmit: async (values, actions) => {
                const { round_id, seed } = values;
+               const { setErrors } = actions;
                const { onCancel } = actions.props;
-               await axios.post(`/api/rounds/${round_id}/setSeed`, {
-                       seed,
-               });
-               if (onCancel) {
-                       onCancel();
+               try {
+                       await axios.post(`/api/rounds/${round_id}/setSeed`, {
+                               seed,
+                       });
+                       toastr.success(i18n.t('rounds.setSeedSuccess'));
+                       if (onCancel) {
+                               onCancel();
+                       }
+               } catch (e) {
+                       toastr.error(i18n.t('rounds.setSeedError'));
+                       if (e.response && e.response.data && e.response.data.errors) {
+                               setErrors(laravelErrorsToFormik(e.response.data.errors));
+                       }
                }
        },
        mapPropsToValues: ({ round }) => ({
@@ -87,4 +98,4 @@ export default withFormik({
        validationSchema: yup.object().shape({
                seed: yup.string().required().url(),
        }),
-})(withTranslation()(ReportForm));
+})(withTranslation()(SeedForm));
diff --git a/resources/js/components/users/EditStreamLinkButton.js b/resources/js/components/users/EditStreamLinkButton.js
new file mode 100644 (file)
index 0000000..a1c8c24
--- /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 EditStreamLinkDialog from './EditStreamLinkDialog';
+import Icon from '../common/Icon';
+import { mayEditStreamLink } from '../../helpers/permissions';
+import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
+
+const EditStreamLinkButton = ({ authUser, user }) => {
+       const [showDialog, setShowDialog] = useState(false);
+
+       if (mayEditStreamLink(authUser, user)) {
+               return <>
+                       <EditStreamLinkDialog
+                               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;
+};
+
+EditStreamLinkButton.propTypes = {
+       authUser: PropTypes.shape({
+       }),
+       user: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(withUser(EditStreamLinkButton, 'authUser'));
diff --git a/resources/js/components/users/EditStreamLinkDialog.js b/resources/js/components/users/EditStreamLinkDialog.js
new file mode 100644 (file)
index 0000000..34db12e
--- /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 EditStreamLinkForm from './EditStreamLinkForm';
+import i18n from '../../i18n';
+
+const EditStreamLinkDialog = ({
+       onHide,
+       show,
+       user,
+}) =>
+<Modal className="edit-stream-link-dialog" onHide={onHide} show={show}>
+       <Modal.Header closeButton>
+               <Modal.Title>
+                       {i18n.t('users.editStreamLink')}
+               </Modal.Title>
+       </Modal.Header>
+       <EditStreamLinkForm
+               onCancel={onHide}
+               user={user}
+       />
+</Modal>;
+
+EditStreamLinkDialog.propTypes = {
+       onHide: PropTypes.func,
+       show: PropTypes.bool,
+       user: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(EditStreamLinkDialog);
diff --git a/resources/js/components/users/EditStreamLinkForm.js b/resources/js/components/users/EditStreamLinkForm.js
new file mode 100644 (file)
index 0000000..78219ac
--- /dev/null
@@ -0,0 +1,101 @@
+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,
+       values,
+}) =>
+<Form noValidate onSubmit={handleSubmit}>
+       <Modal.Body>
+               <Row>
+                       <Form.Group as={Col} controlId="user.stream_link">
+                               <Form.Label>{i18n.t('users.streamLink')}</Form.Label>
+                               <Form.Control
+                                       isInvalid={!!(touched.stream_link && errors.stream_link)}
+                                       name="stream_link"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       placeholder="https://www.twitch.tv/fgfm"
+                                       type="text"
+                                       value={values.stream_link || ''}
+                               />
+                               {touched.stream_link && errors.stream_link ?
+                                       <Form.Control.Feedback type="invalid">
+                                               {i18n.t(errors.stream_link)}
+                                       </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({
+               stream_link: PropTypes.string,
+       }),
+       handleBlur: PropTypes.func,
+       handleChange: PropTypes.func,
+       handleSubmit: PropTypes.func,
+       onCancel: PropTypes.func,
+       touched: PropTypes.shape({
+               stream_link: PropTypes.bool,
+       }),
+       values: PropTypes.shape({
+               stream_link: PropTypes.string,
+       }),
+};
+
+export default withFormik({
+       displayName: 'SeedForm',
+       enableReinitialize: true,
+       handleSubmit: async (values, actions) => {
+               const { user_id, stream_link } = values;
+               const { setErrors } = actions;
+               const { onCancel } = actions.props;
+               try {
+                       await axios.post(`/api/users/${user_id}/setStreamLink`, {
+                               stream_link,
+                       });
+                       toastr.success(i18n.t('users.setStreamLinkSuccess'));
+                       if (onCancel) {
+                               onCancel();
+                       }
+               } catch (e) {
+                       toastr.error(i18n.t('users.setStreamLinkError'));
+                       if (e.response && e.response.data && e.response.data.errors) {
+                               setErrors(laravelErrorsToFormik(e.response.data.errors));
+                       }
+               }
+       },
+       mapPropsToValues: ({ user }) => ({
+               user_id: user.id,
+               stream_link: user.stream_link || '',
+       }),
+       validationSchema: yup.object().shape({
+               stream_link: yup.string().required().url(),
+       }),
+})(withTranslation()(EditStreamLinkForm));
diff --git a/resources/js/components/users/Profile.js b/resources/js/components/users/Profile.js
new file mode 100644 (file)
index 0000000..f38ba2f
--- /dev/null
@@ -0,0 +1,48 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Col, Container, Row } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import Box from './Box';
+import EditStreamLinkButton from './EditStreamLinkButton';
+import Icon from '../common/Icon';
+import i18n from '../../i18n';
+
+const Profile = ({ user }) => <Container>
+       <h1>{user.username}</h1>
+       <Row>
+               <Col md={6}>
+                       <h2>{i18n.t('users.discordTag')}</h2>
+                       <Box discriminator user={user} />
+               </Col>
+               <Col md={6}>
+                       <h2>{i18n.t('users.streamLink')}</h2>
+                       <p>
+                               {user.stream_link ?
+                                       <Button
+                                               href={user.stream_link}
+                                               target="_blank"
+                                               variant="outline-twitch"
+                                       >
+                                               <Icon.STREAM />
+                                               {' '}
+                                               {user.stream_link}
+                                       </Button>
+                               :
+                                       i18n.t('users.noStream')
+                               }
+                               {' '}
+                               <EditStreamLinkButton user={user} />
+                       </p>
+               </Col>
+       </Row>
+</Container>;
+
+Profile.propTypes = {
+       user: PropTypes.shape({
+               stream_link: PropTypes.string,
+               username: PropTypes.string,
+       }),
+};
+
+export default withTranslation()(Profile);
index a44d465a4e57609b0e45ff5e756fde2344a1a8e2..eb943401f6074057b4e6fc625c0d7385c4d15042 100644 (file)
@@ -40,6 +40,18 @@ export const findResult = (participant, round) => {
        return round.results.find(result => result.user_id === participant.user_id);
 };
 
+export const patchUser = (participant, user) => {
+       if (!participant || !user) return participant;
+       if (participant.user_id != user.id) return participant;
+       return {
+               ...participant,
+               user: {
+                       ...participant.user,
+                       ...user,
+               },
+       };
+};
+
 export const sortByResult = (participants, round) => {
        if (!participants || !participants.length) return participants;
        if (!round || !round.results || !round.results.length) return participants;
@@ -50,5 +62,6 @@ export default {
        compareResult,
        compareUsername,
        findResult,
+       patchUser,
        sortByResult,
 };
index ff1a06349424f7567e9de29df8a07f2b3d2a4e30..a3f97cc22b47a6e4a3f135a8bbb506ebf9134b0b 100644 (file)
@@ -68,6 +68,15 @@ export const patchRound = (tournament, round) => {
        };
 };
 
+export const patchUser = (tournament, user) => {
+       if (!tournament || !tournament.participants || !user) return tournament;
+       if (!tournament.participants.find(p => p.user_id == user.id)) return tournament;
+       return {
+               ...tournament,
+               participants: tournament.participants.map(p => Participant.patchUser(p, user)),
+       };
+};
+
 export const sortParticipants = tournament => {
        if (!tournament || !tournament.participants || !tournament.participants.length) {
                return tournament;
@@ -83,5 +92,7 @@ export default {
        compareScore,
        findParticipant,
        patchResult,
+       patchRound,
+       patchUser,
        sortParticipants,
 };
index 769edda98e04d79f0be12c76190998e47e936de3..23272d9ac14b0f561d53b1507d5fda79453d4d1d 100644 (file)
@@ -28,3 +28,8 @@ export const mayViewProtocol = user =>
 
 export const maySeeResults = (user, tournament, round) =>
        isAdmin(user) || hasFinished(user, round) || Round.isComplete(tournament, round);
+
+// Users
+
+export const mayEditStreamLink = (user, subject) =>
+       isAdmin(user) || isSameUser(user, subject);
index e7c508695fb183288574b71ef89445a35e9859a1..6feeb64d35dd951c65bb6ce7f595751d7e6d3d0e 100644 (file)
@@ -15,7 +15,22 @@ export default {
                        save: 'Speichern',
                        search: 'Suche',
                },
+               error: {
+                       403: {
+                               description: 'So aber nicht',
+                               heading: 'Zugriff verweigert',
+                       },
+                       404: {
+                               description: 'Das war aber irgendwo',
+                               heading: 'Nicht gefunden',
+                       },
+                       500: {
+                               description: 'NotLikeThis',
+                               heading: 'Serverfehler',
+                       },
+               },
                general: {
+                       anonymous: 'Anonym',
                        appName: 'ALttP',
                },
                icon: {
@@ -121,12 +136,20 @@ export default {
                        noSeed: 'Noch kein Seed',
                        seed: 'Seed',
                        setSeed: 'Seed eintragen',
+                       setSeedError: 'Seed konnte nicht eintragen werden',
+                       setSeedSuccess: 'Seed eingetragen',
                },
                tournaments: {
                        scoreboard: 'Scoreboard',
                },
                users: {
+                       discordTag: 'Discord Tag',
+                       editStreamLink: 'Stream Link bearbeiten',
+                       noStream: 'Kein Stream gesetzt',
+                       setStreamLinkError: 'Konnte Stream Link nicht speichern',
+                       setStreamLinkSuccess: 'Stream Link gespeichert',
                        stream: 'Stream',
+                       streamLink: 'Stream Link',
                },
                validation: {
                        error: {
index 0f8b744381957b828ea6a4442d74aa3eda547f00..3f01805f73cb986e7843bd5e263dee9d8459af1f 100644 (file)
@@ -15,7 +15,22 @@ export default {
                        save: 'Save',
                        search: 'Search',
                },
+               error: {
+                       403: {
+                               description: 'Um no',
+                               heading: 'Access denied',
+                       },
+                       404: {
+                               description: 'Pretty sure I had that somehere',
+                               heading: 'Not found',
+                       },
+                       500: {
+                               description: 'NotLikeThis',
+                               heading: 'Server error',
+                       },
+               },
                general: {
+                       anonymous: 'Anonym',
                        appName: 'ALttP',
                },
                icon: {
@@ -121,12 +136,20 @@ export default {
                        noSeed: 'No seed set',
                        seed: 'Seed',
                        setSeed: 'Set seed',
+                       setSeedError: 'Seed could not be set',
+                       setSeedSuccess: 'Seed set',
                },
                tournaments: {
                        scoreboard: 'Scoreboard',
                },
                users: {
+                       discordTag: 'Discord tag',
+                       editStreamLink: 'Edit stream link',
+                       noStream: 'No stream set',
+                       setStreamLinkError: 'Could not save stream link',
+                       setStreamLinkSuccess: 'Stream link saved',
                        stream: 'Stream',
+                       streamLink: 'Stream link',
                },
                validation: {
                        error: {
index 57d0d5fc30da60db6c503a76e3f87eb29deb4669..80b8b2a62e83293435f9d35a3b48437c210364e3 100644 (file)
@@ -11,6 +11,13 @@ h1 {
        margin-bottom: 2rem;
 }
 
+.text-discord {
+       color: $discord;
+}
+.text-twitch {
+       color: $twitch;
+}
+
 .text-gold {
        color: $gold;
 }
index 4c55f75eb6a535013f837064ec66d649ea31133d..ab79dce945493aebea14e1b5821286b074701f64 100644 (file)
@@ -27,4 +27,6 @@ Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setS
 
 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}/setStreamLink', 'App\Http\Controllers\UserController@setStreamLink');