diff --git a/packages/components/src/input/mod.tsx b/packages/components/src/input/mod.tsx index 5bd3a9e..1e3787f 100644 --- a/packages/components/src/input/mod.tsx +++ b/packages/components/src/input/mod.tsx @@ -47,7 +47,7 @@ const Close = styled(CloseIcon)` position: absolute; right: 10px; ` -const StyledInput = styled.input<{ +const StyledInput = styled.input.attrs({ autoComplete: 'one-time-code' })<{ $color: string $size: Size $fontSize: string diff --git a/packages/ui/src/components/favoriteStop.tsx b/packages/ui/src/components/favoriteStop.tsx index 66dc278..095e4dd 100644 --- a/packages/ui/src/components/favoriteStop.tsx +++ b/packages/ui/src/components/favoriteStop.tsx @@ -1,15 +1,14 @@ import styled from 'styled-components' +import { useCallback, useMemo } from 'react' import { Star } from '@busmap/components/icons/star' import { Tooltip } from '@busmap/components/tooltip' import { SY30T } from '@busmap/components/colors' -import type { FC } from 'react' -import type { Stop } from '../types.js' +import { useGlobals } from '../globals.js' +import { useStorage, useStorageDispatch } from '../contexts/storage.js' -interface FavoriteStopProps { - stop?: Stop - favorite: boolean -} +import type { FC } from 'react' +import type { Favorite } from '../contexts/storage.js' const Tip = styled(Tooltip)` display: flex; @@ -20,12 +19,43 @@ const Button = styled.button` margin: 0; background: none; ` -const FavoriteStop: FC = ({ stop, favorite = false }) => { +const worker = new Worker(new URL('../workers/favorites.ts', import.meta.url), { + type: 'module' +}) +const FavoriteStop: FC = () => { + const storage = useStorage() + const storageDispatch = useStorageDispatch() + const { agency, route, direction, stop } = useGlobals() + const favorite = useMemo(() => { + return storage.favorites?.find(fav => { + return ( + `${fav.route.id}${fav.direction.id}${fav.stop.id}` === + `${route?.id}${direction?.id}${stop?.id}` + ) + }) + }, [storage.favorites, stop, route, direction]) + const onClick = useCallback(() => { + if (favorite) { + storageDispatch({ type: 'favoriteRemove', value: favorite }) + worker.postMessage({ action: 'stop', favorite }) + } else if (agency && route && direction && stop) { + const add: Favorite = { + stop: stop, + agency: agency, + route: { id: route.id, title: route.title ?? route.shortTitle }, + direction: { id: direction.id, title: direction.title ?? direction.shortTitle } + } + + storageDispatch({ type: 'favoriteAdd', value: add }) + worker.postMessage({ action: 'start', favorite: add }) + } + }, [storageDispatch, agency, route, direction, stop, favorite]) + if (stop) { return ( - + ) diff --git a/packages/ui/src/components/favorites.tsx b/packages/ui/src/components/favorites.tsx new file mode 100644 index 0000000..944745a --- /dev/null +++ b/packages/ui/src/components/favorites.tsx @@ -0,0 +1,15 @@ +import { useStorage } from '../contexts/storage.js' + +import type { FC } from 'react' + +const Favorites: FC = () => { + const { favorites } = useStorage() + + if (!favorites || !favorites.length) { + return
⭐ You can select your favorite stops from the bus selector tab. ⭐
+ } + + return favorites.map(fav =>

{fav.stop.title}

) +} + +export { Favorites } diff --git a/packages/ui/src/components/selectors/stops.tsx b/packages/ui/src/components/selectors/stops.tsx index 70e8703..908b0be 100644 --- a/packages/ui/src/components/selectors/stops.tsx +++ b/packages/ui/src/components/selectors/stops.tsx @@ -40,7 +40,7 @@ const Stops: FC = ({ onClear={onClear ?? true} onSelect={onSelect} /> - + ) } diff --git a/packages/ui/src/components/selectors/useSelectorProps.ts b/packages/ui/src/components/selectors/useSelectorProps.ts index 9deee14..1aa6e23 100644 --- a/packages/ui/src/components/selectors/useSelectorProps.ts +++ b/packages/ui/src/components/selectors/useSelectorProps.ts @@ -10,11 +10,12 @@ interface SelectorProps { } interface SelectorSpreadProps { onClear: AutoSuggestProps['onClear'] - caseInsensitive: boolean inputBoundByItems: boolean size: AutoSuggestProps['size'] color: AutoSuggestProps['color'] value: AutoSuggestProps['value'] + selectOnTextMatch: AutoSuggestProps['selectOnTextMatch'] + caseInsensitive: AutoSuggestProps['caseInsensitive'] } const useSelectorProps = ({ selected }: SelectorProps): SelectorSpreadProps => { @@ -23,8 +24,9 @@ const useSelectorProps = ({ selected }: SelectorProps): SelectorSpreadProp const props = useMemo( () => ({ onClear: true, - caseInsensitive: true, + caseInsensitive: false, inputBoundByItems: true, + selectOnTextMatch: true, size: 'small' as AutoSuggestProps['size'], color: isLightMode ? 'black' : PB90T, value: selected ?? undefined, diff --git a/packages/ui/src/contexts/storage.tsx b/packages/ui/src/contexts/storage.tsx index 83e993e..26c4b6c 100644 --- a/packages/ui/src/contexts/storage.tsx +++ b/packages/ui/src/contexts/storage.tsx @@ -4,14 +4,21 @@ import { isAMode, isASpeedUnit, isAPredictionFormat } from './util.js' import type { FC, ReactNode, Dispatch } from 'react' import type { Mode, SpeedUnit, PredictionFormat } from './util.js' +import type { Agency, RouteName, DirectionName, Stop } from '../types.js' +interface Favorite { + agency: Agency + route: RouteName + direction: DirectionName + stop: Stop +} interface StorageState { predsFormat?: PredictionFormat vehicleSpeedUnit?: SpeedUnit vehicleColorPredicted?: boolean themeMode?: Mode + favorites?: Favorite[] } - interface PredsFormatUpdate { type: 'predsFormat' value?: PredictionFormat @@ -28,16 +35,54 @@ interface ThemeModeUpdate { type: 'themeMode' value?: Mode } +interface FavoriteAdded { + type: 'favoriteAdd' + value: Favorite +} +interface FavoriteRemoved { + type: 'favoriteRemove' + value: Favorite +} type StorageAction = | PredsFormatUpdate | VehicleSpeedUnitUpdate | VehicleColorPredictedUpdate | ThemeModeUpdate + | FavoriteAdded + | FavoriteRemoved const reducer = (state: StorageState, action: StorageAction) => { switch (action.type) { case 'themeMode': return { ...state, themeMode: action.value } + case 'favoriteAdd': { + if (Array.isArray(state.favorites)) { + return { + ...state, + favorites: [...state.favorites, action.value] + } + } + + return { ...state, favorites: [action.value] } + } + case 'favoriteRemove': { + if (Array.isArray(state.favorites)) { + const { value } = action + const { route, direction, stop } = value + const toRemove = `${route.id}${direction.id}${stop.id}` + + return { + ...state, + favorites: state.favorites.filter(fav => { + const thisFav = `${fav.route.id}${fav.direction.id}${fav.stop.id}` + + return toRemove !== thisFav + }) + } + } + + return state + } case 'predsFormat': return { ...state, predsFormat: action.value } case 'vehicleSpeedUnit': @@ -52,7 +97,8 @@ const KEYS = { themeMode: 'busmap-themeMode', vehicleSpeedUnit: 'busmap-vehicleSpeedUnit', vehicleColorPredicted: 'busmap-vehicleColorPredicted', - predsFormat: 'busmap-predsFormat' + predsFormat: 'busmap-predsFormat', + favorites: 'busmap-favorites' } const StorageDispatch = createContext>(() => {}) const Storage = createContext({}) @@ -62,6 +108,7 @@ const init = (): StorageState => { const vehicleSpeedUnit = localStorage.getItem(KEYS.vehicleSpeedUnit) const vehicleColorPredicted = localStorage.getItem(KEYS.vehicleColorPredicted) const predsFormat = localStorage.getItem(KEYS.predsFormat) + const favoritesJson = localStorage.getItem(KEYS.favorites) if (isAMode(themeMode)) { state.themeMode = themeMode @@ -79,12 +126,34 @@ const init = (): StorageState => { state.vehicleColorPredicted = vehicleColorPredicted !== 'false' } + if (favoritesJson) { + let favorites: Favorite[] | null = null + + try { + favorites = JSON.parse(favoritesJson) as Favorite[] + } catch (err) { + // Ignore + } + + if (favorites) { + state.favorites = favorites + } + } + return state } const StorageProvider: FC<{ children: ReactNode }> = ({ children }) => { const [storage, dispatch] = useReducer(reducer, {}, init) const context = useMemo(() => storage, [storage]) + useEffect(() => { + if (storage.favorites) { + localStorage.setItem(KEYS.favorites, JSON.stringify(storage.favorites)) + } else { + localStorage.removeItem(KEYS.favorites) + } + }, [storage.favorites]) + useEffect(() => { if (storage.themeMode) { localStorage.setItem(KEYS.themeMode, storage.themeMode) @@ -134,3 +203,4 @@ const useStorageDispatch = () => { } export { StorageProvider, useStorage, useStorageDispatch } +export type { Favorite } diff --git a/packages/ui/src/home.tsx b/packages/ui/src/home.tsx index 44a4a75..0151ddf 100644 --- a/packages/ui/src/home.tsx +++ b/packages/ui/src/home.tsx @@ -12,6 +12,7 @@ import { useTheme } from './contexts/settings/theme.js' import { BusSelector } from './components/busSelector.js' import { Loading } from './components/loading.js' import { Settings } from './components/settings/index.js' +import { Favorites } from './components/favorites.js' import { Info } from './components/info.js' import { Predictions } from './components/predictions.js' import { Anchor } from './components/anchor.js' @@ -143,6 +144,7 @@ const Home: FC = () => { + @@ -155,6 +157,9 @@ const Home: FC = () => { + + +

Profile

diff --git a/packages/ui/src/modules/favorites/util.ts b/packages/ui/src/modules/favorites/util.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 9b6995d..8b8b5c3 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -166,6 +166,7 @@ export type { Agency, Stop, Direction, + DirectionName, RouteName, Route, Pred, diff --git a/packages/ui/src/workers/favorites.ts b/packages/ui/src/workers/favorites.ts new file mode 100644 index 0000000..677426f --- /dev/null +++ b/packages/ui/src/workers/favorites.ts @@ -0,0 +1,17 @@ +import type { Favorite } from '../contexts/storage.js' + +interface FavoriteMessage { + action: 'start' | 'stop' | 'close' + favorite?: Favorite +} + +addEventListener('message', (evt: MessageEvent) => { + postMessage(`thanks for sending ${evt.data}`) + + if (evt.data.action === 'close') { + postMessage(`Closing worker ${self.name}`) + self.close() + } +}) + +export type { FavoriteMessage }