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: filter trains #279

Merged
merged 27 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
31 changes: 29 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion site/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,22 @@ const preview: Preview = {
countryCode: 'FI',
latitude: 1,
longitude: 2,
stationName: { fi: 'Ainola', en: 'Ainola', sv: 'Ainola' },
stationName: 'Ainola',
stationShortCode: 'AIN'
},
{
countryCode: 'FI',
latitude: 1,
longitude: 2,
stationName: 'Helsinki asema',
stationShortCode: 'HKI'
},
{
countryCode: 'FI',
latitude: 1,
longitude: 2,
stationName: 'Riihimäki asema',
stationShortCode: 'RI'
}
])
)
Expand Down
2 changes: 2 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"@headlessui/react": "^1.7.17",
"@junat/digitraffic": "workspace:*",
"@junat/digitraffic-mqtt": "workspace:*",
"@radix-ui/react-dialog": "^1.0.2",
Expand All @@ -33,6 +34,7 @@
"core-js": "3.27.2",
"formik": "^2.2.9",
"framer-motion": "8.5.4",
"frominto": "^2.0.0",
"fuse.js": "^6.6.2",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
Expand Down
26 changes: 24 additions & 2 deletions site/src/components/dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ import {
DialogContent,
DialogOverlay
} from '@radix-ui/react-dialog'
import React from 'react'

export type DialogProps = ComponentProps<typeof DialogPortal> & {
description: ReactNode | ReactNode[]
title: ReactNode | ReactNode[]
/**
* If this dialog is used inside another modal (e.g. Dropdown) scrollbar is programmatically removed twice, which might lead to unintended layout shift.
* Set to true to fix this behavior.
*/
fixModal?: boolean
/**
* In some cases the autofocus can lead to confusing user experience.
*
Expand All @@ -43,17 +49,33 @@ export function Dialog({
title,
children,
onOpenAutoFocus,
fixModal,

...props
}: DialogProps) {
// JUN-227 — using two components which both use https://www.npmjs.com/package/react-remove-scroll causes
// both to calculate removed scrollbar. Since the first one sets margin-right the other will instead apply to padding,
// effectively offsetting body by --removed-body-scroll-bar-size
React.useEffect(() => {
if (!fixModal) return

const body = document.querySelector('body')
const margin = body?.style.getPropertyValue('margin-right')
const padding = body?.style.getPropertyValue('padding-right')

if (![margin, padding].includes(undefined) && padding === margin) {
body?.style.setProperty('padding-right', '0px')
}
}, [fixModal])

return (
<DialogPortal {...props}>
{/** TODO: https://tailwindcss.com/docs/backdrop-blur does not work here for some reason */}
<DialogOverlay className="[backdrop-filter:blur(3px)] fixed inset-0 bg-[hsla(0,0%,100%,0.87)] grid items-center animate-[fadein_150ms_cubic-bezier(0.16,1,0.3,1)] dark:bg-[hsla(0,0%,0%,0.87)]" />
<DialogOverlay className="[backdrop-filter:blur(3px)] z-[1] fixed inset-0 bg-[hsla(0,0%,100%,0.87)] grid items-center animate-[fadein_150ms_cubic-bezier(0.16,1,0.3,1)] dark:bg-[hsla(0,0%,0%,0.87)]" />
<DialogContent
className={`bg-gray-100 rounded-xl shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px]
p-[25px] fixed top-[50%] left-[50%] [transform:translate(-50%,_-50%)] overflow-clip w-[90vw] max-w-[450px] max-h-[85vh] overflow-y-auto
animate-[dialog-content_150ms_cubic-bezier(0.16,_1,_0.3,_1)] dark:bg-gray-800 [&_>_[data-children]]:overflow-auto`}
animate-[dialog-content_150ms_cubic-bezier(0.16,_1,_0.3,_1)] dark:bg-gray-800 [&_>_[data-children]]:overflow-auto z-[1]`}
onOpenAutoFocus={onOpenAutoFocus}
>
<DialogTitle className="text-gray-900 text-[1.21rem] lg:text-[1.44rem] dark:text-gray-100">
Expand Down
3 changes: 3 additions & 0 deletions site/src/components/icons/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 96 additions & 53 deletions site/src/components/station_dropdown_menu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Locale } from '~/types/common'

import dynamic from 'next/dynamic'
import Link from 'next/link'
import { shallow } from 'zustand/shallow'

import { DialogTrigger } from '@radix-ui/react-dialog'
import {
Arrow,
CheckboxItem,
Expand All @@ -13,7 +15,10 @@ import {
Trigger
} from '@radix-ui/react-dropdown-menu'

import React from 'react'

import CirclesHorizontal from '~/components/icons/circles_horizontal.svg'
import Filter from '~/components/icons/filter.svg'
import GoogleMaps from '~/components/icons/google_maps.svg'
import HeartFilled from '~/components/icons/heart_filled.svg'
import HeartOutline from '~/components/icons/heart_outline.svg'
Expand All @@ -22,6 +27,15 @@ import { useFavorites } from '~/hooks/use_favorites'
import { googleMapsDirections } from '~/utils/services'
import translate from '~/utils/translate'

const DialogProvider = dynamic(() =>
import('../dialog').then(mod => mod.DialogProvider)
)
const TrainsFilterDialog = dynamic(() =>
import('./trains_filter_dialog').then(mod => mod.TrainsFilterDialog)
)

import { useFilters } from '~/hooks/use_filters'

type StationShortCode = string

type StationDropdownMenuProps = {
Expand All @@ -35,6 +49,7 @@ export const CHECKBOX_ITEM_TEST_ID = 'favorites-checkbox-item' as const
export const TRIGGER_BUTTON_TEST_ID = 'trigger-button' as const

export const StationDropdownMenu = (props: StationDropdownMenuProps) => {
const [open, setOpen] = React.useState(false)
const favorites = useFavorites(
state => ({
add: state.addFavorite,
Expand All @@ -48,68 +63,96 @@ export const StationDropdownMenu = (props: StationDropdownMenuProps) => {
})

const t = translate(props.locale)
const filtersActive = useFilters(state => state.destination) !== null

return (
<Root>
<Trigger asChild>
<button
data-testid={TRIGGER_BUTTON_TEST_ID}
className="rounded-full h-[35px] w-[35px] inline-flex items-center justify-center text-primary-900 bg-gray-300 cursor-pointer fill-gray-800
dark:fill-gray-400 dark:bg-gray-700 focus:outline-none focus:[border:2px_solid_theme(colors.primary.500)]"
aria-label="Customise options"
>
<CirclesHorizontal />
</button>
</Trigger>

<Portal>
<Content
className="rounded-md py-2 px-1 min-w-[260px] bg-gray-200 dark:bg-gray-800 [box-shadow:hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_-5px_-10px_25px_-15px]
<DialogProvider open={open} onOpenChange={setOpen}>
<Trigger asChild>
<button
data-testid={TRIGGER_BUTTON_TEST_ID}
className="relative rounded-full h-[35px] w-[35px] inline-flex items-center justify-center text-primary-900 bg-gray-300 cursor-pointer fill-gray-800
dark:fill-gray-400 dark:bg-gray-700 focus:outline-none focus:[border:2px_solid_theme(colors.primary.500)] border-2 border-transparent"
aria-label="Customise options"
>
<CirclesHorizontal />
{filtersActive && (
<svg
width={10}
height={10}
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
className="absolute -top-0.5 -right-0.5 fill-primary-600 [transform:translate(50%,-50%)] [transform-origin:100%_0%]"
>
<circle cx="50" cy="50" r="50" />
</svg>
)}
</button>
</Trigger>

<Portal>
<Content
className="rounded-md py-2 px-1 min-w-[260px] bg-gray-200 dark:bg-gray-800 [box-shadow:hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_-5px_-10px_25px_-15px]
duration-300 flex flex-col gap-1 text-gray-800 dark:text-gray-300 data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade
data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade [border:1px_solid_theme(colors.gray.400)] dark:border-none"
sideOffset={5}
align="end"
alignOffset={1}
onCloseAutoFocus={event => event?.preventDefault()}
>
<Arrow className="dark:fill-gray-800 fill-gray-400" />

<CheckboxItem
data-testid={CHECKBOX_ITEM_TEST_ID}
className="group px-3 rounded-sm select-none transition-[background-color] duration-200 grid grid-cols-[1fr,24px]
items-center cursor-pointer min-h-[35px] text-[13px] font-ui"
// Prevents the menu from closing
onSelect={event => event.preventDefault()}
checked={isFavorite}
onCheckedChange={() => {
favorites[isFavorite ? 'remove' : 'add'](props.currentStation)
}}
sideOffset={5}
align="end"
alignOffset={1}
onCloseAutoFocus={event => event?.preventDefault()}
>
{isFavorite
? t('removeStationFromFavorites')
: t('addStationToFavorites')}
{isFavorite ? (
<HeartFilled className="fill-primary-500" />
) : (
<HeartOutline className="dark:fill-gray-600 fill-gray-400" />
)}
</CheckboxItem>
<Arrow className="dark:fill-gray-800 fill-gray-400" />

<CheckboxItem
data-testid={CHECKBOX_ITEM_TEST_ID}
className="group px-3 rounded-sm select-none transition-[background-color] duration-200 grid grid-cols-[1fr,24px]
items-center cursor-pointer min-h-[35px] text-[13px] font-ui"
// Prevents the menu from closing
onSelect={event => event.preventDefault()}
checked={isFavorite}
onCheckedChange={() => {
favorites[isFavorite ? 'remove' : 'add'](props.currentStation)
}}
>
{isFavorite
? t('removeStationFromFavorites')
: t('addStationToFavorites')}
{isFavorite ? (
<HeartFilled className="fill-primary-500" />
) : (
<HeartOutline className="dark:fill-gray-600 fill-gray-400" />
)}
</CheckboxItem>

<Item
className="group px-3 rounded-sm select-none transition-[background-color] duration-200 grid grid-cols-[1fr,24px]
<DialogTrigger>
<Item
className="group px-3 rounded-sm select-none transition-[background-color] duration-200 grid grid-cols-[1fr,24px]
items-center cursor-pointer min-h-[35px] text-[13px] font-ui"
>
{t('filterTrains')}
<Filter className="dark:fill-gray-600 fill-gray-400" />
</Item>
</DialogTrigger>

<Item
className="group px-3 rounded-sm select-none transition-[background-color] duration-200 grid grid-cols-[1fr,24px]
items-center cursor-pointer min-h-[35px] text-[13px] font-ui"
>
<Link
target="_blank"
href={googleMapsDirections(props.long, props.lat)}
className="decoration-transparent hover:text-gray-800 dark:text-gray-300 dark:hover:text-current "
>
{t('routeToStation')}
</Link>
<GoogleMaps className="w-[24px] h-[24px] aspect-[1]" />
</Item>
</Content>
</Portal>
<Link
target="_blank"
href={googleMapsDirections(props.long, props.lat)}
className="decoration-transparent hover:text-gray-800 dark:text-gray-300 dark:hover:text-current "
>
{t('routeToStation')}
</Link>
<GoogleMaps className="w-[24px] h-[24px] aspect-[1]" />
</Item>
</Content>
</Portal>
<TrainsFilterDialog
locale={props.locale}
onSubmit={() => setOpen(false)}
/>
</DialogProvider>
</Root>
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Meta } from '@storybook/react'
import { fireEvent, within } from '@storybook/testing-library'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useFavorites } from '~/hooks/use_favorites'
import {
StationDropdownMenu,
CHECKBOX_ITEM_TEST_ID,
StationDropdownMenu,
TRIGGER_BUTTON_TEST_ID
} from '.'
import { within, fireEvent } from '@storybook/testing-library'
import { useFavorites } from '~/hooks/use_favorites'

export const Default = () => {
const removeFavorite = useFavorites(state => state.removeFavorite)
Expand All @@ -18,6 +19,13 @@ export const Default = () => {

export default {
component: StationDropdownMenu,
decorators: [
Story => (
<QueryClientProvider client={new QueryClient()}>
<Story />
</QueryClientProvider>
)
],
play: async context => {
const canvas = within(context.canvasElement)
const trigger = await canvas.findByTestId(TRIGGER_BUTTON_TEST_ID)
Expand Down
Loading