Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 10 additions & 23 deletions src/components/DeviceStatistics.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
import { createResource } from 'solid-js'
import type { VoidComponent } from 'solid-js'
import clsx from 'clsx'

import { getDeviceStats } from '~/api/devices'
import { formatDistance, formatDuration } from '~/utils/format'
import StatisticBar from './StatisticBar'

type DeviceStatisticsProps = {
class?: string
dongleId: string
}

const DeviceStatistics: VoidComponent<DeviceStatisticsProps> = (props) => {
const DeviceStatistics: VoidComponent<{ class?: string; dongleId: string }> = (props) => {
const [statistics] = createResource(() => props.dongleId, getDeviceStats)
const allTime = () => statistics()?.all

return (
<div class={clsx('flex h-10 w-full', props.class)}>
<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">Distance</span>
<span class="font-mono text-label-lg uppercase">{formatDistance(allTime()?.distance)}</span>
</div>

<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">Duration</span>
<span class="font-mono text-label-lg uppercase">{formatDuration(allTime()?.minutes)}</span>
</div>

<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">Routes</span>
<span class="font-mono text-label-lg uppercase">{allTime()?.routes ?? 0}</span>
</div>
</div>
<StatisticBar
class={props.class}
statistics={[
{ label: 'Distance', value: () => formatDistance(allTime()?.distance) },
{ label: 'Duration', value: () => formatDuration(allTime()?.minutes) },
{ label: 'Routes', value: () => allTime()?.routes },
]}
/>
)
}

Expand Down
53 changes: 14 additions & 39 deletions src/components/RouteStatistics.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,30 @@
import { createResource, Suspense } from 'solid-js'
import { createResource } from 'solid-js'
import type { VoidComponent } from 'solid-js'
import clsx from 'clsx'

import { TimelineStatistics, getTimelineStatistics } from '~/api/derived'
import type { Route } from '~/types'
import { formatDistance, formatRouteDuration } from '~/utils/format'
import StatisticBar from './StatisticBar'

const formatEngagement = (timeline?: TimelineStatistics): string => {
if (!timeline) return ''
const formatEngagement = (timeline?: TimelineStatistics): string | undefined => {
if (!timeline) return undefined
const { engagedDuration, duration } = timeline
return `${(100 * (engagedDuration / duration)).toFixed(0)}%`
}

const formatUserFlags = (timeline?: TimelineStatistics): string => {
return timeline?.userFlags.toString() ?? ''
}

type RouteStatisticsProps = {
class?: string
route?: Route
}

const RouteStatistics: VoidComponent<RouteStatisticsProps> = (props) => {
const RouteStatistics: VoidComponent<{ class?: string; route: Route }> = (props) => {
const [timeline] = createResource(() => props.route, getTimelineStatistics)

return (
<div class={clsx('flex size-full items-stretch gap-8', props.class)}>
<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">Distance</span>
<span class="font-mono text-label-lg uppercase">{formatDistance(props.route?.length)}</span>
</div>

<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">Duration</span>
<span class="font-mono text-label-lg uppercase">{formatRouteDuration(props.route)}</span>
</div>

<div class="hidden grow flex-col justify-between xs:flex">
<span class="text-body-sm text-on-surface-variant">Engaged</span>
<Suspense>
<span class="font-mono text-label-lg uppercase">{formatEngagement(timeline())}</span>
</Suspense>
</div>

<div class="flex grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">User flags</span>
<Suspense>
<span class="font-mono text-label-lg uppercase">{formatUserFlags(timeline())}</span>
</Suspense>
</div>
</div>
<StatisticBar
class={props.class}
statistics={[
{ label: 'Distance', value: () => formatDistance(props.route?.length) },
{ label: 'Duration', value: () => formatRouteDuration(props.route) },
{ label: 'Engaged', value: () => formatEngagement(timeline()) },
{ label: 'User flags', value: () => timeline()?.userFlags },
]}
/>
)
}

Expand Down
23 changes: 23 additions & 0 deletions src/components/StatisticBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import clsx from 'clsx'
import { For, Suspense, VoidComponent } from 'solid-js'

const StatisticBar: VoidComponent<{ class?: string; statistics: { label: string; value: () => unknown }[] }> = (props) => {
return (
<div class="flex flex-col">
<div class={clsx('flex h-auto w-full justify-between gap-8', props.class)}>
<For each={props.statistics}>
{(statistic) => (
<div class="flex basis-0 grow flex-col justify-between">
<span class="text-body-sm text-on-surface-variant">{statistic.label}</span>
<Suspense fallback={<div class="h-[20px] w-auto skeleton-loader rounded-xs" />}>
<span class="font-mono text-label-lg uppercase">{statistic.value()?.toString() ?? '—'}</span>
</Suspense>
</div>
)}
</For>
</div>
</div>
)
}

export default StatisticBar
8 changes: 4 additions & 4 deletions src/utils/format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ describe('formatDistance', () => {
expect(formatDistance(0)).toBe('0.0 mi')
expect(formatDistance(1.234)).toBe('1.2 mi')
})
it('should be blank for undefined distance', () => {
expect(formatDistance(undefined)).toBe('')
it('should be undefined for undefined distance', () => {
expect(formatDistance(undefined)).toBe(undefined)
})
})

Expand All @@ -20,8 +20,8 @@ describe('formatDuration', () => {
expect(formatDuration(90)).toBe('1h 30m')
expect(formatDuration(120)).toBe('2h 0m')
})
it('should be blank for undefined duration', () => {
expect(formatDuration(undefined)).toBe('')
it('should be undefined for undefined duration', () => {
expect(formatDuration(undefined)).toBe(undefined)
})
})

Expand Down
8 changes: 4 additions & 4 deletions src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const isImperial = (): boolean => {
return locale.startsWith('en-us') || locale.startsWith('en-gb')
}

export const formatDistance = (miles: number | undefined): string => {
if (miles === undefined) return ''
export const formatDistance = (miles: number | undefined): string | undefined => {
if (miles === undefined) return undefined
if (isImperial()) return `${miles.toFixed(1)} mi`
return `${(miles * MI_TO_KM).toFixed(1)} km`
}
Expand All @@ -33,8 +33,8 @@ const _formatDuration = (duration: Duration): string => {
}
}

export const formatDuration = (minutes: number | undefined): string => {
if (minutes === undefined) return ''
export const formatDuration = (minutes: number | undefined): string | undefined => {
if (minutes === undefined) return undefined
const duration = dayjs.duration({
hours: Math.floor(minutes / 60),
minutes: Math.round(minutes % 60),
Expand Down
Loading