]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/common/DiscordSelect.js
compact keysanity tracker
[alttp.git] / resources / js / components / common / DiscordSelect.js
1 import axios from 'axios';
2 import PropTypes from 'prop-types';
3 import React, { useCallback, useEffect, useRef, useState } from 'react';
4 import { Alert, Button, Form, ListGroup } from 'react-bootstrap';
5 import { useTranslation } from 'react-i18next';
6
7 import Icon from './Icon';
8 import GuildBox from '../discord-guilds/Box';
9 import debounce from '../../helpers/debounce';
10
11 const DiscordSelect = ({ onChange, value }) => {
12         const [resolved, setResolved] = useState(null);
13         const [results, setResults] = useState([]);
14         const [search, setSearch] = useState('');
15         const [showResults, setShowResults] = useState(false);
16
17         const ref = useRef(null);
18         const { t } = useTranslation();
19
20         useEffect(() => {
21                 const handleEventOutside = e => {
22                         if (ref.current && !ref.current.contains(e.target)) {
23                                 setShowResults(false);
24                         }
25                 };
26                 document.addEventListener('click', handleEventOutside, true);
27                 document.addEventListener('focus', handleEventOutside, true);
28                 return () => {
29                         document.removeEventListener('click', handleEventOutside, true);
30                         document.removeEventListener('focus', handleEventOutside, true);
31                 };
32         }, []);
33
34         let ctrl = null;
35         const fetch = useCallback(debounce(async phrase => {
36                 if (ctrl) {
37                         ctrl.abort();
38                 }
39                 ctrl = new AbortController();
40                 try {
41                         const response = await axios.get(`/api/discord-guilds`, {
42                                 params: {
43                                         phrase,
44                                 },
45                                 signal: ctrl.signal,
46                         });
47                         ctrl = null;
48                         setResults(response.data);
49                 } catch (e) {
50                         ctrl = null;
51                         console.error(e);
52                 }
53         }, 300), []);
54
55         useEffect(() => {
56                 fetch(search);
57         }, [search]);
58
59         useEffect(() => {
60                 if (value) {
61                         axios
62                                 .get(`/api/discord-guilds/${value}`)
63                         .then(response => {
64                                 setResolved(response.data);
65                         });
66                 } else {
67                         setResolved(null);
68                 }
69         }, [value]);
70
71         if (value) {
72                 return <div className="d-flex align-items-center justify-content-between">
73                         <span>{resolved ? <GuildBox guild={resolved} /> : value}</span>
74                         <Button
75                                 className="ms-2"
76                                 onClick={() => onChange({ guild: null, target: { value: '' }})}
77                                 title={t('button.unset')}
78                                 variant="outline-danger"
79                         >
80                                 <Icon.REMOVE title="" />
81                         </Button>
82                 </div>;
83         }
84         return <div className={`discord-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
85                 <Form.Control
86                         className="search-input"
87                         name={Math.random().toString(20).substr(2, 10)}
88                         onChange={e => setSearch(e.target.value)}
89                         onFocus={() => setShowResults(true)}
90                         type="search"
91                         value={search}
92                 />
93                 <div className="search-results-holder">
94                         {results.length ?
95                                 <ListGroup className="search-results">
96                                         {results.map(result =>
97                                                 <ListGroup.Item
98                                                         action
99                                                         key={result.id}
100                                                         onClick={() => onChange({
101                                                                 guild: result,
102                                                                 target: { value: result.guild_id },
103                                                         })}
104                                                 >
105                                                         <GuildBox guild={result} />
106                                                 </ListGroup.Item>
107                                         )}
108                                 </ListGroup>
109                         :
110                                 <Alert className="search-results" variant="info">
111                                         {t('search.noResults')}
112                                 </Alert>
113                         }
114                 </div>
115         </div>;
116 };
117
118 DiscordSelect.propTypes = {
119         onChange: PropTypes.func,
120         value: PropTypes.string,
121 };
122
123 export default DiscordSelect;