Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C-3521] Add option menu to create collection tile (web) #7046

Merged
merged 4 commits into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common/src/models/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ type EmbedCopy = {
// Track Upload
type TrackUploadOpen = {
eventName: Name.TRACK_UPLOAD_OPEN
source: 'nav' | 'profile' | 'signup'
source: 'nav' | 'profile' | 'signup' | 'library'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
type TrackUploadStartUploading = {
eventName: Name.TRACK_UPLOAD_START_UPLOADING
Expand Down
83 changes: 46 additions & 37 deletions packages/web/src/components/tile/Tile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ReactNode, ComponentType, ComponentProps, ElementType } from 'react'
import {
ReactNode,
ComponentType,
ComponentProps,
ElementType,
forwardRef,
Ref
} from 'react'

import { DogEarType } from '@audius/common'
import cn from 'classnames'
Expand All @@ -19,40 +26,42 @@ 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,
onClick,
as: RootComponent = onClick ? 'button' : 'div',
className,
dogEar,
elevation = 'near',
...other
} = props
export const Tile = forwardRef(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good stuff

<T extends ElementType = ComponentType<ComponentProps<'div'>>>(
props: TileProps<T>,
ref: Ref<HTMLButtonElement>
) => {
const {
children,
size,
onClick,
as: RootComponent = onClick ? 'button' : 'div',
className,
dogEar,
elevation = 'near',
...other
} = props

return (
<RootComponent
className={cn(
styles.root,
size && styles[size],
styles[elevation],
className
)}
type={onClick ? 'button' : undefined}
onClick={onClick}
{...other}
>
{dogEar ? (
<div className={styles.borderOffset}>
<DogEar type={dogEar} />
</div>
) : null}
{children}
</RootComponent>
)
}
return (
<RootComponent
className={cn(
styles.root,
size && styles[size],
styles[elevation],
className
)}
type={onClick ? 'button' : undefined}
onClick={onClick}
ref={ref}
{...other}
>
{dogEar ? (
<div className={styles.borderOffset}>
<DogEar type={dogEar} />
</div>
) : null}
{children}
</RootComponent>
)
}
)
127 changes: 101 additions & 26 deletions packages/web/src/components/upload/UploadChip.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { IconMultiselectAdd } from '@audius/stems'
import { Ref, useCallback, useMemo } from 'react'

import {
CreatePlaylistSource,
FeatureFlags,
Name,
cacheCollectionsActions
} from '@audius/common'
import {
Box,
HTMLButtonProps,
IconCloudUpload,
IconPlaylists,
IconPlus,
Text
} from '@audius/harmony'
import { PopupMenu, PopupMenuItem } from '@audius/stems'
import cn from 'classnames'
import { push as pushRoute } from 'connected-react-router'
import { capitalize } from 'lodash'
import { useDispatch } from 'react-redux'

import IconUpload from 'assets/img/iconUpload.svg'
import { Tile } from 'components/tile'
import { useFlag } from 'hooks/useRemoteConfig'
import { track, make } from 'services/analytics'
import { UPLOAD_PAGE } from 'utils/route'

import styles from './UploadChip.module.css'
const { createAlbum, createPlaylist } = cacheCollectionsActions

const messages = {
const getMessages = (type: 'track' | 'album' | 'playlist') => ({
track: 'Upload Track',
aTrack: 'Upload A Track',
album: 'Upload New Album',
playlist: 'Create Playlist',
artistPlaylist: 'Upload New Playlist',
firstAlbum: 'Upload Your First Album',
firstPlaylist: 'Create Your First Playlist',
firstArtistPlaylist: 'Upload Your First Playlist'
}
createNewCollection: (isFirst: boolean) =>
`Create ${isFirst ? 'Your First' : 'New'} ${capitalize(type)}`,
uploadCollection: `Upload ${type}`,
createCollection: `Create ${type}`
})

type UploadChipProps = {
type?: 'track' | 'album' | 'playlist'
Expand All @@ -30,58 +51,112 @@ type UploadChipProps = {
/**
* Is this upload the user's first of this type
* */
source: 'nav' | 'profile' | CreatePlaylistSource
isFirst?: boolean
onClick: () => void
isArtist?: boolean
}

const UploadChip = ({
type = 'track',
variant = 'tile',
isArtist = false,
isFirst = false,
onClick
source
}: UploadChipProps) => {
const { isEnabled: isEditAlbumsEnabled } = useFlag(FeatureFlags.EDIT_ALBUMS)
const messages = getMessages(type)
const icon =
type === 'track' || type === 'album' ? (
type === 'track' ? (
<IconUpload className={styles.iconUpload} />
) : (
<IconMultiselectAdd className={styles.iconPlus} />
<IconPlus className={styles.iconPlus} color='subdued' />
)

let text
let text: string
switch (type) {
case 'track':
text = variant === 'nav' ? messages.track : messages.aTrack
break
case 'album':
text = isFirst ? messages.firstAlbum : messages.album
break
case 'playlist':
if (isArtist) {
text = isFirst ? messages.firstArtistPlaylist : messages.artistPlaylist
} else {
text = isFirst ? messages.firstPlaylist : messages.playlist
}
text = messages.createNewCollection(isFirst)
break
default:
break
}

return (
const dispatch = useDispatch()

const handleClickUpload = useCallback(() => {
dispatch(pushRoute(UPLOAD_PAGE))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be too hard to make this react-router links? on top of that it would be excellent to have links that can take "analytics" objects to call on click, but maybe thats too much for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah that's a good idea. As you mentioned below, maybe a little tough here, but something we should support.

track(
make({
eventName: Name.TRACK_UPLOAD_OPEN,
source: source as 'nav' | 'profile'
})
)
}, [dispatch, source])

const handleCreateCollection = useCallback(() => {
dispatch(
(type === 'album' ? createAlbum : createPlaylist)(
{ playlist_name: `New ${type}` },
source as CreatePlaylistSource,
undefined,
'route'
)
)
}, [dispatch, source, type])

const items: PopupMenuItem[] = useMemo(
() =>
type === 'track'
? []
: [
{
text: messages.uploadCollection,
icon: <IconCloudUpload />,
onClick: handleClickUpload
},
{
text: messages.createCollection,
icon: <IconPlaylists />,
onClick: handleCreateCollection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah seems like it would maybe be hard to make these links. something to shoot for with harmony popup/menu

}
],
[type, messages, handleClickUpload, handleCreateCollection]
)

const renderTile = (props: HTMLButtonProps, ref?: Ref<HTMLButtonElement>) => (
<Tile
className={cn(styles.root, {
[styles.nav]: variant === 'nav',
[styles.card]: variant === 'card',
[styles.tile]: variant === 'tile'
})}
as='button'
onClick={onClick}
ref={ref}
{...props}
>
<span>{icon}</span>
<span className={styles.text}>{text}</span>
<Box w={100}>
<Text variant='title' size='m' color='subdued' strength='default'>
{text}
</Text>
</Box>
</Tile>
)

return !isEditAlbumsEnabled || type === 'track' ? (
renderTile({ onClick: handleClickUpload })
) : (
<PopupMenu
items={items}
anchorOrigin={{ horizontal: 'right', vertical: 'center' }}
transformOrigin={{ horizontal: 'center', vertical: 'center' }}
renderTrigger={(anchorRef, onClick, triggerProps) =>
renderTile({ onClick: () => onClick(), ...triggerProps }, anchorRef)
}
/>
)
}

export default UploadChip
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useCallback } from 'react'

import { ID, Name, accountSelectors } from '@audius/common'
import { ID, accountSelectors } from '@audius/common'
import cn from 'classnames'
// eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web
import { animated } from 'react-spring'

import { useSelector } from 'common/hooks/useSelector'
import { make, useRecord } from 'common/store/analytics/actions'
import { AiGeneratedCallout } from 'components/ai-generated-button/AiGeneratedCallout'
import Input from 'components/data-entry/Input'
import TextArea from 'components/data-entry/TextArea'
Expand All @@ -20,7 +17,6 @@ import ProfilePageBadge from 'components/user-badges/ProfilePageBadge'
import { Type } from 'pages/profile-page/components/SocialLink'
import SocialLinkInput from 'pages/profile-page/components/SocialLinkInput'
import { ProfileTopTags } from 'pages/profile-page/components/desktop/ProfileTopTags'
import { UPLOAD_PAGE } from 'utils/route'

import { ProfileBio } from './ProfileBio'
import { ProfileMutuals } from './ProfileMutuals'
Expand All @@ -45,7 +41,6 @@ type ProfileLeftNavProps = {
loading: boolean
isDeactivated: boolean
allowAiAttribution: boolean
goToRoute: (route: string) => void
twitterHandle: string
onUpdateTwitterHandle: (handle: string) => void
instagramHandle: string
Expand Down Expand Up @@ -76,7 +71,6 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => {
loading,
isDeactivated,
allowAiAttribution,
goToRoute,
twitterHandle,
onUpdateTwitterHandle,
instagramHandle,
Expand All @@ -97,14 +91,8 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => {
isOwner
} = props

const record = useRecord()
const accountUser = useSelector(getAccountUser)

const onClickUploadChip = useCallback(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice removal

goToRoute(UPLOAD_PAGE)
record(make(Name.TRACK_UPLOAD_OPEN, { source: 'profile' }))
}, [goToRoute, record])

const renderTipAudioButton = (_: any, style: object) => (
<animated.div className={styles.tipAudioButtonContainer} style={style}>
<TipAudioButton />
Expand Down Expand Up @@ -221,11 +209,7 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => {
<RelatedArtists />
{isArtist ? <ProfileTopTags /> : null}
{showUploadChip ? (
<UploadChip
type='track'
variant='nav'
onClick={onClickUploadChip}
/>
<UploadChip type='track' variant='nav' source='nav' />
) : null}
</div>
</div>
Expand Down
Loading