diff --git a/src/music-catalogue-ui/components/albums/deleteAlbumActionIcon.js b/src/music-catalogue-ui/components/albums/deleteAlbumActionIcon.js index c99bbc7..46e3864 100644 --- a/src/music-catalogue-ui/components/albums/deleteAlbumActionIcon.js +++ b/src/music-catalogue-ui/components/albums/deleteAlbumActionIcon.js @@ -45,6 +45,8 @@ const DeleteAlbumActionIcon = ({ logout ); setAlbums(fetchedAlbums); + } else { + setError(`Failed to delete album ${album.title}`); } } catch (ex) { setError(`Error deleting the album: ${ex.message}`); diff --git a/src/music-catalogue-ui/components/componentPicker.js b/src/music-catalogue-ui/components/componentPicker.js index f4fc354..6bc3aa1 100644 --- a/src/music-catalogue-ui/components/componentPicker.js +++ b/src/music-catalogue-ui/components/componentPicker.js @@ -11,6 +11,7 @@ import ArtistStatisticsReport from "./reports/artistStatisticsReport"; import MonthlySpendReport from "./reports/monthlySpendReport"; import GenreAlbumsReport from "./reports/genreAlbumsReport"; import GenreList from "./genres/genreList"; +import GenreEditor from "./genres/genreEditor"; import RetailerList from "./retailers/retailerList"; import RetailerDetails from "./retailers/retailerDetails"; import RetailerEditor from "./retailers/retailerEditor"; @@ -103,6 +104,14 @@ const ComponentPicker = ({ context, navigate, logout }) => { logout={logout} /> ); + case pages.genreEditor: + return ( + + ); case pages.lookup: return ; case pages.retailers: diff --git a/src/music-catalogue-ui/components/genres/deleteGenreActionIcon.js b/src/music-catalogue-ui/components/genres/deleteGenreActionIcon.js new file mode 100644 index 0000000..15751c8 --- /dev/null +++ b/src/music-catalogue-ui/components/genres/deleteGenreActionIcon.js @@ -0,0 +1,50 @@ +import { useCallback } from "react"; +import { apiDeleteGenre, apiFetchGenres } from "@/helpers/api/apiGenres"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; + +/** + * Icon and associated action to delete a genre + * @param {*} genre + * @param {*} logout + * @param {*} setGenres + * @param {*} setError + * @returns + */ +const DeleteGenreActionIcon = ({ genre, logout, setGenres, setError }) => { + /* Callback to prompt for confirmation and delete a genre */ + const confirmDeleteGenre = useCallback( + async (e) => { + // 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 genre "${genre.name}" - click OK to confirm`; + const result = confirm(message); + + // If they've confirmed the deletion ... + if (result) { + try { + // ... delete the genre and confirm the API call was successful + const result = await apiDeleteGenre(genre.id, logout); + if (result) { + // Successful, so refresh the genre list + const fetchedGenres = await apiFetchGenres(null, logout); + setGenres(fetchedGenres); + } else { + setError(`Failed to delete genre ${genre.name}`); + } + } catch (ex) { + setError(`Error deleting the genre: ${ex.message}`); + } + } + }, + [genre, logout, setGenres, setError] + ); + + return ( + confirmDeleteGenre(e)} /> + ); +}; + +export default DeleteGenreActionIcon; diff --git a/src/music-catalogue-ui/components/genres/genreEditor.js b/src/music-catalogue-ui/components/genres/genreEditor.js new file mode 100644 index 0000000..ad97a50 --- /dev/null +++ b/src/music-catalogue-ui/components/genres/genreEditor.js @@ -0,0 +1,101 @@ +import styles from "./genreEditor.module.css"; +import pages from "@/helpers/navigation"; +import FormInputField from "../common/formInputField"; +import { apiCreateGenre, apiUpdateGenre } from "@/helpers/api/apiGenres"; +import { useState, useCallback } from "react"; + +/** + * Component to render a genre editor + * @param {*} genre + * @param {*} navigate + * @param {*} logout + * @returns + */ +const GenreEditor = ({ genre, navigate, logout }) => { + // Setup state + const initialName = genre != null ? genre.name : ""; + const [name, setName] = useState(initialName); + const [error, setError] = useState(""); + + const saveGenre = useCallback( + async (e) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Clear pre-existing errors + setError(""); + + try { + // Either add or update the genre, depending on whether there's an + // existing album or not + let updatedGenre = null; + if (genre == null) { + // Create the genre + updatedGenre = await apiCreateGenre(name, logout); + } else { + // Update the existing genre + updatedGenre = await apiUpdateGenre(genre.id, name, logout); + } + + // Go back to the genre, which should reflect the updated details + navigate({ + page: pages.genres, + }); + } catch (ex) { + setError(`Error saving the updated genre details: ${ex.message}`); + } + }, + [genre, name, navigate, logout] + ); + + // Set the page title + const pageTitle = genre != null ? `Genre - ${genre.name}` : `New Genre`; + + return ( + <> +
+
{pageTitle}
+
+
+
+
+ {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + ); +}; + +export default GenreEditor; diff --git a/src/music-catalogue-ui/components/genres/genreEditor.module.css b/src/music-catalogue-ui/components/genres/genreEditor.module.css new file mode 100644 index 0000000..b392e4c --- /dev/null +++ b/src/music-catalogue-ui/components/genres/genreEditor.module.css @@ -0,0 +1,27 @@ +.genreEditorFormContainer { + display: flex; + justify-content: center; + align-items: center; +} + +.genreEditorForm { + width: 80%; + padding-top: 20px; + padding-bottom: 20px; +} + +.genreEditorFormLabel { + font-size: 14px; + font-weight: 600; + color: rgb(34, 34, 34); +} + +.genreEditorButton { + margin-left: 10px; + float: right; +} + +.genreEditorError { + font-weight: bold; + color: red; +} diff --git a/src/music-catalogue-ui/components/genres/genreList.js b/src/music-catalogue-ui/components/genres/genreList.js index 6561b3f..aa7cb10 100644 --- a/src/music-catalogue-ui/components/genres/genreList.js +++ b/src/music-catalogue-ui/components/genres/genreList.js @@ -1,6 +1,8 @@ +import styles from "./genreList.module.css"; import pages from "@/helpers/navigation"; import useGenres from "@/hooks/useGenres"; import GenreRow from "./genreRow"; +import { useState } from "react"; /** * Component to render a table listing all the genres in the catalogue @@ -11,14 +13,11 @@ import GenreRow from "./genreRow"; */ const GenreList = ({ navigate, logout }) => { const { genres, setGenres } = useGenres(false, logout); + const [error, setError] = useState(""); - // Callback to pass to child components to set the genre - const setGenreCallback = (genre) => { - navigate({ - page: pages.artists, - genre: genre, - filter: "*", - }); + // Callback to pass to child components to set the list of genres + const setGenresCallback = (genres) => { + setGenres(genres); }; return ( @@ -26,6 +25,13 @@ const GenreList = ({ navigate, logout }) => {
Genres
+
+ {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
@@ -35,11 +41,31 @@ const GenreList = ({ navigate, logout }) => { {genres != null && ( {genres.map((g) => ( - + ))} )}
+
+ +
); }; diff --git a/src/music-catalogue-ui/components/genres/genreList.module.css b/src/music-catalogue-ui/components/genres/genreList.module.css new file mode 100644 index 0000000..48ce27d --- /dev/null +++ b/src/music-catalogue-ui/components/genres/genreList.module.css @@ -0,0 +1,9 @@ +.genreListAddButton { + float: right; +} + +.genreListError { + font-weight: bold; + color: red; + text-align: center; +} diff --git a/src/music-catalogue-ui/components/genres/genreRow.js b/src/music-catalogue-ui/components/genres/genreRow.js index bb62b71..0dd9f31 100644 --- a/src/music-catalogue-ui/components/genres/genreRow.js +++ b/src/music-catalogue-ui/components/genres/genreRow.js @@ -1,13 +1,40 @@ +import DeleteGenreActionIcon from "./deleteGenreActionIcon"; +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 genre * @param {*} genre - * @param {*} setGenre + * @param {*} navigate + * @param {*} logout + * @param {*} setGenres + * @param {*} setError * @returns */ -const GenreRow = ({ genre, setGenre }) => { +const GenreRow = ({ genre, navigate, logout, setGenres, setError }) => { return ( - setGenre(genre)}>{genre.name} + {genre.name} + + + + + + navigate({ + page: pages.genreEditor, + genre: genre, + }) + } + /> + ); }; diff --git a/src/music-catalogue-ui/helpers/api/apiGenres.js b/src/music-catalogue-ui/helpers/api/apiGenres.js index 6c77797..d35f1d9 100644 --- a/src/music-catalogue-ui/helpers/api/apiGenres.js +++ b/src/music-catalogue-ui/helpers/api/apiGenres.js @@ -1,6 +1,6 @@ import config from "@/config.json"; import { apiReadResponseData } from "./apiUtils"; -import { apiGetPostHeaders } from "./apiHeaders"; +import { apiGetPostHeaders, apiGetHeaders } from "./apiHeaders"; /** * Fetch a list of genres from the Music Catalogue REST API @@ -27,4 +27,77 @@ const apiFetchGenres = async (isWishList, logout) => { return genres; }; -export { apiFetchGenres }; +/** + * Create a new genre + * @param {*} name + * @param {*} logout + * @returns + */ +const apiCreateGenre = async (name, logout) => { + // Construct the body + const body = JSON.stringify({ + name: name, + }); + + // Call the API to create the genre + const url = `${config.api.baseUrl}/genres`; + const response = await fetch(url, { + method: "POST", + headers: apiGetPostHeaders(), + body: body, + }); + + const genre = await apiReadResponseData(response, logout); + return genre; +}; + +/** + * Update an existing genre + * @param {*} genreId + * @param {*} name + * @param {*} logout + * @returns + */ +const apiUpdateGenre = async (genreId, name, logout) => { + // Construct the body + const body = JSON.stringify({ + id: genreId, + name: name, + }); + + // Call the API to update the genre + const url = `${config.api.baseUrl}/genres`; + const response = await fetch(url, { + method: "PUT", + headers: apiGetPostHeaders(), + body: body, + }); + + const genre = await apiReadResponseData(response, logout); + return genre; +}; + +/** + * Delete an existing genre + * @param {*} genreId + * @param {*} logout + * @returns + */ +const apiDeleteGenre = async (genreId, logout) => { + // Call the API to delete the genre + const url = `${config.api.baseUrl}/genres/${genreId}`; + 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 { apiFetchGenres, apiCreateGenre, apiUpdateGenre, apiDeleteGenre }; diff --git a/src/music-catalogue-ui/helpers/navigation.js b/src/music-catalogue-ui/helpers/navigation.js index 68b092b..c117367 100644 --- a/src/music-catalogue-ui/helpers/navigation.js +++ b/src/music-catalogue-ui/helpers/navigation.js @@ -2,6 +2,7 @@ const pages = { artists: "Artists", artistEditor: "ArtistEditor", genres: "Genres", + genreEditor: "GenreEditor", wishlistArtists: "WishlistArtists", albums: "Albums", albumEditor: "AlbumEditor",