confirmDeleteTrack(e, track)}
+ />
+ );
+};
+
+export default DeleteTrackActionIcon;
diff --git a/src/music-catalogue-ui/components/retailerEditor.js b/src/music-catalogue-ui/components/retailerEditor.js
index 7829469..da5bcdd 100644
--- a/src/music-catalogue-ui/components/retailerEditor.js
+++ b/src/music-catalogue-ui/components/retailerEditor.js
@@ -90,7 +90,7 @@ const RetailerEditor = ({ retailer, navigate, logout }) => {
);
}
- // If all's well, display a confirmation message. Otherwise, show an error
+ // If all's well, navigate back to the retailers page. Otherwise, show an error
if (updatedRetailer == null) {
const action = retailer.Id <= 0 ? "adding" : "updating";
setError(`An error occurred ${action} the retailer`);
diff --git a/src/music-catalogue-ui/components/trackEditor.js b/src/music-catalogue-ui/components/trackEditor.js
new file mode 100644
index 0000000..0ff2efc
--- /dev/null
+++ b/src/music-catalogue-ui/components/trackEditor.js
@@ -0,0 +1,167 @@
+import styles from "./trackEditor.module.css";
+import pages from "@/helpers/navigation";
+import FormInputField from "./formInputField";
+import { useState, useCallback } from "react";
+import { apiCreateTrack, apiUpdateTrack } from "@/helpers/apiTracks";
+
+const TrackEditor = ({ track, album, artist, navigate, logout }) => {
+ // Split the track's formatted duration on the ":"
+ let initialMinutes = null;
+ let initialSeconds = null;
+ if (track != null && track.formattedDuration != null) {
+ const elements = track.formattedDuration.split(":");
+ if (elements.length > 0) {
+ initialMinutes = elements[0];
+ initialSeconds = elements[1];
+ }
+ }
+
+ // Get initial values for the other properties
+ const initialTitle = track != null ? track.title : null;
+ const initialNumber = track != null ? track.number : null;
+
+ const [title, setTitle] = useState(initialTitle);
+ const [number, setNumber] = useState(initialNumber);
+ const [minutes, setMinutes] = useState(initialMinutes);
+ const [seconds, setSeconds] = useState(initialSeconds);
+ const [error, setError] = useState("");
+
+ /* Callback to save retailer details */
+ const saveTrack = useCallback(
+ async (e) => {
+ // Prevent the default action associated with the click event
+ e.preventDefault();
+
+ // Clear pre-existing errors
+ setError("");
+
+ try {
+ // Calculate the duration from the indiviidual minutes and seconds
+ // inputs
+ const durationMinutes = Number(minutes);
+ const durationSeconds = Number(seconds);
+ const duration = 1000 * (60 * durationMinutes + durationSeconds);
+
+ // Either add or update the track, depending on whether there's an
+ // existing track or not
+ let updatedTrack = null;
+ if (track == null) {
+ updatedTrack = await apiCreateTrack(
+ title,
+ number,
+ duration,
+ album.id,
+ logout
+ );
+ } else {
+ updatedTrack = await apiUpdateTrack(
+ track.id,
+ title,
+ number,
+ duration,
+ album.id,
+ logout
+ );
+ }
+
+ // If all's well, navigate back to the track list page. Otherwise, show an error
+ if (updatedTrack == null) {
+ const action = track.Id <= 0 ? "adding" : "updating";
+ setError(`An error occurred ${action} the track`);
+ } else {
+ navigate({
+ page: pages.tracks,
+ artist: artist,
+ album: album,
+ });
+ }
+ } catch (e) {
+ setError(
+ e.message
+ //"Error converting the supplied minutes and seconds to a duration"
+ );
+ }
+ },
+ [album, artist, track, title, number, minutes, seconds, navigate, logout]
+ );
+
+ return (
+ <>
+
+
{title}
+
+
+ >
+ );
+};
+
+export default TrackEditor;
diff --git a/src/music-catalogue-ui/components/trackEditor.module.css b/src/music-catalogue-ui/components/trackEditor.module.css
new file mode 100644
index 0000000..b54b52f
--- /dev/null
+++ b/src/music-catalogue-ui/components/trackEditor.module.css
@@ -0,0 +1,25 @@
+.trackEditorFormContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.trackEditorForm {
+ width: 80%;
+ padding-top: 20px;
+ padding-bottom: 20px;
+}
+
+.trackEditorButton {
+ margin-left: 10px;
+ float: right;
+}
+
+.trackEditorError {
+ font-weight: bold;
+ color: red;
+}
+
+.trackEditorGeocode {
+ margin-left: 10px;
+}
diff --git a/src/music-catalogue-ui/components/trackList.js b/src/music-catalogue-ui/components/trackList.js
index b2f5c0e..822b494 100644
--- a/src/music-catalogue-ui/components/trackList.js
+++ b/src/music-catalogue-ui/components/trackList.js
@@ -2,6 +2,7 @@ import { useCallback } from "react";
import useTracks from "@/hooks/useTracks";
import TrackRow from "./trackRow";
import pages from "@/helpers/navigation";
+import styles from "./trackList.module.css";
/**
* Component to render the list of tracks for the specified album
@@ -43,6 +44,7 @@ const TrackList = ({ artist, album, isWishList, navigate, logout }) => {
No. |
Track |
Duration |
+ |
@@ -54,6 +56,8 @@ const TrackList = ({ artist, album, isWishList, navigate, logout }) => {
track={t}
isWishList={isWishList}
navigate={navigate}
+ logout={logout}
+ setTracks={setTracks}
/>
))}
@@ -61,6 +65,20 @@ const TrackList = ({ artist, album, isWishList, navigate, logout }) => {
+
+
+
>
);
};
diff --git a/src/music-catalogue-ui/components/trackList.module.css b/src/music-catalogue-ui/components/trackList.module.css
new file mode 100644
index 0000000..c31a170
--- /dev/null
+++ b/src/music-catalogue-ui/components/trackList.module.css
@@ -0,0 +1,3 @@
+.trackListAddButton {
+ float: right;
+}
diff --git a/src/music-catalogue-ui/components/trackRow.js b/src/music-catalogue-ui/components/trackRow.js
index e4d0e96..449f954 100644
--- a/src/music-catalogue-ui/components/trackRow.js
+++ b/src/music-catalogue-ui/components/trackRow.js
@@ -1,12 +1,23 @@
+import DeleteTrackActionIcon from "./deleteTrackActionIcon";
import styles from "./trackRow.module.css";
import pages from "@/helpers/navigation";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPenToSquare } from "@fortawesome/free-solid-svg-icons";
/**
* Component to render a row containing the details for a single track
* @param {*} param0
* @returns
*/
-const TrackRow = ({ artist, album, track, isWishList, navigate }) => {
+const TrackRow = ({
+ artist,
+ album,
+ track,
+ isWishList,
+ navigate,
+ logout,
+ setTracks,
+}) => {
return (
{album.title} |
@@ -25,6 +36,26 @@ const TrackRow = ({ artist, album, track, isWishList, navigate }) => {
{track.number} |
{track.title} |
{track.formattedDuration} |
+
+
+ |
+
+
+ navigate({
+ page: pages.trackEditor,
+ artist: artist,
+ album: album,
+ track: track,
+ })
+ }
+ />
+ |
);
};
diff --git a/src/music-catalogue-ui/helpers/apiTracks.js b/src/music-catalogue-ui/helpers/apiTracks.js
new file mode 100644
index 0000000..aedd1d2
--- /dev/null
+++ b/src/music-catalogue-ui/helpers/apiTracks.js
@@ -0,0 +1,91 @@
+import config from "../config.json";
+import { apiReadResponseData } from "./apiUtils";
+import { apiGetPostHeaders, apiGetHeaders } from "./apiHeaders";
+
+/**
+ * Create a track
+ * @param {*} title
+ * @param {*} number
+ * @param {*} duration
+ * @param {*} albumId
+ * @param {*} logout
+ * @returns
+ */
+const apiCreateTrack = async (title, number, duration, albumId, logout) => {
+ // Create the request body
+ const body = JSON.stringify({
+ title: title,
+ number: number,
+ duration: duration,
+ albumId: albumId,
+ });
+
+ // Call the API to create the retailer. This will just return the current
+ // record if it already exists
+ const url = `${config.api.baseUrl}/tracks`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: apiGetPostHeaders(),
+ body: body,
+ });
+
+ const track = await apiReadResponseData(response, logout);
+ return track;
+};
+
+/**
+ * Update track details
+ * @param {*} id
+ * @param {*} title
+ * @param {*} number
+ * @param {*} duration
+ * @param {*} albumId
+ * @param {*} logout
+ * @returns
+ */
+const apiUpdateTrack = async (id, title, number, duration, albumId, logout) => {
+ // Construct the body
+ const body = JSON.stringify({
+ id: id,
+ title: title,
+ number: number,
+ duration: duration,
+ albumId: albumId,
+ });
+
+ // Call the API to set the wish list flag for a given album
+ const url = `${config.api.baseUrl}/tracks`;
+ const response = await fetch(url, {
+ method: "PUT",
+ headers: apiGetPostHeaders(),
+ body: body,
+ });
+
+ const updatedTrack = await apiReadResponseData(response, logout);
+ return updatedTrack;
+};
+
+/**
+ * Delete the track with the specified ID
+ * @param {*} trackId
+ * @param {*} logout
+ * @returns
+ */
+const apiDeleteTrack = async (trackId, logout) => {
+ // Call the API to delete the specified album
+ const url = `${config.api.baseUrl}/tracks/${trackId}`;
+ const response = await fetch(url, {
+ method: "DELETE",
+ headers: apiGetHeaders(),
+ });
+
+ if (response.status == 401) {
+ // Unauthorized so the token's likely expired - force a login
+ logout();
+ } else {
+ // Return the response status code
+ return response.ok;
+ }
+};
+
+export { apiCreateTrack, apiUpdateTrack, apiDeleteTrack };
diff --git a/src/music-catalogue-ui/helpers/navigation.js b/src/music-catalogue-ui/helpers/navigation.js
index 64fda69..f0d8935 100644
--- a/src/music-catalogue-ui/helpers/navigation.js
+++ b/src/music-catalogue-ui/helpers/navigation.js
@@ -3,8 +3,12 @@ const pages = {
genres: "Genres",
wishlistArtists: "WishlistArtists",
albums: "Albums",
- wishlistAlbums: "wishlistAlbums",
+ wishlistAlbums: "WishlistAlbums",
tracks: "Tracks",
+ trackEditor: "TrackEditor",
+ retailers: "Retailers",
+ retailerDetails: "RetailerDetails",
+ retailerEditor: "RetailerEditor",
lookup: "Lookup",
export: "Export",
artistStatisticsReport: "ArtistStatisticsReport",
@@ -12,9 +16,6 @@ const pages = {
jobStatusReport: "JobStatusReport",
monthlySpendReport: "MonthlySpendReport",
albumPurchaseDetails: "AlbumPurchaseDetails",
- retailers: "Retailers",
- retailerDetails: "RetailerDetails",
- retailerEditor: "RetailerEditor",
};
export default pages;