mirror of
https://github.com/fergalmoran/onearmy-community-platform.git
synced 2025-12-22 09:37:54 +00:00
feat: update card list items for new creator data
This commit is contained in:
@@ -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 { Icon } from '../Icon/Icon'
|
||||
import { Loader } from '../Loader/Loader'
|
||||
|
||||
import type { ListItem } from '../CardListItem/CardListItem'
|
||||
import type { ListItem } from '../CardListItem/types'
|
||||
|
||||
export interface IProps {
|
||||
filteredList: ListItem[] | null
|
||||
@@ -16,40 +18,46 @@ export const CardList = (props: IProps) => {
|
||||
const { filteredList, list } = props
|
||||
|
||||
const listToShow = filteredList === null ? list : filteredList
|
||||
const displayItems = listToShow.map((item) => (
|
||||
<CardListItem item={item} key={item._id} />
|
||||
))
|
||||
const displayItems = listToShow
|
||||
.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 hasListLoaded = list
|
||||
const results = `${displayItems.length} ${displayItems.length == 1 ? 'result' : 'results'}`
|
||||
const results = `${displayItems.length} ${
|
||||
displayItems.length == 1 ? 'result' : 'results'
|
||||
}`
|
||||
|
||||
return (
|
||||
<Flex
|
||||
data-cy="CardList"
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{!hasListLoaded && <Loader />}
|
||||
{hasListLoaded && (
|
||||
<>
|
||||
<Flex>
|
||||
<Text data-cy="list-results">{results}</Text>
|
||||
</Flex>
|
||||
<Grid
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
justifyContent: 'space-between',
|
||||
paddingX: 2,
|
||||
fontSize: 2,
|
||||
}}
|
||||
width="250px"
|
||||
columns={3}
|
||||
>
|
||||
{!isListEmpty && displayItems}
|
||||
{isListEmpty && EMPTY_LIST}
|
||||
</Grid>
|
||||
<Text data-cy="list-results">{results}</Text>
|
||||
<Flex sx={{ alignItems: 'center', gap: 1 }}>
|
||||
<Text> Most active</Text>
|
||||
<Icon glyph="arrow-full-down" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{isListEmpty && EMPTY_LIST}
|
||||
{!isListEmpty && <Masonry columnsCount={2}>{displayItems}</Masonry>}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
52
packages/components/src/CardListItem/CardDetailsFallback.tsx
Normal file
52
packages/components/src/CardListItem/CardDetailsFallback.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
import { CardListItem } from './CardListItem'
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react'
|
||||
@@ -12,16 +14,61 @@ export const DefaultMember: StoryFn<typeof CardListItem> = () => {
|
||||
const item = {
|
||||
_id: 'not-selected-onload',
|
||||
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> = () => {
|
||||
const item = {
|
||||
_id: 'not-selected-onload',
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { Card, Flex } from 'theme-ui'
|
||||
|
||||
import { InternalLink } from '../InternalLink/InternalLink'
|
||||
import { MemberBadge } from '../MemberBadge/MemberBadge'
|
||||
import { Username } from '../Username/Username'
|
||||
import { CardDetailsFallback } from './CardDetailsFallback'
|
||||
import { CardDetailsMemberProfile } from './CardDetailsMemberProfile'
|
||||
import { CardDetailsSpaceProfile } from './CardDetailsSpaceProfile'
|
||||
|
||||
import type { ProfileTypeName } from 'oa-shared'
|
||||
|
||||
export interface ListItem {
|
||||
_id: string
|
||||
type: ProfileTypeName
|
||||
}
|
||||
import type { ListItem } from './types'
|
||||
|
||||
export interface IProps {
|
||||
item: ListItem
|
||||
}
|
||||
|
||||
export const CardListItem = (props: IProps) => {
|
||||
const { item } = props
|
||||
export const CardListItem = ({ item }: IProps) => {
|
||||
const { creator } = item
|
||||
|
||||
const isMember = creator?.profileType === 'member'
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
@@ -25,32 +23,41 @@ export const CardListItem = (props: IProps) => {
|
||||
to={`/u/${item._id}`}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
marginTop: '2px',
|
||||
'&: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',
|
||||
},
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<Flex sx={{ flexDirection: 'row', gap: 2, padding: 2 }}>
|
||||
<MemberBadge profileType={item.type} size={30} />
|
||||
<Username
|
||||
user={{ userName: item._id }}
|
||||
hideFlag
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
/>
|
||||
<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',
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ gap: 2, alignItems: 'stretch', alignContent: 'stretch' }}>
|
||||
{isMember && <CardDetailsMemberProfile creator={creator} />}
|
||||
|
||||
{!isMember && creator && (
|
||||
<CardDetailsSpaceProfile creator={creator} />
|
||||
)}
|
||||
|
||||
{!creator && <CardDetailsFallback item={item} />}
|
||||
</Flex>
|
||||
</Card>
|
||||
</InternalLink>
|
||||
|
||||
29
packages/components/src/CardListItem/types.ts
Normal file
29
packages/components/src/CardListItem/types.ts
Normal 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
|
||||
}
|
||||
@@ -12,16 +12,72 @@ import type { User } from '../types/common'
|
||||
|
||||
export interface IProps {
|
||||
user: User
|
||||
hideFlag?: boolean
|
||||
sx?: ThemeUIStyleObject
|
||||
isLink?: boolean
|
||||
}
|
||||
|
||||
const isValidCountryCode = (str: string) =>
|
||||
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 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 (
|
||||
<InternalLink
|
||||
data-cy="Username"
|
||||
@@ -45,62 +101,12 @@ export const Username = ({ user, hideFlag, sx }: IProps) => {
|
||||
'&:hover': {
|
||||
borderColor: '#20B7EB',
|
||||
background: 'softblue',
|
||||
color: 'bluetag',
|
||||
textcolor: 'bluetag',
|
||||
},
|
||||
...(sx || {}),
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
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>
|
||||
{UserNameBody}
|
||||
</InternalLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('[Settings]', () => {
|
||||
cy.get('[data-cy="profile-link"]').should('have.attr', 'href', url)
|
||||
|
||||
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=descriptionMember').should('be.visible')
|
||||
cy.contains('No map pin currently saved')
|
||||
|
||||
@@ -60,11 +60,10 @@ export const MapWithList = (props: IProps) => {
|
||||
background: 'white',
|
||||
flex: 1,
|
||||
overflow: 'scroll',
|
||||
padding: 4,
|
||||
gap: 4,
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Heading data-cy="welome-header">
|
||||
<Heading data-cy="welome-header" sx={{ padding: 2 }}>
|
||||
Welcome to our world!{' '}
|
||||
{pins && `${pins.length} members (and counting...)`}
|
||||
</Heading>
|
||||
|
||||
Reference in New Issue
Block a user