Skip to content
Open
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
21 changes: 21 additions & 0 deletions packages/common/src/store/ui/modals/create-playlist-modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createModal } from '../createModal'

export type CreatePlaylistModalState = {
isAlbum?: boolean
initTrackId?: number
}

const createPlaylistModal = createModal<CreatePlaylistModalState>({
reducerPath: 'CreatePlaylistModal',
initialState: {
isOpen: false,
isAlbum: false
},
sliceSelector: (state) => state.ui.modals
})

export const {
hook: useCreatePlaylistModal,
reducer: createPlaylistModalReducer,
actions: createPlaylistModalActions
} = createPlaylistModal
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export * from './receive-tokens-modal'
export * from './send-tokens-modal'
export * from './coin-success-modal'
export * from './fan-club-details-modal'
export * from './create-playlist-modal'
3 changes: 2 additions & 1 deletion packages/common/src/store/ui/modals/parentSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export const initialState: BasicModalsState = {
CoinSuccessModal: { isOpen: false },
FanClubDetailsModal: { isOpen: false },
VerificationSuccess: { isOpen: false },
VerificationError: { isOpen: false }
VerificationError: { isOpen: false },
CreatePlaylistModal: { isOpen: false }
}

const slice = createSlice({
Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/store/ui/modals/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { coinflowWithdrawModalReducer } from './coinflow-withdraw-modal'
import { connectedWalletsModalReducer } from './connected-wallets-modal'
import { chatBlastModalReducer } from './create-chat-blast-modal'
import { createChatModalReducer } from './create-chat-modal'
import { createPlaylistModalReducer } from './create-playlist-modal'
import { deleteTrackConfirmationModalReducer } from './delete-track-confirmation-modal'
import { downloadTrackArchiveModalReducer } from './download-track-archive-modal'
import { earlyReleaseConfirmationModalReducer } from './early-release-confirmation-modal'
Expand Down Expand Up @@ -92,7 +93,8 @@ const combinedReducers = combineReducers({
ReceiveTokensModal: receiveTokensModalReducer,
SendTokensModal: sendTokensModalReducer,
CoinSuccessModal: coinSuccessModalReducer,
FanClubDetailsModal: fanClubDetailsModalReducer
FanClubDetailsModal: fanClubDetailsModalReducer,
CreatePlaylistModal: createPlaylistModalReducer
})

/**
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export type Modals =
| 'CoinSuccessModal'
| 'VerificationSuccess'
| 'VerificationError'
| 'CreatePlaylistModal'

export type BasicModalsState = {
[modal in Modals]: BaseModalState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { useCallback, useState } from 'react'

import { CreatePlaylistSource } from '@audius/common/models'
import {
cacheCollectionsActions,
useCreatePlaylistModal
} from '@audius/common/store'
import { getErrorMessage } from '@audius/common/utils'
import {
Button,
Flex,
IconPlaylists,
Modal,
ModalContent,
ModalFooter,
ModalHeader,
ModalTitle,
TextArea,
TextInput
} from '@audius/harmony'
import { useDispatch } from 'react-redux'

import UploadArtwork from 'components/upload/UploadArtwork'
import { resizeImage } from 'utils/imageProcessingUtil'

const { createPlaylist, createAlbum } = cacheCollectionsActions

const messages = {
createPlaylistTitle: 'Create New Playlist',
createAlbumTitle: 'Create New Album',
nameLabel: 'Playlist Name',
nameLabelAlbum: 'Album Name',
namePlaceholder: 'Give your playlist a name',
namePlaceholderAlbum: 'Give your album a name',
descriptionLabel: 'Description',
descriptionPlaceholder: 'Describe what makes this special (optional)',
cancel: 'Cancel',
create: 'Create',
defaultName: 'New Playlist',
defaultAlbumName: 'New Album'
}

type ArtworkValue = {
url: string
file: File
source?: string
} | null

export const CreatePlaylistModal = () => {
const dispatch = useDispatch()
const { isOpen, onClose, onClosed, data } = useCreatePlaylistModal()
const { isAlbum = false, initTrackId } = data

const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [artwork, setArtwork] = useState<ArtworkValue>(null)
const [imageProcessingError, setImageProcessingError] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)

const reset = useCallback(() => {
setName('')
setDescription('')
setArtwork(null)
setImageProcessingError(false)
setIsSubmitting(false)
}, [])

const handleClose = useCallback(() => {
onClose()
}, [onClose])

const handleClosed = useCallback(() => {
reset()
onClosed()
}, [onClosed, reset])

const handleDropArtwork = useCallback(
async (selectedFiles: File[], source: string) => {
try {
let file = selectedFiles[0]
file = await resizeImage(file)
// @ts-ignore writing to read-only property; matches ArtworkField pattern
file.name = selectedFiles[0].name
const url = URL.createObjectURL(file)
setArtwork({ url, file, source })
setImageProcessingError(false)
} catch (err) {
// eslint-disable-next-line no-console
console.error(getErrorMessage(err))
setImageProcessingError(true)
}
},
[]
)

const handleRemoveArtwork = useCallback(() => {
setArtwork(null)
}, [])

const handleSubmit = useCallback(() => {
setIsSubmitting(true)
const trimmedName = name.trim()
const trimmedDescription = description.trim()
const playlistName =
trimmedName ||
(isAlbum ? messages.defaultAlbumName : messages.defaultName)
const action = isAlbum ? createAlbum : createPlaylist
dispatch(
action(
{
playlist_name: playlistName,
description: trimmedDescription || undefined,
// Cast: optimisticallySavePlaylist accepts the form-shape artwork.
...(artwork ? { artwork: artwork as any } : {})
},
CreatePlaylistSource.NAV,
initTrackId,
'route'
)
)
onClose()
}, [name, description, artwork, isAlbum, initTrackId, dispatch, onClose])

const title = isAlbum
? messages.createAlbumTitle
: messages.createPlaylistTitle
const nameLabel = isAlbum ? messages.nameLabelAlbum : messages.nameLabel
const namePlaceholder = isAlbum
? messages.namePlaceholderAlbum
: messages.namePlaceholder

return (
<Modal
isOpen={isOpen}
onClose={handleClose}
onClosed={handleClosed}
size='small'
>
<ModalHeader onClose={handleClose}>
<ModalTitle title={title} icon={<IconPlaylists />} />
</ModalHeader>
<ModalContent>
<Flex direction='column' gap='l'>
<Flex justifyContent='center'>
<UploadArtwork
artworkUrl={artwork?.url}
onDropArtwork={handleDropArtwork}
onRemoveArtwork={artwork ? handleRemoveArtwork : undefined}
imageProcessingError={imageProcessingError}
size='small'
isUpload
/>
</Flex>
<TextInput
label={nameLabel}
placeholder={namePlaceholder}
value={name}
onChange={(e) => setName(e.target.value)}
maxLength={64}
/>
<TextArea
aria-label={messages.descriptionLabel}
placeholder={messages.descriptionPlaceholder}
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={1000}
showMaxLength
grows
/>
</Flex>
</ModalContent>
<ModalFooter>
<Flex gap='xl' flex={1}>
<Button variant='secondary' fullWidth onClick={handleClose}>
{messages.cancel}
</Button>
<Button
variant='primary'
fullWidth
isLoading={isSubmitting}
disabled={isSubmitting}
onClick={handleSubmit}
>
{messages.create}
</Button>
</Flex>
</ModalFooter>
</Modal>
)
}

export default CreatePlaylistModal
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useCallback, useMemo, useState } from 'react'

import { useCurrentAccount, useUpdatePlaylistLibrary } from '@audius/common/api'
import { CreatePlaylistSource } from '@audius/common/models'
import {
cacheCollectionsActions,
playlistLibraryHelpers
playlistLibraryHelpers,
useCreatePlaylistModal
} from '@audius/common/store'
import {
IconButton,
Expand All @@ -14,40 +13,32 @@ import {
PopupMenu,
PopupMenuItem
} from '@audius/harmony'
import { useDispatch } from 'react-redux'

import { useRequiresAccountCallback } from 'hooks/useRequiresAccount'

const { createPlaylist } = cacheCollectionsActions
const { addFolderToLibrary, constructPlaylistFolder } = playlistLibraryHelpers

const messages = {
new: 'New',
newPlaylistOrFolderTooltip: 'New Playlist or Folder',
createPlaylist: 'Create Playlist',
createFolder: 'Create Folder',
newPlaylistName: 'New Playlist',
newFolderName: 'New Folder'
}

// Allows user to create a playlist or playlist-folder
export const CreatePlaylistLibraryItemButton = () => {
const dispatch = useDispatch()
const { data: library } = useCurrentAccount({
select: (account) => account?.playlistLibrary
})
const { mutate: updatePlaylistLibrary } = useUpdatePlaylistLibrary()
const { onOpen: openCreatePlaylistModal } = useCreatePlaylistModal()
const [isActive, setIsActive] = useState(false)
const [isHovered, setIsHovered] = useState(false)

const handleSubmitPlaylist = useCallback(() => {
dispatch(
createPlaylist(
{ playlist_name: messages.newPlaylistName },
CreatePlaylistSource.NAV
)
)
}, [dispatch])
openCreatePlaylistModal({ isAlbum: false })
}, [openCreatePlaylistModal])

const handleSubmitFolder = useCallback(() => {
if (!library) return null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
import { useCallback } from 'react'

import { CreatePlaylistSource } from '@audius/common/models'
import { cacheCollectionsActions } from '@audius/common/store'
import { useDispatch } from 'react-redux'
import { useCreatePlaylistModal } from '@audius/common/store'

import { LeftNavLink } from '../LeftNavLink'
const { createPlaylist } = cacheCollectionsActions

const messages = {
empty: 'Create your first playlist!',
newPlaylistName: 'New Playlist'
empty: 'Create your first playlist!'
}

export const EmptyLibraryNavLink = () => {
const dispatch = useDispatch()
const { onOpen: openCreatePlaylistModal } = useCreatePlaylistModal()

const handleCreatePlaylist = useCallback(() => {
dispatch(
createPlaylist(
{ playlist_name: messages.newPlaylistName },
CreatePlaylistSource.NAV
)
)
}, [dispatch])
openCreatePlaylistModal({ isAlbum: false })
}, [openCreatePlaylistModal])

return (
<LeftNavLink disabled onClick={handleCreatePlaylist}>
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/pages/modals/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CoinSuccessModal } from 'components/CoinSuccessModal'
import AppCTAModal from 'components/app-cta-modal/AppCTAModal'
import BrowserPushConfirmationModal from 'components/browser-push-confirmation-modal/BrowserPushConfirmationModal'
import ConfirmerPreview from 'components/confirmer-preview/ConfirmerPreview'
import { CreatePlaylistModal } from 'components/create-playlist-modal/CreatePlaylistModal'
import EmbedModal from 'components/embed-modal/EmbedModal'
import { FeatureFlagOverrideModal } from 'components/feature-flag-override-modal'
import FirstUploadModal from 'components/first-upload-modal/FirstUploadModal'
Expand Down Expand Up @@ -37,7 +38,8 @@ const commonModalsMap: { [Modal in ModalTypes]?: ComponentType } = {
CommentSettings: CommentSettingsModal,
BrowserPushPermissionConfirmation: BrowserPushConfirmationModal,
CreateChatModal,
StripeOnRamp: StripeOnRampModal
StripeOnRamp: StripeOnRampModal,
CreatePlaylistModal
}

const commonModals = Object.entries(commonModalsMap) as [
Expand Down
Loading