feat: update card list items for new creator data

This commit is contained in:
Ben Furber
2024-08-28 16:49:14 +01:00
committed by benfurber
parent 3bab5fe5d1
commit 3ee0a8c8d6
10 changed files with 417 additions and 113 deletions

View File

@@ -1,9 +1,11 @@
import { Flex, Grid, Text } from 'theme-ui' import Masonry from 'react-responsive-masonry'
import { Flex, Text } from 'theme-ui'
import { CardListItem } from '../CardListItem/CardListItem' import { CardListItem } from '../CardListItem/CardListItem'
import { Icon } from '../Icon/Icon'
import { Loader } from '../Loader/Loader' import { Loader } from '../Loader/Loader'
import type { ListItem } from '../CardListItem/CardListItem' import type { ListItem } from '../CardListItem/types'
export interface IProps { export interface IProps {
filteredList: ListItem[] | null filteredList: ListItem[] | null
@@ -16,40 +18,46 @@ export const CardList = (props: IProps) => {
const { filteredList, list } = props const { filteredList, list } = props
const listToShow = filteredList === null ? list : filteredList const listToShow = filteredList === null ? list : filteredList
const displayItems = listToShow.map((item) => ( const displayItems = listToShow
<CardListItem item={item} key={item._id} /> .sort(
)) (a, b) =>
Date.parse(b.creator?._lastActive || '0') -
Date.parse(a.creator?._lastActive || '0'),
)
.map((item) => <CardListItem item={item} key={item._id} />)
const isListEmpty = displayItems.length === 0 const isListEmpty = displayItems.length === 0
const hasListLoaded = list const hasListLoaded = list
const results = `${displayItems.length} ${displayItems.length == 1 ? 'result' : 'results'}` const results = `${displayItems.length} ${
displayItems.length == 1 ? 'result' : 'results'
}`
return ( return (
<Flex <Flex
data-cy="CardList" data-cy="CardList"
sx={{ sx={{
flexDirection: 'column', flexDirection: 'column',
gap: 4, gap: 2,
}} }}
> >
{!hasListLoaded && <Loader />} {!hasListLoaded && <Loader />}
{hasListLoaded && ( {hasListLoaded && (
<> <>
<Flex> <Flex
<Text data-cy="list-results">{results}</Text>
</Flex>
<Grid
sx={{ sx={{
alignItems: 'flex-start', justifyContent: 'space-between',
flexWrap: 'wrap', paddingX: 2,
gap: 4, fontSize: 2,
}} }}
width="250px"
columns={3}
> >
{!isListEmpty && displayItems} <Text data-cy="list-results">{results}</Text>
{isListEmpty && EMPTY_LIST} <Flex sx={{ alignItems: 'center', gap: 1 }}>
</Grid> <Text> Most active</Text>
<Icon glyph="arrow-full-down" />
</Flex>
</Flex>
{isListEmpty && EMPTY_LIST}
{!isListEmpty && <Masonry columnsCount={2}>{displayItems}</Masonry>}
</> </>
)} )}
</Flex> </Flex>

View File

@@ -0,0 +1,52 @@
import { Flex } from 'theme-ui'
import { Category } from '../Category/Category'
import { MemberBadge } from '../MemberBadge/MemberBadge'
import { Username } from '../Username/Username'
import type { ListItem } from './types'
interface IProps {
item: ListItem
}
export const CardDetailsFallback = ({ item }: IProps) => {
const { _id, isSupporter, isVerified, subType, type } = item
return (
<Flex sx={{ padding: 2, gap: 2 }}>
<MemberBadge profileType={type} size={50} />
<Flex sx={{ flexDirection: 'column', gap: 2 }}>
<Username
user={{
userName: _id,
isVerified: isVerified || false,
isSupporter: isSupporter || false,
}}
sx={{ alignSelf: 'flex-start' }}
isLink={false}
/>
{subType && (
<Category
category={{ label: 'Wants to get started' }}
sx={{
border: '1px solid #0087B6',
backgroundColor: '#ECFAFF',
color: '#0087B6',
}}
/>
)}
{type === 'member' && (
<Category
category={{ label: 'Wants to get started' }}
sx={{
border: '1px solid #A72E5A',
backgroundColor: '#F7C7D9',
color: '#A72E5A',
}}
/>
)}
</Flex>
</Flex>
)
}

View File

@@ -0,0 +1,70 @@
import { Avatar, Box, Flex } from 'theme-ui'
import { Category } from '../Category/Category'
import { MemberBadge } from '../MemberBadge/MemberBadge'
import { Username } from '../Username/Username'
import type { IProfileCreator } from './types'
interface IProps {
creator: IProfileCreator
}
export const CardDetailsMemberProfile = ({ creator }: IProps) => {
const { _id, badges, countryCode, profileType, userImage } = creator
return (
<Flex
sx={{
gap: 2,
justifyContent: 'center',
alignItems: 'center',
padding: 2,
alignContent: 'stretch',
}}
>
{userImage && (
<Box sx={{ aspectRatio: 1, width: '60px', height: '60px' }}>
<Flex
sx={{
alignContent: 'flex-start',
justifyContent: 'flex-end',
flexWrap: 'wrap',
}}
>
<Avatar
src={userImage}
sx={{ width: '60px', height: '60px', objectFit: 'cover' }}
/>
<MemberBadge
profileType={profileType}
size={22}
sx={{ transform: 'translateY(-22px)' }}
/>
</Flex>
</Box>
)}
{!userImage && <MemberBadge profileType={profileType} size={50} />}
<Flex sx={{ flexDirection: 'column', gap: 1, flex: 1 }}>
<Username
user={{
userName: _id,
countryCode,
isSupporter: badges?.supporter || false,
isVerified: badges?.verified || false,
}}
sx={{ alignSelf: 'flex-start' }}
isLink={false}
/>
<Category
category={{ label: 'Wants to get started' }}
sx={{
border: '1px solid #A72E5A',
backgroundColor: '#F7C7D9',
color: '#A72E5A',
}}
/>
</Flex>
</Flex>
)
}

View File

@@ -0,0 +1,86 @@
import { Flex, Image, Text } from 'theme-ui'
import { Category } from '../Category/Category'
import { MemberBadge } from '../MemberBadge/MemberBadge'
import { Username } from '../Username/Username'
import type { IProfileCreator } from './types'
interface IProps {
creator: IProfileCreator
}
export const CardDetailsSpaceProfile = ({ creator }: IProps) => {
const { _id, about, badges, countryCode, coverImage, profileType, subType } =
creator
const aboutTextStart =
about && about.length > 80 ? about.slice(0, 78) + '...' : false
return (
<Flex sx={{ flexDirection: 'column', width: '100%' }}>
{coverImage && (
<Flex sx={{ flexDirection: 'column' }}>
<Flex sx={{ aspectRatio: 16 / 6, overflow: 'hidden' }}>
<Image
src={coverImage}
sx={{
aspectRatio: 16 / 6,
alignSelf: 'stretch',
objectFit: 'cover',
}}
/>
</Flex>
<MemberBadge
profileType={profileType}
size={40}
sx={{
alignSelf: 'flex-end',
transform: 'translateY(-20px)',
marginX: 2,
}}
/>
</Flex>
)}
<Flex
sx={{
alignItems: 'flex-start',
flexDirection: 'column',
gap: 1,
transform: coverImage ? 'translateY(-20px)' : '',
paddingX: 2,
paddingY: coverImage ? 0 : 2,
}}
>
<Flex sx={{ gap: 2 }}>
{!coverImage && <MemberBadge profileType={profileType} size={30} />}
<Username
user={{
userName: _id,
countryCode,
isVerified: badges?.verified || false,
isSupporter: badges?.supporter || false,
}}
sx={{ alignSelf: 'flex-start' }}
isLink={false}
/>
</Flex>
{subType && (
<Category
category={{ label: 'Wants to get started' }}
sx={{
border: '1px solid #0087B6',
backgroundColor: '#ECFAFF',
color: '#0087B6',
}}
/>
)}
{about && (
<Text variant="quiet" sx={{ fontSize: 2 }}>
{aboutTextStart || about}
</Text>
)}
</Flex>
</Flex>
)
}

View File

@@ -1,3 +1,5 @@
import { faker } from '@faker-js/faker'
import { CardListItem } from './CardListItem' import { CardListItem } from './CardListItem'
import type { Meta, StoryFn } from '@storybook/react' import type { Meta, StoryFn } from '@storybook/react'
@@ -12,16 +14,61 @@ export const DefaultMember: StoryFn<typeof CardListItem> = () => {
const item = { const item = {
_id: 'not-selected-onload', _id: 'not-selected-onload',
type: 'member' as ProfileTypeName, type: 'member' as ProfileTypeName,
creator: {
_id: 'member_no2',
_lastActive: 'string',
countryCode: 'br',
userImage: faker.image.avatar(),
displayName: 'member_no1',
isContactableByPublic: false,
profileType: 'member' as ProfileTypeName,
},
} }
return <CardListItem item={item} /> return (
<div style={{ width: '500px' }}>
<CardListItem item={item} />
</div>
)
} }
export const DefaultSpace: StoryFn<typeof CardListItem> = () => { export const DefaultSpace: StoryFn<typeof CardListItem> = () => {
const item = { const item = {
_id: 'not-selected-onload', _id: 'not-selected-onload',
type: 'workspace' as ProfileTypeName, type: 'workspace' as ProfileTypeName,
creator: {
_id: 'string',
_lastActive: 'string',
about:
'Lorem ipsum odor amet, consectetuer adipiscing elit. Lorem ipsum odor amet, consectetuer adipiscing elit.',
badges: {
supporter: true,
verified: false,
},
countryCode: 'uk',
displayName: 'user',
isContactableByPublic: false,
profileType: 'workspace' as ProfileTypeName,
subType: 'Sheetpress',
},
} }
return <CardListItem item={item} /> return (
<div style={{ width: '500px' }}>
<CardListItem item={item} />
</div>
)
}
export const DefaultFallback: StoryFn<typeof CardListItem> = () => {
const item = {
_id: 'not-selected-onload',
type: 'member' as ProfileTypeName,
}
return (
<div style={{ width: '500px' }}>
<CardListItem item={item} />
</div>
)
} }

View File

@@ -1,22 +1,20 @@
import { Card, Flex } from 'theme-ui' import { Card, Flex } from 'theme-ui'
import { InternalLink } from '../InternalLink/InternalLink' import { InternalLink } from '../InternalLink/InternalLink'
import { MemberBadge } from '../MemberBadge/MemberBadge' import { CardDetailsFallback } from './CardDetailsFallback'
import { Username } from '../Username/Username' import { CardDetailsMemberProfile } from './CardDetailsMemberProfile'
import { CardDetailsSpaceProfile } from './CardDetailsSpaceProfile'
import type { ProfileTypeName } from 'oa-shared' import type { ListItem } from './types'
export interface ListItem {
_id: string
type: ProfileTypeName
}
export interface IProps { export interface IProps {
item: ListItem item: ListItem
} }
export const CardListItem = (props: IProps) => { export const CardListItem = ({ item }: IProps) => {
const { item } = props const { creator } = item
const isMember = creator?.profileType === 'member'
return ( return (
<InternalLink <InternalLink
@@ -25,32 +23,41 @@ export const CardListItem = (props: IProps) => {
to={`/u/${item._id}`} to={`/u/${item._id}`}
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
marginTop: '2px', padding: 2,
'&:hover': {
animationSpeed: '0.3s',
cursor: 'pointer',
marginTop: '0',
borderBottom: '2px solid',
transform: 'translateY(-2px)',
transition: 'borderBottom 0.2s, transform 0.2s',
borderColor: 'black',
},
'&:active': {
transform: 'translateY(1px)',
borderBottom: '1px solid',
borderColor: 'grey',
transition: 'borderBottom 0.2s, transform 0.2s, borderColor 0.2s',
},
}} }}
> >
<Card> <Card
<Flex sx={{ flexDirection: 'row', gap: 2, padding: 2 }}> sx={{
<MemberBadge profileType={item.type} size={30} /> marginTop: '2px',
<Username borderRadius: 2,
user={{ userName: item._id }} padding: 0,
hideFlag '&:hover': {
sx={{ alignSelf: 'flex-start' }} 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' }}>
{isMember && <CardDetailsMemberProfile creator={creator} />}
{!isMember && creator && (
<CardDetailsSpaceProfile creator={creator} />
)}
{!creator && <CardDetailsFallback item={item} />}
</Flex> </Flex>
</Card> </Card>
</InternalLink> </InternalLink>

View File

@@ -0,0 +1,29 @@
import type { ProfileTypeName } from 'oa-shared'
type UserBadges = {
verified: boolean
supporter: boolean
}
export interface IProfileCreator {
_id: string
_lastActive: string
about?: string
badges?: UserBadges
countryCode: string
coverImage?: string
displayName: string
isContactableByPublic: boolean
profileType: ProfileTypeName
subType?: string
userImage?: string
}
export interface ListItem {
_id: string
type: ProfileTypeName
isVerified?: boolean
isSupporter?: boolean
subType?: string
creator?: IProfileCreator
}

View File

@@ -12,16 +12,72 @@ import type { User } from '../types/common'
export interface IProps { export interface IProps {
user: User user: User
hideFlag?: boolean
sx?: ThemeUIStyleObject sx?: ThemeUIStyleObject
isLink?: boolean
} }
const isValidCountryCode = (str: string) => const isValidCountryCode = (str: string) =>
str && twoCharacterCountryCodes.has(str.toUpperCase()) str && twoCharacterCountryCodes.has(str.toUpperCase())
export const Username = ({ user, hideFlag, sx }: IProps) => { export const Username = ({ user, sx, isLink = true }: IProps) => {
const { countryCode, userName, isSupporter, isVerified } = user const { countryCode, userName, isSupporter, isVerified } = user
const UserNameBody = (
<Flex
sx={{
fontFamily: 'body',
alignItems: 'center',
}}
>
<Flex mr={1}>
{countryCode && isValidCountryCode(countryCode) ? (
<Flex data-testid="Username: known flag">
<FlagIconHowTos
countryCode={countryCode}
svg={true}
title={countryCode}
/>
</Flex>
) : (
<Flex
data-testid="Username: unknown flag"
sx={{
backgroundImage: `url("${flagUnknownSVG}")`,
backgroundSize: 'cover',
borderRadius: '3px',
height: '14px',
width: '21px !important',
justifyContent: 'center',
alignItems: 'center',
lineHeight: 0,
overflow: 'hidden',
}}
></Flex>
)}
</Flex>
<Text sx={{ color: 'black' }}>{userName}</Text>
{isVerified && (
<Image
src={VerifiedBadgeIcon}
sx={{ ml: 1, height: 16, width: 16 }}
data-testid="Username: verified badge"
/>
)}
{isSupporter && !isVerified && (
<Image
src={SupporterBadgeIcon}
sx={{ ml: 1, height: 16, width: 16 }}
data-testid="Username: supporter badge"
/>
)}
</Flex>
)
if (!isLink) {
return UserNameBody
}
return ( return (
<InternalLink <InternalLink
data-cy="Username" data-cy="Username"
@@ -45,62 +101,12 @@ export const Username = ({ user, hideFlag, sx }: IProps) => {
'&:hover': { '&:hover': {
borderColor: '#20B7EB', borderColor: '#20B7EB',
background: 'softblue', background: 'softblue',
color: 'bluetag', textcolor: 'bluetag',
}, },
...(sx || {}), ...(sx || {}),
}} }}
> >
<Flex {UserNameBody}
sx={{
fontFamily: 'body',
justifyContent: 'center',
alignItems: 'center',
}}
>
{!hideFlag && (
<Flex mr={1} sx={{ display: 'inline-flex' }}>
{countryCode && isValidCountryCode(countryCode) ? (
<Flex data-testid="Username: known flag">
<FlagIconHowTos
countryCode={countryCode}
svg={true}
title={countryCode}
/>
</Flex>
) : (
<Flex
data-testid="Username: unknown flag"
sx={{
backgroundImage: `url("${flagUnknownSVG}")`,
backgroundSize: 'cover',
borderRadius: '3px',
height: '14px',
width: '21px !important',
justifyContent: 'center',
alignItems: 'center',
lineHeight: 0,
overflow: 'hidden',
}}
></Flex>
)}
</Flex>
)}
<Text>{userName}</Text>
{isVerified && (
<Image
src={VerifiedBadgeIcon}
sx={{ ml: 1, height: 16, width: 16 }}
data-testid="Username: verified badge"
/>
)}
{isSupporter && !isVerified && (
<Image
src={SupporterBadgeIcon}
sx={{ ml: 1, height: 16, width: 16 }}
data-testid="Username: supporter badge"
/>
)}
</Flex>
</InternalLink> </InternalLink>
) )
} }

View File

@@ -124,7 +124,7 @@ describe('[Settings]', () => {
cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url) cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url)
cy.step('Can add map pin') cy.step('Can add map pin')
cy.get('[data-cy=EditYourProfile]').click({force: true}) cy.get('[data-cy=EditYourProfile]').click({ force: true })
cy.get('[data-cy="tab-Map"]').click() cy.get('[data-cy="tab-Map"]').click()
cy.get('[data-cy=descriptionMember').should('be.visible') cy.get('[data-cy=descriptionMember').should('be.visible')
cy.contains('No map pin currently saved') cy.contains('No map pin currently saved')

View File

@@ -60,11 +60,10 @@ export const MapWithList = (props: IProps) => {
background: 'white', background: 'white',
flex: 1, flex: 1,
overflow: 'scroll', overflow: 'scroll',
padding: 4, padding: 2,
gap: 4,
}} }}
> >
<Heading data-cy="welome-header"> <Heading data-cy="welome-header" sx={{ padding: 2 }}>
Welcome to our world!{' '} Welcome to our world!{' '}
{pins && `${pins.length} members (and counting...)`} {pins && `${pins.length} members (and counting...)`}
</Heading> </Heading>