diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index 9105800b62..45f80de4cf 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -575,7 +575,8 @@ export enum ShareSource { PAGE = 'page', NOW_PLAYING = 'now playing', OVERFLOW = 'overflow', - LEFT_NAV = 'left-nav' + LEFT_NAV = 'left-nav', + UPLOAD = 'upload' } export enum RepostSource { TILE = 'tile', diff --git a/packages/common/src/store/upload/selectors.ts b/packages/common/src/store/upload/selectors.ts index df35eb1a92..9fad0c9447 100644 --- a/packages/common/src/store/upload/selectors.ts +++ b/packages/common/src/store/upload/selectors.ts @@ -6,3 +6,16 @@ export const getUploadProgress = (state: CommonState) => export const getUploadSuccess = (state: CommonState) => state.upload.success export const getTracks = (state: CommonState) => state.upload.tracks export const getIsUploading = (state: CommonState) => state.upload.uploading + +export const getUploadPercentage = (state: CommonState) => { + const uploadProgress = getUploadProgress(state) + const fullProgress = uploadProgress + ? uploadProgress.reduce((acc, progress) => acc + progress.loaded, 0) + : 0 + + const totalProgress = uploadProgress + ? uploadProgress.reduce((acc, progress) => acc + progress.total, 0) + : 1 + + return (fullProgress / totalProgress) * 100 +} diff --git a/packages/common/src/store/upload/types.ts b/packages/common/src/store/upload/types.ts index 5a71359bb4..9e1877775c 100644 --- a/packages/common/src/store/upload/types.ts +++ b/packages/common/src/store/upload/types.ts @@ -41,7 +41,8 @@ export interface ExtendedCollectionMetadata extends CollectionMetadata { export enum ProgressStatus { UPLOADING = 'UPLOADING', PROCESSING = 'PROCESSING', - COMPLETE = 'COMPLETE' + COMPLETE = 'COMPLETE', + ERROR = 'ERROR' } export type Progress = { diff --git a/packages/web/src/pages/upload-page/UploadPageNew.tsx b/packages/web/src/pages/upload-page/UploadPageNew.tsx index fd3c96fa65..a04d4db7b7 100644 --- a/packages/web/src/pages/upload-page/UploadPageNew.tsx +++ b/packages/web/src/pages/upload-page/UploadPageNew.tsx @@ -1,15 +1,19 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' -import { UploadType } from '@audius/common' +import { uploadActions, UploadType } 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' +const { uploadTracks } = uploadActions + type UploadPageProps = { uploadType: UploadType } @@ -20,7 +24,16 @@ enum Phase { FINISH } +const messages = { + selectPageTitle: 'Upload Your Music', + editSingleTrackPageTitle: 'Complete Your Track', + editMultiTrackPageTitle: 'Complete Your Tracks', + finishSingleTrackPageTitle: 'Uploading Your Track', + finishMultiTrackPageTitle: 'Uploading Your Tracks' +} + export const UploadPageNew = (props: UploadPageProps) => { + const dispatch = useDispatch() const [phase, setPhase] = useState(Phase.SELECT) const [tracks, setTracks] = useState([]) @@ -48,6 +61,22 @@ export const UploadPageNew = (props: UploadPageProps) => { injectPrettifyScript() }, [phase]) + const pageTitle = useMemo(() => { + switch (phase) { + case Phase.EDIT: + return tracks.length > 1 + ? messages.editMultiTrackPageTitle + : messages.editSingleTrackPageTitle + case Phase.FINISH: + return tracks.length > 1 + ? messages.finishMultiTrackPageTitle + : messages.finishSingleTrackPageTitle + case Phase.SELECT: + default: + return messages.selectPageTitle + } + }, [phase, tracks]) + let page switch (phase) { case Phase.SELECT: @@ -55,7 +84,9 @@ export const UploadPageNew = (props: UploadPageProps) => { setPhase(Phase.EDIT)} + onContinue={() => { + setPhase(Phase.EDIT) + }} /> ) break @@ -64,20 +95,56 @@ export const UploadPageNew = (props: UploadPageProps) => { setPhase(Phase.FINISH)} + onContinue={() => { + setPhase(Phase.FINISH) + }} /> ) break case Phase.FINISH: - console.log(tracks[0]) - page =
{JSON.stringify(tracks, null, 2)}
+ page = ( + { + setTracks([]) + setPhase(Phase.SELECT) + }} + /> + ) } + + const handleUpload = useCallback(() => { + console.log('Handling upload') + const trackStems = tracks.reduce((acc, track) => { + // @ts-ignore - This has stems in it sometimes + acc = [...acc, ...(track.metadata.stems ?? [])] + return acc + }, []) + + dispatch( + uploadTracks( + // @ts-ignore - This has artwork on it + tracks, + // NOTE: Need to add metadata for collections here for collection upload + undefined, + tracks.length > 1 + ? UploadType.INDIVIDUAL_TRACKS + : UploadType.INDIVIDUAL_TRACK, + trackStems + ) + ) + }, [dispatch, tracks]) + + useEffect(() => { + if (phase === Phase.FINISH) handleUpload() + }, [handleUpload, phase]) + return ( } + header={
} > {page} diff --git a/packages/web/src/pages/upload-page/components/EditPageNew.tsx b/packages/web/src/pages/upload-page/components/EditPageNew.tsx index 3bb6aa1d10..85d36316ec 100644 --- a/packages/web/src/pages/upload-page/components/EditPageNew.tsx +++ b/packages/web/src/pages/upload-page/components/EditPageNew.tsx @@ -66,7 +66,7 @@ const createUploadTrackMetadataSchema = () => download: z.optional( z .object({ - cid: z.string(), + cid: z.optional(z.string()), isDownloadable: z.boolean(), requiresFollow: z.boolean() }) diff --git a/packages/web/src/pages/upload-page/components/FinishPage.module.css b/packages/web/src/pages/upload-page/components/FinishPage.module.css index ef5f279bf1..3ea0267c23 100644 --- a/packages/web/src/pages/upload-page/components/FinishPage.module.css +++ b/packages/web/src/pages/upload-page/components/FinishPage.module.css @@ -36,6 +36,7 @@ height: 12px; width: 12px; } + .iconArrow path { fill: var(--static-neutral); } @@ -90,3 +91,81 @@ .iconVerified { margin-left: 4px; } + +.page { + display: flex; + flex-direction: column; + gap: 32px; +} + +.uploadProgress { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.uploadProgressBar { + height: 16px; +} + +.uploadHeader { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + background-color: var(--background-surface-1); + border-bottom: 1px solid var(--border-strong); +} + +.headerInfo { + display: flex; + align-items: center; + justify-content: space-between; +} + +.headerProgressInfo { + display: flex; + gap: 16px; + align-items: center; +} + +.uploadFooter { + display: flex; + justify-content: space-between; + padding: 16px; + background-color: var(--background-surface-1); + border-top: 1px solid var(--border-strong); +} + +.uploadTrackList { + display: flex; + flex-direction: column; + padding: 16px; +} + +.uploadTrackItem { + display: flex; + gap: 12px; + align-items: center; + padding: 8px; +} + +.trackItemArtwork { + height: 40px; + width: 40px; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border-strong); +} + +.emptyProgressIndicator { + height: 16px; + width: 16px; + border-radius: 100%; + border: 2px solid var(--neutral); +} + +.progressIndicator { + height: 16px; + width: 16px; +} diff --git a/packages/web/src/pages/upload-page/components/FinishPageNew.tsx b/packages/web/src/pages/upload-page/components/FinishPageNew.tsx new file mode 100644 index 0000000000..51d0bd2ec5 --- /dev/null +++ b/packages/web/src/pages/upload-page/components/FinishPageNew.tsx @@ -0,0 +1,200 @@ +import { useCallback, useMemo } from 'react' + +import { + accountSelectors, + CommonState, + imageBlank as placeholderArt, + Progress, + ProgressStatus, + uploadSelectors +} from '@audius/common' +import { + HarmonyButton, + HarmonyButtonSize, + HarmonyButtonType, + IconArrow, + IconError, + IconUpload, + IconValidationCheck, + ProgressBar +} from '@audius/stems' +import { push } from 'connected-react-router' +import { round } from 'lodash' +import { useDispatch, useSelector } from 'react-redux' + +import DynamicImage from 'components/dynamic-image/DynamicImage' +import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' +import { Tile } from 'components/tile' +import { Text } from 'components/typography' +import { profilePage } from 'utils/route' + +import styles from './FinishPage.module.css' +import { ShareBannerNew } from './ShareBannerNew' +import { TrackForUpload } from './types' + +const { getAccountUser } = accountSelectors +const { getUploadPercentage } = uploadSelectors + +const messages = { + uploadInProgress: 'Upload In Progress', + uploadComplete: 'Upload Complete', + uploadMore: 'Upload More', + visitProfile: 'Visit Your Profile' +} + +const ProgressIndicator = (props: { status?: ProgressStatus }) => { + const { status } = props + + switch (status) { + case ProgressStatus.UPLOADING: + case ProgressStatus.PROCESSING: + return + case ProgressStatus.COMPLETE: + return + case ProgressStatus.ERROR: + return + default: + return
+ } +} + +type UploadTrackItemProps = { + index: number + displayIndex?: boolean + track: TrackForUpload + trackProgress?: Progress + hasError: boolean +} + +const UploadTrackItem = (props: UploadTrackItemProps) => { + const { index, hasError, track, trackProgress, displayIndex = false } = props + // @ts-ignore - Artwork exists on track metadata object + const artworkUrl = track.metadata.artwork.url + + return ( +
+ + {displayIndex ? {index + 1} : null} + + {track.metadata.title} +
+ ) +} + +type FinishPageProps = { + tracks: TrackForUpload[] + onContinue: () => void +} + +export const FinishPageNew = (props: FinishPageProps) => { + const { tracks, onContinue } = props + const accountUser = useSelector(getAccountUser) + const upload = useSelector((state: CommonState) => state.upload) + const user = useSelector(getAccountUser) + const fullUploadPercent = useSelector(getUploadPercentage) + const dispatch = useDispatch() + + const uploadComplete = useMemo(() => { + if (!upload.uploadProgress) return false + return ( + upload.success && + upload.uploadProgress.reduce((acc, progress) => { + return acc && progress.status === ProgressStatus.COMPLETE + }, true) + ) + }, [upload]) + + const handleUploadMoreClick = useCallback(() => { + onContinue() + }, [onContinue]) + + const handleVisitProfileClick = useCallback(() => { + if (user) { + dispatch(push(profilePage(user.handle))) + } + }, [dispatch, user]) + + return ( +
+ {uploadComplete ? : null} + +
+
+ + {uploadComplete + ? messages.uploadComplete + : messages.uploadInProgress} + +
+ + {round(fullUploadPercent)}% + + +
+
+ {!uploadComplete ? ( + + ) : null} +
+
+ {tracks.map((track, idx) => { + const trackProgress = upload.uploadProgress?.[idx] + const trackError = upload.failedTrackIndices.find( + (index) => index === idx + ) + return ( + 1} + index={idx} + trackProgress={trackProgress} + hasError={trackError !== undefined} + /> + ) + })} +
+ {uploadComplete ? ( +
+ + +
+ ) : null} +
+
+ ) +} diff --git a/packages/web/src/pages/upload-page/components/ShareBanner.module.css b/packages/web/src/pages/upload-page/components/ShareBanner.module.css index 9af7ff79a4..9b1dcd07d8 100644 --- a/packages/web/src/pages/upload-page/components/ShareBanner.module.css +++ b/packages/web/src/pages/upload-page/components/ShareBanner.module.css @@ -132,3 +132,28 @@ flex-wrap: wrap; justify-content: center; } + +.containerNew { + display: flex; + flex-direction: column; + align-content: center; + align-items: center; + gap: 24px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 8px; + transition: all ease-in-out 0.2s; + margin-bottom: 0px; + overflow: hidden; + padding: 40px; +} + +.buttonContainerNew { + width: 100%; + max-width: 750px; + display: flex; + gap: 16px; + align-items: center; + justify-content: center; +} diff --git a/packages/web/src/pages/upload-page/components/ShareBannerNew.tsx b/packages/web/src/pages/upload-page/components/ShareBannerNew.tsx new file mode 100644 index 0000000000..f011058d6c --- /dev/null +++ b/packages/web/src/pages/upload-page/components/ShareBannerNew.tsx @@ -0,0 +1,95 @@ +import { useCallback, useContext } from 'react' + +import { Name, ShareSource, User, usersSocialActions } from '@audius/common' +import { Button, ButtonType, IconLink, IconTwitterBird } from '@audius/stems' +import { useDispatch } from 'react-redux' + +import backgroundPlaceholder from 'assets/img/1-Concert-3-1.jpg' +import { make, useRecord } from 'common/store/analytics/actions' +import { getTwitterShareText } from 'components/share-modal/utils' +import { ToastContext } from 'components/toast/ToastContext' +import { Text } from 'components/typography' +import { SHARE_TOAST_TIMEOUT_MILLIS } from 'utils/constants' +import { openTwitterLink } from 'utils/tweet' + +import styles from './ShareBanner.module.css' + +const { shareUser } = usersSocialActions + +const messages = { + uploadComplete: 'Your Upload is Complete!', + shareText: 'Share your profile with your fans', + copyProfileToast: 'Copied Link to Profile', + twitterButtonText: 'Twitter', + copyLinkButtonText: 'Copy Link' +} + +type ShareBannerProps = { + user: User +} + +export const ShareBannerNew = (props: ShareBannerProps) => { + const { user } = props + const dispatch = useDispatch() + const { toast } = useContext(ToastContext) + const record = useRecord() + + const handleTwitterShare = useCallback(async () => { + const { twitterText, link, analyticsEvent } = await getTwitterShareText({ + type: 'profile', + profile: user + }) + openTwitterLink(link, twitterText) + record( + make(Name.SHARE_TO_TWITTER, { + source: ShareSource.UPLOAD, + ...analyticsEvent + }) + ) + }, [record, user]) + + const handleCopyTrackLink = useCallback(() => { + dispatch(shareUser(user.user_id, ShareSource.UPLOAD)) + toast(messages.copyProfileToast, SHARE_TOAST_TIMEOUT_MILLIS) + }, [dispatch, toast, user.user_id]) + + return ( +
+ + {messages.uploadComplete} + + + {messages.shareText} + +
+
+
+ ) +}