From 652c04993cad89e693953b2c03f61e9bd8fab78d Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 16 Apr 2024 17:32:19 -0700 Subject: [PATCH 1/3] Add hidden and premium variants --- .../src/foundations/spacing/spacing.ts | 1 + .../collection/CollectionCard.test.tsx | 119 +++++++++++++----- .../components/collection/CollectionCard.tsx | 97 +++++++++++--- .../src/components/dog-ear/DogEar.module.css | 49 -------- .../web/src/components/dog-ear/DogEar.tsx | 72 +++++++---- .../src/components/now-playing/NowPlaying.tsx | 2 +- .../desktop/components/PlayingTrackInfo.tsx | 2 +- .../components/play-bar/mobile/PlayBar.tsx | 2 +- .../track/LockedStatusBadge.module.css | 19 --- .../components/track/LockedStatusBadge.tsx | 52 +++++--- packages/web/src/test/test-utils.tsx | 9 +- 11 files changed, 255 insertions(+), 169 deletions(-) delete mode 100644 packages/web/src/components/dog-ear/DogEar.module.css diff --git a/packages/harmony/src/foundations/spacing/spacing.ts b/packages/harmony/src/foundations/spacing/spacing.ts index cbc67cc1757..48b80318cc3 100644 --- a/packages/harmony/src/foundations/spacing/spacing.ts +++ b/packages/harmony/src/foundations/spacing/spacing.ts @@ -44,6 +44,7 @@ export const spacing = { } export const iconSizes = { + '2xs': 12, xs: 14, s: 16, m: 20, diff --git a/packages/web/src/components/collection/CollectionCard.test.tsx b/packages/web/src/components/collection/CollectionCard.test.tsx index e763a9b2349..479a5d1d14b 100644 --- a/packages/web/src/components/collection/CollectionCard.test.tsx +++ b/packages/web/src/components/collection/CollectionCard.test.tsx @@ -1,13 +1,14 @@ import { SquareSizes } from '@audius/common/models' import { Text } from '@audius/harmony' +import { merge } from 'lodash' import { Routes, Route } from 'react-router-dom-v5-compat' import { describe, it, expect } from 'vitest' -import { render, screen } from 'test/test-utils' +import { RenderOptions, render, screen } from 'test/test-utils' import { CollectionCard } from './CollectionCard' -function renderCollectionCard() { +function renderCollectionCard(options?: RenderOptions) { return render( } /> @@ -20,42 +21,45 @@ function renderCollectionCard() { element={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' + merge( + { + 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' } } + }, + users: { + entries: { + 2: { metadata: { handle: 'test-user', name: 'Test User' } } + } } } - } - } + }, + options + ) ) } describe('CollectionCard', () => { - it('renders a button with the label comprising the collection and artist name', () => { + it('renders a button with the label comprising the collection and artist name, and favorites and reposts', () => { renderCollectionCard() expect( screen.getByRole('button', { - name: /test collection test user/i + name: /test collection test user reposts 10 favorites 5/i }) ).toBeInTheDocument() }) @@ -89,12 +93,63 @@ describe('CollectionCard', () => { ).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() + it('hidden collections are rendered correctly', () => { + renderCollectionCard({ + reduxState: { + collections: { entries: { 1: { metadata: { is_private: true } } } } + } + }) + + expect( + screen.getByRole('button', { name: /test collection test user hidden/i }) + ).toBeInTheDocument() + }) + + it('premium locked collections are rendered correctly', () => { + renderCollectionCard({ + reduxState: { + collections: { + entries: { + 1: { + metadata: { + access: { stream: false }, + stream_conditions: { + usdc_purchase: { price: 10, albumTrackPrice: 1, splits: {} } + } + } + } + } + } + } + }) + expect( + screen.getByRole('button', { + name: /test collection test user reposts 10 favorites 5 available for purchase/i + }) + ) + }) - expect(screen.getByTitle('Favorites')).toBeInTheDocument() - expect(screen.getByText('5')).toBeInTheDocument() + it('premium unlocked collections are rendered correctly', () => { + renderCollectionCard({ + reduxState: { + collections: { + entries: { + 1: { + metadata: { + access: { stream: true }, + stream_conditions: { + usdc_purchase: { price: 10, albumTrackPrice: 1, splits: {} } + } + } + } + } + } + } + }) + expect( + screen.getByRole('button', { + name: /test collection test user reposts 10 favorites 5 purchased/i + }) + ) }) }) diff --git a/packages/web/src/components/collection/CollectionCard.tsx b/packages/web/src/components/collection/CollectionCard.tsx index 31a6415483f..4dda1c51d0e 100644 --- a/packages/web/src/components/collection/CollectionCard.tsx +++ b/packages/web/src/components/collection/CollectionCard.tsx @@ -1,11 +1,18 @@ -import { ID, SquareSizes } from '@audius/common/models' +import { + DogEarType, + ID, + SquareSizes, + isContentUSDCPurchaseGated +} 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 { DogEar } from 'components/dog-ear' import { UserLink } from 'components/link' +import { LockedStatusBadge } from 'components/track/LockedStatusBadge' import { useSelector } from 'utils/reducer' import { CollectionImage } from './CollectionImage' @@ -13,7 +20,8 @@ const { getCollection } = cacheCollectionsSelectors const messages = { repost: 'Reposts', - favorites: 'Favorites' + favorites: 'Favorites', + hidden: 'Hidden' } type CardSize = 's' | 'm' | 'l' @@ -51,50 +59,103 @@ export const CollectionCard = (props: CollectionCardProps) => { permalink, playlist_owner_id, repost_count, - save_count + save_count, + is_private, + access, + stream_conditions } = collection + const isPurchase = isContentUSDCPurchaseGated(stream_conditions) + + const dogEarType = is_private + ? DogEarType.HIDDEN + : isPurchase && !access.stream + ? DogEarType.USDC_PURCHASE + : null + return ( + {dogEarType ? ( + + ) : null} - - + + {playlist_name} - - - - - {repost_count} - - - - {' '} - - {save_count} + + {is_private ? ( + + {messages.hidden} - + ) : ( + <> + + + + {repost_count} + + + + + + {save_count} + + + {isPurchase ? ( + + ) : null} + + )} ) diff --git a/packages/web/src/components/dog-ear/DogEar.module.css b/packages/web/src/components/dog-ear/DogEar.module.css deleted file mode 100644 index beb7a3827f4..00000000000 --- a/packages/web/src/components/dog-ear/DogEar.module.css +++ /dev/null @@ -1,49 +0,0 @@ -.container { - position: absolute; - top: 0; - left: 0; - z-index: 10; - border-radius: var(--harmony-unit-2) 0px 0px 0px; - width: var(--harmony-unit-12); - height: var(--harmony-unit-12); - pointer-events: none; - overflow: hidden; -} - -.rectangle { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -/* Note: dogEarRectangle has multiple `path` elements, but we only want to fill one of them. -That path uses `fill='currentColor'`, allowing us to set `color` here instead of `fill` */ -.gated { - color: var(--harmony-blue); -} - -.purchase { - color: var(--harmony-light-green); -} - -.artistPick { - color: var(--harmony-secondary); -} - -.hidden { - color: var(--harmony-neutral); -} - -.icon { - position: absolute; - top: var(--harmony-unit-1); - left: var(--harmony-unit-1); - width: var(--harmony-unit-4); - height: var(--harmony-unit-4); -} - -.icon path { - fill: var(--harmony-static-white); -} diff --git a/packages/web/src/components/dog-ear/DogEar.tsx b/packages/web/src/components/dog-ear/DogEar.tsx index e47141c7115..b46a1961b95 100644 --- a/packages/web/src/components/dog-ear/DogEar.tsx +++ b/packages/web/src/components/dog-ear/DogEar.tsx @@ -5,15 +5,13 @@ import { IconSpecialAccess, IconCart, IconCollectible, - IconLock + IconLock, + useTheme, + ColorTheme, + Box } from '@audius/harmony' -import cn from 'classnames' import Rectangle from 'assets/img/dogEarRectangle.svg' -import { useIsMobile } from 'hooks/useIsMobile' -import { isMatrix } from 'utils/theme/theme' - -import styles from './DogEar.module.css' export type DogEarProps = { type: DogEarType @@ -38,32 +36,56 @@ const getIcon = (type: DogEarType) => { } } +const getColor = (type: DogEarType, color: ColorTheme['day']) => { + switch (type) { + case DogEarType.COLLECTIBLE_GATED: + case DogEarType.SPECIAL_ACCESS: + case DogEarType.LOCKED: + return color.special.blue + case DogEarType.USDC_PURCHASE: + return color.special.lightGreen + case DogEarType.STAR: + return color.secondary.secondary + case DogEarType.HIDDEN: + return color.neutral.neutral + } +} + export const DogEar = (props: DogEarProps) => { const { type, className } = props - const isMatrixMode = isMatrix() - const isMobile = useIsMobile() const Icon = getIcon(type) + const { spacing, color } = useTheme() + const tagColor = getColor(type, color) return ( -
+ - -
+ ) } diff --git a/packages/web/src/components/now-playing/NowPlaying.tsx b/packages/web/src/components/now-playing/NowPlaying.tsx index bafcf735e34..32b417dfbde 100644 --- a/packages/web/src/components/now-playing/NowPlaying.tsx +++ b/packages/web/src/components/now-playing/NowPlaying.tsx @@ -489,7 +489,7 @@ const NowPlaying = g( {shouldShowPurchasePreview ? ( ) : null} diff --git a/packages/web/src/components/track/LockedStatusBadge.module.css b/packages/web/src/components/track/LockedStatusBadge.module.css index db6e1907aa6..f1d9d711734 100644 --- a/packages/web/src/components/track/LockedStatusBadge.module.css +++ b/packages/web/src/components/track/LockedStatusBadge.module.css @@ -22,22 +22,3 @@ .coloredWhenLocked.premium { background-color: var(--harmony-light-green); } - -.icon.medium { - width: 14px; - height: 14px; -} - -.icon.small { - width: 12px; - height: 12px; -} - -.icon path { - fill: var(--harmony-static-white); -} - -.text { - color: var(--harmony-static-white); - text-decoration: none; -} diff --git a/packages/web/src/components/track/LockedStatusBadge.tsx b/packages/web/src/components/track/LockedStatusBadge.tsx index a57b5bcf46f..45c221049f6 100644 --- a/packages/web/src/components/track/LockedStatusBadge.tsx +++ b/packages/web/src/components/track/LockedStatusBadge.tsx @@ -1,26 +1,37 @@ -import { IconLock, IconLockUnlocked, Text } from '@audius/harmony' +import { IconLockUnlocked, IconSize, Text } from '@audius/harmony' +import IconLock from '@audius/harmony/src/assets/icons/Lock.svg' import cn from 'classnames' import styles from './LockedStatusBadge.module.css' +const messages = { + premiumLocked: 'Available for purchase', + premiumUnlocked: 'Purchased' +} + export type LockedStatusBadgeProps = { locked: boolean variant?: 'premium' | 'gated' text?: string /** Whether the badge is colored when locked */ coloredWhenLocked?: boolean - iconSize?: 'medium' | 'small' + iconSize?: IconSize + id?: string } /** Renders a small badge with locked or unlocked icon */ -export const LockedStatusBadge = ({ - locked, - variant = 'gated', - text, - coloredWhenLocked = false, - iconSize = 'medium' -}: LockedStatusBadgeProps) => { +export const LockedStatusBadge = (props: LockedStatusBadgeProps) => { + const { + locked, + variant = 'gated', + text, + coloredWhenLocked = false, + iconSize = 'xs', + id + } = props + const LockComponent = locked ? IconLock : IconLockUnlocked + return (
{text ? ( - + {text} ) : null} diff --git a/packages/web/src/test/test-utils.tsx b/packages/web/src/test/test-utils.tsx index 25e45d6a33c..659bc4c5538 100644 --- a/packages/web/src/test/test-utils.tsx +++ b/packages/web/src/test/test-utils.tsx @@ -40,10 +40,11 @@ const TestProviders = ) } -const customRender = ( - ui: ReactElement, - options?: Omit & TestOptions -) => render(ui, { wrapper: TestProviders(options), ...options }) +type CustomRenderOptions = Omit & TestOptions + +const customRender = (ui: ReactElement, options?: CustomRenderOptions) => + render(ui, { wrapper: TestProviders(options), ...options }) export * from '@testing-library/react' +export type { CustomRenderOptions as RenderOptions } export { customRender as render } From 94c6e2dae45abbf0e8c686ed6cb5140271932609 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 16 Apr 2024 18:51:25 -0700 Subject: [PATCH 2/3] Finalize --- .../components/collection/CollectionCard.tsx | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/web/src/components/collection/CollectionCard.tsx b/packages/web/src/components/collection/CollectionCard.tsx index 4dda1c51d0e..d890ed9e398 100644 --- a/packages/web/src/components/collection/CollectionCard.tsx +++ b/packages/web/src/components/collection/CollectionCard.tsx @@ -82,13 +82,10 @@ export const CollectionCard = (props: CollectionCardProps) => { direction='column' border='default' w={cardSizes[size]} - css={{ cursor: 'pointer' }} + css={{ cursor: 'pointer', overflow: 'unset' }} > {dogEarType ? ( - + ) : null} { size={cardSizeToCoverArtSizeMap[size]} data-testid={`${id}-cover-art`} /> - - + {playlist_name} - - + + - + {is_private ? ( Date: Wed, 17 Apr 2024 17:33:31 -0700 Subject: [PATCH 3/3] Address comments --- .../components/collection/CollectionCard.tsx | 4 +- .../LockedStatusPill.tsx} | 46 +++++++++++-------- .../components/locked-status-pill/index.ts | 1 + .../src/components/now-playing/NowPlaying.tsx | 4 +- .../desktop/components/PlayingTrackInfo.tsx | 4 +- .../components/play-bar/mobile/PlayBar.tsx | 4 +- .../components/PayToUnlockInfo.tsx | 4 +- .../components/track/GatedContentSection.tsx | 7 +-- .../track/LockedStatusBadge.module.css | 24 ---------- .../components/track/desktop/TrackTile.tsx | 9 ++-- .../src/components/track/mobile/TrackTile.tsx | 9 ++-- 11 files changed, 55 insertions(+), 61 deletions(-) rename packages/web/src/components/{track/LockedStatusBadge.tsx => locked-status-pill/LockedStatusPill.tsx} (62%) create mode 100644 packages/web/src/components/locked-status-pill/index.ts delete mode 100644 packages/web/src/components/track/LockedStatusBadge.module.css diff --git a/packages/web/src/components/collection/CollectionCard.tsx b/packages/web/src/components/collection/CollectionCard.tsx index d890ed9e398..858a8d1afd3 100644 --- a/packages/web/src/components/collection/CollectionCard.tsx +++ b/packages/web/src/components/collection/CollectionCard.tsx @@ -12,7 +12,7 @@ import { Link, useLinkClickHandler } from 'react-router-dom-v5-compat' import { DogEar } from 'components/dog-ear' import { UserLink } from 'components/link' -import { LockedStatusBadge } from 'components/track/LockedStatusBadge' +import { LockedStatusPill } from 'components/locked-status-pill' import { useSelector } from 'utils/reducer' import { CollectionImage } from './CollectionImage' @@ -145,7 +145,7 @@ export const CollectionCard = (props: CollectionCardProps) => { {isPurchase ? ( - { +export const LockedStatusPill = (props: LockedStatusPillProps) => { const { locked, variant = 'gated', @@ -32,17 +34,25 @@ export const LockedStatusBadge = (props: LockedStatusBadgeProps) => { const LockComponent = locked ? IconLock : IconLockUnlocked + const { color } = useTheme() + + const backgroundColor = + !locked || coloredWhenLocked + ? variant === 'gated' + ? color.special.blue + : color.special.lightGreen + : color.neutral.n400 + return ( -
{ {text} ) : null} -
+
) } diff --git a/packages/web/src/components/locked-status-pill/index.ts b/packages/web/src/components/locked-status-pill/index.ts new file mode 100644 index 00000000000..41f199262f1 --- /dev/null +++ b/packages/web/src/components/locked-status-pill/index.ts @@ -0,0 +1 @@ +export * from './LockedStatusPill' diff --git a/packages/web/src/components/now-playing/NowPlaying.tsx b/packages/web/src/components/now-playing/NowPlaying.tsx index 32b417dfbde..46baf1825af 100644 --- a/packages/web/src/components/now-playing/NowPlaying.tsx +++ b/packages/web/src/components/now-playing/NowPlaying.tsx @@ -43,6 +43,7 @@ import { useRecord, make } from 'common/store/analytics/actions' import CoSign, { Size } from 'components/co-sign/CoSign' import { DogEar } from 'components/dog-ear' import DynamicImage from 'components/dynamic-image/DynamicImage' +import { LockedStatusPill } from 'components/locked-status-pill' import PlayButton from 'components/play-bar/PlayButton' import NextButtonProvider from 'components/play-bar/next-button/NextButtonProvider' import PreviousButtonProvider from 'components/play-bar/previous-button/PreviousButtonProvider' @@ -50,7 +51,6 @@ import RepeatButtonProvider from 'components/play-bar/repeat-button/RepeatButton import ShuffleButtonProvider from 'components/play-bar/shuffle-button/ShuffleButtonProvider' import { PlayButtonStatus } from 'components/play-bar/types' import { GatedConditionsPill } from 'components/track/GatedConditionsPill' -import { LockedStatusBadge } from 'components/track/LockedStatusBadge' import UserBadges from 'components/user-badges/UserBadges' import { useAuthenticatedClickCallback } from 'hooks/useAuthenticatedCallback' import { useFlag } from 'hooks/useRemoteConfig' @@ -487,7 +487,7 @@ const NowPlaying = g( {title}
{shouldShowPurchasePreview ? ( - {shouldShowPreviewLock ? ( - {name} {shouldShowPreviewLock ? (
- { className={styles.header} > {messages.payToUnlock} - + {messages.copyPart1} diff --git a/packages/web/src/components/track/GatedContentSection.tsx b/packages/web/src/components/track/GatedContentSection.tsx index 35d9aeaf17f..dbfbc5f1169 100644 --- a/packages/web/src/components/track/GatedContentSection.tsx +++ b/packages/web/src/components/track/GatedContentSection.tsx @@ -49,8 +49,9 @@ import { emptyStringGuard } from 'pages/track-page/utils' import { AppState } from 'store/types' import { profilePage } from 'utils/route' +import { LockedStatusPill } from '../locked-status-pill' + import styles from './GiantTrackTile.module.css' -import { LockedStatusBadge } from './LockedStatusBadge' const { getUsers } = cacheUsersSelectors const { beginTip } = tippingActions @@ -317,7 +318,7 @@ const LockedGatedContentSection = ({
- @@ -522,7 +523,7 @@ const UnlockedGatedContentSection = ({ {isOwner ? ( ) : ( - & - Pick) => { + Pick) => { if (isStreamGated && !isOwner) { - return + return } const hidePlays = fieldVisibility diff --git a/packages/web/src/components/track/mobile/TrackTile.tsx b/packages/web/src/components/track/mobile/TrackTile.tsx index 3476afe5414..3edbd534575 100644 --- a/packages/web/src/components/track/mobile/TrackTile.tsx +++ b/packages/web/src/components/track/mobile/TrackTile.tsx @@ -42,7 +42,10 @@ import { TrackTileProps } from 'components/track/types' import UserBadges from 'components/user-badges/UserBadges' import { useAuthenticatedClickCallback } from 'hooks/useAuthenticatedCallback' -import { LockedStatusBadge, LockedStatusBadgeProps } from '../LockedStatusBadge' +import { + LockedStatusPill, + LockedStatusPillProps +} from '../../locked-status-pill' import { messages } from '../trackTileMessages' import BottomButtons from './BottomButtons' @@ -85,9 +88,9 @@ const renderLockedOrPlaysContent = ({ | 'isStreamGated' | 'listenCount' > & - Pick) => { + Pick) => { if (isStreamGated && !isOwner) { - return + return } const hidePlays = fieldVisibility