Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@
cursor: pointer;
}

.editableTitleButton {
background: none;
border: 0;
padding: 0;
margin: 0;
text-align: left;
font: inherit;
color: inherit;
}

.titleHeader {
transition:
opacity var(--harmony-calm),
Expand All @@ -107,6 +117,59 @@
opacity: 1;
}

.titleInput {
width: 100%;
background: var(--harmony-white);
border: 1px solid var(--harmony-border-default);
border-radius: var(--harmony-unit-1);
padding: var(--harmony-unit-1) var(--harmony-unit-2);
font-family: inherit;
font-weight: 700;
font-size: clamp(24px, calc(1.6cqi + 18.75px), 36px);
line-height: 1.33;
color: var(--harmony-text-default);
outline: none;
}

.titleInput:focus {
border-color: var(--harmony-border-accent);
}

.editableDescription {
background: none;
border: 0;
padding: 0;
margin: 0;
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
width: 100%;
}

.editableDescription:hover .editIcon {
opacity: 1;
}

.descriptionInput {
width: 100%;
background: var(--harmony-white);
border: 1px solid var(--harmony-border-default);
border-radius: var(--harmony-unit-1);
padding: var(--harmony-unit-2);
font-family: inherit;
font-size: var(--harmony-font-s);
line-height: var(--harmony-line-height-s);
color: var(--harmony-text-default);
outline: none;
resize: vertical;
min-height: 60px;
}

.descriptionInput:focus {
border-color: var(--harmony-border-accent);
}

.imageEditButtonWrapper {
opacity: 0;
transition: opacity var(--harmony-expressive);
Expand Down
120 changes: 65 additions & 55 deletions packages/web/src/components/collection/desktop/CollectionHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { useCallback } from 'react'

import { useCollection, useCurrentUserId } from '@audius/common/api'
import { ModalSource, isContentUSDCPurchaseGated } from '@audius/common/models'
import { PurchaseableContentType } from '@audius/common/store'
import {
EditCollectionValues,
PurchaseableContentType,
cacheCollectionsActions
} from '@audius/common/store'
import { dayjs, formatReleaseDate } from '@audius/common/utils'
import {
Text,
IconVisibilityHidden,
IconPencil,
Flex,
IconCart,
useTheme,
MusicBadge,
IconCalendarMonth
} from '@audius/harmony'
import cn from 'classnames'
import { pick } from 'lodash'
import { Link } from 'react-router'
import { useDispatch } from 'react-redux'

import { UserLink } from 'components/link'
import Skeleton from 'components/skeleton/Skeleton'
Expand All @@ -28,6 +31,10 @@ import { CollectionHeaderProps } from '../types'
import { Artwork } from './Artwork'
import { CollectionActionButtons } from './CollectionActionButtons'
import styles from './CollectionHeader.module.css'
import { EditableCollectionDescription } from './EditableCollectionDescription'
import { EditableCollectionTitle } from './EditableCollectionTitle'

const { editPlaylist } = cacheCollectionsActions

const messages = {
premiumLabel: 'premium',
Expand Down Expand Up @@ -64,22 +71,40 @@ export const CollectionHeader = (props: CollectionHeaderProps) => {
} = props

const { spacing } = useTheme()
const dispatch = useDispatch()
const { data: currentUserId } = useCurrentUserId()
const { data: partialCollection } = useCollection(collectionId, {
select: (collection) =>
pick(collection, [
'is_scheduled_release',
'release_date',
'permalink',
'is_private'
])
})
const { data: fullCollection } = useCollection(collectionId)
const {
is_scheduled_release: isScheduledRelease,
release_date: releaseDate,
permalink,
is_private: isPrivate
} = partialCollection ?? {}
} = fullCollection ?? {}

const handleSaveTitle = useCallback(
(next: string) => {
if (!fullCollection || !collectionId) return
dispatch(
editPlaylist(collectionId, {
...fullCollection,
playlist_name: next
} as unknown as EditCollectionValues)
)
},
[dispatch, fullCollection, collectionId]
)

const handleSaveDescription = useCallback(
(next: string) => {
if (!fullCollection || !collectionId) return
dispatch(
editPlaylist(collectionId, {
...fullCollection,
description: next
} as unknown as EditCollectionValues)
)
},
[dispatch, fullCollection, collectionId]
)

const hasStreamAccess = access?.stream
const shouldShowStats = !isPrivate || isOwner
Expand Down Expand Up @@ -134,44 +159,24 @@ export const CollectionHeader = (props: CollectionHeaderProps) => {
gap='s'
className={styles.titleArtistSection}
>
<Flex
as={isOwner ? Link : 'span'}
css={{ background: 0, border: 0, padding: 0, margin: 0 }}
gap='s'
alignItems='center'
className={cn({
[styles.editableTitle]: isOwner
})}
// @ts-ignore -- Flex Link doesn't type `to` correctly
to={
isOwner
? { pathname: `${permalink}/edit`, search: '?focus=name' }
: undefined
}
>
{isLoading ? (
<Skeleton height='48px' width='300px' />
) : (
<>
<Text
variant='heading'
size='xl'
className={cn(styles.titleHeader)}
textAlign='left'
css={{
fontSize: 'clamp(24px, calc(1.6cqi + 18.75px), 36px)',
lineHeight: 1.33
}}
>
{title}
</Text>

{!isLoading && isOwner ? (
<IconPencil className={styles.editIcon} color='subdued' />
) : null}
</>
)}
</Flex>
{isLoading ? (
<Skeleton height='48px' width='300px' />
) : isOwner ? (
<EditableCollectionTitle value={title} onSave={handleSaveTitle} />
) : (
<Text
variant='heading'
size='xl'
className={styles.titleHeader}
textAlign='left'
css={{
fontSize: 'clamp(24px, calc(1.6cqi + 18.75px), 36px)',
lineHeight: 1.33
}}
>
{title}
</Text>
)}
{isLoading ? (
<Skeleton height='24px' width='150px' />
) : userId !== null ? (
Expand Down Expand Up @@ -263,7 +268,12 @@ export const CollectionHeader = (props: CollectionHeaderProps) => {
<Skeleton height='40px' width='100%' />
) : (
<Flex gap='l' direction='column'>
{description ? (
{isOwner ? (
<EditableCollectionDescription
value={description ?? ''}
onSave={handleSaveDescription}
/>
) : description ? (
<UserGeneratedText
size='s'
linkSource='collection page'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'

import { Flex, IconPencil, Text } from '@audius/harmony'

import { UserGeneratedText } from 'components/user-generated-text'

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

const DESCRIPTION_MAX_LENGTH = 1000

const messages = {
addDescription: 'Add a description...',
ariaLabel: 'Edit description'
}

type EditableCollectionDescriptionProps = {
value: string
onSave: (next: string) => void
}

export const EditableCollectionDescription = ({
value,
onSave
}: EditableCollectionDescriptionProps) => {
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(value)
const textareaRef = useRef<HTMLTextAreaElement>(null)

useEffect(() => {
if (!editing) setDraft(value)
}, [value, editing])

useEffect(() => {
if (editing) {
const el = textareaRef.current
if (!el) return
el.focus()
el.setSelectionRange(el.value.length, el.value.length)
}
}, [editing])

const commit = useCallback(() => {
if (draft !== value) onSave(draft)
setEditing(false)
}, [draft, value, onSave])

const cancel = useCallback(() => {
setDraft(value)
setEditing(false)
}, [value])

const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') {
e.preventDefault()
cancel()
}
},
[cancel]
)

if (editing) {
return (
<textarea
ref={textareaRef}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onBlur={commit}
onKeyDown={onKeyDown}
maxLength={DESCRIPTION_MAX_LENGTH}
rows={3}
placeholder={messages.addDescription}
aria-label={messages.ariaLabel}
className={styles.descriptionInput}
/>
)
}

return (
<button
type='button'
onClick={() => setEditing(true)}
aria-label={messages.ariaLabel}
className={styles.editableDescription}
>
<Flex gap='s' alignItems='flex-start'>
{value ? (
<UserGeneratedText
size='s'
linkSource='collection page'
css={{ textAlign: 'left' }}
>
{value}
</UserGeneratedText>
) : (
<Text size='s' color='subdued' textAlign='left'>
{messages.addDescription}
</Text>
)}
<IconPencil className={styles.editIcon} color='subdued' />
</Flex>
</button>
)
}
Loading
Loading