diff --git a/packages/common/src/models/Collection.ts b/packages/common/src/models/Collection.ts index 61a4cca41f5..8935cf56151 100644 --- a/packages/common/src/models/Collection.ts +++ b/packages/common/src/models/Collection.ts @@ -50,7 +50,7 @@ export type CollectionMetadata = { cover_art: CID | null cover_art_sizes: Nullable cover_art_cids?: Nullable - permalink?: string + permalink: string playlist_name: string playlist_owner_id: ID repost_count: number diff --git a/packages/common/src/services/audius-api-client/ResponseAdapter.ts b/packages/common/src/services/audius-api-client/ResponseAdapter.ts index 2bd3f740278..36388a78234 100644 --- a/packages/common/src/services/audius-api-client/ResponseAdapter.ts +++ b/packages/common/src/services/audius-api-client/ResponseAdapter.ts @@ -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 = ( diff --git a/packages/harmony/src/components/icon.ts b/packages/harmony/src/components/icon.ts index 8a344521370..68dc9afcd32 100644 --- a/packages/harmony/src/components/icon.ts +++ b/packages/harmony/src/components/icon.ts @@ -14,6 +14,7 @@ export type IconProps = { height?: number width?: number shadow?: ShadowOptions + title?: string } type SVGIconProps = SVGBaseProps & IconProps diff --git a/packages/web/src/components/collection/CollectionCard.test.tsx b/packages/web/src/components/collection/CollectionCard.test.tsx new file mode 100644 index 00000000000..e763a9b2349 --- /dev/null +++ b/packages/web/src/components/collection/CollectionCard.test.tsx @@ -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( + + } /> + Test Collection Page} + /> + Test User Page} + /> + , + { + 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() + }) +}) diff --git a/packages/web/src/components/collection/CollectionCard.tsx b/packages/web/src/components/collection/CollectionCard.tsx new file mode 100644 index 00000000000..31a6415483f --- /dev/null +++ b/packages/web/src/components/collection/CollectionCard.tsx @@ -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( + collection?.permalink ?? '' + ) + + if (!collection) return null + + const { + playlist_name, + permalink, + playlist_owner_id, + repost_count, + save_count + } = collection + + return ( + + + + + + {playlist_name} + + + + + + + + + + {repost_count} + + + + {' '} + + {save_count} + + + + + ) +} diff --git a/packages/web/src/components/collection/CollectionImage.tsx b/packages/web/src/components/collection/CollectionImage.tsx new file mode 100644 index 00000000000..25bded42a78 --- /dev/null +++ b/packages/web/src/components/collection/CollectionImage.tsx @@ -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 +}