Skip to content

Commit

Permalink
added route like and options UI
Browse files Browse the repository at this point in the history
  • Loading branch information
sarem-h authored and incognitojam committed Jul 6, 2024
1 parent af1f08d commit 112582c
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 21 deletions.
125 changes: 105 additions & 20 deletions src/components/RouteCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createSignal, createEffect, Suspense, type VoidComponent } from 'solid-js'
import { createSignal, createEffect, Suspense, Show, type Component } from 'solid-js'
import dayjs from 'dayjs'

import Avatar from '~/components/material/Avatar'
import { CardContent, CardHeader } from '~/components/material/Card'
import Icon from '~/components/material/Icon'
import RouteOptions from '~/components/RouteOptions'
import RouteStaticMap from '~/components/RouteStaticMap'
import RouteStatistics from '~/components/RouteStatistics'
import Timeline from './Timeline'
Expand All @@ -12,13 +13,84 @@ import type { Route, RouteSegments } from '~/types'

import { reverseGeocode } from '~/map'

type RouteOptionsProps = {
route?: Route;
}

const [showRouteOptionsCard, setShowRouteOptionsCard] = createSignal(false)

const RouteOptionsCard: Component<RouteOptionsProps> = (props) => {
const [isMdOrLarger, setIsMdOrLarger] = createSignal(false)

// listen isMdOrLarger
createEffect(() => {
const updateSize = () => {
setIsMdOrLarger(window.innerWidth >= 768)
}
window.addEventListener('resize', updateSize)
// Initial check
updateSize()

return () => window.removeEventListener('resize', updateSize)
})

const stopPropagation = (event: MouseEvent) => {
event.stopPropagation()

setShowRouteOptionsCard(false)
}

return (
<Show when={showRouteOptionsCard()}>
<div onClick={stopPropagation} classList={{
'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 transition duration-800 ease-in-out': isMdOrLarger(),
'fixed inset-0 z-50 flex flex-col-reverse bg-black bg-opacity-50 p-4 transition duration-800 ease-in-out': !isMdOrLarger(),
'transform translate-y-0': !isMdOrLarger() && showRouteOptionsCard(),
'transform translate-y-full': !isMdOrLarger() && !showRouteOptionsCard(),
}}>
<div onClick={(event: MouseEvent) => event.stopPropagation()}>
<RouteOptions {...props} />
</div>
</div>
</Show>
)
}

type FavoriteRoutes = string[]

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

const headline = () => startTime()?.format('ddd, MMM D, YYYY')
const subhead = () => `${startTime()?.format('h:mm A')} to ${endTime()?.format('h:mm A')}`

const [isFavorite, setIsFavorite] = createSignal(false)

const toggleFavorite = (routeName: string) => {
let favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes
const isFavorite = favorites.includes(routeName)
favorites = isFavorite ? favorites.filter(name => name !== routeName) : [...favorites, routeName]
localStorage.setItem('favoriteRoutes', JSON.stringify(favorites))
setIsFavorite(!isFavorite)
}

createEffect(() => {
const favorites: FavoriteRoutes = JSON.parse(localStorage.getItem('favoriteRoutes') || '[]') as FavoriteRoutes
setIsFavorite(favorites.includes(props.route?.fullname ?? ''))
})

const handleLikeClick = (event: MouseEvent) => {
event.stopPropagation()
props.route?.fullname && toggleFavorite(props.route.fullname)
}

const handleMoreOptionsClick = (event: MouseEvent) => {
event.stopPropagation()
setShowRouteOptionsCard(true)
}

return (
<CardHeader
headline={headline()}
Expand All @@ -28,6 +100,16 @@ const RouteHeader = (props: { route?: RouteSegments }) => {
<Icon>directions_car</Icon>
</Avatar>
}
trailing={
<div class="flex items-center gap-3.5">
<button onClick={(event) => handleLikeClick(event)}>
<Icon filled={isFavorite()} class={isFavorite() ? 'text-red-400 hover:text-white' : 'text-white hover:text-red-400'}>favorite_border</Icon>
</button>
<button class="hover:text-blue-400" onClick={(event) => handleMoreOptionsClick(event)}>
<Icon>more_vert</Icon>
</button>
</div>
}
/>
)
}
Expand Down Expand Up @@ -130,31 +212,34 @@ type RouteCardProps = {
route?: Route;
}

const RouteCard: VoidComponent<RouteCardProps> = (props) => {
const RouteCard: Component<RouteCardProps> = (props) => {
const route = () => props.route

const navigateToRouteActivity = () => {
location.href = `/${route()?.dongle_id}/${route()?.fullname?.slice(17)}`
}

return (
<a href={`/${route()?.dongle_id}/${route()?.fullname?.slice(17)}`}>
<div class="custom-card flex shrink-0 flex-col rounded-lg md:flex-row">
<div class="h-full w-[410px]">
<Suspense
fallback={<div class="skeleton-loader size-full bg-surface" />}
>
<RouteStaticMap route={route()} />
</Suspense>
</div>
<div class="custom-card flex shrink-0 flex-col rounded-lg md:flex-row" onClick={navigateToRouteActivity}>
<RouteOptionsCard route={route()} />
<div class="h-full lg:w-[410px]">
<Suspense
fallback={<div class="skeleton-loader size-full bg-surface" />}
>
<RouteStaticMap route={route()} />
</Suspense>
</div>

<div class="flex flex-col">
<RouteHeader route={route()} />
<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>
<CardContent class="py-0">
<RouteRevGeo route={route()} />
<Timeline route={route()} rounded="rounded-sm" />
<RouteStatistics route={route()} />
</CardContent>
</div>
</a>
</div>
)
}

Expand Down
68 changes: 68 additions & 0 deletions src/components/RouteOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createSignal, createEffect, type Component } from 'solid-js'
import type { Route } from '~/types'

type RouteOptionsProps = {
route?: Route;
}

const RouteOptions: Component<RouteOptionsProps> = (props) => {
const [isPreservedChecked, setIsPreservedChecked] = createSignal(true)
const [isPublicAccessChecked, setIsPublicAccessChecked] = createSignal(false)

const [routeId, setRouteId] = createSignal<string | undefined>()

createEffect(() => {
const routeFullName = props?.route?.fullname
setRouteId(routeFullName?.split('|')[1])
})

return (
<div class="mt-6 flex w-full flex-col gap-6 rounded-lg bg-surface p-5 md:w-[400px]">
<div class="flex justify-between">
<div class="flex cursor-pointer flex-col items-center justify-center gap-1 rounded-md bg-surface-bright p-2 px-12 py-2.5 font-semibold hover:opacity-80">
<span class="material-symbols-outlined icon-filled">file_copy</span>
Route ID
</div>
<div class="flex cursor-pointer flex-col items-center justify-center gap-1 rounded-md bg-surface-bright p-2 px-12 py-2.5 font-semibold hover:opacity-80">
<span class="material-symbols-outlined icon-filled">share</span>
Share
</div>
</div>
<div class="flex items-center gap-2 rounded-md bg-surface-bright p-3 text-sm font-semibold">
Route ID: <span class="font-regular">{routeId()}</span>
</div>
<div class="flex flex-col rounded-md bg-surface-bright">
<div class="flex items-center justify-between border-b border-gray-900 p-3 pr-5 text-[15px] font-semibold">
Preserved
<label class="custom-switch">
<input type="checkbox" checked={isPreservedChecked()} onChange={(e) => setIsPreservedChecked(e.currentTarget.checked)} />
<span class="custom-slider round" />
</label>
</div>
<div class="flex items-center justify-between border-b border-gray-900 p-3 pr-5 text-[15px] font-semibold">
Public Access
<label class="custom-switch">
<input type="checkbox" checked={isPublicAccessChecked()} onChange={(e) => setIsPublicAccessChecked(e.currentTarget.checked)} />
<span class="custom-slider round" />
</label>
</div>
</div>
<div class="flex flex-col rounded-md bg-surface-bright">
<div class="flex cursor-pointer items-center justify-between border-b border-gray-900 p-3 pr-4 text-[15px] font-semibold hover:opacity-60">
View in useradmin
<span class="material-symbols-outlined text-[35px]">
keyboard_arrow_right
</span>
</div>
<div class="flex cursor-pointer items-center justify-between border-b border-gray-900 p-3 pr-5 text-[15px] font-semibold hover:opacity-60">
Upload Options
<span class="material-symbols-outlined icon-filled text-[25px]">
cloud_upload
</span>
</div>
</div>
</div>
)
}

export default RouteOptions
2 changes: 1 addition & 1 deletion src/components/RouteStaticMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const RouteStaticMap: VoidComponent<RouteStaticMapProps> = (props) => {
</Match>
<Match when={url() && loadedUrl()} keyed>
<img
class="pointer-events-none size-full object-contain"
class="pointer-events-none size-full rounded-t-lg object-contain md:rounded-none md:rounded-l-lg"
src={loadedUrl()}
alt=""
/>
Expand Down
53 changes: 53 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,57 @@
.custom-card:hover {
background-color: var(--color-surface-container);
}

.custom-switch {
position: relative;
display: inline-block;
width: 45px;
height: 28px;
}

.custom-switch input {
opacity: 0;
width: 0;
height: 0;
}

.custom-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #8d8d8d;
transition: .4s;
}

.custom-slider:before {
position: absolute;
content: "";
height: 100%;
width: 60%;
background-color: #fff;
transition: .4s;
}

.custom-switch input:checked + .custom-slider {
background-color: #32CD32;
}

.custom-switch input:focus + .custom-slider {
box-shadow: 0 0 1px #32CD32;
}

.custom-switch input:checked + .custom-slider:before {
transform: translateX(26px);
}

.custom-slider.round {
border-radius: 34px;
}

.custom-slider.round:before {
border-radius: 50%;
}
}

0 comments on commit 112582c

Please sign in to comment.