diff --git a/apps/studio/src/pages/home/releases/ReleaseList.tsx b/apps/studio/src/pages/home/releases/ReleaseList.tsx new file mode 100644 index 00000000..f2dd66b1 --- /dev/null +++ b/apps/studio/src/pages/home/releases/ReleaseList.tsx @@ -0,0 +1,447 @@ +import React, { MouseEvent, useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { + Box, + Link, + Table, + TableBody, + TableContainer, + TableRow, + Typography, +} from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import LinkIcon from "@mui/icons-material/Link"; +import DeleteIcon from "@mui/icons-material/Delete"; + +import theme from "@newm-web/theme"; +import { + ActionMenu, + type ActionMenuItem, + ActionMenuTrigger, +} from "@newm-web/components"; +import { TableCell, TablePagination, Tooltip } from "@newm-web/elements"; +import { + getImageSrc, + resizeCloudinaryImage, + useWindowDimensions, +} from "@newm-web/utils"; +import { MintingStatus as MintingStatusType } from "@newm-web/types"; +import { NEWMCoverArtPlaceholderMin } from "@newm-web/assets"; + +import NoSongsYet from "./NoSongsYet"; +import { ErrorOccurredMintingStatuses, MintingStatus } from "./MintingStatus"; +import TableHead from "./Table/TableHead"; +import ReleaseDeletionHelp from "./ReleaseDeletionHelp"; +import { convertMillisecondsToSongFormat } from "../../../modules/song"; +import { NEWM_SUPPORT_LINK } from "../../../common"; + +// ! DO NOT USE IN PRODUCTION YET <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +// * Tentative type definition for releases. +// eslint-disable-next-line max-len +// * based on: https://github.com/projectNEWM/newm-server/blob/master/newm-server/src/main/kotlin/io/newm/server/features/song/model/Release.kt +// TODO: Move to a shared types file once we have the real data. +export interface Release { + archived?: boolean | null; + barcodeNumber?: string | null; + barcodeType?: string | null; + coverArtUrl?: string | null; + createdAt?: string | null; + errorMessage?: string | null; + genres?: string[] | null; + hasSubmittedForDistribution?: boolean | null; + id: string; + locked?: boolean | null; + mintingStatus?: MintingStatusType | null; + ownerId?: string | null; + publicationDate?: string | null; + releaseDate?: string | null; + releaseType?: string | null; + title?: string | null; + totalTracksLength?: number | null; +} + +interface ReleaseListProps { + query: string; + releaseData?: Release[]; + rowHeight?: number; + totalCountOfSongs?: number; +} + +// TODO: REmove once we have the real data. +const mockReleases: Release[] = [ + { + coverArtUrl: + // eslint-disable-next-line max-len + "https://res.cloudinary.com/newm/image/upload/w_40,h_40,c_fill,q_auto,f_auto/v1770039488/czjfak8zxdsspxf3ihfj.jpg", + genres: ["Pop"], + hasSubmittedForDistribution: false, + id: "release-1", + locked: false, + mintingStatus: MintingStatusType.SubmittedForDistribution, + title: "Indoor Kites", + }, + { + coverArtUrl: "", + genres: ["Rock"], + hasSubmittedForDistribution: true, + id: "release-2", + locked: false, + mintingStatus: MintingStatusType.Undistributed, + title: "The Major League", + }, + { + coverArtUrl: + // eslint-disable-next-line max-len + "https://res.cloudinary.com/newm/image/upload/w_40,h_40,c_fill,q_auto,f_auto/v1770039488/czjfak8zxdsspxf3ihfj.jpg", + genres: ["Alternative Rock", "Indie Rock", "Rock", "Alternative", "Indie"], + hasSubmittedForDistribution: true, + id: "release-3", + locked: true, + mintingStatus: MintingStatusType.Minted, + title: "Go!", + totalTracksLength: 460000, + }, + { + coverArtUrl: "", + errorMessage: "Distribution error", + genres: ["Pop"], + id: "release-4", + locked: true, + mintingStatus: MintingStatusType.DistributionException, + title: "All I Want", + }, + { + coverArtUrl: "", + errorMessage: "Declined", + genres: ["Electronic", "Instrumental"], + id: "release-5", + locked: true, + mintingStatus: MintingStatusType.Declined, + title: "Lafayette Blvd", + totalTracksLength: 220000, + }, +]; + +const getTooltipContent = (mintingStatus: MintingStatusType) => { + const isErrorMintingStatus = + ErrorOccurredMintingStatuses.includes(mintingStatus); + + let content: JSX.Element | string = ""; + + if (isErrorMintingStatus) { + content = ( + + An error has occurred. Please reach out via the{ " " } + event.stopPropagation() } + > + NEWM Support Portal + { " " } + for assistance distributing your release. + + ); + } else if (mintingStatus === MintingStatusType.Declined) { + content = + "One or more issues occurred resulting in your " + + "distribution being declined. Please check your " + + "email for information on how to correct and resubmit your release."; + } + + return content; +}; + +export default function ReleaseList({ + totalCountOfSongs, + query = "", + // TODO: Remove mock data once we have the real data. + releaseData = mockReleases, +}: ReleaseListProps) { + const navigate = useNavigate(); + const viewportHeight = useWindowDimensions()?.height; + + const [rowsPerPage, setRowsPerPage] = useState(1); + const [page, setPage] = useState(1); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [menuRelease, setMenuRelease] = useState(null); + + const headerHeight = 245; + const footerHeight = 40; + const bottomPadding = 30; + const rowHeight = 65; + let releasesToRequest = rowsPerPage; + + const filteredReleases = useMemo(() => { + if (!query) return releaseData; + const lowerQuery = query.toLowerCase(); + + return releaseData.filter((release) => + release.title?.toLowerCase().includes(lowerQuery) + ); + }, [query, releaseData]); + + // TODO(webStudioAlbumPhaseTwo): Use total count from the RTK query response. + const totalCountOfReleases = totalCountOfSongs ?? filteredReleases.length; + const lastRowOnPage = (page - 1) * rowsPerPage + rowsPerPage; + const totalPagesCount = Math.ceil(totalCountOfReleases / rowsPerPage); + const remainingRowsOnLastPage = totalCountOfReleases % rowsPerPage; + + // * Determines how many releases to request for the last page. + if (page === totalPagesCount) { + releasesToRequest = + remainingRowsOnLastPage > 0 ? remainingRowsOnLastPage : rowsPerPage; + } + + const pagedReleases = filteredReleases.slice( + (page - 1) * rowsPerPage, + (page - 1) * rowsPerPage + releasesToRequest + ); + + const handleRowClick = ( + event: MouseEvent, + releaseId: string + ) => { + event.stopPropagation(); + navigate(`/home/releases/${releaseId}`); + }; + + const handlePageChange = ( + _event: React.ChangeEvent, + page: number + ) => { + setPage(page); + }; + + const handleMenuOpen = + (release: Release) => (event: MouseEvent) => { + event.stopPropagation(); + setMenuAnchorEl(event.currentTarget); + setMenuRelease(release); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + setMenuRelease(null); + }; + + const actionMenuItems = useMemo>(() => { + if (!menuRelease) return []; + + const isReleaseEditable = !menuRelease.locked; + const isReleaseDeletable = !menuRelease.locked; + + const items: ActionMenuItem[] = [ + { + icon: isReleaseEditable ? ( + + ) : ( + + ), + id: "view-edit", + label: isReleaseEditable ? "Edit" : "View", + onClick: () => navigate(`/home/releases/${menuRelease.id}`), + }, + { + icon: , + id: "view-outlet-urls", + label: "View outlet URLs", + onClick: () => null, + }, + { + color: "danger", + disabled: !isReleaseDeletable, + icon: , + id: "delete", + label: "Delete", + // TODO: Implement delete functionality | Render the DeleteReleaseModal. + onClick: () => null, + tooltip: !isReleaseDeletable ? : undefined, + tooltipPlacement: "left", + }, + ]; + + return items; + }, [menuRelease, navigate]); + + useEffect(() => { + setPage(1); + }, [query]); + + useEffect(() => { + setMenuAnchorEl(null); + setMenuRelease(null); + }, [page, query, releaseData]); + + // sets the # of rows per page depending on viewport height + useEffect(() => { + if (viewportHeight) { + const rowsWithCurrentHeight = Math.abs( + Math.floor( + (viewportHeight - headerHeight - footerHeight - bottomPadding) / + rowHeight + ) + ); + + setRowsPerPage(rowsWithCurrentHeight ? rowsWithCurrentHeight : 1); + setPage(1); + } + }, [viewportHeight]); + + if (!filteredReleases.length && !query) { + return ; + } + + if (!pagedReleases.length) { + return No releases matched your search.; + } + + return ( + + + + + { pagedReleases.map((release) => { + return ( + handleRowClick(event, release.id) } + > + + + { release.coverArtUrl ? ( + Album cover + ) : ( + + ) } + + { release.title ?? "--" } + + + + + { release.mintingStatus ? ( + + + + + + ) : ( + "--" + ) } + + + { release.genres?.length ? ( + + + { release.genres.join(", ") } + + + ) : ( + "--" + ) } + + + { /* // ! Phase 1 → NO. OF TRACKS = 1; single-track until /v1/releases supports 'trackCount' */ } + 1 + + + { release.totalTracksLength + ? convertMillisecondsToSongFormat(release.totalTracksLength) + : "--:--" } + + + + + + ); + }) } + + + { totalCountOfReleases > pagedReleases.length && ( + + ) } +
+ + +
+ ); +} diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index f1fa5c0b..1790ab99 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -41,6 +41,7 @@ import referralBannerBackground from "./lib/images/referral-banner-background.we import { default as NEWMMonsterPercussion } from "./lib/images/NEWMMonsterPercussion.png"; import { default as NEWMMonsterRock } from "./lib/images/NEWMMonsterRock.png"; import { default as NEWMMonsterGuitar } from "./lib/images/NEWMMonsterGuitar.png"; +import { default as NEWMCoverArtPlaceholderMin } from "./lib/images/cover-art-placeholder-min.png"; export { AddImage, @@ -64,6 +65,7 @@ export { InfoCircleLine, InstagramLogo, ItunesLogo, + NEWMCoverArtPlaceholderMin, NEWMLogo, NEWMLogoSmInverse, NEWMMarketplaceLogo, diff --git a/packages/assets/src/lib/images/cover-art-placeholder-min.png b/packages/assets/src/lib/images/cover-art-placeholder-min.png new file mode 100644 index 00000000..78df285b Binary files /dev/null and b/packages/assets/src/lib/images/cover-art-placeholder-min.png differ diff --git a/packages/assets/src/lib/images/cover-art-placeholder.png b/packages/assets/src/lib/images/cover-art-placeholder.png new file mode 100644 index 00000000..328184e0 Binary files /dev/null and b/packages/assets/src/lib/images/cover-art-placeholder.png differ