• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

code4recovery / tsml-ui / 14920598081

09 May 2025 03:12AM UTC coverage: 60.536% (-0.5%) from 61.057%
14920598081

Pull #447

github

web-flow
Merge 1f46fce2b into 9519f3f31
Pull Request #447: Feat: Add support for OSM map if no mapbox key is provided

530 of 968 branches covered (54.75%)

Branch coverage included in aggregate %.

4 of 10 new or added lines in 2 files covered. (40.0%)

108 existing lines in 7 files now uncovered.

668 of 1011 relevant lines covered (66.07%)

9.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.61
/src/components/Controls.tsx
1
import {
2
  Dispatch,
3
  SetStateAction,
4
  FormEvent,
5
  useEffect,
6
  useRef,
7
  useState,
8
  MouseEvent,
9
} from 'react';
10

11
import { useSearchParams } from 'react-router-dom';
12

13
import { analyticsEvent, useSettings } from '../helpers';
14
import {
15
  controlsCss,
16
  controlsGroupFirstCss,
17
  controlsGroupLastCss,
18
  controlsInputCss,
19
  controlsInputFirstCss,
20
  controlsInputSearchSubmitCss,
21
  controlsSearchDropdownCss,
22
  dropdownButtonLastCss,
23
  dropdownCss,
24
} from '../styles';
25

26
import Dropdown from './Dropdown';
27

28
import type { State } from '../types';
29

30
export default function Controls({
31
  state,
32
  setState,
33
  mapbox,
34
}: {
35
  state: State;
36
  setState: Dispatch<SetStateAction<State>>;
37
  mapbox?: string;
38
}) {
39
  const { settings, strings } = useSettings();
14✔
40
  const [dropdown, setDropdown] = useState<string>();
14✔
41
  const [search, setSearch] = useState(
14✔
42
    state.input.mode === 'location' ? state.input.search : ''
14✔
43
  );
44
  const [searchParams, setSearchParams] = useSearchParams();
14✔
45
  const searchInput = useRef<HTMLInputElement>(null);
14✔
46

47
  //get available search options based on capabilities
48
  const allModes = ['search', 'location', 'me'] as const;
14✔
49
  const modes = allModes
14✔
50
    .filter(
51
      mode => mode !== 'location' || state.capabilities.coordinates
42✔
52
    )
53
    .filter(
54
      mode =>
55
        mode !== 'me' ||
41✔
56
        (state.capabilities.coordinates && state.capabilities.geolocation)
57
    );
58

59
  //get available filters
60
  const filters = settings.filters
14✔
61
    .filter(filter => state.capabilities[filter])
70✔
62
    .filter(filter => filter !== 'region' || state.input.mode === 'search')
26✔
63
    .filter(filter => filter !== 'distance' || state.input.mode !== 'search');
19✔
64

65
  //get available views
66
  const allViews = ['table', 'map'] as const;
14✔
67
  const views = allViews.filter(
14✔
68
    view => view !== 'map' || state.capabilities.coordinates
28✔
69
  );
70

71
  //whether to show the views segmented button
72
  const canShowViews = views.length > 1;
14✔
73

74
  //add click listener for dropdowns (in lieu of including bootstrap js + jquery)
75
  useEffect(() => {
14✔
76
    document.body.addEventListener('click', closeDropdown);
4✔
77
    return () => {
4✔
78
      document.body.removeEventListener('click', closeDropdown);
4✔
79
    };
80
  }, [document]);
81

82
  //search effect
83
  useEffect(() => {
14✔
84
    const timer = setTimeout(() => {
4✔
85
      if (!state.input.search) return;
2✔
86
      analyticsEvent({
1✔
87
        category: 'search',
88
        action: state.input.mode,
89
        label: state.input.search,
90
      });
91
    }, 2000);
92
    return () => clearTimeout(timer);
4✔
93
  }, [state.input.search]);
94

95
  // update url params when search changes
96
  useEffect(() => {
14✔
97
    if (!searchInput.current) return;
6✔
98

99
    const { value } = searchInput.current;
5✔
100

101
    if (value === search) return;
5!
102
    if (searchParams.get('search') === value) return;
×
103
    if (value) {
×
104
      searchParams.set('search', value);
×
105
    } else {
106
      searchParams.delete('search');
×
107
    }
108

109
    setSearchParams(searchParams);
×
110
  }, [searchInput.current?.value]);
111

112
  //close current dropdown (on body click)
113
  const closeDropdown = () => {
14✔
114
    setDropdown(undefined);
3✔
115
  };
116

117
  //near location search
118
  const locationSearch = (e: FormEvent<HTMLFormElement>) => {
14✔
119
    e.preventDefault();
2✔
120

121
    if (state.input.mode !== 'location') return;
2✔
122

123
    setState({
1✔
124
      ...state,
125
      input: {
126
        ...state.input,
127
        latitude: undefined,
128
        longitude: undefined,
129
        search,
130
      },
131
    });
132

133
    if (search) {
1!
134
      searchParams.set('search', search);
1✔
135
    } else {
136
      searchParams.delete('search');
×
137
    }
138

139
    setSearchParams(searchParams);
1✔
140
  };
141

142
  //set search mode dropdown and clear all distances
143
  const setMode = (e: MouseEvent, mode: 'search' | 'location' | 'me') => {
14✔
144
    e.preventDefault();
1✔
145

146
    Object.keys(state.meetings).forEach(slug => {
1✔
147
      state.meetings[slug].distance = undefined;
2✔
148
    });
149

150
    setSearch('');
1✔
151
    if (mode !== 'search') {
1!
152
      searchParams.delete('search');
1✔
153
      searchParams.set('mode', mode);
1✔
154
    } else {
155
      searchParams.delete('mode');
×
156
    }
157

158
    //focus after waiting for disabled to clear
159
    setTimeout(() => searchInput.current?.focus(), 100);
1✔
160

161
    setState({
1✔
162
      ...state,
163
      capabilities: {
164
        ...state.capabilities,
165
        distance: false,
166
      },
167
      indexes: {
168
        ...state.indexes,
169
        distance: [],
170
      },
171
      input: {
172
        ...state.input,
173
        distance: [],
174
        latitude: undefined,
175
        longitude: undefined,
176
        mode: mode,
177
        search: '',
178
      },
179
    });
180

181
    setSearchParams(searchParams);
1✔
182
  };
183

184
  //toggle list/map view
185
  const setView = (e: MouseEvent, view: 'table' | 'map') => {
14✔
186
    e.preventDefault();
1✔
187

188
    if (view !== 'table') {
1!
189
      searchParams.set('view', view);
1✔
190
    } else {
UNCOV
191
      searchParams.delete('view');
×
192
    }
193

194
    setState({ ...state, input: { ...state.input, view } });
1✔
195

196
    setSearchParams(searchParams);
1✔
197
  };
198

199
  return !Object.keys(state.meetings).length ? null : (
14✔
200
    <div css={controlsCss}>
201
      <form onSubmit={locationSearch} css={dropdownCss}>
202
        <fieldset role="group">
203
          <input
204
            aria-label={strings.modes[state.input.mode]}
205
            css={modes.length > 1 ? controlsInputFirstCss : controlsInputCss}
13!
206
            disabled={state.input.mode === 'me'}
207
            onChange={e => {
208
              if (state.input.mode === 'search') {
2✔
209
                state.input.search = e.target.value;
1✔
210
                setState({ ...state });
1✔
211
              } else {
212
                setSearch(e.target.value);
1✔
213
              }
214
            }}
215
            placeholder={strings.modes[state.input.mode]}
216
            ref={searchInput}
217
            spellCheck="false"
218
            type="search"
219
            value={
220
              state.input.mode === 'location' ? search : state.input.search
13✔
221
            }
222
          />
223
          <input type="submit" hidden css={controlsInputSearchSubmitCss} />
224
          {modes.length > 1 && (
26✔
225
            <button
226
              aria-label={strings.modes[state.input.mode]}
227
              css={dropdownButtonLastCss}
228
              onClick={e => {
229
                setDropdown(dropdown === 'search' ? undefined : 'search');
2✔
230
                e.stopPropagation();
2✔
231
              }}
232
              type="button"
233
            />
234
          )}
235
        </fieldset>
236
        {modes.length > 1 && (
26✔
237
          <div
238
            css={controlsSearchDropdownCss}
239
            style={{
240
              display: dropdown === 'search' ? 'block' : 'none',
13✔
241
            }}
242
          >
243
            {modes.map(mode => (
244
              <div 
39✔
245
                className="tsml-dropdown__item"
246
                data-active={state.input.mode === mode}
247
                key={mode}
248
                >
249
                <button
250
                  className="tsml-dropdown__button"
251
                  onClick={e => setMode(e, mode)}
1✔
252
                >
253
                  {strings.modes[mode]}
254
                </button>
255
              </div>
256
            ))}
257
          </div>
258
        )}
259
      </form>
260
      {filters.map(filter => (
261
        <div key={filter}>
13✔
262
          <Dropdown
263
            defaultValue={
264
              strings[`${filter}_any` as keyof typeof strings] as string
265
            }
266
            filter={filter}
267
            open={dropdown === filter}
268
            setDropdown={setDropdown}
269
            state={state}
270
          />
271
        </div>
272
      ))}
273
      {canShowViews && (
26✔
274
        <div role="group">
275
          {views.map((view, index) => (
276
            <button
26✔
277
              css={index ? controlsGroupLastCss : controlsGroupFirstCss}
26✔
278
              data-active={state.input.view === view}
279
              key={view}
280
              onClick={e => setView(e, view)}
1✔
281
              type="button"
282
            >
283
              {strings.views[view]}
284
            </button>
285
          ))}
286
        </div>
287
      )}
288
    </div>
289
  );
290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc