Skip to content

Commit

Permalink
[PAY-2661] Add collection card (#8100)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Apr 16, 2024
1 parent f3716b5 commit 5e1ceb4
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/models/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type CollectionMetadata = {
cover_art: CID | null
cover_art_sizes: Nullable<CID>
cover_art_cids?: Nullable<CoverArtSizesCids>
permalink?: string
permalink: string
playlist_name: string
playlist_owner_id: ID
repost_count: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export const makePlaylist = (
delete marshalled.favorite_count
delete marshalled.added_timestamps

return marshalled as UserCollectionMetadata
return marshalled as unknown as UserCollectionMetadata
}

export const makeActivity = (
Expand Down
1 change: 1 addition & 0 deletions packages/harmony/src/components/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type IconProps = {
height?: number
width?: number
shadow?: ShadowOptions
title?: string
}

type SVGIconProps = SVGBaseProps & IconProps
Expand Down
100 changes: 100 additions & 0 deletions packages/web/src/components/collection/CollectionCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { SquareSizes } from '@audius/common/models'
import { Text } from '@audius/harmony'
import { Routes, Route } from 'react-router-dom-v5-compat'
import { describe, it, expect } from 'vitest'

import { render, screen } from 'test/test-utils'

import { CollectionCard } from './CollectionCard'

function renderCollectionCard() {
return render(
<Routes>
<Route path='/' element={<CollectionCard id={1} size='s' />} />
<Route
path='/test-user/test-collection'
element={<Text variant='heading'>Test Collection Page</Text>}
/>
<Route
path='/test-user'
element={<Text variant='heading'>Test User Page</Text>}
/>
</Routes>,
{
reduxState: {
collections: {
entries: {
1: {
metadata: {
playlist_id: 1,
playlist_name: 'Test Collection',
playlist_owner_id: 2,
permalink: '/test-user/test-collection',
repost_count: 10,
save_count: 5,
_cover_art_sizes: {
[SquareSizes.SIZE_150_BY_150]: 'image-small.jpg',
[SquareSizes.SIZE_480_BY_480]: 'image-medium.jpg'
}
}
}
}
},
users: {
entries: {
2: { metadata: { handle: 'test-user', name: 'Test User' } }
}
}
}
}
)
}

describe('CollectionCard', () => {
it('renders a button with the label comprising the collection and artist name', () => {
renderCollectionCard()
expect(
screen.getByRole('button', {
name: /test collection test user/i
})
).toBeInTheDocument()
})

it('navigates to collection page when clicked', async () => {
renderCollectionCard()
screen.getByRole('button').click()
expect(
await screen.findByRole('heading', { name: /test collection page/i })
).toBeInTheDocument()
})

it('renders the cover image', () => {
renderCollectionCard()
expect(screen.getByTestId(`${1}-cover-art`)).toBeInTheDocument()
})

it('renders the title', () => {
renderCollectionCard()
const titleElement = screen.getByText('Test Collection')
expect(titleElement).toBeInTheDocument()
})

it('renders the collection owner link which navigates to user page', async () => {
renderCollectionCard()
const userNameElement = screen.getByRole('link', { name: 'Test User' })
expect(userNameElement).toBeInTheDocument()
userNameElement.click()
expect(
await screen.findByRole('heading', { name: /test user page/i })
).toBeInTheDocument()
})

it('shows the number of reposts and favorites with the correct screen-reader text', () => {
renderCollectionCard()
expect(screen.getByTitle('Reposts')).toBeInTheDocument()
expect(screen.getByText('10')).toBeInTheDocument()

expect(screen.getByTitle('Favorites')).toBeInTheDocument()
expect(screen.getByText('5')).toBeInTheDocument()
})
})
101 changes: 101 additions & 0 deletions packages/web/src/components/collection/CollectionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ID, SquareSizes } from '@audius/common/models'
import { cacheCollectionsSelectors } from '@audius/common/store'
import { Divider, Flex, Paper, Text } from '@audius/harmony'
import IconHeart from '@audius/harmony/src/assets/icons/Heart.svg'
import IconRepost from '@audius/harmony/src/assets/icons/Repost.svg'
import { Link, useLinkClickHandler } from 'react-router-dom-v5-compat'

import { UserLink } from 'components/link'
import { useSelector } from 'utils/reducer'

import { CollectionImage } from './CollectionImage'
const { getCollection } = cacheCollectionsSelectors

const messages = {
repost: 'Reposts',
favorites: 'Favorites'
}

type CardSize = 's' | 'm' | 'l'

type CollectionCardProps = {
id: ID
size: CardSize
}

const cardSizeToCoverArtSizeMap = {
s: SquareSizes.SIZE_150_BY_150,
m: SquareSizes.SIZE_480_BY_480,
l: SquareSizes.SIZE_480_BY_480
}

const cardSizes = {
s: 200,
m: 224,
l: 320
}

export const CollectionCard = (props: CollectionCardProps) => {
const { id, size } = props

const collection = useSelector((state) => getCollection(state, { id }))

const handleClick = useLinkClickHandler<HTMLDivElement>(
collection?.permalink ?? ''
)

if (!collection) return null

const {
playlist_name,
permalink,
playlist_owner_id,
repost_count,
save_count
} = collection

return (
<Paper
role='button'
tabIndex={0}
onClick={handleClick}
aria-labelledby={`${id}-title ${id}-artist`}
direction='column'
w={cardSizes[size]}
css={{ cursor: 'pointer' }}
>
<Flex direction='column' p='s' gap='s'>
<CollectionImage
collectionId={id}
size={cardSizeToCoverArtSizeMap[size]}
data-testid={`${id}-cover-art`}
/>
<Link id={`${id}-title`} to={permalink} css={{ alignSelf: 'center' }}>
<Text variant='title' tag='span' color='default' textAlign='center'>
{playlist_name}
</Text>
</Link>
<UserLink
id={`${id}-artist`}
userId={playlist_owner_id}
css={{ alignSelf: 'center' }}
/>
</Flex>
<Divider orientation='horizontal' />
<Flex gap='l' p='s' justifyContent='center'>
<Flex gap='xs' alignItems='center'>
<IconRepost size='s' color='subdued' title={messages.repost} />
<Text color='subdued' variant='body' size='s'>
{repost_count}
</Text>
</Flex>
<Flex gap='xs' alignItems='center'>
<IconHeart size='s' color='subdued' title={messages.favorites} />{' '}
<Text color='subdued' variant='body' size='s'>
{save_count}
</Text>
</Flex>
</Flex>
</Paper>
)
}
27 changes: 27 additions & 0 deletions packages/web/src/components/collection/CollectionImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ComponentProps } from 'react'

import { ID, SquareSizes } from '@audius/common/models'
import { cacheCollectionsSelectors } from '@audius/common/store'
import { Artwork } from '@audius/harmony'

import { useCollectionCoverArt } from 'hooks/useCollectionCoverArt'
import { useSelector } from 'utils/reducer'
const { getCollection } = cacheCollectionsSelectors

type CollectionImageProps = {
collectionId: ID
size: SquareSizes
} & ComponentProps<'img'>

export const CollectionImage = (props: CollectionImageProps) => {
const { collectionId, size, ...other } = props

const coverArtSizes = useSelector(
(state) =>
getCollection(state, { id: collectionId })?._cover_art_sizes ?? null
)

const imageSource = useCollectionCoverArt(collectionId, coverArtSizes, size)

return <Artwork src={imageSource} {...other} />
}

0 comments on commit 5e1ceb4

Please sign in to comment.