diff --git a/website/src/views/components/SearchBox.tsx b/website/src/views/components/SearchBox.tsx index f3147f92771..24afde044c6 100644 --- a/website/src/views/components/SearchBox.tsx +++ b/website/src/views/components/SearchBox.tsx @@ -16,18 +16,18 @@ import styles from './SearchBox.scss'; export type Props = { className?: string; throttle: number; - isLoading: boolean; + isLoading?: boolean; value: string | null; placeholder?: string; onChange: (value: string) => void; - onSearch: () => void; + onSearch?: () => void; onBlur?: () => void; }; const SearchBox: FC = ({ className, throttle, - isLoading, + isLoading = false, value, placeholder, onChange, @@ -55,7 +55,7 @@ const SearchBox: FC = ({ debounce( () => { isDirty.current = false; - onSearch(); + onSearch?.(); }, throttle, { leading: false }, diff --git a/website/src/views/venues/AvailabilitySearch.test.tsx b/website/src/views/venues/AvailabilitySearch.test.tsx index 2a4f3b401a3..6257641d6fc 100644 --- a/website/src/views/venues/AvailabilitySearch.test.tsx +++ b/website/src/views/venues/AvailabilitySearch.test.tsx @@ -1,6 +1,6 @@ import { defaultSearchOptions } from 'views/venues/AvailabilitySearch'; -describe('defaultSearchOptions', () => { +describe(defaultSearchOptions, () => { test('should the nearest slots during school hours', () => { // Monday expect(defaultSearchOptions(new Date('2018-01-15T12:30:00'))).toMatchObject({ diff --git a/website/src/views/venues/VenuesContainer.scss b/website/src/views/venues/VenuesContainer.scss index c49700392b8..82d07a40c84 100644 --- a/website/src/views/venues/VenuesContainer.scss +++ b/website/src/views/venues/VenuesContainer.scss @@ -113,6 +113,17 @@ $venue-list-width: 16rem; border-radius: 0 0 $border-radius $border-radius; } +.availabilitySpinner { + display: inline-block; + width: 1.4rem; + height: 1.4rem; + margin: 0 0.3rem -0.1rem 0; + border-width: 0.2rem; + // Use parent button's text color so that the spinner is still visible when + // the button is hovered over. + border-left-color: initial; +} + .noVenueSelected { $icon-size: 6rem; diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index 2b3cb2738c7..f98ba52d059 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -13,7 +13,7 @@ import { Location, locationsAreEqual } from 'history'; import classnames from 'classnames'; import axios from 'axios'; import qs from 'query-string'; -import { isEqual, mapValues, noop, pick, size } from 'lodash'; +import { isEqual, mapValues, pick, size } from 'lodash'; import type { TimePeriod, Venue, VenueDetailList, VenueSearchOptions } from 'types/venues'; import type { Subtract } from 'types/utils'; @@ -53,7 +53,6 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const location = useLocation(); const matchParams = useParams(); - // Search state const [ /** Value of the controlled search box; updated real-time */ searchQuery, @@ -61,10 +60,13 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ] = useState(() => qs.parse(location.search).q || ''); /** Actual string to search with; deferred update */ const deferredSearchQuery = useDeferredValue(searchQuery); + const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); return !!(params.time && params.day && params.duration); }); + const deferredIsAvailabilityEnabled = useDeferredValue(isAvailabilityEnabled); + const [searchOptions, setSearchOptions] = useState(() => { const params = qs.parse(location.search); // Extract searchOptions from the query string if they are present @@ -103,14 +105,19 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ); const highlightPeriod = useMemo(() => { - if (!isAvailabilityEnabled) return undefined; + if (!deferredIsAvailabilityEnabled) return undefined; return { day: searchOptions.day, startTime: convertIndexToTime(searchOptions.time * 2), endTime: convertIndexToTime(2 * (searchOptions.time + searchOptions.duration)), }; - }, [isAvailabilityEnabled, searchOptions.day, searchOptions.duration, searchOptions.time]); + }, [ + deferredIsAvailabilityEnabled, + searchOptions.day, + searchOptions.duration, + searchOptions.time, + ]); const selectedVenue = useMemo( () => (matchParams.venue ? decodeURIComponent(matchParams.venue) : undefined), @@ -121,7 +128,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { useEffect(() => { let query: Partial = {}; if (deferredSearchQuery) query.q = deferredSearchQuery; - if (isAvailabilityEnabled) query = { ...query, ...searchOptions }; + if (deferredIsAvailabilityEnabled) query = { ...query, ...searchOptions }; const search = qs.stringify(query); const pathname = venuePage(selectedVenue); @@ -139,17 +146,17 @@ export const VenuesContainerComponent: FC = ({ venues }) => { } }, [ debouncedHistory, + deferredIsAvailabilityEnabled, deferredSearchQuery, history, - isAvailabilityEnabled, searchOptions, selectedVenue, ]); const matchedVenues = useMemo(() => { const matched = searchVenue(venues, deferredSearchQuery); - return isAvailabilityEnabled ? filterAvailability(matched, searchOptions) : matched; - }, [isAvailabilityEnabled, searchOptions, deferredSearchQuery, venues]); + return deferredIsAvailabilityEnabled ? filterAvailability(matched, searchOptions) : matched; + }, [deferredIsAvailabilityEnabled, searchOptions, deferredSearchQuery, venues]); const matchedVenueNames = useMemo(() => matchedVenues.map(([venue]) => venue), [matchedVenues]); function renderSearch() { @@ -160,23 +167,25 @@ export const VenuesContainerComponent: FC = ({ venues }) => { {isAvailabilityEnabled && (