mirror of
https://github.com/fergalmoran/onearmy-community-platform.git
synced 2025-12-22 09:37:54 +00:00
feat: add profile types filtering to new map
This commit is contained in:
16
packages/components/src/CardButton/CardButton.stories.tsx
Normal file
16
packages/components/src/CardButton/CardButton.stories.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CardButton } from './CardButton'
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react'
|
||||
|
||||
export default {
|
||||
title: 'Components/CardButton',
|
||||
component: CardButton,
|
||||
} as Meta<typeof CardButton>
|
||||
|
||||
export const Basic: StoryFn<typeof CardButton> = () => (
|
||||
<div style={{ width: '300px' }}>
|
||||
<CardButton>
|
||||
<div style={{ padding: '20px' }}>Basic Implementation</div>
|
||||
</CardButton>
|
||||
</div>
|
||||
)
|
||||
43
packages/components/src/CardButton/CardButton.tsx
Normal file
43
packages/components/src/CardButton/CardButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Card } from 'theme-ui'
|
||||
|
||||
import type { BoxProps, ThemeUIStyleObject } from 'theme-ui'
|
||||
|
||||
export interface IProps extends BoxProps {
|
||||
children: React.ReactNode
|
||||
extraSx?: ThemeUIStyleObject | undefined
|
||||
}
|
||||
|
||||
export const CardButton = (props: IProps) => {
|
||||
const { children, extraSx } = props
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
marginTop: '2px',
|
||||
borderRadius: 2,
|
||||
padding: 0,
|
||||
transition: 'borderBottom 0.2s, transform 0.2s',
|
||||
'&:hover': {
|
||||
animationSpeed: '0.3s',
|
||||
cursor: 'pointer',
|
||||
marginTop: '0',
|
||||
borderBottom: '4px solid',
|
||||
transform: 'translateY(-2px)',
|
||||
borderColor: 'black',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(1px)',
|
||||
borderBottom: '3px solid',
|
||||
borderColor: 'grey',
|
||||
transition: 'borderBottom 0.2s, transform 0.2s, borderColor 0.2s',
|
||||
},
|
||||
...extraSx,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -30,9 +30,7 @@ export const CardList = (props: IProps) => {
|
||||
|
||||
const isListEmpty = displayItems.length === 0
|
||||
const hasListLoaded = list
|
||||
const results = `${displayItems.length} ${
|
||||
displayItems.length == 1 ? 'result' : 'results'
|
||||
}`
|
||||
const results = `${displayItems.length} result${displayItems.length == 1 ? '' : 's'} in view`
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -40,6 +38,7 @@ export const CardList = (props: IProps) => {
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
{!hasListLoaded && <Loader />}
|
||||
@@ -49,6 +48,7 @@ export const CardList = (props: IProps) => {
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
paddingX: 2,
|
||||
paddingTop: 2,
|
||||
fontSize: 2,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Card, Flex } from 'theme-ui'
|
||||
import { Flex } from 'theme-ui'
|
||||
|
||||
import { CardButton } from '../CardButton/CardButton'
|
||||
import { InternalLink } from '../InternalLink/InternalLink'
|
||||
import { CardDetailsFallback } from './CardDetailsFallback'
|
||||
import { CardDetailsMemberProfile } from './CardDetailsMemberProfile'
|
||||
@@ -26,30 +27,7 @@ export const CardListItem = ({ item }: IProps) => {
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
marginTop: '2px',
|
||||
borderRadius: 2,
|
||||
padding: 0,
|
||||
'&:hover': {
|
||||
animationSpeed: '0.3s',
|
||||
cursor: 'pointer',
|
||||
marginTop: '0',
|
||||
borderBottom: '4px solid',
|
||||
transform: 'translateY(-2px)',
|
||||
transition: 'borderBottom 0.2s, transform 0.2s',
|
||||
borderColor: 'black',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(1px)',
|
||||
borderBottom: '3px solid',
|
||||
borderColor: 'grey',
|
||||
transition: 'borderBottom 0.2s, transform 0.2s, borderColor 0.2s',
|
||||
},
|
||||
alignItems: 'stretch',
|
||||
alignContent: 'stretch',
|
||||
}}
|
||||
>
|
||||
<CardButton>
|
||||
<Flex sx={{ gap: 2, alignItems: 'stretch', alignContent: 'stretch' }}>
|
||||
{isMember && <CardDetailsMemberProfile creator={creator} />}
|
||||
|
||||
@@ -59,7 +37,7 @@ export const CardListItem = ({ item }: IProps) => {
|
||||
|
||||
{!creator && <CardDetailsFallback item={item} />}
|
||||
</Flex>
|
||||
</Card>
|
||||
</CardButton>
|
||||
</InternalLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export const DonationRequest = (props: IProps) => {
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
backgroundColor: 'offwhite',
|
||||
backgroundColor: 'offWhite',
|
||||
borderTop: '2px solid',
|
||||
flexDirection: ['column', 'row'],
|
||||
padding: 2,
|
||||
|
||||
107
packages/components/src/FilterList/FilterList.stories.tsx
Normal file
107
packages/components/src/FilterList/FilterList.stories.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { FilterList } from './FilterList'
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react'
|
||||
import type { ProfileTypeName } from 'oa-shared'
|
||||
|
||||
export default {
|
||||
title: 'Components/FilterList',
|
||||
component: FilterList,
|
||||
} as Meta<typeof FilterList>
|
||||
|
||||
const availableFilters = [
|
||||
{
|
||||
label: 'Workspace',
|
||||
type: 'workspace' as ProfileTypeName,
|
||||
},
|
||||
{
|
||||
label: 'Machine Builder',
|
||||
type: 'machine-builder' as ProfileTypeName,
|
||||
},
|
||||
{
|
||||
label: 'Collection Point',
|
||||
type: 'collection-point' as ProfileTypeName,
|
||||
},
|
||||
{
|
||||
label: 'Want to get started',
|
||||
type: 'member' as ProfileTypeName,
|
||||
},
|
||||
]
|
||||
|
||||
export const Basic: StoryFn<typeof FilterList> = () => {
|
||||
const [activeFilters, setActiveFilters] = useState<string[]>([])
|
||||
|
||||
const onFilterChange = (label: string) => {
|
||||
const filter = label.toLowerCase()
|
||||
const isFilterPresent = !!activeFilters.find(
|
||||
(existing) => existing === filter,
|
||||
)
|
||||
if (isFilterPresent) {
|
||||
return setActiveFilters(activeFilters.filter((f) => f !== filter))
|
||||
}
|
||||
return setActiveFilters((existing) => [...existing, filter])
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '500px' }}>
|
||||
<FilterList
|
||||
activeFilters={activeFilters}
|
||||
availableFilters={availableFilters}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const OnlyOne: StoryFn<typeof FilterList> = () => {
|
||||
const [activeFilters, setActiveFilters] = useState<string[]>([])
|
||||
|
||||
const onFilterChange = (label: string) => {
|
||||
const filter = label.toLowerCase()
|
||||
const isFilterPresent = !!activeFilters.find(
|
||||
(existing) => existing === filter,
|
||||
)
|
||||
if (isFilterPresent) {
|
||||
return setActiveFilters(activeFilters.filter((f) => f !== filter))
|
||||
}
|
||||
return setActiveFilters((existing) => [...existing, filter])
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '500px' }}>
|
||||
<FilterList
|
||||
activeFilters={activeFilters}
|
||||
availableFilters={[availableFilters[0]]}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
(Shouldn't see anything, only renders for two or more)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const OnlyTwo: StoryFn<typeof FilterList> = () => {
|
||||
const [activeFilters, setActiveFilters] = useState<string[]>([])
|
||||
|
||||
const onFilterChange = (label: string) => {
|
||||
const filter = label.toLowerCase()
|
||||
const isFilterPresent = !!activeFilters.find(
|
||||
(existing) => existing === filter,
|
||||
)
|
||||
if (isFilterPresent) {
|
||||
return setActiveFilters(activeFilters.filter((f) => f !== filter))
|
||||
}
|
||||
return setActiveFilters((existing) => [...existing, filter])
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '500px' }}>
|
||||
<FilterList
|
||||
activeFilters={activeFilters}
|
||||
availableFilters={[availableFilters[0], availableFilters[1]]}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
(No buttons rendered)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
157
packages/components/src/FilterList/FilterList.tsx
Normal file
157
packages/components/src/FilterList/FilterList.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Flex, Text } from 'theme-ui'
|
||||
|
||||
import { Button } from '../Button/Button'
|
||||
import { CardButton } from '../CardButton/CardButton'
|
||||
import { MemberBadge } from '../MemberBadge/MemberBadge'
|
||||
|
||||
import type { ProfileTypeName } from 'oa-shared'
|
||||
|
||||
type FilterOption = {
|
||||
label: string
|
||||
type: ProfileTypeName
|
||||
}
|
||||
|
||||
export interface IProps {
|
||||
activeFilters: string[]
|
||||
availableFilters: FilterOption[]
|
||||
onFilterChange: (label: string) => void
|
||||
}
|
||||
|
||||
export const FilterList = (props: IProps) => {
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
const [disableLeftArrow, setDisableLeftArrow] = useState<boolean>(true)
|
||||
const [disableRightArrow, setDisableRightArrow] = useState<boolean>(false)
|
||||
|
||||
const { activeFilters, availableFilters, onFilterChange } = props
|
||||
|
||||
if (!availableFilters || availableFilters.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleHorizantalScroll = (step: number) => {
|
||||
const distance = 121
|
||||
const element = elementRef.current
|
||||
const speed = 10
|
||||
let scrollAmount = 0
|
||||
|
||||
const slideTimer = setInterval(() => {
|
||||
if (element) {
|
||||
element.scrollLeft += step
|
||||
scrollAmount += Math.abs(step)
|
||||
if (scrollAmount >= distance) {
|
||||
clearInterval(slideTimer)
|
||||
}
|
||||
const { scrollLeft, scrollWidth, clientWidth } = element
|
||||
switch (scrollLeft + clientWidth) {
|
||||
case clientWidth:
|
||||
setDisableLeftArrow(true)
|
||||
// scrollWidth && setDisableRightArrow(true)
|
||||
break
|
||||
case scrollWidth:
|
||||
setDisableRightArrow(true)
|
||||
break
|
||||
default:
|
||||
setDisableLeftArrow(false)
|
||||
setDisableRightArrow(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, speed)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleHorizantalScroll(0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
as="ul"
|
||||
data-cy="FilterList"
|
||||
ref={elementRef}
|
||||
sx={{
|
||||
listStyle: 'none',
|
||||
flexWrap: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
paddingY: 2,
|
||||
paddingX: 4,
|
||||
gap: 2,
|
||||
zIndex: 1,
|
||||
top: '-80px',
|
||||
height: 'auto',
|
||||
}}
|
||||
>
|
||||
{availableFilters.map(({ label, type }, index) => {
|
||||
const active = activeFilters.find((filter) => filter === type)
|
||||
return (
|
||||
<CardButton
|
||||
as="li"
|
||||
data-cy={`MapListFilter${active ? '-active' : ''}`}
|
||||
key={index}
|
||||
onClick={() => onFilterChange(type)}
|
||||
extraSx={{
|
||||
backgroundColor: 'offWhite',
|
||||
padding: 1,
|
||||
textAlign: 'center',
|
||||
width: '130px',
|
||||
minWidth: '130px',
|
||||
height: '75px',
|
||||
flexDirection: 'column',
|
||||
...(active
|
||||
? {
|
||||
borderColor: 'green',
|
||||
':hover': { borderColor: 'green' },
|
||||
}
|
||||
: {
|
||||
borderColor: 'offWhite',
|
||||
':hover': { borderColor: 'offWhite' },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<MemberBadge size={30} profileType={type} />
|
||||
<br />
|
||||
<Text variant="quiet" sx={{ fontSize: 1 }}>
|
||||
{label}
|
||||
</Text>
|
||||
</CardButton>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
{availableFilters.length > 3 && (
|
||||
<Flex
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
zIndex: 2,
|
||||
paddingX: 2,
|
||||
position: 'relative',
|
||||
top: '-65px',
|
||||
height: 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleHorizantalScroll(-10)}
|
||||
icon="chevron-left"
|
||||
disabled={disableLeftArrow}
|
||||
sx={{ borderRadius: 99 }}
|
||||
showIconOnly
|
||||
small
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleHorizantalScroll(10)}
|
||||
icon="chevron-right"
|
||||
disabled={disableRightArrow}
|
||||
sx={{ borderRadius: 99, zIndex: 2 }}
|
||||
showIconOnly
|
||||
small
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export const ImageGallery = (props: ImageGalleryProps) => {
|
||||
height: 67,
|
||||
objectFit: props.allowPortrait ? 'contain' : 'cover',
|
||||
borderRadius: 1,
|
||||
border: '1px solid offwhite',
|
||||
border: '1px solid offWhite',
|
||||
}}
|
||||
crossOrigin=""
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,10 @@ export const MemberBadge = (props: Props) => {
|
||||
const profileType = props.profileType || 'member'
|
||||
const badgeSize = size ? size : MINIMUM_SIZE
|
||||
|
||||
const title =
|
||||
profileType.charAt(0).toUpperCase() +
|
||||
profileType.slice(1).replace(/-/g, ' ')
|
||||
|
||||
return (
|
||||
<Image
|
||||
loading="lazy"
|
||||
@@ -29,6 +33,7 @@ export const MemberBadge = (props: Props) => {
|
||||
sx={{ width: badgeSize, borderRadius: '50%', ...sx }}
|
||||
width={badgeSize}
|
||||
height={badgeSize}
|
||||
title={title}
|
||||
src={
|
||||
(badgeSize > MINIMUM_SIZE && !useLowDetailVersion
|
||||
? theme.badges[profileType]?.normal
|
||||
|
||||
@@ -4,6 +4,7 @@ export { BlockedRoute } from './BlockedRoute/BlockedRoute'
|
||||
export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'
|
||||
export { Button } from './Button/Button'
|
||||
export { ButtonShowReplies } from './ButtonShowReplies/ButtonShowReplies'
|
||||
export { CardButton } from './CardButton/CardButton'
|
||||
export { CardList } from './CardList/CardList'
|
||||
export { CardListItem } from './CardListItem/CardListItem'
|
||||
export { Category } from './Category/Category'
|
||||
@@ -29,6 +30,7 @@ export { FieldTextarea } from './FieldTextarea/FieldTextarea'
|
||||
export { Guidelines } from './Guidelines/Guidelines'
|
||||
export { FlagIcon, FlagIconHowTos, FlagIconEvents } from './FlagIcon/FlagIcon'
|
||||
export { FollowButton } from './FollowButton/FollowButton'
|
||||
export { FilterList } from './FilterList/FilterList'
|
||||
export { GlobalStyles } from './GlobalStyles/GlobalStyles'
|
||||
export { HeroBanner } from './HeroBanner/HeroBanner'
|
||||
export { Icon } from './Icon/Icon'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const userId = 'davehakkens'
|
||||
const profileTypesCount = 5
|
||||
|
||||
describe('[Map]', () => {
|
||||
it('[Shows expected pins]', () => {
|
||||
@@ -26,7 +27,17 @@ describe('[Map]', () => {
|
||||
cy.step('New map shows the cards')
|
||||
cy.get('[data-cy="welome-header"]').should('be.visible')
|
||||
cy.get('[data-cy="CardList-desktop"]').should('be.visible')
|
||||
cy.get('[data-cy="list-results"]').contains('51 results')
|
||||
cy.get('[data-cy="list-results"]').contains('51 results in view')
|
||||
|
||||
cy.step('Map filters can be used')
|
||||
cy.get('[data-cy=FilterList]')
|
||||
.first()
|
||||
.children()
|
||||
.should('have.length', profileTypesCount)
|
||||
cy.get('[data-cy=MapListFilter]').first().click()
|
||||
cy.get('[data-cy="list-results"]').contains('6 results in view')
|
||||
cy.get('[data-cy=MapListFilter-active]').first().click()
|
||||
cy.get('[data-cy="list-results"]').contains('51 results in view')
|
||||
|
||||
cy.step('As the user moves in the list updates')
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const commonStyles = {
|
||||
},
|
||||
colors: {
|
||||
white: 'white',
|
||||
offwhite: '#ececec',
|
||||
offWhite: '#ececec',
|
||||
black: '#1b1b1b',
|
||||
softyellow: '#f5ede2',
|
||||
blue: '#83ceeb',
|
||||
|
||||
@@ -87,7 +87,7 @@ export interface ThemeWithName {
|
||||
background: string
|
||||
silver: string
|
||||
softgrey: string
|
||||
offwhite: string
|
||||
offWhite: string
|
||||
lightgrey: string
|
||||
darkGrey: string
|
||||
subscribed: string
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ImageConverter = (props: IProps) => {
|
||||
}}
|
||||
sx={{
|
||||
border: '1px solid ',
|
||||
borderColor: 'offwhite',
|
||||
borderColor: 'offWhite',
|
||||
borderRadius: 1,
|
||||
}}
|
||||
id="preview"
|
||||
|
||||
@@ -24,6 +24,7 @@ const asOptions = (mapPins, items: Array<IMapGrouping>): FilterGroupOption[] =>
|
||||
: item.type.split(' ')
|
||||
|
||||
const value = item.subType ? item.subType : item.type
|
||||
const profileType = transformSpecialistWorkspaceTypeToWorkspace(value)
|
||||
|
||||
return {
|
||||
label: item.displayName,
|
||||
@@ -33,10 +34,7 @@ const asOptions = (mapPins, items: Array<IMapGrouping>): FilterGroupOption[] =>
|
||||
(item.type as string) === 'verified' ? (
|
||||
<Image src={VerifiedBadgeIcon} width={ICON_SIZE} />
|
||||
) : (
|
||||
<MemberBadge
|
||||
size={ICON_SIZE}
|
||||
profileType={transformSpecialistWorkspaceTypeToWorkspace(value)}
|
||||
/>
|
||||
<MemberBadge size={ICON_SIZE} profileType={profileType} />
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, CardList, Map } from 'oa-components'
|
||||
import { Box, Flex, Heading } from 'theme-ui'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Map } from 'oa-components'
|
||||
import { Box, Flex } from 'theme-ui'
|
||||
|
||||
import { Clusters } from './Cluster'
|
||||
import { latLongFilter } from './latLongFilter'
|
||||
import { MapWithListHeader } from './MapWithListHeader'
|
||||
import { Popup } from './Popup'
|
||||
|
||||
import type { LatLngExpression } from 'leaflet'
|
||||
@@ -11,6 +12,33 @@ import type { Map as MapType } from 'react-leaflet'
|
||||
import type { ILatLng } from 'shared/models'
|
||||
import type { IMapPin } from 'src/models/maps.models'
|
||||
|
||||
const allFilters = [
|
||||
{
|
||||
label: 'Workspace',
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
label: 'Machine Builder',
|
||||
type: 'machine-builder',
|
||||
},
|
||||
{
|
||||
label: 'Community Point',
|
||||
type: 'community-builder',
|
||||
},
|
||||
{
|
||||
label: 'Collection Point',
|
||||
type: 'collection-point',
|
||||
},
|
||||
{
|
||||
label: 'Space',
|
||||
type: 'space',
|
||||
},
|
||||
{
|
||||
label: 'Want to get started',
|
||||
type: 'member',
|
||||
},
|
||||
]
|
||||
|
||||
interface IProps {
|
||||
activePin: IMapPin | null
|
||||
center: ILatLng
|
||||
@@ -18,14 +46,12 @@ interface IProps {
|
||||
notification?: string
|
||||
pins: IMapPin[]
|
||||
zoom: number
|
||||
onPinClicked: (pin: IMapPin) => void
|
||||
onBlur: () => void
|
||||
onPinClicked: (pin: IMapPin) => void
|
||||
setZoom: (arg: number) => void
|
||||
}
|
||||
|
||||
export const MapWithList = (props: IProps) => {
|
||||
const [filteredPins, setFilteredPins] = useState<IMapPin[] | null>(null)
|
||||
const [showMobileList, setShowMobileList] = useState<boolean>(false)
|
||||
const {
|
||||
activePin,
|
||||
center,
|
||||
@@ -38,15 +64,52 @@ export const MapWithList = (props: IProps) => {
|
||||
setZoom,
|
||||
} = props
|
||||
|
||||
const [activePinFilters, setActivePinFilters] = useState<string[]>([])
|
||||
const [allPinsInView, setAllPinsInView] = useState<IMapPin[]>(pins)
|
||||
const [filteredPins, setFilteredPins] = useState<IMapPin[] | null>(
|
||||
pins || null,
|
||||
)
|
||||
const [showMobileList, setShowMobileList] = useState<boolean>(false)
|
||||
|
||||
const availableFilters = useMemo(() => {
|
||||
const pinTypes = pins.map(({ creator }) => creator?.profileType)
|
||||
const filtersNeeded = [...new Set(pinTypes)]
|
||||
return allFilters.filter(({ type }) =>
|
||||
filtersNeeded.some((filter) => filter === type),
|
||||
)
|
||||
}, [pins])
|
||||
|
||||
useEffect(() => {
|
||||
if (activePinFilters.length === 0) {
|
||||
return setFilteredPins(allPinsInView)
|
||||
}
|
||||
const filteredPins = allPinsInView.filter((pin) =>
|
||||
activePinFilters.includes(pin.type),
|
||||
)
|
||||
|
||||
setFilteredPins(filteredPins)
|
||||
}, [activePinFilters, allPinsInView])
|
||||
|
||||
const handleLocationFilter = () => {
|
||||
if (mapRef.current) {
|
||||
const boundaries = mapRef.current.leafletElement.getBounds()
|
||||
// Map.getBounds() is wrongly typed
|
||||
const results = latLongFilter(boundaries as any, pins)
|
||||
setFilteredPins(results)
|
||||
setAllPinsInView(results)
|
||||
}
|
||||
}
|
||||
|
||||
const onFilterChange = (label: string) => {
|
||||
const filter = label.toLowerCase()
|
||||
const isFilterPresent = !!activePinFilters.find(
|
||||
(pinFilter) => pinFilter === filter,
|
||||
)
|
||||
if (isFilterPresent) {
|
||||
return setActivePinFilters((pins) => pins.filter((pin) => pin !== filter))
|
||||
}
|
||||
return setActivePinFilters((pins) => [...pins, filter])
|
||||
}
|
||||
|
||||
const isViewportGreaterThanTablet = window.innerWidth > 1024
|
||||
const mapCenter: LatLngExpression = center ? [center.lat, center.lng] : [0, 0]
|
||||
const mapZoom = center ? zoom : 2
|
||||
@@ -67,14 +130,16 @@ export const MapWithList = (props: IProps) => {
|
||||
background: 'white',
|
||||
flex: 1,
|
||||
overflow: 'scroll',
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Heading data-cy="welome-header" sx={{ padding: 2 }}>
|
||||
Welcome to our world!{' '}
|
||||
{pins && `${pins.length} members (and counting...)`}
|
||||
</Heading>
|
||||
<CardList dataCy="desktop" list={pins} filteredList={filteredPins} />
|
||||
<MapWithListHeader
|
||||
pins={pins}
|
||||
activePinFilters={activePinFilters}
|
||||
availableFilters={availableFilters}
|
||||
onFilterChange={onFilterChange}
|
||||
filteredPins={filteredPins}
|
||||
viewport="desktop"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Mobile/tablet list view */}
|
||||
@@ -84,7 +149,6 @@ export const MapWithList = (props: IProps) => {
|
||||
background: 'white',
|
||||
width: '100%',
|
||||
overflow: 'scroll',
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
@@ -99,26 +163,20 @@ export const MapWithList = (props: IProps) => {
|
||||
<Button
|
||||
data-cy="ShowMapButton"
|
||||
icon="map"
|
||||
sx={{ position: 'sticky' }}
|
||||
sx={{ position: 'sticky', marginTop: 2 }}
|
||||
onClick={() => setShowMobileList(false)}
|
||||
small
|
||||
>
|
||||
Show map view
|
||||
</Button>
|
||||
</Flex>
|
||||
<Heading
|
||||
data-cy="welome-header"
|
||||
variant="small"
|
||||
sx={{ padding: 2, paddingTop: '50px' }}
|
||||
>
|
||||
Welcome to our world!{' '}
|
||||
{pins && `${pins.length} members (and counting...)`}
|
||||
</Heading>
|
||||
<CardList
|
||||
columnsCountBreakPoints={{ 300: 1, 600: 2 }}
|
||||
dataCy="mobile"
|
||||
list={pins}
|
||||
filteredList={filteredPins}
|
||||
<MapWithListHeader
|
||||
pins={pins}
|
||||
activePinFilters={activePinFilters}
|
||||
availableFilters={availableFilters}
|
||||
onFilterChange={onFilterChange}
|
||||
filteredPins={filteredPins}
|
||||
viewport="mobile"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
62
src/pages/Maps/Content/MapView/MapWithListHeader.tsx
Normal file
62
src/pages/Maps/Content/MapView/MapWithListHeader.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { CardList, FilterList } from 'oa-components'
|
||||
import { Flex, Heading } from 'theme-ui'
|
||||
|
||||
import type { IMapPin } from 'src/models'
|
||||
|
||||
interface IProps {
|
||||
activePinFilters: string[]
|
||||
availableFilters: any
|
||||
filteredPins: IMapPin[] | null
|
||||
onFilterChange: (label: string) => void
|
||||
pins: IMapPin[]
|
||||
viewport: 'desktop' | 'mobile'
|
||||
}
|
||||
|
||||
export const MapWithListHeader = (props: IProps) => {
|
||||
const {
|
||||
activePinFilters,
|
||||
availableFilters,
|
||||
filteredPins,
|
||||
onFilterChange,
|
||||
pins,
|
||||
viewport,
|
||||
} = props
|
||||
const isMobile = viewport === 'mobile'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'background',
|
||||
gap: 2,
|
||||
paddingY: 2,
|
||||
paddingTop: isMobile ? '50px' : 2,
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
data-cy="welome-header"
|
||||
sx={{
|
||||
paddingX: 4,
|
||||
}}
|
||||
variant={isMobile ? 'small' : 'heading'}
|
||||
>
|
||||
Welcome to our world!{' '}
|
||||
{pins && `${pins.length} members (and counting...)`}
|
||||
</Heading>
|
||||
|
||||
<FilterList
|
||||
activeFilters={activePinFilters}
|
||||
availableFilters={availableFilters}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
</Flex>
|
||||
<CardList
|
||||
columnsCountBreakPoints={isMobile ? { 300: 1, 600: 2 } : undefined}
|
||||
dataCy={viewport}
|
||||
list={pins}
|
||||
filteredList={filteredPins}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -153,7 +153,7 @@ const MapsPage = observer(() => {
|
||||
|
||||
return (
|
||||
// the calculation for the height is kind of hacky for now, will set properly on final mockups
|
||||
<Box id="mapPage" sx={{ height: 'calc(100vh - 150px)', width: '100%' }}>
|
||||
<Box id="mapPage" sx={{ height: 'calc(100vh - 120px)', width: '100%' }}>
|
||||
<NewMapBanner showNewMap={showNewMap} setShowNewMap={setShowNewMap} />
|
||||
{!showNewMap && (
|
||||
<>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const PatreonIntegration = ({ user }) => {
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: ['flex-start', 'flex-start', 'flex-start'],
|
||||
backgroundColor: 'offwhite',
|
||||
backgroundColor: 'offWhite',
|
||||
borderRadius: 3,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
Reference in New Issue
Block a user