Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[C-2475] Add desktop favorites playlist tab (#3637)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Jun 23, 2023
1 parent 394cacb commit 2f1f40d
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 142 deletions.
4 changes: 4 additions & 0 deletions packages/web/src/assets/img/iconSaveFilled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions packages/web/src/components/tile/Tile.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.root {
cursor: pointer;
position: relative;
border: 1px solid var(--neutral-light-8);
border-radius: 8px;
display: inline-flex;
flex-direction: column;
align-items: center;
background-color: var(--white);
box-shadow: 0 0 1px 0 var(--tile-shadow-1), 0 1px 0 0 var(--tile-shadow-2),
0 2px 5px -2px var(--tile-shadow-3);
transition: all 0.18s ease-in-out;
}

.root:hover {
background-color: var(--neutral-light-10);
box-shadow: 0 1px 5px 1px var(--tile-shadow-1-alt),
0 1px 0 0 var(--tile-shadow-2), 0 2px 10px -2px var(--tile-shadow-3);
transform: scale3d(1.01, 1.01, 1.01);
}

.root:active {
background-color: var(--neutral-light-10);
box-shadow: 0 0 1px 0 var(--tile-shadow-1),
0 2px 3px -2px var(--tile-shadow-3);
}

.small {
height: 226px;
width: 168px;
}

.medium {
height: 304px;
width: 232px;
}

.large {
height: 338px;
width: 258px;
}
38 changes: 38 additions & 0 deletions packages/web/src/components/tile/Tile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReactNode, ComponentType, ComponentProps, ElementType } from 'react'

import cn from 'classnames'

import styles from './Tile.module.css'

type TileOwnProps<TileComponentType extends ElementType = 'div'> = {
children: ReactNode
size?: 'small' | 'medium' | 'large'
as?: TileComponentType
}

export type TileProps<TileComponentType extends ElementType> =
TileOwnProps<TileComponentType> &
Omit<ComponentProps<TileComponentType>, keyof TileOwnProps>

export const Tile = <
T extends ElementType = ComponentType<ComponentProps<'div'>>
>(
props: TileProps<T>
) => {
const {
children,
size,
as: RootComponent = 'div',
className,
...other
} = props

return (
<RootComponent
className={cn(styles.root, size && styles[size], className)}
{...other}
>
{children}
</RootComponent>
)
}
1 change: 1 addition & 0 deletions packages/web/src/components/tile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tile'
2 changes: 2 additions & 0 deletions packages/web/src/components/tracks-table/EmptyTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const EmptyTable = (props) => {
type={ButtonType.SECONDARY}
text={props.buttonLabel}
onClick={props.onClick}
leftIcon={props.buttonIcon}
/>
) : null}
</div>
Expand All @@ -28,6 +29,7 @@ EmptyTable.propTypes = {
primaryText: PropTypes.string,
secondaryText: PropTypes.string,
buttonLabel: PropTypes.string,
buttonIcon: PropTypes.any,
onClick: PropTypes.func
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useMemo } from 'react'

import {
Status,
statusIsNotFinalized,
useFetchedSavedCollections,
useAccountAlbums
} from '@audius/common'

import { InfiniteCardLineup } from 'components/lineup/InfiniteCardLineup'
import EmptyTable from 'components/tracks-table/EmptyTable'
import { useGoToRoute } from 'hooks/useGoToRoute'
import { useOrderedLoad } from 'hooks/useOrderedLoad'

import { CollectionCard } from './CollectionCard'
import styles from './SavedPage.module.css'

const messages = {
emptyAlbumsHeader: 'You haven’t favorited any albums yet.',
emptyAlbumsBody: 'Once you have, this is where you’ll find them!',
goToTrending: 'Go to Trending'
}

export const AlbumsTabPage = () => {
const goToRoute = useGoToRoute()

const { data: savedAlbums, status: accountAlbumsStatus } = useAccountAlbums()
const savedAlbumIds = useMemo(
() => savedAlbums.map((a) => a.id),
[savedAlbums]
)

const {
data: fetchedAlbumIds,
status,
hasMore,
fetchMore
} = useFetchedSavedCollections({
collectionIds: savedAlbumIds,
type: 'albums',
pageSize: 20
})
const { isLoading: isAlbumLoading, setDidLoad } = useOrderedLoad(
fetchedAlbumIds.length
)
const cards = fetchedAlbumIds.map((id, i) => {
return (
<CollectionCard
index={i}
isLoading={isAlbumLoading(i)}
setDidLoad={setDidLoad}
key={id}
albumId={id}
/>
)
})

const noSavedAlbums =
accountAlbumsStatus === Status.SUCCESS && savedAlbumIds.length === 0
const noFetchedResults = !statusIsNotFinalized(status) && cards.length === 0

if (noSavedAlbums || noFetchedResults) {
return (
<EmptyTable
primaryText={messages.emptyAlbumsHeader}
secondaryText={messages.emptyAlbumsBody}
buttonLabel={messages.goToTrending}
onClick={() => goToRoute('/trending')}
/>
)
}

return (
<InfiniteCardLineup
hasMore={hasMore}
loadMore={fetchMore}
cards={cards}
cardsClassName={styles.cardsContainer}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useCallback } from 'react'

import {
cacheCollectionsSelectors,
cacheUsersSelectors,
ID,
CommonState
} from '@audius/common'
import { useSelector } from 'react-redux'

import Card, { CardProps } from 'components/card/desktop/Card'
import { useGoToRoute } from 'hooks/useGoToRoute'
import { albumPage } from 'utils/route'

import { formatCardSecondaryText } from '../utils'

const { getCollection } = cacheCollectionsSelectors
const { getUser } = cacheUsersSelectors

type CollectionCardProps = Pick<
CardProps,
'index' | 'isLoading' | 'setDidLoad'
> & {
albumId: ID
}

export const CollectionCard = (props: CollectionCardProps) => {
const { albumId, index, isLoading, setDidLoad } = props
const goToRoute = useGoToRoute()
const collection = useSelector((state: CommonState) =>
getCollection(state, { id: albumId })
)
const ownerHandle = useSelector((state: CommonState) => {
if (collection == null) {
return ''
}
const user = getUser(state, { id: collection.playlist_owner_id })
return user?.handle ?? ''
})

const handleClick = useCallback(() => {
if (ownerHandle && collection) {
goToRoute(
albumPage(ownerHandle, collection.playlist_name, collection.playlist_id)
)
}
}, [collection, ownerHandle, goToRoute])

return collection ? (
<Card
index={index}
isLoading={isLoading}
setDidLoad={setDidLoad}
key={collection.playlist_id}
id={collection.playlist_id}
userId={collection.playlist_owner_id}
imageSize={collection._cover_art_sizes}
size='medium'
playlistName={collection.playlist_name}
playlistId={collection.playlist_id}
isPlaylist={!collection.is_album}
isPublic={!collection.is_private}
handle={ownerHandle}
primaryText={collection.playlist_name}
secondaryText={formatCardSecondaryText(
collection.save_count,
collection.playlist_contents.track_ids.length
)}
isReposted={collection.has_current_user_reposted}
isSaved={collection.has_current_user_saved}
cardCoverImageSizes={collection._cover_art_sizes}
onClick={handleClick}
/>
) : null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useCallback, useMemo } from 'react'

import {
useFetchedSavedCollections,
useAccountPlaylists,
cacheCollectionsActions,
CreatePlaylistSource,
Status,
statusIsNotFinalized
} from '@audius/common'
import { IconPlus } from '@audius/stems'
import { useDispatch } from 'react-redux'

import { ReactComponent as IconSaveFilled } from 'assets/img/iconSaveFilled.svg'
import { InfiniteCardLineup } from 'components/lineup/InfiniteCardLineup'
import { Tile } from 'components/tile'
import EmptyTable from 'components/tracks-table/EmptyTable'
import { useOrderedLoad } from 'hooks/useOrderedLoad'

import { CollectionCard } from './CollectionCard'
import styles from './SavedPage.module.css'
const { createPlaylist } = cacheCollectionsActions

const messages = {
emptyPlaylistsHeader: 'You haven’t created or favorited any playlists yet.',
emptyPlaylistsBody: 'Once you have, this is where you’ll find them!',
createPlaylist: 'Create Playlist',
newPlaylist: 'New Playlist'
}

export const PlaylistsTabPage = () => {
const dispatch = useDispatch()

const { data: savedAlbums, status: accountPlaylistsStatus } =
useAccountPlaylists()
const savedAlbumIds = useMemo(
() => savedAlbums.map((a) => a.id),
[savedAlbums]
)

const {
data: fetchedAlbumIds,
status,
hasMore,
fetchMore
} = useFetchedSavedCollections({
collectionIds: savedAlbumIds,
type: 'albums',
pageSize: 20
})
const { isLoading, setDidLoad } = useOrderedLoad(fetchedAlbumIds.length)
const cards = fetchedAlbumIds.map((id, i) => {
return (
<CollectionCard
index={i}
isLoading={isLoading(i)}
setDidLoad={setDidLoad}
key={id}
albumId={id}
/>
)
})

const handleCreatePlaylist = useCallback(() => {
dispatch(
createPlaylist(
{ playlist_name: messages.newPlaylist },
CreatePlaylistSource.FAVORITES_PAGE
)
)
}, [dispatch])

const noSavedPlaylists =
accountPlaylistsStatus === Status.SUCCESS && savedAlbumIds.length === 0
const noFetchedResults = !statusIsNotFinalized(status) && cards.length === 0

if (noSavedPlaylists || noFetchedResults) {
return (
<EmptyTable
primaryText={messages.emptyPlaylistsHeader}
secondaryText={messages.emptyPlaylistsBody}
buttonLabel={messages.createPlaylist}
buttonIcon={<IconPlus />}
onClick={handleCreatePlaylist}
/>
)
}

const createPlaylistCard = (
<Tile
key='create_playlist'
size='medium'
as='button'
onClick={handleCreatePlaylist}
className={styles.createPlaylistCard}
>
<IconSaveFilled className={styles.createPlaylistIcon} />
<h4 className={styles.createPlaylistText}>{messages.createPlaylist}</h4>
</Tile>
)
cards.unshift(createPlaylistCard)

return (
<InfiniteCardLineup
hasMore={hasMore}
loadMore={fetchMore}
cards={cards}
cardsClassName={styles.cardsContainer}
/>
)
}
Loading

0 comments on commit 2f1f40d

Please sign in to comment.