mirror of
https://github.com/fergalmoran/onearmy-community-platform.git
synced 2026-01-06 08:54:02 +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 isListEmpty = displayItems.length === 0
|
||||||
const hasListLoaded = list
|
const hasListLoaded = list
|
||||||
const results = `${displayItems.length} ${
|
const results = `${displayItems.length} result${displayItems.length == 1 ? '' : 's'} in view`
|
||||||
displayItems.length == 1 ? 'result' : 'results'
|
|
||||||
}`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -40,6 +38,7 @@ export const CardList = (props: IProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!hasListLoaded && <Loader />}
|
{!hasListLoaded && <Loader />}
|
||||||
@@ -49,6 +48,7 @@ export const CardList = (props: IProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingX: 2,
|
paddingX: 2,
|
||||||
|
paddingTop: 2,
|
||||||
fontSize: 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 { InternalLink } from '../InternalLink/InternalLink'
|
||||||
import { CardDetailsFallback } from './CardDetailsFallback'
|
import { CardDetailsFallback } from './CardDetailsFallback'
|
||||||
import { CardDetailsMemberProfile } from './CardDetailsMemberProfile'
|
import { CardDetailsMemberProfile } from './CardDetailsMemberProfile'
|
||||||
@@ -26,30 +27,7 @@ export const CardListItem = ({ item }: IProps) => {
|
|||||||
padding: 2,
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<CardButton>
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex sx={{ gap: 2, alignItems: 'stretch', alignContent: 'stretch' }}>
|
<Flex sx={{ gap: 2, alignItems: 'stretch', alignContent: 'stretch' }}>
|
||||||
{isMember && <CardDetailsMemberProfile creator={creator} />}
|
{isMember && <CardDetailsMemberProfile creator={creator} />}
|
||||||
|
|
||||||
@@ -59,7 +37,7 @@ export const CardListItem = ({ item }: IProps) => {
|
|||||||
|
|
||||||
{!creator && <CardDetailsFallback item={item} />}
|
{!creator && <CardDetailsFallback item={item} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</CardButton>
|
||||||
</InternalLink>
|
</InternalLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const DonationRequest = (props: IProps) => {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: 'offwhite',
|
backgroundColor: 'offWhite',
|
||||||
borderTop: '2px solid',
|
borderTop: '2px solid',
|
||||||
flexDirection: ['column', 'row'],
|
flexDirection: ['column', 'row'],
|
||||||
padding: 2,
|
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,
|
height: 67,
|
||||||
objectFit: props.allowPortrait ? 'contain' : 'cover',
|
objectFit: props.allowPortrait ? 'contain' : 'cover',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
border: '1px solid offwhite',
|
border: '1px solid offWhite',
|
||||||
}}
|
}}
|
||||||
crossOrigin=""
|
crossOrigin=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const MemberBadge = (props: Props) => {
|
|||||||
const profileType = props.profileType || 'member'
|
const profileType = props.profileType || 'member'
|
||||||
const badgeSize = size ? size : MINIMUM_SIZE
|
const badgeSize = size ? size : MINIMUM_SIZE
|
||||||
|
|
||||||
|
const title =
|
||||||
|
profileType.charAt(0).toUpperCase() +
|
||||||
|
profileType.slice(1).replace(/-/g, ' ')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -29,6 +33,7 @@ export const MemberBadge = (props: Props) => {
|
|||||||
sx={{ width: badgeSize, borderRadius: '50%', ...sx }}
|
sx={{ width: badgeSize, borderRadius: '50%', ...sx }}
|
||||||
width={badgeSize}
|
width={badgeSize}
|
||||||
height={badgeSize}
|
height={badgeSize}
|
||||||
|
title={title}
|
||||||
src={
|
src={
|
||||||
(badgeSize > MINIMUM_SIZE && !useLowDetailVersion
|
(badgeSize > MINIMUM_SIZE && !useLowDetailVersion
|
||||||
? theme.badges[profileType]?.normal
|
? theme.badges[profileType]?.normal
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export { BlockedRoute } from './BlockedRoute/BlockedRoute'
|
|||||||
export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'
|
export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'
|
||||||
export { Button } from './Button/Button'
|
export { Button } from './Button/Button'
|
||||||
export { ButtonShowReplies } from './ButtonShowReplies/ButtonShowReplies'
|
export { ButtonShowReplies } from './ButtonShowReplies/ButtonShowReplies'
|
||||||
|
export { CardButton } from './CardButton/CardButton'
|
||||||
export { CardList } from './CardList/CardList'
|
export { CardList } from './CardList/CardList'
|
||||||
export { CardListItem } from './CardListItem/CardListItem'
|
export { CardListItem } from './CardListItem/CardListItem'
|
||||||
export { Category } from './Category/Category'
|
export { Category } from './Category/Category'
|
||||||
@@ -29,6 +30,7 @@ export { FieldTextarea } from './FieldTextarea/FieldTextarea'
|
|||||||
export { Guidelines } from './Guidelines/Guidelines'
|
export { Guidelines } from './Guidelines/Guidelines'
|
||||||
export { FlagIcon, FlagIconHowTos, FlagIconEvents } from './FlagIcon/FlagIcon'
|
export { FlagIcon, FlagIconHowTos, FlagIconEvents } from './FlagIcon/FlagIcon'
|
||||||
export { FollowButton } from './FollowButton/FollowButton'
|
export { FollowButton } from './FollowButton/FollowButton'
|
||||||
|
export { FilterList } from './FilterList/FilterList'
|
||||||
export { GlobalStyles } from './GlobalStyles/GlobalStyles'
|
export { GlobalStyles } from './GlobalStyles/GlobalStyles'
|
||||||
export { HeroBanner } from './HeroBanner/HeroBanner'
|
export { HeroBanner } from './HeroBanner/HeroBanner'
|
||||||
export { Icon } from './Icon/Icon'
|
export { Icon } from './Icon/Icon'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const userId = 'davehakkens'
|
const userId = 'davehakkens'
|
||||||
|
const profileTypesCount = 5
|
||||||
|
|
||||||
describe('[Map]', () => {
|
describe('[Map]', () => {
|
||||||
it('[Shows expected pins]', () => {
|
it('[Shows expected pins]', () => {
|
||||||
@@ -26,7 +27,17 @@ describe('[Map]', () => {
|
|||||||
cy.step('New map shows the cards')
|
cy.step('New map shows the cards')
|
||||||
cy.get('[data-cy="welome-header"]').should('be.visible')
|
cy.get('[data-cy="welome-header"]').should('be.visible')
|
||||||
cy.get('[data-cy="CardList-desktop"]').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')
|
cy.step('As the user moves in the list updates')
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const commonStyles = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
white: 'white',
|
white: 'white',
|
||||||
offwhite: '#ececec',
|
offWhite: '#ececec',
|
||||||
black: '#1b1b1b',
|
black: '#1b1b1b',
|
||||||
softyellow: '#f5ede2',
|
softyellow: '#f5ede2',
|
||||||
blue: '#83ceeb',
|
blue: '#83ceeb',
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export interface ThemeWithName {
|
|||||||
background: string
|
background: string
|
||||||
silver: string
|
silver: string
|
||||||
softgrey: string
|
softgrey: string
|
||||||
offwhite: string
|
offWhite: string
|
||||||
lightgrey: string
|
lightgrey: string
|
||||||
darkGrey: string
|
darkGrey: string
|
||||||
subscribed: string
|
subscribed: string
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ImageConverter = (props: IProps) => {
|
|||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
border: '1px solid ',
|
border: '1px solid ',
|
||||||
borderColor: 'offwhite',
|
borderColor: 'offWhite',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
}}
|
}}
|
||||||
id="preview"
|
id="preview"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const asOptions = (mapPins, items: Array<IMapGrouping>): FilterGroupOption[] =>
|
|||||||
: item.type.split(' ')
|
: item.type.split(' ')
|
||||||
|
|
||||||
const value = item.subType ? item.subType : item.type
|
const value = item.subType ? item.subType : item.type
|
||||||
|
const profileType = transformSpecialistWorkspaceTypeToWorkspace(value)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: item.displayName,
|
label: item.displayName,
|
||||||
@@ -33,10 +34,7 @@ const asOptions = (mapPins, items: Array<IMapGrouping>): FilterGroupOption[] =>
|
|||||||
(item.type as string) === 'verified' ? (
|
(item.type as string) === 'verified' ? (
|
||||||
<Image src={VerifiedBadgeIcon} width={ICON_SIZE} />
|
<Image src={VerifiedBadgeIcon} width={ICON_SIZE} />
|
||||||
) : (
|
) : (
|
||||||
<MemberBadge
|
<MemberBadge size={ICON_SIZE} profileType={profileType} />
|
||||||
size={ICON_SIZE}
|
|
||||||
profileType={transformSpecialistWorkspaceTypeToWorkspace(value)}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Button, CardList, Map } from 'oa-components'
|
import { Button, Map } from 'oa-components'
|
||||||
import { Box, Flex, Heading } from 'theme-ui'
|
import { Box, Flex } from 'theme-ui'
|
||||||
|
|
||||||
import { Clusters } from './Cluster'
|
import { Clusters } from './Cluster'
|
||||||
import { latLongFilter } from './latLongFilter'
|
import { latLongFilter } from './latLongFilter'
|
||||||
|
import { MapWithListHeader } from './MapWithListHeader'
|
||||||
import { Popup } from './Popup'
|
import { Popup } from './Popup'
|
||||||
|
|
||||||
import type { LatLngExpression } from 'leaflet'
|
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 { ILatLng } from 'shared/models'
|
||||||
import type { IMapPin } from 'src/models/maps.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 {
|
interface IProps {
|
||||||
activePin: IMapPin | null
|
activePin: IMapPin | null
|
||||||
center: ILatLng
|
center: ILatLng
|
||||||
@@ -18,14 +46,12 @@ interface IProps {
|
|||||||
notification?: string
|
notification?: string
|
||||||
pins: IMapPin[]
|
pins: IMapPin[]
|
||||||
zoom: number
|
zoom: number
|
||||||
onPinClicked: (pin: IMapPin) => void
|
|
||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
|
onPinClicked: (pin: IMapPin) => void
|
||||||
setZoom: (arg: number) => void
|
setZoom: (arg: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MapWithList = (props: IProps) => {
|
export const MapWithList = (props: IProps) => {
|
||||||
const [filteredPins, setFilteredPins] = useState<IMapPin[] | null>(null)
|
|
||||||
const [showMobileList, setShowMobileList] = useState<boolean>(false)
|
|
||||||
const {
|
const {
|
||||||
activePin,
|
activePin,
|
||||||
center,
|
center,
|
||||||
@@ -38,15 +64,52 @@ export const MapWithList = (props: IProps) => {
|
|||||||
setZoom,
|
setZoom,
|
||||||
} = props
|
} = 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 = () => {
|
const handleLocationFilter = () => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
const boundaries = mapRef.current.leafletElement.getBounds()
|
const boundaries = mapRef.current.leafletElement.getBounds()
|
||||||
// Map.getBounds() is wrongly typed
|
// Map.getBounds() is wrongly typed
|
||||||
const results = latLongFilter(boundaries as any, pins)
|
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 isViewportGreaterThanTablet = window.innerWidth > 1024
|
||||||
const mapCenter: LatLngExpression = center ? [center.lat, center.lng] : [0, 0]
|
const mapCenter: LatLngExpression = center ? [center.lat, center.lng] : [0, 0]
|
||||||
const mapZoom = center ? zoom : 2
|
const mapZoom = center ? zoom : 2
|
||||||
@@ -67,14 +130,16 @@ export const MapWithList = (props: IProps) => {
|
|||||||
background: 'white',
|
background: 'white',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'scroll',
|
overflow: 'scroll',
|
||||||
padding: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading data-cy="welome-header" sx={{ padding: 2 }}>
|
<MapWithListHeader
|
||||||
Welcome to our world!{' '}
|
pins={pins}
|
||||||
{pins && `${pins.length} members (and counting...)`}
|
activePinFilters={activePinFilters}
|
||||||
</Heading>
|
availableFilters={availableFilters}
|
||||||
<CardList dataCy="desktop" list={pins} filteredList={filteredPins} />
|
onFilterChange={onFilterChange}
|
||||||
|
filteredPins={filteredPins}
|
||||||
|
viewport="desktop"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Mobile/tablet list view */}
|
{/* Mobile/tablet list view */}
|
||||||
@@ -84,7 +149,6 @@ export const MapWithList = (props: IProps) => {
|
|||||||
background: 'white',
|
background: 'white',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'scroll',
|
overflow: 'scroll',
|
||||||
padding: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -99,26 +163,20 @@ export const MapWithList = (props: IProps) => {
|
|||||||
<Button
|
<Button
|
||||||
data-cy="ShowMapButton"
|
data-cy="ShowMapButton"
|
||||||
icon="map"
|
icon="map"
|
||||||
sx={{ position: 'sticky' }}
|
sx={{ position: 'sticky', marginTop: 2 }}
|
||||||
onClick={() => setShowMobileList(false)}
|
onClick={() => setShowMobileList(false)}
|
||||||
small
|
small
|
||||||
>
|
>
|
||||||
Show map view
|
Show map view
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Heading
|
<MapWithListHeader
|
||||||
data-cy="welome-header"
|
pins={pins}
|
||||||
variant="small"
|
activePinFilters={activePinFilters}
|
||||||
sx={{ padding: 2, paddingTop: '50px' }}
|
availableFilters={availableFilters}
|
||||||
>
|
onFilterChange={onFilterChange}
|
||||||
Welcome to our world!{' '}
|
filteredPins={filteredPins}
|
||||||
{pins && `${pins.length} members (and counting...)`}
|
viewport="mobile"
|
||||||
</Heading>
|
|
||||||
<CardList
|
|
||||||
columnsCountBreakPoints={{ 300: 1, 600: 2 }}
|
|
||||||
dataCy="mobile"
|
|
||||||
list={pins}
|
|
||||||
filteredList={filteredPins}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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 (
|
return (
|
||||||
// the calculation for the height is kind of hacky for now, will set properly on final mockups
|
// 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} />
|
<NewMapBanner showNewMap={showNewMap} setShowNewMap={setShowNewMap} />
|
||||||
{!showNewMap && (
|
{!showNewMap && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const PatreonIntegration = ({ user }) => {
|
|||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
alignItems: ['flex-start', 'flex-start', 'flex-start'],
|
alignItems: ['flex-start', 'flex-start', 'flex-start'],
|
||||||
backgroundColor: 'offwhite',
|
backgroundColor: 'offWhite',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
Reference in New Issue
Block a user