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}
+
+
+ >
+ );
+};
+
+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",