Skip to content

Commit

Permalink
[C-5364] Track and Collection Tile Stat updates (#10379)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Nov 7, 2024
1 parent 45f2e3f commit 9dabd1b
Show file tree
Hide file tree
Showing 40 changed files with 780 additions and 1,159 deletions.
12 changes: 6 additions & 6 deletions packages/harmony/src/components/text-link/TextLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const TextLink = forwardRef((props: TextLinkProps, ref: Ref<'a'>) => {
onClick,
textVariant,
showUnderline,
noUnderlineOnHover,
applyHoverStylesToInnerSvg,
disabled,
...other
Expand All @@ -46,14 +47,10 @@ export const TextLink = forwardRef((props: TextLinkProps, ref: Ref<'a'>) => {
}

const hoverStyles = {
textDecoration: 'underline',
textDecoration: noUnderlineOnHover ? 'none' : 'underline',
color: variantHoverColors[variant],
...(applyHoverStylesToInnerSvg
? {
path: {
fill: variantHoverColors[variant]
}
}
? { path: { fill: variantHoverColors[variant] } }
: {})
}

Expand All @@ -70,6 +67,9 @@ export const TextLink = forwardRef((props: TextLinkProps, ref: Ref<'a'>) => {
transition: `color ${motion.hover}`,
cursor: 'pointer',
pointerEvents: disabled ? 'none' : undefined,
...(applyHoverStylesToInnerSvg && {
path: { transition: `fill ${motion.hover}` }
}),
':hover': hoverStyles,
...(isActive && { ...hoverStyles, textDecoration: 'none' }),
...(showUnderline && hoverStyles)
Expand Down
5 changes: 5 additions & 0 deletions packages/harmony/src/components/text-link/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export type TextLinkProps = Omit<TextProps, 'variant' | 'onClick' | 'color'> &
*/
showUnderline?: boolean

/**
* When true, hide the underline when the link is active.
*/
noUnderlineOnHover?: boolean

/**
* When `true`, render link in active style (e.g. hover color)
*/
Expand Down
2 changes: 0 additions & 2 deletions packages/mobile/src/components/lineup-tile/LineupTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export const LineupTile = ({
commentCount,
renderImage,
repostType,
showRankIcon,
title,
item,
user,
Expand Down Expand Up @@ -138,7 +137,6 @@ export const LineupTile = ({
repostCount={repost_count}
saveCount={save_count}
commentCount={commentCount}
showRankIcon={showRankIcon}
hasStreamAccess={hasStreamAccess}
streamConditions={streamConditions}
isOwner={isOwner}
Expand Down
1 change: 0 additions & 1 deletion packages/mobile/src/components/lineup/Lineup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ const LineupTileView = memo(function LineupTileView({
{...item}
index={index}
isTrending={isTrending}
showRankIcon={index < rankIconCount}
togglePlay={togglePlay}
onPress={onPress}
uid={item.uid}
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const messages = {
profile: 'profile'
}

type AvatarProps = Omit<HarmonyAvatarProps, 'src'> & {
export type AvatarProps = Omit<HarmonyAvatarProps, 'src'> & {
'aria-hidden'?: true
userId: Maybe<ID>
onClick?: () => void
Expand Down
26 changes: 26 additions & 0 deletions packages/web/src/components/avatar/AvatarList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ID } from '@audius/common/models'
import { Flex } from '@audius/harmony'

import { Avatar, AvatarProps } from './Avatar'

type AvatarListProps = {
users: ID[]
avatarProps?: Partial<AvatarProps>
}

export const AvatarList = (props: AvatarListProps) => {
const { users, avatarProps } = props
return (
<Flex alignItems='center'>
{users.slice(0, 3).map((user, index) => (
<Avatar
key={user}
userId={user}
size='small'
css={{ marginRight: index === 2 ? 0 : -4.8, zIndex: 3 - index }}
{...avatarProps}
/>
))}
</Flex>
)
}
1 change: 1 addition & 0 deletions packages/web/src/components/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Avatar'
export * from './AvatarLegacy'
export * from './AvatarList'
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useGetPlaylistById } from '@audius/common/api'
import { useGatedContentAccess } from '@audius/common/hooks'
import {
Collection,
ID,
isContentUSDCPurchaseGated
} from '@audius/common/models'
import { Nullable } from '@audius/common/utils'

import {
LockedStatusPill,
LockedStatusPillProps
} from 'components/locked-status-pill'

type CollectionLockedStatusPillProps = {
collectionId: ID
}

export const CollectionLockedStatusPill = (
props: CollectionLockedStatusPillProps
) => {
const { collectionId } = props
const { data: collection } = useGetPlaylistById(
{ playlistId: collectionId },
{ disabled: !collectionId }
)

const { hasStreamAccess } = useGatedContentAccess(
collection as Nullable<Collection>
)

if (!collection) return null
const { stream_conditions } = collection

const isPurchaseable = isContentUSDCPurchaseGated(stream_conditions)

let variant: Nullable<LockedStatusPillProps['variant']> = null
if (isPurchaseable) {
variant = 'premium'
}

if (!variant) return null

return <LockedStatusPill variant={variant} locked={!hasStreamAccess} />
}

export const useIsCollectionUnlockable = (collectionId: ID) => {
const { data: collection } = useGetPlaylistById({
playlistId: collectionId
})

if (!collection) return false
const { stream_conditions } = collection
const isPurchaseable = isContentUSDCPurchaseGated(stream_conditions)

return isPurchaseable
}
151 changes: 151 additions & 0 deletions packages/web/src/components/collection/CollectionTileMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { useCallback } from 'react'

import { useGetPlaylistById } from '@audius/common/api'
import { ID } from '@audius/common/models'
import { repostsUserListActions, RepostType } from '@audius/common/store'
import { formatCount, route } from '@audius/common/utils'
import { Text, Flex, IconRepost, IconHeart } from '@audius/harmony'
import { push } from 'connected-react-router'
import { useDispatch } from 'react-redux'

import { AvatarList } from 'components/avatar'
import { UserName, VanityMetric } from 'components/entity/VanityMetrics'
import { TrackTileSize } from 'components/track/types'
import { useIsMobile } from 'hooks/useIsMobile'
import {
setUsers,
setVisibility
} from 'store/application/ui/userListModal/slice'
import {
UserListEntityType,
UserListType
} from 'store/application/ui/userListModal/types'
import { pluralize } from 'utils/stringUtils'

const { REPOSTING_USERS_ROUTE, FAVORITING_USERS_ROUTE } = route
const { setRepost } = repostsUserListActions

type RepostsMetricProps = {
collectionId: ID
size?: TrackTileSize
}

export const RepostsMetric = (props: RepostsMetricProps) => {
const { collectionId, size } = props
const { data: playlist } = useGetPlaylistById(
{ playlistId: collectionId },
{ disabled: !collectionId }
)
const isMobile = useIsMobile()
const dispatch = useDispatch()

const handleClick = useCallback(() => {
if (isMobile) {
dispatch(setRepost(collectionId, RepostType.COLLECTION))
dispatch(push(REPOSTING_USERS_ROUTE))
} else {
dispatch(
setUsers({
userListType: UserListType.REPOST,
entityType: UserListEntityType.COLLECTION,
id: collectionId
})
)
dispatch(setVisibility(true))
}
}, [dispatch, isMobile, collectionId])

if (!playlist) return null
const { repost_count = 0, followee_reposts = [], is_album } = playlist

if (repost_count === 0)
return (
<VanityMetric disabled>
<IconRepost size='s' color='subdued' />
<Text>
Be the first to repost this {is_album ? 'album' : 'playlist'}
</Text>
</VanityMetric>
)

const renderName = () => {
const [{ user_id }] = followee_reposts

const remainingCount = repost_count - 1
const remainingText =
remainingCount > 0
? ` + ${formatCount(remainingCount)} ${pluralize(
'Repost',
remainingCount
)}`
: ' Reposted'

return (
<Text>
<UserName userId={user_id} />
{remainingText}
</Text>
)
}

const isLargeSize = size === TrackTileSize.LARGE && !isMobile

return (
<VanityMetric
css={(theme) => ({ gap: theme.spacing.l })}
onClick={handleClick}
>
{isLargeSize && followee_reposts.length >= 3 ? (
<AvatarList users={followee_reposts.map(({ user_id }) => user_id)} />
) : null}
<Flex gap='xs'>
<IconRepost size='s' color='subdued' />
{isLargeSize && followee_reposts.length > 0
? renderName()
: formatCount(repost_count)}
</Flex>
</VanityMetric>
)
}

type SavesMetricProps = {
collectionId: ID
}

export const SavesMetric = (props: SavesMetricProps) => {
const { collectionId } = props
const { data: playlist } = useGetPlaylistById(
{ playlistId: collectionId },
{ disabled: !collectionId }
)
const isMobile = useIsMobile()
const dispatch = useDispatch()

const handleClick = useCallback(() => {
if (isMobile) {
dispatch(setRepost(collectionId, RepostType.COLLECTION))
dispatch(push(FAVORITING_USERS_ROUTE))
} else {
dispatch(
setUsers({
userListType: UserListType.FAVORITE,
entityType: UserListEntityType.COLLECTION,
id: collectionId
})
)
dispatch(setVisibility(true))
}
}, [dispatch, isMobile, collectionId])

if (!playlist) return null
const { save_count = 0 } = playlist

if (save_count === 0) return null

return (
<VanityMetric onClick={handleClick}>
<IconHeart size='s' color='subdued' />
{formatCount(save_count)}
</VanityMetric>
)
}
64 changes: 64 additions & 0 deletions packages/web/src/components/collection/CollectionTileStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useGetPlaylistById } from '@audius/common/api'
import { ID } from '@audius/common/models'
import { Flex, Skeleton } from '@audius/harmony'

import { EntityRank } from 'components/lineup/EntityRank'
import { TrackTileSize } from 'components/track/types'
import { useIsMobile } from 'hooks/useIsMobile'

import { CollectionAccessTypeLabel } from './CollectionAccessTypeLabel'
import {
CollectionLockedStatusPill,
useIsCollectionUnlockable
} from './CollectionLockedStatusPill'
import { RepostsMetric, SavesMetric } from './CollectionTileMetrics'

type CollectionTileStatsProps = {
collectionId: ID
isTrending?: boolean
rankIndex?: number
size: TrackTileSize
isLoading?: boolean
}

export const CollectionTileStats = (props: CollectionTileStatsProps) => {
const { collectionId, isTrending, rankIndex, size, isLoading } = props

const isMobile = useIsMobile()
const isUnlockable = useIsCollectionUnlockable(collectionId)

const { data: collection } = useGetPlaylistById(
{ playlistId: collectionId },
{ disabled: !!collectionId }
)

if (isLoading || !collection) {
return <Skeleton w='30%' h={isMobile ? 16 : 20} />
}

const { is_private } = collection

return (
<Flex
justifyContent='space-between'
alignItems='center'
pv={isMobile ? 's' : 'xs'}
>
<Flex gap='l'>
{isTrending && rankIndex !== undefined ? (
<EntityRank index={rankIndex} />
) : null}
<CollectionAccessTypeLabel collectionId={collectionId} />
{is_private ? null : (
<>
<RepostsMetric collectionId={collectionId} size={size} />
<SavesMetric collectionId={collectionId} />
</>
)}
</Flex>
{isUnlockable ? (
<CollectionLockedStatusPill collectionId={collectionId} />
) : null}
</Flex>
)
}
Loading

0 comments on commit 9dabd1b

Please sign in to comment.