diff --git a/packages/web/src/components/collection/desktop/Artwork.tsx b/packages/web/src/components/collection/desktop/Artwork.tsx index 21dc77cd021..f035577e7ad 100644 --- a/packages/web/src/components/collection/desktop/Artwork.tsx +++ b/packages/web/src/components/collection/desktop/Artwork.tsx @@ -1,14 +1,19 @@ +import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react' + import { useCollection } from '@audius/common/api' import { imageBlank } from '@audius/common/assets' import { SquareSizes } from '@audius/common/models' +import { getErrorMessage } from '@audius/common/utils' import { Button, IconPencil, Image } from '@audius/harmony' import cn from 'classnames' import { pick } from 'lodash' import { Link } from 'react-router' import { useCollectionCoverArt } from 'hooks/useCollectionCoverArt' +import { resizeImage } from 'utils/imageProcessingUtil' import styles from './CollectionHeader.module.css' +import { usePlaylistEditMode } from './edit-mode/PlaylistEditModeContext' const messages = { addArtwork: 'Add Artwork', @@ -38,31 +43,85 @@ export const Artwork = (props: ArtworkProps) => { const hasImage = image && image !== imageBlank + const editMode = usePlaylistEditMode() + const isEditingThis = + editMode.isEditMode && editMode.collectionId === collectionId + + const fileInputRef = useRef(null) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isEditingThis) setError(null) + }, [isEditingThis]) + + const handleFileChange = useCallback( + async (e: ChangeEvent) => { + const file = e.target.files?.[0] + e.target.value = '' + if (!file) return + try { + const resized = await resizeImage(file) + // @ts-ignore writing to read-only property; matches ArtworkField pattern + resized.name = file.name + const url = URL.createObjectURL(resized) + editMode.setField('artwork', { url, file: resized, source: 'inline' }) + setError(null) + } catch (err) { + setError(getErrorMessage(err)) + } + }, + [editMode] + ) + + const draftArtwork = editMode.draft.artwork + const displaySrc = isEditingThis && draftArtwork ? draftArtwork.url : image + return ( {messages.coverArtAltText} {isOwner ? ( - + {isEditingThis ? ( + <> + + + + ) : ( + + )} ) : null} diff --git a/packages/web/src/components/collection/desktop/CollectionHeader.tsx b/packages/web/src/components/collection/desktop/CollectionHeader.tsx index 5818ec62122..134194c3bb4 100644 --- a/packages/web/src/components/collection/desktop/CollectionHeader.tsx +++ b/packages/web/src/components/collection/desktop/CollectionHeader.tsx @@ -10,7 +10,10 @@ import { IconCart, useTheme, MusicBadge, - IconCalendarMonth + IconCalendarMonth, + Switch, + TextArea, + TextInput } from '@audius/harmony' import cn from 'classnames' import { pick } from 'lodash' @@ -28,13 +31,21 @@ import { CollectionHeaderProps } from '../types' import { Artwork } from './Artwork' import { CollectionActionButtons } from './CollectionActionButtons' import styles from './CollectionHeader.module.css' +import { usePlaylistEditMode } from './edit-mode/PlaylistEditModeContext' const messages = { premiumLabel: 'premium', by: 'By ', hidden: 'Hidden', releases: (releaseDate: string) => - `Releases ${formatReleaseDate({ date: releaseDate, withHour: true })}` + `Releases ${formatReleaseDate({ date: releaseDate, withHour: true })}`, + titleLabel: 'Playlist title', + titleAlbumLabel: 'Album title', + descriptionLabel: 'Description', + descriptionPlaceholder: 'Add a description', + visibility: 'Visibility', + publicLabel: 'Public', + privateLabel: 'Hidden' } export const CollectionHeader = (props: CollectionHeaderProps) => { @@ -71,16 +82,35 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { 'is_scheduled_release', 'release_date', 'permalink', - 'is_private' + 'is_private', + 'is_album' ]) }) const { is_scheduled_release: isScheduledRelease, release_date: releaseDate, permalink, - is_private: isPrivate + is_private: isPrivate, + is_album: isAlbumFromCollection } = partialCollection ?? {} + const editMode = usePlaylistEditMode() + const isEditingThis = + editMode.isEditMode && editMode.collectionId === collectionId + + const stagedTitle = + editMode.draft.playlist_name !== undefined + ? editMode.draft.playlist_name + : title + const stagedDescription = + editMode.draft.description !== undefined + ? editMode.draft.description + : description + const stagedIsPrivate = + editMode.draft.is_private !== undefined + ? editMode.draft.is_private + : (isPrivate ?? false) + const hasStreamAccess = access?.stream const shouldShowStats = !isPrivate || isOwner const shouldShowScheduledRelease = @@ -134,44 +164,62 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { gap='s' className={styles.titleArtistSection} > - - {isLoading ? ( - - ) : ( - <> - - {title} - + {isEditingThis ? ( + + + editMode.setField('playlist_name', e.target.value) + } + maxLength={64} + autoFocus + /> + + ) : ( + + {isLoading ? ( + + ) : ( + <> + + {title} + - {!isLoading && isOwner ? ( - - ) : null} - - )} - + {!isLoading && isOwner ? ( + + ) : null} + + )} + + )} {isLoading ? ( ) : userId !== null ? ( @@ -191,6 +239,25 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { ) : null} + {isEditingThis ? ( + + + {messages.visibility} + + + editMode.setField('is_private', !e.target.checked) + } + aria-label={messages.visibility} + /> + + {stagedIsPrivate + ? messages.privateLabel + : messages.publicLabel} + + + ) : null}
{renderStatsRow(isLoading)}
@@ -263,7 +330,17 @@ export const CollectionHeader = (props: CollectionHeaderProps) => { ) : ( - {description ? ( + {isEditingThis ? ( +