diff --git a/packages/web/src/components/data-entry/InputV2.module.css b/packages/web/src/components/data-entry/InputV2.module.css index 773b52821d..a8f05d98cb 100644 --- a/packages/web/src/components/data-entry/InputV2.module.css +++ b/packages/web/src/components/data-entry/InputV2.module.css @@ -1,8 +1,13 @@ .root { + display: flex; + flex-direction: column; + width: 100%; +} + +.inputRoot { display: flex; align-items: center; gap: var(--unit-2); - width: 100%; padding: 0 var(--unit-4); border: 1px solid var(--neutral-light-8); border-radius: var(--unit-1); diff --git a/packages/web/src/components/data-entry/InputV2.tsx b/packages/web/src/components/data-entry/InputV2.tsx index 3726387cee..fa1fc36e5e 100644 --- a/packages/web/src/components/data-entry/InputV2.tsx +++ b/packages/web/src/components/data-entry/InputV2.tsx @@ -97,7 +97,6 @@ export const InputV2 = (props: InputV2Props) => { onFocus={handleFocus} onBlur={handleBlur} ref={inputRef} - required={required} className={cn(styles.textInput, inputClassName)} value={value} maxLength={maxLength} @@ -119,8 +118,8 @@ export const InputV2 = (props: InputV2Props) => { ) return ( - <> -
+
+
{elevatePlaceholder ? (
{helperText ? {helperText} : null} - +
) } diff --git a/packages/web/src/components/form-fields/ArtworkField.tsx b/packages/web/src/components/form-fields/ArtworkField.tsx index 9d7322419e..16536156c2 100644 --- a/packages/web/src/components/form-fields/ArtworkField.tsx +++ b/packages/web/src/components/form-fields/ArtworkField.tsx @@ -37,7 +37,7 @@ export const ArtworkField = (props: ArtworkFieldProps) => { const hasError = Boolean(touched && error) return ( - <> +
{ {...other} /> {hasError ? {error} : null} - +
) } diff --git a/packages/web/src/pages/upload-page/UploadPageNew.tsx b/packages/web/src/pages/upload-page/UploadPageNew.tsx index a04d4db7b7..f652f523e5 100644 --- a/packages/web/src/pages/upload-page/UploadPageNew.tsx +++ b/packages/web/src/pages/upload-page/UploadPageNew.tsx @@ -1,29 +1,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { uploadActions, UploadType } from '@audius/common' +import { UploadType, uploadActions } from '@audius/common' import { useDispatch } from 'react-redux' import Header from 'components/header/desktop/Header' import Page from 'components/page/Page' import styles from './UploadPage.module.css' -import { EditPageNew } from './components/EditPageNew' import { FinishPageNew } from './components/FinishPageNew' import SelectPageNew from './components/SelectPageNew' -import { TrackForUpload } from './components/types' +import { EditPage } from './pages/EditPage' +import { UploadFormState } from './types' const { uploadTracks } = uploadActions -type UploadPageProps = { - uploadType: UploadType -} - -enum Phase { - SELECT, - EDIT, - FINISH -} - const messages = { selectPageTitle: 'Upload Your Music', editSingleTrackPageTitle: 'Complete Your Track', @@ -32,10 +22,22 @@ const messages = { finishMultiTrackPageTitle: 'Uploading Your Tracks' } -export const UploadPageNew = (props: UploadPageProps) => { +enum Phase { + SELECT, + EDIT, + FINISH +} + +export const UploadPageNew = () => { const dispatch = useDispatch() const [phase, setPhase] = useState(Phase.SELECT) - const [tracks, setTracks] = useState([]) + const [formState, setFormState] = useState({ + uploadType: undefined, + metadata: undefined, + tracks: undefined + }) + + const { tracks } = formState // Pretty print json just for testing useEffect(() => { @@ -64,11 +66,11 @@ export const UploadPageNew = (props: UploadPageProps) => { const pageTitle = useMemo(() => { switch (phase) { case Phase.EDIT: - return tracks.length > 1 + return tracks && tracks.length > 1 ? messages.editMultiTrackPageTitle : messages.editSingleTrackPageTitle case Phase.FINISH: - return tracks.length > 1 + return tracks && tracks.length > 1 ? messages.finishMultiTrackPageTitle : messages.finishSingleTrackPageTitle case Phase.SELECT: @@ -82,39 +84,48 @@ export const UploadPageNew = (props: UploadPageProps) => { case Phase.SELECT: page = ( { + formState={formState} + onContinue={(formState: UploadFormState) => { + setFormState(formState) setPhase(Phase.EDIT) }} /> ) break case Phase.EDIT: - page = ( - { - setPhase(Phase.FINISH) - }} - /> - ) + if (formState.uploadType) { + page = ( + { + setFormState(formState) + setPhase(Phase.FINISH) + }} + /> + ) + } break case Phase.FINISH: - page = ( - { - setTracks([]) - setPhase(Phase.SELECT) - }} - /> - ) + if (formState.uploadType) { + page = ( + { + setFormState({ + tracks: undefined, + uploadType: undefined, + metadata: undefined + }) + setPhase(Phase.SELECT) + }} + /> + ) + } } const handleUpload = useCallback(() => { - console.log('Handling upload') + if (!formState.tracks) return + const { tracks } = formState const trackStems = tracks.reduce((acc, track) => { // @ts-ignore - This has stems in it sometimes acc = [...acc, ...(track.metadata.stems ?? [])] @@ -133,7 +144,7 @@ export const UploadPageNew = (props: UploadPageProps) => { trackStems ) ) - }, [dispatch, tracks]) + }, [dispatch, formState]) useEffect(() => { if (phase === Phase.FINISH) handleUpload() diff --git a/packages/web/src/pages/upload-page/components/FinishPageNew.tsx b/packages/web/src/pages/upload-page/components/FinishPageNew.tsx index 51d0bd2ec5..f1aaf402bd 100644 --- a/packages/web/src/pages/upload-page/components/FinishPageNew.tsx +++ b/packages/web/src/pages/upload-page/components/FinishPageNew.tsx @@ -28,9 +28,10 @@ import { Tile } from 'components/tile' import { Text } from 'components/typography' import { profilePage } from 'utils/route' +import { CollectionFormState, TrackFormState, TrackForUpload } from '../types' + import styles from './FinishPage.module.css' import { ShareBannerNew } from './ShareBannerNew' -import { TrackForUpload } from './types' const { getAccountUser } = accountSelectors const { getUploadPercentage } = uploadSelectors @@ -93,12 +94,13 @@ const UploadTrackItem = (props: UploadTrackItemProps) => { } type FinishPageProps = { - tracks: TrackForUpload[] + formState: TrackFormState | CollectionFormState onContinue: () => void } export const FinishPageNew = (props: FinishPageProps) => { - const { tracks, onContinue } = props + const { formState, onContinue } = props + const { tracks } = formState const accountUser = useSelector(getAccountUser) const upload = useSelector((state: CommonState) => state.upload) const user = useSelector(getAccountUser) diff --git a/packages/web/src/pages/upload-page/components/SelectPageNew.tsx b/packages/web/src/pages/upload-page/components/SelectPageNew.tsx index d9de846b45..f6aa38712f 100644 --- a/packages/web/src/pages/upload-page/components/SelectPageNew.tsx +++ b/packages/web/src/pages/upload-page/components/SelectPageNew.tsx @@ -5,7 +5,8 @@ import { playerActions, playerSelectors, UploadType, - removeNullable + removeNullable, + newCollectionMetadata } from '@audius/common' import { Button, ButtonType, IconArrow } from '@audius/stems' import cn from 'classnames' @@ -16,10 +17,10 @@ import { Dropzone } from 'components/upload/Dropzone' import InvalidFileType from 'components/upload/InvalidFileType' import { processFiles } from '../store/utils/processFiles' +import { UploadFormState } from '../types' import styles from './SelectPage.module.css' import TracksPreview from './TracksPreview' -import { TrackForUpload } from './types' const { pause } = playerActions const { getPlaying } = playerSelectors @@ -27,22 +28,21 @@ const { getPlaying } = playerSelectors type ErrorType = { reason: 'size' | 'type' } | null type SelectPageProps = { - tracks: TrackForUpload[] - setTracks: (tracks: TrackForUpload[]) => void - onContinue: () => void + formState: UploadFormState + onContinue: (formState: UploadFormState) => void } export const SelectPageNew = (props: SelectPageProps) => { - const { tracks, setTracks, onContinue } = props + const { formState, onContinue } = props const dispatch = useDispatch() const playing = useSelector(getPlaying) + const [tracks, setTracks] = useState(formState.tracks ?? []) + const [uploadType, setUploadType] = useState( + formState.uploadType ?? UploadType.INDIVIDUAL_TRACK + ) const [uploadTrackError, setUploadTrackError] = useState>(null) - // TODO: support upload types when directing to next page - const [uploadType, setUploadType] = useState( - UploadType.INDIVIDUAL_TRACK - ) const [previewIndex, setPreviewIndex] = useState(-1) const [preview, setPreview] = useState>() @@ -72,14 +72,14 @@ export const SelectPageNew = (props: SelectPageProps) => { if ( uploadType === UploadType.INDIVIDUAL_TRACK && - tracks.length + processedTracks.length > 1 + selectedFiles.length + processedTracks.length > 1 ) { setUploadType(UploadType.INDIVIDUAL_TRACKS) } setTracks([...tracks, ...processedTracks]) }, - [setTracks, tracks, uploadType] + [setTracks, tracks, uploadType, setUploadType] ) const onRemoveTrack = useCallback( @@ -89,7 +89,7 @@ export const SelectPageNew = (props: SelectPageProps) => { tracks.length === 2 ? UploadType.INDIVIDUAL_TRACK : uploadType ) }, - [setTracks, tracks, uploadType] + [setTracks, setUploadType, tracks, uploadType] ) const stopPreview = useCallback(() => { @@ -121,6 +121,21 @@ export const SelectPageNew = (props: SelectPageProps) => { stopPreview() }) + const handleContinue = useCallback(() => { + if (uploadType !== undefined && tracks) { + switch (uploadType) { + case UploadType.INDIVIDUAL_TRACK: + case UploadType.INDIVIDUAL_TRACKS: + onContinue({ tracks, uploadType }) + break + case UploadType.ALBUM: + case UploadType.PLAYLIST: + onContinue({ tracks, uploadType, metadata: newCollectionMetadata() }) + break + } + } + }, [onContinue, tracks, uploadType]) + const textAboveIcon = tracks.length > 0 ? 'More to Upload?' : undefined return (
@@ -165,7 +180,7 @@ export const SelectPageNew = (props: SelectPageProps) => { text='Continue' name='continue' rightIcon={} - onClick={onContinue} + onClick={handleContinue} textClassName={styles.continueButtonText} className={styles.continueButton} /> diff --git a/packages/web/src/pages/upload-page/components/types.ts b/packages/web/src/pages/upload-page/components/types.ts deleted file mode 100644 index f622265888..0000000000 --- a/packages/web/src/pages/upload-page/components/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TrackMetadata } from '@audius/common' - -export type TrackForUpload = { - file: File - preview: HTMLAudioElement - metadata: TrackMetadata -} diff --git a/packages/web/src/pages/upload-page/pages/UploadCollectionForm.module.css b/packages/web/src/pages/upload-page/fields/CollectionTrackField.module.css similarity index 62% rename from packages/web/src/pages/upload-page/pages/UploadCollectionForm.module.css rename to packages/web/src/pages/upload-page/fields/CollectionTrackField.module.css index 01efed7307..556e0a537c 100644 --- a/packages/web/src/pages/upload-page/pages/UploadCollectionForm.module.css +++ b/packages/web/src/pages/upload-page/fields/CollectionTrackField.module.css @@ -1,45 +1,4 @@ .root { - display: inline-flex; - flex-direction: column; - gap: var(--unit-2); -} - -.collectionFields { - display: flex; - padding: var(--unit-4); - flex-direction: column; - gap: var(--unit-4); -} - -.row { - display: flex; - gap: var(--unit-4); -} - -.collectionInfo { - display: flex; - flex-grow: 1; - flex-direction: column; - gap: var(--unit-4); -} - -.artwork { - height: 248px; - width: 248px; -} - -.description { - flex-grow: 1; -} - -.trackDetails { - display: flex; - padding: var(--unit-4) var(--unit-6); - flex-direction: column; - gap: var(--unit-4); -} - -.trackField { display: flex; padding: var(--unit-4); margin-bottom: var(--unit-2); diff --git a/packages/web/src/pages/upload-page/pages/CollectionTrackField.tsx b/packages/web/src/pages/upload-page/fields/CollectionTrackField.tsx similarity index 94% rename from packages/web/src/pages/upload-page/pages/CollectionTrackField.tsx rename to packages/web/src/pages/upload-page/fields/CollectionTrackField.tsx index ac229b3d26..796a8665ec 100644 --- a/packages/web/src/pages/upload-page/pages/CollectionTrackField.tsx +++ b/packages/web/src/pages/upload-page/fields/CollectionTrackField.tsx @@ -21,7 +21,7 @@ import { SelectMoodField } from '../fields/SelectMoodField' import { TrackNameField } from '../fields/TrackNameField' import { CollectionTrackForUpload } from '../types' -import styles from './UploadCollectionForm.module.css' +import styles from './CollectionTrackField.module.css' const messages = { overrideLabel: 'Override details for this track', @@ -62,11 +62,7 @@ export const CollectionTrackField = (props: CollectionTrackFieldProps) => { }, [remove, index]) return ( - +
diff --git a/packages/web/src/pages/upload-page/pages/CollectionTrackFieldArray.tsx b/packages/web/src/pages/upload-page/fields/CollectionTrackFieldArray.tsx similarity index 100% rename from packages/web/src/pages/upload-page/pages/CollectionTrackFieldArray.tsx rename to packages/web/src/pages/upload-page/fields/CollectionTrackFieldArray.tsx diff --git a/packages/web/src/pages/upload-page/forms/EditCollectionForm.module.css b/packages/web/src/pages/upload-page/forms/EditCollectionForm.module.css new file mode 100644 index 0000000000..d16054c832 --- /dev/null +++ b/packages/web/src/pages/upload-page/forms/EditCollectionForm.module.css @@ -0,0 +1,40 @@ +.root { + display: inline-flex; + flex-direction: column; + gap: var(--unit-2); +} + +.collectionFields { + display: flex; + padding: var(--unit-4); + flex-direction: column; + gap: var(--unit-4); +} + +.row { + display: flex; + gap: var(--unit-4); +} + +.collectionInfo { + display: flex; + flex-grow: 1; + flex-direction: column; + gap: var(--unit-4); +} + +.artwork { + height: 248px; + width: 248px; +} + +.description { + flex-grow: 1; +} + +.trackDetails { + display: flex; + padding: var(--unit-4) var(--unit-6); + flex-direction: column; + gap: var(--unit-4); +} diff --git a/packages/web/src/pages/upload-page/pages/UploadCollectionForm.tsx b/packages/web/src/pages/upload-page/forms/EditCollectionForm.tsx similarity index 52% rename from packages/web/src/pages/upload-page/pages/UploadCollectionForm.tsx rename to packages/web/src/pages/upload-page/forms/EditCollectionForm.tsx index 97956c9d15..2e10598d20 100644 --- a/packages/web/src/pages/upload-page/pages/UploadCollectionForm.tsx +++ b/packages/web/src/pages/upload-page/forms/EditCollectionForm.tsx @@ -1,9 +1,10 @@ import { useCallback } from 'react' -import { EditPlaylistValues, Nullable } from '@audius/common' +import { UploadType } from '@audius/common' +import { HarmonyButton, IconUpload } from '@audius/stems' import { Form, Formik } from 'formik' -import { capitalize } from 'lodash' import moment from 'moment' +import { toFormikValidationSchema } from 'zod-formik-adapter' import { ArtworkField, @@ -14,14 +15,14 @@ import { import { Tile } from 'components/tile' import { Text } from 'components/typography' -import { TrackForUpload } from '../components/types' +import { CollectionTrackFieldArray } from '../fields/CollectionTrackFieldArray' import { ReleaseDateField } from '../fields/ReleaseDateField' import { SelectGenreField } from '../fields/SelectGenreField' import { SelectMoodField } from '../fields/SelectMoodField' -import { CollectionTrackForUpload } from '../types' +import { CollectionFormState } from '../types' +import { AlbumSchema, CollectionValues, PlaylistSchema } from '../validation' -import { CollectionTrackFieldArray } from './CollectionTrackFieldArray' -import styles from './UploadCollectionForm.module.css' +import styles from './EditCollectionForm.module.css' const messages = { name: 'Name', @@ -30,68 +31,76 @@ const messages = { title: 'Track Details', description: "Set defaults for all tracks in this collection. Use 'Override' to personalize individual track details." - } -} - -type UploadCollectionFormProps = { - collectionType: 'album' | 'playlist' - tracks: TrackForUpload[] - onSubmit: () => void + }, + completeButton: 'Complete Upload' } -type CollectionValues = Pick< - EditPlaylistValues, - 'artwork' | 'playlist_name' | 'description' -> & { - releaseDate: string - trackDetails: { - genre: Nullable - mood: Nullable - tags: string - } - tracks: CollectionTrackForUpload[] +type EditCollectionFormProps = { + formState: CollectionFormState + onContinue: (formState: CollectionFormState) => void } -export const UploadCollectionForm = (props: UploadCollectionFormProps) => { - const { collectionType, tracks, onSubmit } = props +export const EditCollectionForm = (props: EditCollectionFormProps) => { + const { formState, onContinue } = props + const { tracks, uploadType, metadata } = formState const initialValues: CollectionValues = { - artwork: { url: '' }, + ...metadata, + is_album: uploadType === UploadType.ALBUM, + artwork: null, playlist_name: '', description: '', - releaseDate: moment().startOf('day').toString(), + releaseDate: moment().startOf('day').toDate(), trackDetails: { genre: null, mood: null, tags: '' }, + // @ts-expect-error issues with track schema tracks: tracks.map((track) => ({ ...track, override: false })) } const handleSubmit = useCallback( (values: CollectionValues) => { - onSubmit() + const { + tracks, + trackDetails: ignoredTrackDetails, + ...collectionMetadata + } = values + + // @ts-expect-error more issues with tracks + onContinue({ uploadType, tracks, metadata: collectionMetadata }) }, - [onSubmit] + [onContinue, uploadType] ) + const collectionTypeName = + uploadType === UploadType.ALBUM ? 'Album' : 'Playlist' + + const validationSchema = + uploadType === UploadType.ALBUM ? AlbumSchema : PlaylistSchema + return ( - +
- +
{
+ ) diff --git a/packages/web/src/pages/upload-page/components/EditPageNew.module.css b/packages/web/src/pages/upload-page/forms/EditTrackForm.module.css similarity index 100% rename from packages/web/src/pages/upload-page/components/EditPageNew.module.css rename to packages/web/src/pages/upload-page/forms/EditTrackForm.module.css diff --git a/packages/web/src/pages/upload-page/components/EditPageNew.tsx b/packages/web/src/pages/upload-page/forms/EditTrackForm.tsx similarity index 57% rename from packages/web/src/pages/upload-page/components/EditPageNew.tsx rename to packages/web/src/pages/upload-page/forms/EditTrackForm.tsx index 355e8db323..2ddb7b4fb2 100644 --- a/packages/web/src/pages/upload-page/components/EditPageNew.tsx +++ b/packages/web/src/pages/upload-page/forms/EditTrackForm.tsx @@ -1,13 +1,5 @@ import { useCallback, useMemo } from 'react' -import { - Genre, - HashId, - Mood, - PremiumConditionsFollowUserId, - PremiumConditionsNFTCollection, - PremiumConditionsTipUserId -} from '@audius/sdk' import { HarmonyButton, HarmonyButtonType, @@ -33,10 +25,10 @@ import { RemixSettingsField } from '../fields/RemixSettingsField' import { SourceFilesField } from '../fields/SourceFilesField' import { TrackMetadataFields } from '../fields/TrackMetadataFields' import { defaultHiddenFields } from '../fields/availability/HiddenAvailabilityFields' -import { TrackEditFormValues } from '../types' +import { TrackEditFormValues, TrackFormState } from '../types' +import { TrackMetadataSchema } from '../validation' -import styles from './EditPageNew.module.css' -import { TrackForUpload } from './types' +import styles from './EditTrackForm.module.css' const messages = { titleError: 'Your track must have a name', @@ -45,118 +37,21 @@ const messages = { multiTrackCount: (index: number, total: number) => `TRACK ${index} of ${total}`, prev: 'Prev', - next: 'Next Track', - titleRequiredError: 'Your track must have a name', - artworkRequiredError: 'Artwork is required', - genreRequiredError: 'Genre is required', - invalidReleaseDateError: 'Release date should no be in the future' -} - -type EditPageProps = { - tracks: TrackForUpload[] - setTracks: (tracks: TrackForUpload[]) => void - onContinue: () => void + next: 'Next Track' } -// TODO: KJ - Need to update the schema in sdk and then import here -const createUploadTrackMetadataSchema = () => - z.object({ - aiAttributionUserId: z.optional(HashId), - description: z.optional(z.string().max(1000)), - download: z.optional( - z - .object({ - cid: z.optional(z.string()), - isDownloadable: z.boolean(), - requiresFollow: z.boolean() - }) - .strict() - .nullable() - ), - fieldVisibility: z.optional( - z.object({ - mood: z.optional(z.boolean()), - tags: z.optional(z.boolean()), - genre: z.optional(z.boolean()), - share: z.optional(z.boolean()), - playCount: z.optional(z.boolean()), - remixes: z.optional(z.boolean()) - }) - ), - genre: z - .enum(Object.values(Genre) as [Genre, ...Genre[]]) - .nullable() - .refine((val) => val !== null, { - message: messages.genreRequiredError - }), - isPremium: z.optional(z.boolean()), - isrc: z.optional(z.string().nullable()), - isUnlisted: z.optional(z.boolean()), - iswc: z.optional(z.string().nullable()), - license: z.optional(z.string().nullable()), - mood: z - .optional(z.enum(Object.values(Mood) as [Mood, ...Mood[]])) - .nullable(), - premiumConditions: z.optional( - z.union([ - PremiumConditionsNFTCollection, - PremiumConditionsFollowUserId, - PremiumConditionsTipUserId - ]) - ), - releaseDate: z.optional( - z.date().max(new Date(), { message: messages.invalidReleaseDateError }) - ), - remixOf: z.optional( - z - .object({ - tracks: z - .array( - z.object({ - parentTrackId: HashId - }) - ) - .min(1) - }) - .strict() - ), - tags: z.optional(z.string()), - title: z.string({ - required_error: messages.titleRequiredError - }), - previewStartSeconds: z.optional(z.number()), - audioUploadId: z.optional(z.string()), - previewCid: z.optional(z.string()) - }) - -const createTrackMetadataSchema = () => { - return createUploadTrackMetadataSchema() - .merge( - z.object({ - artwork: z - .object({ - url: z.string() - }) - .nullable() - }) - ) - .refine((form) => form.artwork !== null, { - message: messages.artworkRequiredError, - path: ['artwork'] - }) +type EditTrackFormProps = { + formState: TrackFormState + onContinue: (formState: TrackFormState) => void } -export type TrackMetadataValues = z.input< - ReturnType -> - -const EditFormValidationSchema = () => - z.object({ - trackMetadatas: z.array(createTrackMetadataSchema()) - }) +const EditFormValidationSchema = z.object({ + trackMetadatas: z.array(TrackMetadataSchema) +}) -export const EditPageNew = (props: EditPageProps) => { - const { tracks, setTracks, onContinue } = props +export const EditTrackForm = (props: EditTrackFormProps) => { + const { formState, onContinue } = props + const { tracks } = formState // @ts-ignore - Slight differences in the sdk vs common track metadata types const initialValues: TrackEditFormValues = useMemo( @@ -185,24 +80,27 @@ export const EditPageNew = (props: EditPageProps) => { const onSubmit = useCallback( (values: TrackEditFormValues) => { - const tracksForUpload: TrackForUpload[] = tracks.map((track, i) => ({ - ...track, - metadata: values.trackMetadatas[i] - })) - setTracks(tracksForUpload) - onContinue() + const tracksForUpload = tracks.map((track, i) => { + const metadata = values.trackMetadatas[i] + const { licenseType: ignoredLicenseType, ...restMetadata } = metadata + return { + ...track, + metadata: { ...restMetadata } + } + }) + onContinue({ ...formState, tracks: tracksForUpload }) }, - [onContinue, setTracks, tracks] + [formState, onContinue, tracks] ) return ( initialValues={initialValues} onSubmit={onSubmit} - // @ts-ignore - There are slight mismatches between the sdk and common track metadata types - validationSchema={toFormikValidationSchema(EditFormValidationSchema())} + // @ts-expect-error issue with track types + validationSchema={toFormikValidationSchema(EditFormValidationSchema)} > - {TrackEditForm} + {(props) => } ) } diff --git a/packages/web/src/pages/upload-page/pages/EditPage.tsx b/packages/web/src/pages/upload-page/pages/EditPage.tsx new file mode 100644 index 0000000000..104d09018f --- /dev/null +++ b/packages/web/src/pages/upload-page/pages/EditPage.tsx @@ -0,0 +1,24 @@ +import { UploadType } from '@audius/common' + +import { EditCollectionForm } from '../forms/EditCollectionForm' +import { EditTrackForm } from '../forms/EditTrackForm' +import { CollectionFormState, TrackFormState, UploadFormState } from '../types' + +type EditPageProps = { + formState: TrackFormState | CollectionFormState + onContinue: (formState: UploadFormState) => void +} + +export const EditPage = (props: EditPageProps) => { + const { formState, onContinue } = props + switch (formState.uploadType) { + case UploadType.INDIVIDUAL_TRACK: + case UploadType.INDIVIDUAL_TRACKS: + return + case UploadType.ALBUM: + case UploadType.PLAYLIST: + return ( + + ) + } +} diff --git a/packages/web/src/pages/upload-page/types.ts b/packages/web/src/pages/upload-page/types.ts index 2cb60ee517..80e4a8c842 100644 --- a/packages/web/src/pages/upload-page/types.ts +++ b/packages/web/src/pages/upload-page/types.ts @@ -1,9 +1,37 @@ -import { ExtendedTrackMetadata, Nullable } from '@audius/common' +import { Nullable, TrackMetadata, UploadType } from '@audius/common' import moment from 'moment' -import { TrackForUpload } from './components/types' +import { CollectionValues } from './validation' -export type SingleTrackEditValues = ExtendedTrackMetadata & { +export type TrackForUpload = { + file: File + preview: HTMLAudioElement + metadata: TrackMetadata +} + +export type InitialFormState = { + uploadType: undefined + tracks: undefined + metadata: undefined +} + +export type TrackFormState = { + uploadType: UploadType.INDIVIDUAL_TRACK | UploadType.INDIVIDUAL_TRACKS + tracks: TrackForUpload[] +} + +export type CollectionFormState = { + uploadType: UploadType.ALBUM | UploadType.PLAYLIST + tracks: TrackForUpload[] + metadata: CollectionValues +} + +export type UploadFormState = + | TrackFormState + | CollectionFormState + | InitialFormState + +export type SingleTrackEditValues = TrackMetadata & { releaseDate: moment.Moment licenseType: { allowAttribution: Nullable diff --git a/packages/web/src/pages/upload-page/validation.ts b/packages/web/src/pages/upload-page/validation.ts new file mode 100644 index 0000000000..9ad577df45 --- /dev/null +++ b/packages/web/src/pages/upload-page/validation.ts @@ -0,0 +1,150 @@ +import { + Genre, + HashId, + Mood, + PremiumConditionsFollowUserId, + PremiumConditionsNFTCollection, + PremiumConditionsTipUserId +} from '@audius/sdk' +import { z } from 'zod' + +const messages = { + invalidReleaseDateError: 'Release date should not be in the future', + artworkRequiredError: 'Artwork is required', + genreRequiredError: 'Genre is required', + track: { + titleRequiredError: 'Your track must have a name' + }, + playlist: { + nameRequiredError: 'Your playlist must have a name' + }, + album: { + nameRequiredError: 'Your album must have a name' + } +} + +const GenreSchema = z + .enum(Object.values(Genre) as [Genre, ...Genre[]]) + .nullable() + .refine((val) => val !== null, { + message: messages.genreRequiredError + }) + +const MoodSchema = z + .optional(z.enum(Object.values(Mood) as [Mood, ...Mood[]])) + .nullable() + +// TODO: KJ - Need to update the schema in sdk and then import here +const SdkTrackMetadataSchema = z.object({ + aiAttributionUserId: z.optional(HashId), + description: z.optional(z.string().max(1000)), + download: z.optional( + z + .object({ + cid: z.optional(z.string()), + isDownloadable: z.boolean(), + requiresFollow: z.boolean() + }) + .strict() + .nullable() + ), + fieldVisibility: z.optional( + z.object({ + mood: z.optional(z.boolean()), + tags: z.optional(z.boolean()), + genre: z.optional(z.boolean()), + share: z.optional(z.boolean()), + playCount: z.optional(z.boolean()), + remixes: z.optional(z.boolean()) + }) + ), + genre: GenreSchema, + isPremium: z.optional(z.boolean()), + isrc: z.optional(z.string().nullable()), + isUnlisted: z.optional(z.boolean()), + iswc: z.optional(z.string().nullable()), + license: z.optional(z.string().nullable()), + mood: MoodSchema, + premiumConditions: z.optional( + z.union([ + PremiumConditionsNFTCollection, + PremiumConditionsFollowUserId, + PremiumConditionsTipUserId + ]) + ), + releaseDate: z.optional( + z.date().max(new Date(), { message: messages.invalidReleaseDateError }) + ), + remixOf: z.optional( + z + .object({ + tracks: z + .array( + z.object({ + parentTrackId: HashId + }) + ) + .min(1) + }) + .strict() + ), + tags: z.optional(z.string()), + title: z.string({ + required_error: messages.track.titleRequiredError + }), + previewStartSeconds: z.optional(z.number()), + audioUploadId: z.optional(z.string()), + previewCid: z.optional(z.string()) +}) + +export const TrackMetadataSchema = SdkTrackMetadataSchema.merge( + z.object({ + artwork: z + .object({ + url: z.string() + }) + .nullable() + }) +).refine((form) => form.artwork !== null, { + message: messages.artworkRequiredError, + path: ['artwork'] +}) + +export type TrackMetadata = z.input + +const createCollectionSchema = (collectionType: 'playlist' | 'album') => + z.object({ + artwork: z + .object({ + url: z.string() + }) + .nullable() + + .refine((artwork) => artwork !== null, { + message: messages.artworkRequiredError + }), + playlist_name: z.string({ + required_error: messages[collectionType].nameRequiredError + }), + description: z.optional(z.string().max(1000)), + releaseDate: z.optional( + z.coerce + .date() + .max(new Date(), { message: messages.invalidReleaseDateError }) + ), + trackDetails: z.object({ + genre: GenreSchema, + mood: MoodSchema, + tags: z.optional(z.string()) + }), + is_album: z.literal(collectionType === 'album'), + tracks: z.array(TrackMetadataSchema) + }) + +export const PlaylistSchema = createCollectionSchema('playlist') +export type PlaylistValues = z.input + +export const AlbumSchema = createCollectionSchema('album') +export type AlbumValues = z.input + +export type CollectionValues = PlaylistValues | AlbumValues