]> git.localhorst.tv Git - alttp.git/commitdiff
temporary user select
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 25 Oct 2022 14:13:26 +0000 (16:13 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 25 Oct 2022 14:13:26 +0000 (16:13 +0200)
still need to fix the FK datatypes

app/Http/Controllers/RoundController.php
app/Http/Controllers/UserController.php
resources/js/components/common/Icon.js
resources/js/components/common/UserSelect.js [new file with mode: 0644]
resources/js/components/rounds/EditForm.js
resources/js/components/rounds/SeedCodeInput.js
resources/js/components/users/Box.js
routes/api.php

index 337e1111fea27dcde1c2a229121e9d4cb429f8a9..6d31e9020897ee8968d7b2ce5040197a68368b86 100644 (file)
@@ -45,11 +45,13 @@ class RoundController extends Controller
                $validatedData = $request->validate([
                        'code' => 'array',
                        'code.*' => 'string',
+                       'rolled_by' => 'nullable|exists:App\\Models\\User,id',
                        'seed' => 'url',
                        'title' => 'string',
                ]);
 
                $round->code = array_filter($validatedData['code']);
+               $round->rolled_by = $validatedData['rolled_by'];
                $round->seed = $validatedData['seed'];
                $round->title = $validatedData['title'];
                $round->update();
index da3ebde1bf113bd18e2b56b6fee72ab23a155fd3..4bf2bb30324cf88c73c4b0225123de81793f1e7d 100644 (file)
@@ -9,6 +9,20 @@ use Illuminate\Http\Request;
 class UserController extends Controller
 {
 
+       public function search(Request $request) {
+               $validatedData = $request->validate([
+                       'phrase' => 'string|nullable',
+               ]);
+
+               $users = User::query();
+               if (!empty($validatedData['phrase'])) {
+                       $users = $users->where('username', 'LIKE', '%'.$validatedData['phrase'].'%')
+                               ->orWhere('nickname', 'LIKE', '%'.$validatedData['phrase'].'%');
+               }
+               $users = $users->limit(5);
+               return $users->get()->toJson();
+       }
+
        public function setLanguage(Request $request) {
                $user = $request->user();
                if (!$user) return;
index f3e63e57ad3c3d7dddc369eba19c32c2d0e53d85..617ca0385d8997f217cbf3ba1bcf2d177c75f8ec 100644 (file)
@@ -73,6 +73,7 @@ Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt');
 Icon.PENDING = makePreset('PendingIcon', 'clock');
 Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt');
 Icon.REJECT = makePreset('RejectIcon', 'square-xmark');
+Icon.REMOVE = makePreset('RemoveIcon', 'square-xmark');
 Icon.RESULT = makePreset('ResultIcon', 'clock');
 Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
 Icon.SETTINGS = makePreset('SettingsIcon', 'cog');
diff --git a/resources/js/components/common/UserSelect.js b/resources/js/components/common/UserSelect.js
new file mode 100644 (file)
index 0000000..be473b6
--- /dev/null
@@ -0,0 +1,118 @@
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { Button, Form, ListGroup } from 'react-bootstrap';
+
+import Icon from '../common/Icon';
+import UserBox from '../users/Box';
+import debounce from '../../helpers/debounce';
+
+const UserSelect = ({ name, onChange, value }) => {
+       const [resolved, setResolved] = useState(null);
+       const [results, setResults] = useState([]);
+       const [search, setSearch] = useState('');
+       const [showResults, setShowResults] = useState(false);
+
+       const ref = useRef(null);
+
+       useEffect(() => {
+               const handleEventOutside = e => {
+                       if (ref.current && !ref.current.contains(e.target)) {
+                               setShowResults(false);
+                       }
+               };
+               document.addEventListener('click', handleEventOutside, true);
+               document.addEventListener('focus', handleEventOutside, true);
+               return () => {
+                       document.removeEventListener('click', handleEventOutside, true);
+                       document.removeEventListener('focus', handleEventOutside, true);
+               };
+       }, []);
+
+       let ctrl = null;
+       const fetch = useCallback(debounce(async phrase => {
+               if (ctrl) {
+                       ctrl.abort();
+               }
+               ctrl = new AbortController();
+               if (!phrase || phrase.length < 3) {
+                       setResults([]);
+                       return;
+               }
+               try {
+                       const response = await axios.get(`/api/users`, {
+                               params: {
+                                       phrase,
+                               },
+                               signal: ctrl.signal,
+                       });
+                       ctrl = null;
+                       setResults(response.data);
+               } catch (e) {
+                       ctrl = null;
+                       console.error(e);
+               }
+       }, 300), []);
+
+       useEffect(() => {
+               fetch(search);
+       }, [search]);
+
+       useEffect(() => {
+               if (value) {
+                       axios
+                               .get(`/api/users/${value}`)
+                       .then(response => {
+                               setResolved(response.data);
+                       });
+               } else {
+                       setResolved(null);
+               }
+       }, [value]);
+
+       if (value) {
+               return <div className="d-flex justify-space-between">
+                       {resolved ? <UserBox discriminator noLink user={resolved} /> : <span>value</span>}
+                       <Button
+                               onClick={() => onChange({ target: { name, value: null }})}
+                               size="sm"
+                               variant="outline-danger"
+                       >
+                               <Icon.REMOVE />
+                       </Button>
+               </div>;
+       }
+       return <div className={`user-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+               <Form.Control
+                       className="search-input"
+                       name={Math.random().toString(20).substr(2, 10)}
+                       onChange={e => setSearch(e.target.value)}
+                       onFocus={() => setShowResults(true)}
+                       type="search"
+                       value={search}
+               />
+               <div className="search-results-holder">
+                       <ListGroup className="search-results">
+                               {results.map(result =>
+                                       <ListGroup.Item
+                                               action
+                                               key={result.id}
+                                               onClick={() => onChange({
+                                                       target: { name, value: result.id },
+                                               })}
+                                       >
+                                               <UserBox discriminator noLink user={result} />
+                                       </ListGroup.Item>
+                               )}
+                       </ListGroup>
+               </div>
+       </div>;
+};
+
+UserSelect.propTypes = {
+       name: PropTypes.string,
+       onChange: PropTypes.func,
+       value: PropTypes.string,
+};
+
+export default UserSelect;
index 3d0d0b368ad0749008b4153327a5f85c37cc37ed..6472c842a27a28298a13298f2bb5ece02b7bf44a 100644 (file)
@@ -7,6 +7,7 @@ import { withTranslation } from 'react-i18next';
 import toastr from 'toastr';
 
 import SeedCodeInput from './SeedCodeInput';
+import UserSelect from '../common/UserSelect';
 import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
 import i18n from '../../i18n';
 import yup from '../../schema/yup';
@@ -77,6 +78,24 @@ const EditForm = ({
                                : null}
                        </Form.Group>
                </Row>
+               <Row>
+                       <Form.Group as={Col}>
+                               <Form.Label>{i18n.t('rounds.rolled_by')}</Form.Label>
+                               <Form.Control
+                                       as={UserSelect}
+                                       isInvalid={!!(touched.rolled_by && errors.rolled_by)}
+                                       name="rolled_by"
+                                       onBlur={handleBlur}
+                                       onChange={handleChange}
+                                       value={values.rolled_by || null}
+                               />
+                               {touched.rolled_by && errors.rolled_by ?
+                                       <Form.Control.Feedback type="invalid">
+                                               {i18n.t(errors.rolled_by)}
+                                       </Form.Control.Feedback>
+                               : null}
+                       </Form.Group>
+               </Row>
        </Modal.Body>
        <Modal.Footer>
                {onCancel ?
@@ -93,6 +112,7 @@ const EditForm = ({
 EditForm.propTypes = {
        errors: PropTypes.shape({
                code: PropTypes.arrayOf(PropTypes.string),
+               rolled_by: PropTypes.string,
                seed: PropTypes.string,
                title: PropTypes.string,
        }),
@@ -102,12 +122,14 @@ EditForm.propTypes = {
        onCancel: PropTypes.func,
        touched: PropTypes.shape({
                code: PropTypes.arrayOf(PropTypes.bool),
+               rolled_by: PropTypes.bool,
                seed: PropTypes.bool,
                title: PropTypes.bool,
        }),
        values: PropTypes.shape({
                code: PropTypes.arrayOf(PropTypes.string),
                game: PropTypes.string,
+               rolled_by: PropTypes.string,
                seed: PropTypes.string,
                title: PropTypes.string,
        }),
@@ -136,12 +158,14 @@ export default withFormik({
        mapPropsToValues: ({ round }) => ({
                code: round.code || [],
                game: round.game || 'mixed',
+               rolled_by: round.rolled_by || null,
                round_id: round.id,
                seed: round.seed || '',
                title: round.title || '',
        }),
        validationSchema: yup.object().shape({
                code: yup.array().of(yup.string()),
+               rolled_by: yup.string(),
                seed: yup.string().url(),
                title: yup.string(),
        }),
index 95ef7888697900bc77963b5b5594ef59954a1907..4dc8abe3ee335c442da606bf78f9be53dd05b106 100644 (file)
@@ -7,7 +7,6 @@ import i18n from '../../i18n';
 
 const ALTTPR_CODES = [
        'big-key',
-       'bow',
        'blue-boomerang',
        'bomb',
        'bombos',
index bc4673ee9e12d818c209ced29906346bd3ff8f22..70006cb13f2998df557130cbb0336efd7b49f597 100644 (file)
@@ -7,18 +7,14 @@ import { useNavigate } from 'react-router-dom';
 import { getAvatarUrl } from '../../helpers/User';
 import i18n from '../../i18n';
 
-const Box = ({ discriminator, user }) => {
+const Box = ({ discriminator, noLink, user }) => {
        const navigate = useNavigate();
 
        if (!user) {
                return <span>{i18n.t('general.anonymous')}</span>;
        }
 
-       return <Button
-               className="user-box"
-               onClick={() => navigate(`/users/${user.id}`)}
-               variant="link"
-       >
+       const content = <>
                <img alt="" src={getAvatarUrl(user)} />
                <span>{discriminator || !user.nickname ? user.username : user.nickname}</span>
                {discriminator ?
@@ -27,11 +23,24 @@ const Box = ({ discriminator, user }) => {
                                {user.discriminator}
                        </span>
                : null}
+       </>;
+
+       if (noLink) {
+               return <span className="user-box">{content}</span>;
+       }
+
+       return <Button
+               className="user-box"
+               onClick={() => navigate(`/users/${user.id}`)}
+               variant="link"
+       >
+               {content}
        </Button>;
 };
 
 Box.propTypes = {
        discriminator: PropTypes.bool,
+       noLink: PropTypes.bool,
        user: PropTypes.shape({
                discriminator: PropTypes.string,
                id: PropTypes.string,
index d9f44ad4c1db850919998d7ae83f5f7dd2a9cddf..3d7062adbe6661e7ee3aaad9ab232697a04c9be5 100644 (file)
@@ -54,6 +54,7 @@ Route::post('tournaments/{tournament}/lock', 'App\Http\Controllers\TournamentCon
 Route::post('tournaments/{tournament}/open', 'App\Http\Controllers\TournamentController@open');
 Route::post('tournaments/{tournament}/unlock', 'App\Http\Controllers\TournamentController@unlock');
 
+Route::get('users', 'App\Http\Controllers\UserController@search');
 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');