-
Notifications
You must be signed in to change notification settings - Fork 111
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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' | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
) | ||
} | ||
) |
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' | ||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
|
@@ -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' | ||
|
@@ -45,7 +41,6 @@ type ProfileLeftNavProps = { | |
loading: boolean | ||
isDeactivated: boolean | ||
allowAiAttribution: boolean | ||
goToRoute: (route: string) => void | ||
twitterHandle: string | ||
onUpdateTwitterHandle: (handle: string) => void | ||
instagramHandle: string | ||
|
@@ -76,7 +71,6 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => { | |
loading, | ||
isDeactivated, | ||
allowAiAttribution, | ||
goToRoute, | ||
twitterHandle, | ||
onUpdateTwitterHandle, | ||
instagramHandle, | ||
|
@@ -97,14 +91,8 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => { | |
isOwner | ||
} = props | ||
|
||
const record = useRecord() | ||
const accountUser = useSelector(getAccountUser) | ||
|
||
const onClickUploadChip = useCallback(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 /> | ||
|
@@ -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> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!