Skip to content

Commit

Permalink
[C-2685 C-2686] Implement collection upload form (#3870)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers committed Aug 15, 2023
1 parent 1c7ec6f commit bae86c3
Show file tree
Hide file tree
Showing 16 changed files with 488 additions and 48 deletions.
3 changes: 2 additions & 1 deletion packages/web/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type IconProps = {
*/
export const Icon = (props: IconProps) => {
const {
className,
color,
icon: IconComponent,
size = 'small',
Expand All @@ -48,7 +49,7 @@ export const Icon = (props: IconProps) => {

return (
<IconComponent
className={cn(styles.icon, styles[size])}
className={cn(styles.icon, styles[size], className)}
style={style}
{...iconProps}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/form-fields/DropdownField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import DropdownInput, {
DropdownInputProps
} from 'components/data-entry/DropdownInput'

type DropdownFieldProps = SetRequired<
export type DropdownFieldProps = SetRequired<
Partial<DropdownInputProps>,
'placeholder' | 'menu'
> & {
Expand Down
13 changes: 13 additions & 0 deletions packages/web/src/components/form-fields/SwitchField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Switch, SwitchProps } from '@audius/stems'
import { useField } from 'formik'

type SwitchFieldProps = SwitchProps & {
name: string
}

export const SwitchField = (props: SwitchFieldProps) => {
const { name, ...other } = props
const [field] = useField({ name, type: 'checkbox' })

return <Switch {...field} {...other} />
}
2 changes: 1 addition & 1 deletion packages/web/src/components/form-fields/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
InputV2Variant
} from 'components/data-entry/InputV2'

type TextFieldProps = InputV2Props & {
export type TextFieldProps = InputV2Props & {
name: string
}

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/upload/UploadArtwork.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type UploadArtworkProps = {
className?: string
artworkUrl?: string
onDropArtwork: (selectedFiles: File[], source: string) => Promise<void>
onRemoveArtwork?: () => void
Expand Down
10 changes: 7 additions & 3 deletions packages/web/src/components/upload/UploadArtwork.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ const UploadArtwork = (props) => {

return (
<div
className={cn(styles.uploadArtwork, {
[styles.error]: props.error
})}
className={cn(
styles.uploadArtwork,
{
[styles.error]: props.error
},
props.className
)}
ref={imageSelectionAnchorRef}
>
<div
Expand Down
27 changes: 27 additions & 0 deletions packages/web/src/pages/upload-page/fields/SelectGenreField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GENRES } from '@audius/common'

import { DropdownField, DropdownFieldProps } from 'components/form-fields'

const messages = {
genre: 'Pick a Genre'
}

type SelectGenreFieldProps = Partial<DropdownFieldProps> & {
name: string
}

const menu = { items: GENRES }

export const SelectGenreField = (props: SelectGenreFieldProps) => {
return (
<DropdownField
aria-label={messages.genre}
placeholder={messages.genre}
mount='parent'
// TODO: Use correct value for Genres based on label (see `convertGenreLabelToValue`)
menu={menu}
size='large'
{...props}
/>
)
}
30 changes: 30 additions & 0 deletions packages/web/src/pages/upload-page/fields/SelectMoodField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { DropdownField, DropdownFieldProps } from 'components/form-fields'
import { moodMap } from 'utils/Moods'

const MOODS = Object.keys(moodMap).map((k) => ({
text: k,
el: moodMap[k]
}))

const menu = { items: MOODS }

const messages = {
mood: 'Pick a Mood'
}

type SelectMoodFieldProps = Partial<DropdownFieldProps> & {
name: string
}

export const SelectMoodField = (props: SelectMoodFieldProps) => {
return (
<DropdownField
aria-label={messages.mood}
placeholder={messages.mood}
mount='parent'
menu={menu}
size='large'
{...props}
/>
)
}
47 changes: 7 additions & 40 deletions packages/web/src/pages/upload-page/fields/TrackMetadataFields.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { GENRES } from '@audius/common'
import { useField } from 'formik'

import {
ArtworkField,
DropdownField,
TagField,
TextAreaField,
TextField
} from 'components/form-fields'
import { moodMap } from 'utils/Moods'
import { ArtworkField, TagField, TextAreaField } from 'components/form-fields'

import { getTrackFieldName } from '../hooks'

import { SelectGenreField } from './SelectGenreField'
import { SelectMoodField } from './SelectMoodField'
import styles from './TrackMetadataFields.module.css'

const MOODS = Object.keys(moodMap).map((k) => ({
text: k,
el: moodMap[k]
}))
import { TrackNameField } from './TrackNameField'

const messages = {
trackName: 'Track Name',
genre: 'Pick a Genre',
mood: 'Pick a Mood',
description: 'Description'
}

Expand All @@ -36,31 +23,11 @@ export const TrackMetadataFields = () => {
</div>
<div className={styles.fields}>
<div className={styles.trackName}>
<TextField
name={getTrackFieldName(index, 'title')}
label={messages.trackName}
maxLength={64}
required
/>
<TrackNameField name={getTrackFieldName(index, 'title')} />
</div>
<div className={styles.categorization}>
<DropdownField
name={getTrackFieldName(index, 'genre')}
aria-label={messages.genre}
placeholder={messages.genre}
mount='parent'
// TODO: Use correct value for Genres based on label (see `convertGenreLabelToValue`)
menu={{ items: GENRES }}
size='large'
/>
<DropdownField
name={getTrackFieldName(index, 'mood')}
aria-label={messages.mood}
placeholder={messages.mood}
mount='parent'
menu={{ items: MOODS }}
size='large'
/>
<SelectGenreField name={getTrackFieldName(index, 'genre')} />
<SelectMoodField name={getTrackFieldName(index, 'mood')} />
</div>
<div className={styles.tags}>
<TagField name={getTrackFieldName(index, 'tags')} />
Expand Down
15 changes: 15 additions & 0 deletions packages/web/src/pages/upload-page/fields/TrackNameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { TextField, TextFieldProps } from 'components/form-fields'

const messages = {
trackName: 'Track Name'
}

type TrackNameFieldProps = Partial<TextFieldProps> & {
name: string
}

export const TrackNameField = (props: TrackNameFieldProps) => {
return (
<TextField label={messages.trackName} maxLength={64} required {...props} />
)
}
6 changes: 4 additions & 2 deletions packages/web/src/pages/upload-page/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Maybe } from '@audius/common'
import { useField } from 'formik'

const getFieldName = (base: string, index: number, path: string) =>
`${base}.${index}.${path}`

export const useIndexedField = <T>(
base: string,
index: number,
index: Maybe<number>,
path: string
) => {
return useField<T>(getFieldName(base, index, path))
const fieldName = index === undefined ? path : getFieldName(base, index, path)
return useField<T>(fieldName)
}

export const getTrackFieldName = (index: number, path: string) => {
Expand Down
111 changes: 111 additions & 0 deletions packages/web/src/pages/upload-page/pages/CollectionTrackField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useCallback, useEffect } from 'react'

import {
HarmonyButton,
HarmonyButtonSize,
HarmonyButtonType,
IconDrag,
IconPlay,
IconTrash
} from '@audius/stems'
import { useField } from 'formik'

import { Icon } from 'components/Icon'
import { TagField } from 'components/form-fields'
import { SwitchField } from 'components/form-fields/SwitchField'
import { Tile } from 'components/tile'
import { Text } from 'components/typography'

import { SelectGenreField } from '../fields/SelectGenreField'
import { SelectMoodField } from '../fields/SelectMoodField'
import { TrackNameField } from '../fields/TrackNameField'
import { CollectionTrackForUpload } from '../types'

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

const messages = {
overrideLabel: 'Override details for this track',
preview: 'Preview',
delete: 'Delete'
}

type CollectionTrackFieldProps = {
index: number
remove: (index: number) => void
}

export const CollectionTrackField = (props: CollectionTrackFieldProps) => {
const { index, remove } = props
const [{ value: track }] = useField<CollectionTrackForUpload>(
`tracks.${index}`
)

const [{ value: metadata }, , { setValue }] = useField<
CollectionTrackForUpload['metadata']
>(`tracks.${index}.metadata`)

const [{ value }] = useField('trackDetails')

const { override } = track

useEffect(() => {
if (override) {
setValue({ ...metadata, ...value })
} else {
setValue({ ...metadata, genre: '', mood: null, tags: null })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [override])

const handleRemove = useCallback(() => {
remove(index)
}, [remove, index])

return (
<Tile
className={styles.trackField}
key={track.metadata.track_id}
elevation='mid'
>
<div className={styles.trackNameRow}>
<span className={styles.iconDrag}>
<Icon icon={IconDrag} size='large' />
</span>
<Text size='small' className={styles.trackindex}>
{index}
</Text>
<TrackNameField name={`tracks.${index}.metadata.title`} />
</div>
{override ? (
<div className={styles.trackInformation}>
<div className={styles.genreMood}>
<SelectGenreField name={`tracks.${index}.metadata.genre`} />
<SelectMoodField name={`tracks.${index}.metadata.mood`} />
</div>
<TagField name={`tracks.${index}.metadata.tags`} />
</div>
) : null}
<div className={styles.overrideRow}>
<div className={styles.overrideSwitch}>
<SwitchField name={`tracks.${index}.override`} />
<Text>{messages.overrideLabel}</Text>
</div>
<div className={styles.actions}>
<HarmonyButton
variant={HarmonyButtonType.GHOST}
size={HarmonyButtonSize.SMALL}
text={messages.preview}
iconLeft={IconPlay}
/>
<HarmonyButton
variant={HarmonyButtonType.GHOST}
size={HarmonyButtonSize.SMALL}
text={messages.delete}
iconLeft={IconTrash}
onClick={handleRemove}
/>
</div>
</div>
</Tile>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FieldArray, useField } from 'formik'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'

import { CollectionTrackForUpload } from '../types'

import { CollectionTrackField } from './CollectionTrackField'

export const CollectionTrackFieldArray = () => {
const [{ value: tracks }] = useField<CollectionTrackForUpload[]>('tracks')

return (
<FieldArray name='tracks'>
{({ move, remove }) => (
<DragDropContext
onDragEnd={(result) => {
if (!result.destination) {
return
}
move(result.source.index, result.destination.index)
}}
>
<Droppable droppableId='tracks'>
{(provided, snapshot) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{tracks.map((track, index) => (
<Draggable
key={track.metadata.title}
draggableId={track.metadata.title}
index={index}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<CollectionTrackField index={index} remove={remove} />
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)}
</FieldArray>
)
}

0 comments on commit bae86c3

Please sign in to comment.