-
Notifications
You must be signed in to change notification settings - Fork 16
🔖 streams filterbar #6358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🔖 streams filterbar #6358
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import {timezone} from '@frogpond/constants' | |
| import * as c from '@frogpond/colors' | ||
| import {ListSeparator, ListSectionHeader} from '@frogpond/lists' | ||
| import {NoticeView, LoadingView} from '@frogpond/notice' | ||
| import {FilterToolbar, ListType} from '@frogpond/filter' | ||
| import {StreamRow} from './row' | ||
| import toPairs from 'lodash/toPairs' | ||
| import groupBy from 'lodash/groupBy' | ||
|
|
@@ -12,7 +13,7 @@ import type {Moment} from 'moment-timezone' | |
| import {toLaxTitleCase as titleCase} from '@frogpond/titlecase' | ||
| import type {StreamType} from './types' | ||
| import {API} from '@frogpond/api' | ||
| import {fetch} from '@frogpond/fetch' | ||
| import {useFetch} from 'react-async' | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| listContainer: { | ||
|
|
@@ -23,99 +24,137 @@ const styles = StyleSheet.create({ | |
| }, | ||
| }) | ||
|
|
||
| export const StreamListView = (): JSX.Element => { | ||
| let [error, setError] = React.useState<Error | null>(null) | ||
| let [loading, setLoading] = React.useState(true) | ||
| let [refreshing, setRefreshing] = React.useState(false) | ||
| let [streams, setStreams] = React.useState< | ||
| Array<{title: string; data: StreamType[]}> | ||
| >([]) | ||
| let groupStreams = (entries: StreamType[]) => { | ||
| let grouped = groupBy(entries, (j) => j.$groupBy) | ||
| return toPairs(grouped).map(([title, data]) => ({title, data})) | ||
| } | ||
|
|
||
| React.useEffect(() => { | ||
| try { | ||
| getStreams().then(() => { | ||
| setLoading(false) | ||
| }) | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| setError(error) | ||
| } else { | ||
| setError(new Error('unknown error - not an Error')) | ||
| } | ||
| return | ||
| let filterStreams = (entries: StreamType[], filters: ListType[]) => { | ||
| return entries.filter((stream) => { | ||
| let enabledCategories = filters.flatMap((f: ListType) => | ||
| f.spec.selected.flatMap((s) => s.title), | ||
| ) | ||
|
|
||
| if (enabledCategories.length === 0) { | ||
| return entries | ||
| } | ||
|
Comment on lines
+34
to
40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this get pulled out of the filter function? Seems like you're not doing anything that uses
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you asking if lines 34-40 can be extracted into their own function? |
||
| }, []) | ||
|
|
||
| let refresh = async (): Promise<void> => { | ||
| setRefreshing(true) | ||
| await getStreams(true) | ||
| setRefreshing(false) | ||
| } | ||
| return enabledCategories.includes(stream.category) | ||
| }) | ||
| } | ||
|
|
||
| let getStreams = async ( | ||
| reload?: boolean, | ||
| date: Moment = moment.tz(timezone()), | ||
| ) => { | ||
| let dateFrom = date.format('YYYY-MM-DD') | ||
| let dateTo = date.clone().add(2, 'month').format('YYYY-MM-DD') | ||
|
|
||
| let data = await fetch(API('/streams/upcoming'), { | ||
| searchParams: { | ||
| sort: 'ascending', | ||
| dateFrom, | ||
| dateTo, | ||
| }, | ||
| delay: reload ? 500 : 0, | ||
| }).json<Array<StreamType>>() | ||
|
|
||
| data = data | ||
| .filter((stream) => stream.category !== 'athletics') | ||
| .map((stream) => { | ||
| let date: Moment = moment(stream.starttime) | ||
| let dateGroup = date.format('dddd, MMMM Do') | ||
|
|
||
| let group = stream.status.toLowerCase() !== 'live' ? dateGroup : 'Live' | ||
|
|
||
| return { | ||
| ...stream, | ||
| // force title-case on the stream types, to prevent not-actually-duplicate headings | ||
| category: titleCase(stream.category), | ||
| date: date, | ||
| $groupBy: group, | ||
| } | ||
| }) | ||
|
|
||
| let grouped = groupBy(data, (j) => j.$groupBy) | ||
| let mapped = toPairs(grouped).map(([title, data]) => ({title, data})) | ||
|
|
||
| setStreams(mapped) | ||
| } | ||
| let useStreams = (date: Moment = moment.tz(timezone())) => { | ||
| let dateFrom = date.format('YYYY-MM-DD') | ||
| let dateTo = date.clone().add(2, 'month').format('YYYY-MM-DD') | ||
|
|
||
| return useFetch<StreamType[]>( | ||
| API('/streams/upcoming', { | ||
| sort: 'ascending', | ||
| dateFrom, | ||
| dateTo, | ||
| }), | ||
| { | ||
| headers: {accept: 'application/json'}, | ||
| }, | ||
| ) | ||
| } | ||
|
Comment on lines
+27
to
+60
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these really need to be fat-arrow functions? If so, can they be
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They can definitely be |
||
|
|
||
| let keyExtractor = (item: StreamType) => item.eid | ||
| export const StreamListView = (): JSX.Element => { | ||
| let {data = [], error, reload, isPending, isInitial, isLoading} = useStreams() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we give this an interface? These fields seem magical without a type.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does look magical up-front but I guarantee that the types are all pretty clear since this now uses react-async's The type info on line 63 for let useStreams: (date?: Moment) => AsyncState<StreamType[], FetchRun<StreamType[]>>and hovering over each of these types reflects the type information that let data: StreamType[]
let error: Error | undefined
let reload: () => void
let isPending: boolean
let isInitial: false
let isLoading: boolean |
||
|
|
||
| let renderItem = ({item}: {item: StreamType}) => <StreamRow stream={item} /> | ||
| let [filters, setFilters] = React.useState<ListType[]>([]) | ||
|
|
||
| if (loading) { | ||
| return <LoadingView /> | ||
| } | ||
| let entries = React.useMemo(() => { | ||
| return data.map((stream) => { | ||
| let date: Moment = moment(stream.starttime) | ||
| let dateGroup = date.format('dddd, MMMM Do') | ||
|
|
||
| let group = stream.status.toLowerCase() !== 'live' ? dateGroup : 'Live' | ||
|
|
||
| return { | ||
| ...stream, | ||
| // force title-case on the stream types, to prevent not-actually-duplicate headings | ||
| category: titleCase(stream.category), | ||
| date: date, | ||
| $groupBy: group, | ||
| } | ||
| }) | ||
| }, [data]) | ||
|
|
||
| React.useEffect(() => { | ||
| let allCategories = data.flatMap((stream) => titleCase(stream.category)) | ||
|
|
||
| if (allCategories.length === 0) { | ||
| return | ||
| } | ||
|
|
||
| let categories = [...new Set(allCategories)].sort() | ||
| let filterCategories = categories.map((c) => { | ||
| return {title: c} | ||
| }) | ||
|
|
||
| let streamFilters: ListType[] = [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to tighten
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you asking if export type ToggleType = {
type: 'toggle'
key: string
enabled: boolean
spec: ToggleSpecType
apply: ToggleFilterFunctionType
}
export type PickerType = {
type: 'picker'
key: string
enabled: true
spec: PickerSpecType
apply: PickerFilterFunctionType
}
export type ListType = {
type: 'list'
key: string
enabled: boolean
spec: ListSpecType
apply: ListFilterFunctionType
}
export type FilterType = ToggleType | PickerType | ListType |
||
| { | ||
| type: 'list', | ||
| key: 'category', | ||
| enabled: true, | ||
| spec: { | ||
| title: 'Categories', | ||
| options: filterCategories, | ||
| selected: filterCategories, | ||
| mode: 'OR', | ||
| displayTitle: true, | ||
| }, | ||
| apply: {key: 'category'}, | ||
| }, | ||
| ] | ||
| setFilters(streamFilters) | ||
| }, [data]) | ||
|
|
||
| if (error) { | ||
| return <NoticeView text={`Error: ${error.message}`} /> | ||
| return ( | ||
| <NoticeView | ||
| buttonText="Try Again" | ||
| onPress={reload} | ||
| text={`A problem occured while loading the streams. ${error.message}`} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| const header = ( | ||
| <FilterToolbar | ||
| filters={filters} | ||
| onPopoverDismiss={(newFilter) => { | ||
| let edited = filters.map((f) => | ||
| f.key === newFilter.key ? newFilter : f, | ||
| ) | ||
| setFilters(edited as ListType[]) | ||
| }} | ||
| /> | ||
| ) | ||
|
|
||
| return ( | ||
| <SectionList | ||
| ItemSeparatorComponent={ListSeparator} | ||
| ListEmptyComponent={<NoticeView text="No Streams" />} | ||
| ListEmptyComponent={ | ||
| isLoading ? ( | ||
| <LoadingView /> | ||
| ) : filters.some((f: ListType) => f.spec.selected.length) ? ( | ||
| <NoticeView text="No streams to show. Try changing the filters." /> | ||
| ) : ( | ||
| <NoticeView text="No streams." /> | ||
| ) | ||
| } | ||
| ListHeaderComponent={header} | ||
| contentContainerStyle={styles.contentContainer} | ||
| keyExtractor={keyExtractor} | ||
| onRefresh={refresh} | ||
| refreshing={refreshing} | ||
| renderItem={renderItem} | ||
| keyExtractor={(item: StreamType) => item.eid} | ||
| onRefresh={reload} | ||
| refreshing={isPending && !isInitial} | ||
| renderItem={({item}: {item: StreamType}) => <StreamRow stream={item} />} | ||
| renderSectionHeader={({section: {title}}) => ( | ||
| <ListSectionHeader title={title} /> | ||
| )} | ||
| sections={streams} | ||
| sections={groupStreams(filterStreams(entries, filters))} | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an obvious way we can improve on this? |
||
| style={styles.listContainer} | ||
| /> | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entriescould have a more descriptive name likestreamEntriesorstreams.streams.filter((stream) => { /* */ })is a nice pattern imo.