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