$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();
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;
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');
--- /dev/null
+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;
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';
: 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 ?
EditForm.propTypes = {
errors: PropTypes.shape({
code: PropTypes.arrayOf(PropTypes.string),
+ rolled_by: PropTypes.string,
seed: PropTypes.string,
title: PropTypes.string,
}),
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,
}),
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(),
}),
const ALTTPR_CODES = [
'big-key',
- 'bow',
'blue-boomerang',
'bomb',
'bombos',
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 ?
{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,
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');