Skip to content
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

feat: favorites endpoints, resolves #103 #131

Merged
merged 1 commit into from
Dec 8, 2023
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
54 changes: 51 additions & 3 deletions packages/api/src/handlers/favorite.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import makeDebug from 'debug'
import errors from 'http-errors'

import { addRiderFavorite } from '../queries/favorite.js'
import {
addRiderFavorite,
getRiderFavorites,
removeRiderFavorite
} from '../queries/favorite.js'

import type { Request, Response } from 'express'
import type { Favorite } from '@busmap/common/types/favorites'
import type { Favorite, RiderFavoriteListItem } from '@busmap/common/types/favorites'
import type { RiderFavorite } from '../types.js'
import type { HttpError } from 'http-errors'

const debug = makeDebug('busmap')
const favorite = {
Expand All @@ -16,7 +22,7 @@ const favorite = {
if (favorite && typeof favorite === 'object' && req.session?.userId) {
const { agency, route, stop } = favorite

if (agency?.id && route?.id && stop?.id) {
if (agency.id && route.id && stop.id) {
const { userId } = req.session

try {
Expand All @@ -34,6 +40,48 @@ const favorite = {
}
}

return res.status(400).json(new errors.BadRequest())
},

async remove(
req: Request<object, object, { favorite: Favorite }>,
res: Response<RiderFavorite | HttpError<500> | HttpError<400>>
) {
const { favorite } = req.body

if (favorite && typeof favorite === 'object' && req.session?.userId) {
const { agency, route, stop } = favorite

if (agency.id && route.id && stop.id) {
try {
const removed = await removeRiderFavorite(req.session.userId, favorite)

debug('removed rider favorite', removed)

return res.json(removed[0])
} catch (err) {
return res.status(500).json(new errors.InternalServerError())
}
}
}

return res.status(400).json(new errors.BadRequest())
},

async list(
req: Request,
res: Response<RiderFavoriteListItem[] | HttpError<500> | HttpError<400>>
) {
if (req.session?.userId) {
try {
const favorites = await getRiderFavorites(req.session.userId)

return res.json(favorites)
} catch (err) {
return res.status(500).json(new errors.InternalServerError())
}
}

return res.status(400).json(new errors.BadRequest())
}
}
Expand Down
29 changes: 27 additions & 2 deletions packages/api/src/queries/favorite.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sql } from '../db.js'

import type { Favorite } from '@busmap/common/types/favorites'
import type { Favorite, RiderFavoriteListItem } from '@busmap/common/types/favorites'
import type { RiderFavorite } from '../types.js'

const addRiderFavorite = async (favorite: Favorite, userId: number) => {
Expand Down Expand Up @@ -37,5 +37,30 @@ const addRiderFavorite = async (favorite: Favorite, userId: number) => {

return riderFavoriteRow
}
const getRiderFavorites = async (userId: number) => {
const favorites = await sql<RiderFavoriteListItem[]>`
SELECT created, rank, agency_id, route_id, stop_id, id as favorite_id, ui
FROM rider_favorite
JOIN favorite ON favorite = favorite.id WHERE rider=${userId}
`

return favorites
}
const removeRiderFavorite = async (userId: number, favorite: Favorite) => {
const { agency, route, stop } = favorite
const deleted = await sql<RiderFavorite[]>`
DELETE FROM rider_favorite
WHERE
favorite = (
SELECT DISTINCT(id)
FROM favorite
WHERE agency_id=${agency.id} AND route_id=${route.id} AND stop_id=${stop.id}
)
AND rider = ${userId}
RETURNING *
`

return deleted
}

export { addRiderFavorite }
export { addRiderFavorite, getRiderFavorites, removeRiderFavorite }
2 changes: 2 additions & 0 deletions packages/api/src/routes/favorite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ import { favorite as handler } from '../handlers/favorite.js'
const favorite = Router()

favorite.put('/add', json(), handler.add)
favorite.delete('/remove', json(), handler.remove)
favorite.get('/list', handler.list)

export { favorite }
2 changes: 1 addition & 1 deletion packages/api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
interface RiderFavorite {
rider: number
favorite: number
rank: number
created: string
}

interface AddRiderFavorite {
created: string
}
Expand Down
14 changes: 13 additions & 1 deletion packages/common/src/types/favorites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,16 @@ interface Favorite {
stop: Stop
}

export type { RouteMeta, Favorite }
// API models

interface RiderFavoriteListItem {
created: string
rank: number
agency_id: string
route_id: string
stop_id: string
favorte_id: number
ui: string
}

export type { RouteMeta, Favorite, RiderFavoriteListItem }
6 changes: 6 additions & 0 deletions packages/ui/src/components/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { touch } from '@core/api/authn.js'
import { useGlobals } from '@core/globals.js'
import { useTheme } from '@module/settings/contexts/theme.js'
import { get as getFavorites } from '@module/favorites/api/get.js'

import logoSvg from '../../assets/svg/logo.svg?raw'

Expand Down Expand Up @@ -217,7 +218,12 @@ const Navigation: FC<NavigationProps> = ({ status }) => {

useEffect(() => {
if (status?.user) {
const getUserFavorites = async () => {
await getFavorites()
}

dispatch({ type: 'user', value: status.user })
getUserFavorites()
}
}, [dispatch, status])

Expand Down
13 changes: 9 additions & 4 deletions packages/ui/src/components/signIn.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'
import styled from 'styled-components'
import { toast } from '@busmap/components/toast'

import { login } from '@core/api/authn.js'
import { useGlobals } from '@core/globals.js'
Expand All @@ -23,10 +24,14 @@ const SignIn: FC = () => {
client_id: import.meta.env.VITE_GOOG_CLIENT_ID,
nonce: btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))),
callback: async response => {
const user = await login(response.credential)

dispatch({ type: 'user', value: user })
dispatch({ type: 'page', value: 'profile' })
try {
const user = await login(response.credential)

dispatch({ type: 'user', value: user })
dispatch({ type: 'page', value: 'profile' })
} catch (err) {
toast({ type: 'error', message: 'Error signing in.' })
}
}
})
google.accounts.id.renderButton(ref.current, {
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/modules/favorites/api/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { transport } from '@core/api/transport.js'
import { errors } from '@core/api/errors.js'

import type { Favorite } from '../types.js'

const remove = async (favorite: Favorite) => {
if (!favorite) {
throw errors.create('GET', 400, 'Bad Request')
}

const resp = await transport.fetch('/favorite/remove', {
method: 'DELETE',
body: JSON.stringify({
favorite
})
})

return resp
}

export { remove }
11 changes: 11 additions & 0 deletions packages/ui/src/modules/favorites/api/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { transport } from '@core/api/transport.js'

import type { RiderFavoriteListItem } from '@busmap/common/types/favorites'

const get = async () => {
const resp = await transport.fetch<RiderFavoriteListItem[]>('/favorite/list')

return resp
}

export { get }
17 changes: 15 additions & 2 deletions packages/ui/src/modules/favorites/components/favoriteStop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { same } from '@module/util.js'

import { MAX_FAVORITES } from '../common.js'
import { put } from '../api/put.js'
import { remove } from '../api/delete.js'

import type { FC } from 'react'
import type { RouteName, DirectionName } from '@core/types.js'
import type { Selection } from '@module/util.js'
import type { Favorite } from '../types.js'
import type { Favorite } from '@busmap/common/types/favorites'

interface SelectionMeta extends Selection {
route: RouteName & {
Expand Down Expand Up @@ -51,6 +52,9 @@ const FavoriteStop: FC<FavoriteStopProps> = ({ selection, size = 'medium' }) =>
const mutation = useMutation({
mutationFn: (fav: Favorite) => put(fav)
})
const removal = useMutation({
mutationFn: (fav: Favorite) => remove(fav)
})
const favorite = useMemo(() => {
return favorites.find(fav => {
if (route && direction && stop && agency) {
Expand All @@ -61,6 +65,15 @@ const FavoriteStop: FC<FavoriteStopProps> = ({ selection, size = 'medium' }) =>
const onClick = useCallback(async () => {
if (favorite) {
storageDispatch({ type: 'favoriteRemove', value: favorite })

if (user) {
try {
await removal.mutateAsync(favorite)
toast({ type: 'info', message: 'Favorite removed.' })
} catch (err) {
toast({ type: 'error', message: 'Error removing favorite.' })
}
}
} else if (agency && route && direction && stop) {
const add: Favorite = {
stop: stop,
Expand All @@ -85,7 +98,7 @@ const FavoriteStop: FC<FavoriteStopProps> = ({ selection, size = 'medium' }) =>
}
}
}
}, [storageDispatch, mutation, agency, route, direction, stop, favorite, user])
}, [storageDispatch, mutation, removal, agency, route, direction, stop, favorite, user])
const isFavoritable = Boolean(stop) && (favorites.length < MAX_FAVORITES || favorite)

if (isFavoritable) {
Expand Down
30 changes: 23 additions & 7 deletions packages/ui/src/modules/favorites/components/favorites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MapMarked } from '@busmap/components/icons/mapMarked'
import { Trash } from '@busmap/components/icons/trash'
import { PB50T } from '@busmap/components/colors'

import { useGlobals } from '@core/globals.js'
import { useMap } from '@core/contexts/map.js'
import { useStorage, useStorageDispatch } from '@core/contexts/storage.js'
import { useHomeStop } from '@core/hooks/useHomeStop.js'
Expand All @@ -30,8 +31,10 @@ import { groupBy } from '@module/util.js'

import { getPredsKey } from '../util.js'
import { MAX_FAVORITES } from '../common.js'
import { remove } from '../api/delete.js'

import type { MouseEvent } from 'react'
import type { Favorite } from '@busmap/common/types/favorites'
import type {
ErrorsMap,
WorkerMessage,
Expand All @@ -57,6 +60,7 @@ const Section = styled(Page)`
}
`
const Favorites = memo(function Favorites() {
const { user } = useGlobals()
const map = useMap()
const workerRef = useRef<Worker>()
const homeStop = useHomeStop()
Expand Down Expand Up @@ -88,6 +92,24 @@ const Favorites = memo(function Favorites() {
},
[map]
)
const onClickDeleteFavorite = useCallback(
async (fav: Favorite) => {
storageDispatch({
type: 'favoriteRemove',
value: fav
})

if (user) {
try {
await remove(fav)
toast({ type: 'info', message: 'Favorite removed.' })
} catch (err) {
toast({ type: 'error', message: 'Error removing favorite.' })
}
}
},
[storageDispatch, user]
)
const PredFormat = format === 'minutes' ? Minutes : Time

useEffect(() => {
Expand Down Expand Up @@ -231,13 +253,7 @@ const Favorites = memo(function Favorites() {
)}
<footer>
<Tooltip title="Delete">
<button
onClick={() => {
storageDispatch({
type: 'favoriteRemove',
value: fav
})
}}>
<button onClick={() => onClickDeleteFavorite(fav)}>
<Trash size="small" color={PB50T} />
</button>
</Tooltip>
Expand Down