-
Notifications
You must be signed in to change notification settings - Fork 39
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
Route card redesign #35
base: master
Are you sure you want to change the base?
Changes from 6 commits
66779ee
e83adbf
b99492d
712d714
d6f85fd
355085f
4918550
6f71332
72d634e
4a00fb2
bd62fd7
ca1ec01
83431ec
7f99f59
42432a1
0e2f4d1
cad8e21
f84c6bb
d5b3972
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 |
---|---|---|
@@ -1,20 +1,24 @@ | ||
import { Suspense, type VoidComponent } from 'solid-js' | ||
import { createSignal, createEffect, Suspense, type Component } from 'solid-js' | ||
import dayjs from 'dayjs' | ||
|
||
import Avatar from '~/components/material/Avatar' | ||
import Card, { CardContent, CardHeader } from '~/components/material/Card' | ||
import { CardContent, CardHeader } from '~/components/material/Card' | ||
import Icon from '~/components/material/Icon' | ||
import RouteStaticMap from '~/components/RouteStaticMap' | ||
import RouteStatistics from '~/components/RouteStatistics' | ||
import Timeline from './Timeline' | ||
|
||
import type { RouteSegments } from '~/types' | ||
import type { Route, RouteSegments } from '~/types' | ||
|
||
const RouteHeader = (props: { route: RouteSegments }) => { | ||
const startTime = () => dayjs(props.route.segment_start_times[0]) | ||
const endTime = () => dayjs(props.route.segment_end_times.at(-1)) | ||
import { reverseGeocode } from '~/map' | ||
|
||
const headline = () => startTime().format('ddd, MMM D, YYYY') | ||
const subhead = () => `${startTime().format('h:mm A')} to ${endTime().format('h:mm A')}` | ||
const RouteHeader = (props: { route?: RouteSegments }) => { | ||
|
||
const startTime = () => props?.route?.segment_start_times ? dayjs(props.route.segment_start_times[0]) : null | ||
const endTime = () => props?.route?.segment_end_times ? dayjs(props.route.segment_end_times.at(-1)) : null | ||
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. same here with segment_end_times |
||
|
||
const headline = () => startTime()?.format('ddd, MMM D, YYYY') | ||
const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}` | ||
|
||
return ( | ||
<CardHeader | ||
|
@@ -29,27 +33,131 @@ const RouteHeader = (props: { route: RouteSegments }) => { | |
) | ||
} | ||
|
||
interface RouteCardProps { | ||
route: RouteSegments | ||
interface GeoResult { | ||
features?: Array<{ | ||
properties?: { | ||
context?: { | ||
neighborhood?: string | null, | ||
region?: string | null, | ||
place?: string | null | ||
} | ||
} | ||
}> | ||
} | ||
|
||
interface LocationContext { | ||
neighborhood?: { | ||
name: string | null, | ||
}, | ||
region?: { | ||
region_code: string | null, | ||
}, | ||
place?: { | ||
name: string | null, | ||
} | ||
} | ||
|
||
async function fetchGeoData(lng: number, lat: number): Promise<GeoResult | null> { | ||
try { | ||
const revGeoResult = await reverseGeocode(lng, lat) as GeoResult | ||
if (revGeoResult instanceof Error) throw revGeoResult | ||
return revGeoResult | ||
} catch (error) { | ||
console.error(error) | ||
// To allow execution to continue for the next location. | ||
return null | ||
} | ||
} | ||
|
||
function processGeoResult( | ||
result: GeoResult | null, | ||
setLocation: (location: { neighborhood?: string | null, region?: string | null }) => void, | ||
) { | ||
if (result) { | ||
const { neighborhood, region, place } = | ||
(result?.features?.[0]?.properties?.context || {}) as LocationContext | ||
setLocation({ | ||
neighborhood: neighborhood?.name || place?.name, | ||
region: region?.region_code, | ||
}) | ||
} | ||
} | ||
|
||
const RouteCard: VoidComponent<RouteCardProps> = (props) => { | ||
type LocationState = { neighborhood?: string | null, region?: string | null } | ||
|
||
const RouteRevGeo = (props: { route?: Route }) => { | ||
const [startLocation, setStartLocation] = createSignal<LocationState>({ | ||
neighborhood: null, | ||
region: null, | ||
}) | ||
const [endLocation, setEndLocation] = createSignal<LocationState>({ | ||
neighborhood: null, | ||
region: null, | ||
}) | ||
const [error, setError] = createSignal<Error | null>(null) | ||
|
||
createEffect(() => { | ||
if (!props.route) return | ||
const { start_lng, start_lat, end_lng, end_lat } = props.route | ||
if (!start_lng || !start_lat || !end_lng || !end_lat) return | ||
|
||
Promise.all([ | ||
fetchGeoData(start_lng, start_lat), | ||
fetchGeoData(end_lng, end_lat), | ||
]).then(([startResult, endResult]) => { | ||
processGeoResult(startResult, setStartLocation) | ||
processGeoResult(endResult, setEndLocation) | ||
}).catch((error) => { | ||
setError(error as Error) | ||
console.error('An error occurred while fetching geolocation data:', error) | ||
}) | ||
}) | ||
|
||
return ( | ||
<Card href={`/${props.route.dongle_id}/${props.route.fullname.slice(17)}`}> | ||
<RouteHeader route={props.route} /> | ||
<div> | ||
{error() && <div>Error: {error()?.message}</div>} | ||
<div class="flex w-fit items-center gap-2 rounded-xl border border-gray-700 bg-black px-4 py-1 text-[13px]"> | ||
{startLocation().neighborhood && <div>{startLocation().neighborhood}, {startLocation().region}</div>} | ||
<span class="material-symbols-outlined icon-outline" style={{ 'font-size': '14px' }}> | ||
arrow_right_alt | ||
</span> | ||
{endLocation().neighborhood && <div>{endLocation().neighborhood}, {endLocation().region}</div>} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
type RouteCardProps = { | ||
route?: Route; | ||
} | ||
|
||
<div class="mx-2 h-48 overflow-hidden rounded-lg"> | ||
const RouteCard: Component<RouteCardProps> = (props) => { | ||
const route = () => props.route | ||
|
||
const navigateToRouteActivity = () => { | ||
location.href = `/${route()?.dongle_id}/${route()?.fullname?.slice(17)}` | ||
} | ||
|
||
return ( | ||
<div class="custom-card flex shrink-0 flex-col rounded-lg md:flex-row" onClick={navigateToRouteActivity}> | ||
<div class="h-full lg:w-[410px]"> | ||
<Suspense | ||
fallback={<div class="skeleton-loader size-full bg-surface" />} | ||
> | ||
<RouteStaticMap route={props.route} /> | ||
<RouteStaticMap route={route()} /> | ||
</Suspense> | ||
</div> | ||
|
||
<CardContent> | ||
<RouteStatistics route={props.route} /> | ||
</CardContent> | ||
</Card> | ||
<div class="flex flex-col"> | ||
<RouteHeader route={route()} /> | ||
|
||
<CardContent class="py-0"> | ||
<RouteRevGeo route={route()} /> | ||
<Timeline route={route()} rounded="rounded-sm" /> | ||
<RouteStatistics route={route()} /> | ||
</CardContent> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,10 +50,12 @@ export interface Route { | |
create_time: number | ||
devicetype: number | ||
dongle_id: string | ||
start_lng?: number | ||
start_lat?: number | ||
end_lat?: number | ||
end_lng?: number | ||
end_time?: string | ||
fullname: string | ||
end_time: string | ||
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. this might not exist: #60 |
||
fullname?: string | ||
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. this is guaranteed to exist |
||
git_branch?: string | ||
git_commit?: string | ||
git_dirty?: boolean | ||
|
@@ -88,14 +90,24 @@ export interface RouteShareSignature extends Record<string, string> { | |
} | ||
|
||
export interface RouteSegments extends Route { | ||
end_time_utc_millis: number | ||
is_preserved: boolean | ||
segment_end_times: number[] | ||
segment_numbers: number[] | ||
segment_start_times: number[] | ||
share_exp: RouteShareSignature['exp'] | ||
share_sig: RouteShareSignature['sig'] | ||
start_time_utc_millis: number | ||
end_time_utc_millis?: number | ||
is_preserved?: boolean | ||
segment_end_times?: number[] | ||
segment_numbers?: number[] | ||
segment_start_times?: number[] | ||
share_exp?: RouteShareSignature['exp'] | ||
share_sig?: RouteShareSignature['sig'] | ||
start_time_utc_millis?: number | ||
} | ||
|
||
export interface GeocodeResult { | ||
formatted_address: string; | ||
geometry: { | ||
location: { | ||
lat: number; | ||
lng: number; | ||
}; | ||
}; | ||
} | ||
|
||
export interface Clip { | ||
|
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.
it's best not to add new usages of segment_start_times: #53