Skip to content

Commit 9ac9b56

Browse files
authored
refactor: create StatisticsBar (#239)
Would love this StatisticBar for commaai/connect#203 to reuse spacing and styling also inlined a few types to reduce LoC
1 parent 6ba6ae6 commit 9ac9b56

File tree

5 files changed

+55
-70
lines changed

5 files changed

+55
-70
lines changed

src/components/DeviceStatistics.tsx

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
11
import { createResource } from 'solid-js'
22
import type { VoidComponent } from 'solid-js'
3-
import clsx from 'clsx'
43

54
import { getDeviceStats } from '~/api/devices'
65
import { formatDistance, formatDuration } from '~/utils/format'
6+
import StatisticBar from './StatisticBar'
77

8-
type DeviceStatisticsProps = {
9-
class?: string
10-
dongleId: string
11-
}
12-
13-
const DeviceStatistics: VoidComponent<DeviceStatisticsProps> = (props) => {
8+
const DeviceStatistics: VoidComponent<{ class?: string; dongleId: string }> = (props) => {
149
const [statistics] = createResource(() => props.dongleId, getDeviceStats)
1510
const allTime = () => statistics()?.all
1611

1712
return (
18-
<div class={clsx('flex h-10 w-full', props.class)}>
19-
<div class="flex grow flex-col justify-between">
20-
<span class="text-body-sm text-on-surface-variant">Distance</span>
21-
<span class="font-mono text-label-lg uppercase">{formatDistance(allTime()?.distance)}</span>
22-
</div>
23-
24-
<div class="flex grow flex-col justify-between">
25-
<span class="text-body-sm text-on-surface-variant">Duration</span>
26-
<span class="font-mono text-label-lg uppercase">{formatDuration(allTime()?.minutes)}</span>
27-
</div>
28-
29-
<div class="flex grow flex-col justify-between">
30-
<span class="text-body-sm text-on-surface-variant">Routes</span>
31-
<span class="font-mono text-label-lg uppercase">{allTime()?.routes ?? 0}</span>
32-
</div>
33-
</div>
13+
<StatisticBar
14+
class={props.class}
15+
statistics={[
16+
{ label: 'Distance', value: () => formatDistance(allTime()?.distance) },
17+
{ label: 'Duration', value: () => formatDuration(allTime()?.minutes) },
18+
{ label: 'Routes', value: () => allTime()?.routes },
19+
]}
20+
/>
3421
)
3522
}
3623

src/components/RouteStatistics.tsx

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,30 @@
1-
import { createResource, Suspense } from 'solid-js'
1+
import { createResource } from 'solid-js'
22
import type { VoidComponent } from 'solid-js'
3-
import clsx from 'clsx'
43

54
import { TimelineStatistics, getTimelineStatistics } from '~/api/derived'
65
import type { Route } from '~/types'
76
import { formatDistance, formatRouteDuration } from '~/utils/format'
7+
import StatisticBar from './StatisticBar'
88

9-
const formatEngagement = (timeline?: TimelineStatistics): string => {
10-
if (!timeline) return ''
9+
const formatEngagement = (timeline?: TimelineStatistics): string | undefined => {
10+
if (!timeline) return undefined
1111
const { engagedDuration, duration } = timeline
1212
return `${(100 * (engagedDuration / duration)).toFixed(0)}%`
1313
}
1414

15-
const formatUserFlags = (timeline?: TimelineStatistics): string => {
16-
return timeline?.userFlags.toString() ?? ''
17-
}
18-
19-
type RouteStatisticsProps = {
20-
class?: string
21-
route?: Route
22-
}
23-
24-
const RouteStatistics: VoidComponent<RouteStatisticsProps> = (props) => {
15+
const RouteStatistics: VoidComponent<{ class?: string; route: Route }> = (props) => {
2516
const [timeline] = createResource(() => props.route, getTimelineStatistics)
2617

2718
return (
28-
<div class={clsx('flex size-full items-stretch gap-8', props.class)}>
29-
<div class="flex grow flex-col justify-between">
30-
<span class="text-body-sm text-on-surface-variant">Distance</span>
31-
<span class="font-mono text-label-lg uppercase">{formatDistance(props.route?.length)}</span>
32-
</div>
33-
34-
<div class="flex grow flex-col justify-between">
35-
<span class="text-body-sm text-on-surface-variant">Duration</span>
36-
<span class="font-mono text-label-lg uppercase">{formatRouteDuration(props.route)}</span>
37-
</div>
38-
39-
<div class="hidden grow flex-col justify-between xs:flex">
40-
<span class="text-body-sm text-on-surface-variant">Engaged</span>
41-
<Suspense>
42-
<span class="font-mono text-label-lg uppercase">{formatEngagement(timeline())}</span>
43-
</Suspense>
44-
</div>
45-
46-
<div class="flex grow flex-col justify-between">
47-
<span class="text-body-sm text-on-surface-variant">User flags</span>
48-
<Suspense>
49-
<span class="font-mono text-label-lg uppercase">{formatUserFlags(timeline())}</span>
50-
</Suspense>
51-
</div>
52-
</div>
19+
<StatisticBar
20+
class={props.class}
21+
statistics={[
22+
{ label: 'Distance', value: () => formatDistance(props.route?.length) },
23+
{ label: 'Duration', value: () => formatRouteDuration(props.route) },
24+
{ label: 'Engaged', value: () => formatEngagement(timeline()) },
25+
{ label: 'User flags', value: () => timeline()?.userFlags },
26+
]}
27+
/>
5328
)
5429
}
5530

src/components/StatisticBar.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import clsx from 'clsx'
2+
import { For, Suspense, VoidComponent } from 'solid-js'
3+
4+
const StatisticBar: VoidComponent<{ class?: string; statistics: { label: string; value: () => unknown }[] }> = (props) => {
5+
return (
6+
<div class="flex flex-col">
7+
<div class={clsx('flex h-auto w-full justify-between gap-8', props.class)}>
8+
<For each={props.statistics}>
9+
{(statistic) => (
10+
<div class="flex basis-0 grow flex-col justify-between">
11+
<span class="text-body-sm text-on-surface-variant">{statistic.label}</span>
12+
<Suspense fallback={<div class="h-[20px] w-auto skeleton-loader rounded-xs" />}>
13+
<span class="font-mono text-label-lg uppercase">{statistic.value()?.toString() ?? '—'}</span>
14+
</Suspense>
15+
</div>
16+
)}
17+
</For>
18+
</div>
19+
</div>
20+
)
21+
}
22+
23+
export default StatisticBar

src/utils/format.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ describe('formatDistance', () => {
77
expect(formatDistance(0)).toBe('0.0 mi')
88
expect(formatDistance(1.234)).toBe('1.2 mi')
99
})
10-
it('should be blank for undefined distance', () => {
11-
expect(formatDistance(undefined)).toBe('')
10+
it('should be undefined for undefined distance', () => {
11+
expect(formatDistance(undefined)).toBe(undefined)
1212
})
1313
})
1414

@@ -20,8 +20,8 @@ describe('formatDuration', () => {
2020
expect(formatDuration(90)).toBe('1h 30m')
2121
expect(formatDuration(120)).toBe('2h 0m')
2222
})
23-
it('should be blank for undefined duration', () => {
24-
expect(formatDuration(undefined)).toBe('')
23+
it('should be undefined for undefined duration', () => {
24+
expect(formatDuration(undefined)).toBe(undefined)
2525
})
2626
})
2727

src/utils/format.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const isImperial = (): boolean => {
1919
return locale.startsWith('en-us') || locale.startsWith('en-gb')
2020
}
2121

22-
export const formatDistance = (miles: number | undefined): string => {
23-
if (miles === undefined) return ''
22+
export const formatDistance = (miles: number | undefined): string | undefined => {
23+
if (miles === undefined) return undefined
2424
if (isImperial()) return `${miles.toFixed(1)} mi`
2525
return `${(miles * MI_TO_KM).toFixed(1)} km`
2626
}
@@ -33,8 +33,8 @@ const _formatDuration = (duration: Duration): string => {
3333
}
3434
}
3535

36-
export const formatDuration = (minutes: number | undefined): string => {
37-
if (minutes === undefined) return ''
36+
export const formatDuration = (minutes: number | undefined): string | undefined => {
37+
if (minutes === undefined) return undefined
3838
const duration = dayjs.duration({
3939
hours: Math.floor(minutes / 60),
4040
minutes: Math.round(minutes % 60),

0 commit comments

Comments
 (0)