diff --git a/README.md b/README.md index 18f6b73..09a1439 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,12 @@ MusicCatalogue.LookupTool --lookup "John Coltrane" "Blue Train" catalogue Track List - Clicking on the artist name in any row in the track list or clicking on the "Back" button returns to the album list for that artist +- Clicking on the "Delete" icon will prompt for confirmation and, if the action is confirmed, will delete the track +- Clicking on the "Edit" icon opens the track editor to edit the track properties: + +Track List + +- Clicking on the "Add" button at the bottom of the track list will open a blank track editor to add and save a new track #### Browsing By Genre diff --git a/diagrams/track-editor.png b/diagrams/track-editor.png new file mode 100644 index 0000000..671be7a Binary files /dev/null and b/diagrams/track-editor.png differ diff --git a/diagrams/track-list.png b/diagrams/track-list.png index e6d529d..da78f05 100644 Binary files a/diagrams/track-list.png and b/diagrams/track-list.png differ diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index b43423c..459abe2 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,4 +1,4 @@ FROM mcr.microsoft.com/dotnet/core/aspnet:latest -COPY musiccatalogue.api-1.21.0.0 /opt/musiccatalogue.api-1.21.0.0 -WORKDIR /opt/musiccatalogue.api-1.21.0.0/bin +COPY musiccatalogue.api-1.22.0.0 /opt/musiccatalogue.api-1.22.0.0 +WORKDIR /opt/musiccatalogue.api-1.22.0.0/bin ENTRYPOINT [ "./MusicCatalogue.Api" ] diff --git a/docker/ui/Dockerfile b/docker/ui/Dockerfile index 91e12c3..354d8fb 100644 --- a/docker/ui/Dockerfile +++ b/docker/ui/Dockerfile @@ -1,6 +1,6 @@ FROM node:20-alpine -COPY musiccatalogue.ui-1.24.0.0 /opt/musiccatalogue.ui-1.24.0.0 -WORKDIR /opt/musiccatalogue.ui-1.24.0.0 +COPY musiccatalogue.ui-1.25.0.0 /opt/musiccatalogue.ui-1.25.0.0 +WORKDIR /opt/musiccatalogue.ui-1.25.0.0 RUN npm install RUN npm run build ENTRYPOINT [ "npm", "start" ] diff --git a/src/MusicCatalogue.Api/Controllers/TracksController.cs b/src/MusicCatalogue.Api/Controllers/TracksController.cs new file mode 100644 index 0000000..ffd5942 --- /dev/null +++ b/src/MusicCatalogue.Api/Controllers/TracksController.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MusicCatalogue.Entities.Database; +using MusicCatalogue.Entities.Interfaces; + +namespace MusicCatalogue.Api.Controllers +{ + [Authorize] + [ApiController] + [ApiConventionType(typeof(DefaultApiConventions))] + [Route("[controller]")] + public class TracksController : Controller + { + private readonly IMusicCatalogueFactory _factory; + + public TracksController(IMusicCatalogueFactory factory) + { + _factory = factory; + } + + /// + /// Add a track to the catalogue + /// + /// + /// + [HttpPost] + [Route("")] + public async Task> AddTrackAsync([FromBody] Track template) + { + var track = await _factory.Tracks.AddAsync(template.AlbumId, template.Title, template.Number, template.Duration); + return track; + } + + /// + /// Update an existing track + /// + /// + /// + [HttpPut] + [Route("")] + public async Task> UpdateTrackAsync([FromBody] Track template) + { + var track = await _factory.Tracks.UpdateAsync( + template.Id, + template.AlbumId, + template.Title, + template.Number, + template.Duration); + return track; + } + + /// + /// Delete an existing track + /// + /// + /// + [HttpDelete] + [Route("{id}")] + public async Task DeleteTrackAsync(int id) + { + // Make sure the track exists + var track = await _factory.Tracks.GetAsync(x => x.Id == id); + + // If the track doesn't exist, return a 404 + if (track == null) + { + return NotFound(); + } + + // Delete the track + await _factory.Tracks.DeleteAsync(id); + + return Ok(); + } + } +} diff --git a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj index fc093cb..5a22a47 100644 --- a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj @@ -2,9 +2,9 @@ net7.0 - 1.20.0.0 - 1.20.0.0 - 1.20.0 + 1.21.0.0 + 1.21.0.0 + 1.21.0 enable enable diff --git a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj index 3804cff..bee13e5 100644 --- a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj +++ b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Data - 1.19.0.0 + 1.20.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.19.0.0 + 1.20.0.0 diff --git a/src/MusicCatalogue.Entities/Interfaces/ITrackManager.cs b/src/MusicCatalogue.Entities/Interfaces/ITrackManager.cs index 66eb211..32a2d3d 100644 --- a/src/MusicCatalogue.Entities/Interfaces/ITrackManager.cs +++ b/src/MusicCatalogue.Entities/Interfaces/ITrackManager.cs @@ -6,8 +6,10 @@ namespace MusicCatalogue.Entities.Interfaces public interface ITrackManager { Task AddAsync(int albumId, string title, int? number, int? duration); - Task GetAsync(Expression> predicate); + Task GetAsync(Expression> predicate); Task> ListAsync(Expression> predicate); - Task DeleteAsync(int albumId); + Task UpdateAsync(int trackId, int albumId, string title, int? number, int? duration); + Task DeleteAsync(int trackId); + Task DeleteAllTracksForAlbumAsync(int albumId); } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj index 6b7c282..b2ced07 100644 --- a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj +++ b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Entities - 1.19.0.0 + 1.20.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.19.0.0 + 1.20.0.0 diff --git a/src/MusicCatalogue.Logic/Database/AlbumManager.cs b/src/MusicCatalogue.Logic/Database/AlbumManager.cs index 0237d85..3419e80 100644 --- a/src/MusicCatalogue.Logic/Database/AlbumManager.cs +++ b/src/MusicCatalogue.Logic/Database/AlbumManager.cs @@ -149,7 +149,7 @@ public async Task DeleteAsync(int albumId) if (album != null) { // Delete the associated tracks - await Factory.Tracks.DeleteAsync(albumId); + await Factory.Tracks.DeleteAllTracksForAlbumAsync(albumId); // Delete the album record and save changes Factory.Context.Remove(album); diff --git a/src/MusicCatalogue.Logic/Database/TrackManager.cs b/src/MusicCatalogue.Logic/Database/TrackManager.cs index 135d21f..0f9679d 100644 --- a/src/MusicCatalogue.Logic/Database/TrackManager.cs +++ b/src/MusicCatalogue.Logic/Database/TrackManager.cs @@ -16,13 +16,10 @@ internal TrackManager(IMusicCatalogueFactory factory) : base(factory) /// /// /// - public async Task GetAsync(Expression> predicate) + public async Task GetAsync(Expression> predicate) { - List tracks = await ListAsync(predicate); - -#pragma warning disable CS8603 + var tracks = await ListAsync(predicate); return tracks.FirstOrDefault(); -#pragma warning restore CS8603 } /// @@ -65,14 +62,53 @@ public async Task AddAsync(int albumId, string title, int? number, int? d return track; } + /// + /// Update an existing track + /// + /// + /// + /// + /// + /// + /// + public async Task UpdateAsync(int trackId, int albumId, string title, int? number, int? duration) + { + var track = Context.Tracks.FirstOrDefault(x => x.Id == trackId); + if (track != null) + { + track.AlbumId = albumId; + track.Title = title; + track.Number = number; + track.Duration = duration; + await Context.SaveChangesAsync(); + } + + return track; + } + + /// + /// Delete the track with the specified Id + /// + /// + /// + public async Task DeleteAsync(int trackId) + { + var track = Context.Tracks.FirstOrDefault(x => x.Id == trackId); + if (track != null) + { + Context.Tracks.Remove(track); + await Context.SaveChangesAsync(); + } + } + /// /// Delete the tracks associated with an album, given its ID /// /// /// - public async Task DeleteAsync(int albumId) + public async Task DeleteAllTracksForAlbumAsync(int albumId) { - List tracks = await ListAsync(x => x.AlbumId == albumId); + var tracks = await ListAsync(x => x.AlbumId == albumId); if (tracks.Any()) { Context.Tracks.RemoveRange(tracks); diff --git a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj index 3e3bea1..c0939a0 100644 --- a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj +++ b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Logic - 1.19.0.0 + 1.20.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.19.0.0 + 1.20.0.0 diff --git a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj index c9f1f07..f863bfc 100644 --- a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj +++ b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj @@ -3,9 +3,9 @@ Exe net7.0 - 1.19.0.0 - 1.19.0.0 - 1.19.0 + 1.20.0.0 + 1.20.0.0 + 1.20.0 enable enable false diff --git a/src/MusicCatalogue.Tests/TrackManagerTest.cs b/src/MusicCatalogue.Tests/TrackManagerTest.cs index adb3b9b..b6bb3ff 100644 --- a/src/MusicCatalogue.Tests/TrackManagerTest.cs +++ b/src/MusicCatalogue.Tests/TrackManagerTest.cs @@ -16,9 +16,14 @@ public class TrackManagerTest private const int TrackNumber = 1; private const int TrackDuration = 643200; + private const string UpdatedTrackTitle = "Moment's Notice"; + private const int UpdatedTrackNumber = 2; + private const int UpdatedTrackDuration = 550760; + private ITrackManager? _manager = null; private int _artistId; private int _albumId; + private int _trackId; [TestInitialize] public void TestInitialize() @@ -33,7 +38,7 @@ public void TestInitialize() // Create a track manager and add a test track _manager = factory.Tracks; - Task.Run(() => _manager.AddAsync(_albumId, TrackTitle, TrackNumber, TrackDuration)).Wait(); + _trackId = Task.Run(() => _manager.AddAsync(_albumId, TrackTitle, TrackNumber, TrackDuration)).Result.Id; } [TestMethod] @@ -47,7 +52,7 @@ public async Task AddDuplicateTest() [TestMethod] public async Task AddAndGetTest() { - var track = await _manager!.GetAsync(a => a.Title == TrackTitle); + var track = await _manager!.GetAsync(t => t.Title == TrackTitle); Assert.IsNotNull(track); Assert.IsTrue(track.Id > 0); Assert.AreEqual(_albumId, track.AlbumId); @@ -59,7 +64,7 @@ public async Task AddAndGetTest() [TestMethod] public async Task GetMissingTest() { - var track = await _manager!.GetAsync(a => a.Title == "Missing"); + var track = await _manager!.GetAsync(t => t.Title == "Missing"); Assert.IsNull(track); } @@ -74,15 +79,36 @@ public async Task ListAllTest() [TestMethod] public async Task ListMissingTest() { - var tracks = await _manager!.ListAsync(e => e.Title == "Missing"); + var tracks = await _manager!.ListAsync(t => t.Title == "Missing"); Assert.AreEqual(0, tracks!.Count); } + [TestMethod] + public async Task UpdateTest() + { + await _manager!.UpdateAsync(_trackId, _albumId, UpdatedTrackTitle, UpdatedTrackNumber, UpdatedTrackDuration); + var track = await _manager!.GetAsync(t => t.Id == _trackId); + Assert.IsNotNull(track); + Assert.AreEqual(_trackId, track.Id); + Assert.AreEqual(_albumId, track.AlbumId); + Assert.AreEqual(UpdatedTrackTitle, track.Title); + Assert.AreEqual(UpdatedTrackNumber, track.Number); + Assert.AreEqual(UpdatedTrackDuration, track.Duration); + } + [TestMethod] public async Task DeleteTest() { - await _manager!.DeleteAsync(_albumId); - var tracks = await _manager!.ListAsync(e => e.AlbumId == _albumId); + await _manager!.DeleteAsync(_trackId); + var tracks = await _manager!.ListAsync(t => true); + Assert.AreEqual(0, tracks!.Count); + } + + [TestMethod] + public async Task DeleteAllForAlbumTest() + { + await _manager!.DeleteAllTracksForAlbumAsync(_albumId); + var tracks = await _manager!.ListAsync(t => t.AlbumId == _albumId); Assert.AreEqual(0, tracks!.Count); } diff --git a/src/music-catalogue-ui/components/app.js b/src/music-catalogue-ui/components/app.js index 1135a57..765f939 100644 --- a/src/music-catalogue-ui/components/app.js +++ b/src/music-catalogue-ui/components/app.js @@ -15,9 +15,10 @@ const defaultContext = { // Current page page: pages.artists, - // Artist, album and retailer context + // Artist, album, track and retailer context artist: null, album: null, + track: null, retailer: null, // Data retrieval/filering criteria @@ -38,6 +39,7 @@ const App = () => { page = pages.artists, artist = null, album = null, + track = null, retailer = null, genre = null, filter = "A", @@ -48,6 +50,7 @@ const App = () => { page: page, artist: typeof artist != "undefined" ? artist : null, album: typeof album != "undefined" ? album : null, + track: typeof track != "undefined" ? track : null, retailer: typeof retailer != "undefined" ? retailer : null, genre: typeof genre != "undefined" ? genre : null, filter: typeof filter != "undefined" ? filter : "A", diff --git a/src/music-catalogue-ui/components/componentPicker.js b/src/music-catalogue-ui/components/componentPicker.js index 9202b5d..e777872 100644 --- a/src/music-catalogue-ui/components/componentPicker.js +++ b/src/music-catalogue-ui/components/componentPicker.js @@ -13,6 +13,7 @@ import GenreList from "./genreList"; import RetailerList from "./retailerList"; import RetailerDetails from "./retailerDetails"; import RetailerEditor from "./retailerEditor"; +import TrackEditor from "./trackEditor"; /** * Component using the current context to select and render the current page @@ -33,8 +34,6 @@ const ComponentPicker = ({ context, navigate, logout }) => { logout={logout} /> ); - case pages.genres: - return ; case pages.albums: return ( { logout={logout} /> ); + case pages.albumPurchaseDetails: + return ( + + ); case pages.tracks: return ( { logout={logout} /> ); - case pages.lookup: - return ; - case pages.export: - return ; - case pages.artistStatisticsReport: - return ; - case pages.genreStatisticsReport: - return ; - case pages.jobStatusReport: - return ; - case pages.monthlySpendReport: - return ; - case pages.albumPurchaseDetails: + case pages.trackEditor: return ( - ); + case pages.lookup: + return ; case pages.retailers: return ; case pages.retailerDetails: @@ -93,6 +92,18 @@ const ComponentPicker = ({ context, navigate, logout }) => { logout={logout} /> ); + case pages.genres: + return ; + case pages.export: + return ; + case pages.artistStatisticsReport: + return ; + case pages.genreStatisticsReport: + return ; + case pages.jobStatusReport: + return ; + case pages.monthlySpendReport: + return ; default: return ; } diff --git a/src/music-catalogue-ui/components/deleteTrackActionIcon.js b/src/music-catalogue-ui/components/deleteTrackActionIcon.js new file mode 100644 index 0000000..72b09e2 --- /dev/null +++ b/src/music-catalogue-ui/components/deleteTrackActionIcon.js @@ -0,0 +1,45 @@ +import { apiDeleteTrack } from "@/helpers/apiTracks"; +import { apiFetchAlbumById } from "@/helpers/apiAlbums"; +import { useCallback } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; + +/** + * Icon and associated action to delete a track + * @param {*} track + * @param {*} logout + * @param {*} setTracks + * @returns + */ +const DeleteTrackActionIcon = ({ track, logout, setTracks }) => { + /* Callback to prompt for confirmation and delete an album */ + const confirmDeleteTrack = useCallback(async (e, track) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Show a confirmation message and get the user response + const message = `This will delete the track "${track.title}" - click OK to confirm`; + const result = confirm(message); + + // If they've confirmed the deletion ... + if (result) { + // ... cdelete the track and confirm the API call was successful + const result = await apiDeleteTrack(track.id, logout); + if (result) { + // Successful, so get the album from the service and set the tracks to + // the appropriate member of the returned object + var fetchedAlbum = await apiFetchAlbumById(track.albumId, logout); + setTracks(fetchedAlbum.tracks); + } + } + }, []); + + return ( + 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}
+
+
+
+
+ {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+ + ); +}; + +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;