diff --git a/src/App.tsx b/src/App.tsx
index b0149a6e..32f743ad 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,6 +11,7 @@ import { ErrorBanner } from "./components/error";
import { useFetchDevices } from "./use-fetch-devices.ts";
import { FlexContainer } from "./components/generic-components.ts";
import { DisplayWarning } from "./components/display-box.tsx";
+import { ManageProductions } from "./components/manage-productions/manage-productions.tsx";
const DisplayBoxPositioningContainer = styled(FlexContainer)`
justify-content: center;
@@ -60,6 +61,11 @@ const App = () => {
element={}
errorElement={}
/>
+ }
+ errorElement={}
+ />
}
diff --git a/src/api/api.ts b/src/api/api.ts
index a23d505d..59b56c5c 100644
--- a/src/api/api.ts
+++ b/src/api/api.ts
@@ -83,10 +83,9 @@ export const API = {
handleFetchRequest(
fetch(`${API_URL}productions/${id}`, { method: "GET" })
),
- // TODO apply handleFetchRequest
- deleteProduction: (id: number) =>
- fetch(`${API_URL}productions/${id}`, { method: "DELETE" }).then(
- (response) => response.json()
+ deleteProduction: (id: number): Promise =>
+ handleFetchRequest(
+ fetch(`${API_URL}productions/${id}`, { method: "DELETE" })
),
listProductionLines: (id: number) =>
handleFetchRequest(
diff --git a/src/assets/icons/arrow_back.svg b/src/assets/icons/arrow_back.svg
new file mode 100644
index 00000000..c96460fd
--- /dev/null
+++ b/src/assets/icons/arrow_back.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon.tsx b/src/assets/icons/icon.tsx
index 9b0233e7..7e63bb41 100644
--- a/src/assets/icons/icon.tsx
+++ b/src/assets/icons/icon.tsx
@@ -1,6 +1,7 @@
import styled from "@emotion/styled";
import MicMute from "./mic_off.svg";
import MicUnmute from "./mic_on.svg";
+import Arrow from "./arrow_back.svg";
import RemoveSvg from "./clear.svg";
const Icon = styled.img`
@@ -13,4 +14,6 @@ export const MicMuted = () => ;
export const MicUnmuted = () => ;
+export const BackArrow = () => ;
+
export const RemoveIcon = () => ;
diff --git a/src/components/landing-page/form-elements.tsx b/src/components/landing-page/form-elements.tsx
index be589e38..8214e905 100644
--- a/src/components/landing-page/form-elements.tsx
+++ b/src/components/landing-page/form-elements.tsx
@@ -1,6 +1,6 @@
import styled from "@emotion/styled";
-export const FormContainer = styled.div``;
+export const FormContainer = styled.form``;
export const FormInput = styled.input`
width: 100%;
diff --git a/src/components/landing-page/manage-production-button.tsx b/src/components/landing-page/manage-production-button.tsx
new file mode 100644
index 00000000..c85d0c84
--- /dev/null
+++ b/src/components/landing-page/manage-production-button.tsx
@@ -0,0 +1,24 @@
+import { useNavigate } from "react-router-dom";
+import styled from "@emotion/styled";
+import { ActionButton } from "./form-elements";
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: end;
+ padding: 2rem;
+`;
+
+export const ManageProductionButton = () => {
+ const navigate = useNavigate();
+
+ return (
+
+ navigate("/manage-productions")}
+ >
+ Manage Productions
+
+
+ );
+};
diff --git a/src/components/landing-page/productions-list.tsx b/src/components/landing-page/productions-list.tsx
index bf34c7a0..a874622a 100644
--- a/src/components/landing-page/productions-list.tsx
+++ b/src/components/landing-page/productions-list.tsx
@@ -7,6 +7,7 @@ import { LoaderDots } from "../loader/loader.tsx";
import { useRefreshAnimation } from "./use-refresh-animation.ts";
import { DisplayContainerHeader } from "./display-container-header.tsx";
import { DisplayContainer } from "../generic-components.ts";
+import { ManageProductionButton } from "./manage-production-button.tsx";
import { LocalError } from "../error.tsx";
const ProductionListContainer = styled.div`
@@ -115,6 +116,7 @@ export const ProductionsList = () => {
))}
+ {productions.length && }
>
);
};
diff --git a/src/components/manage-productions/manage-productions.tsx b/src/components/manage-productions/manage-productions.tsx
new file mode 100644
index 00000000..9553cda8
--- /dev/null
+++ b/src/components/manage-productions/manage-productions.tsx
@@ -0,0 +1,207 @@
+import { ErrorMessage } from "@hookform/error-message";
+import { SubmitHandler, useForm, useWatch } from "react-hook-form";
+import { useEffect, useState } from "react";
+import styled from "@emotion/styled";
+import { DisplayContainerHeader } from "../landing-page/display-container-header";
+import {
+ ActionButton,
+ DecorativeLabel,
+ FormInput,
+ FormLabel,
+ StyledWarningMessage,
+} from "../landing-page/form-elements";
+import { Spinner } from "../loader/loader";
+import { useFetchProduction } from "../landing-page/use-fetch-production";
+import { darkText, errorColour } from "../../css-helpers/defaults";
+import { useDeleteProduction } from "./use-delete-production";
+import { NavigateToRootButton } from "../navigate-to-root-button/navigate-to-root-button";
+
+type FormValue = {
+ productionId: string;
+};
+
+const Container = styled.form`
+ max-width: 45rem;
+ padding: 1rem 0 0 2rem;
+`;
+
+const RemoveConfirmation = styled.div`
+ background: #91fa8c;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ border: 1px solid #b2ffa1;
+ color: #1a1a1a;
+`;
+
+const FetchErrorMessage = styled.div`
+ background: ${errorColour};
+ color: ${darkText};
+ padding: 0.5rem;
+ margin: 1rem 0;
+`;
+
+const VerifyBtnWrapper = styled.div`
+ padding: 1rem 0 0 2rem;
+`;
+
+const VerifyButtons = styled.div`
+ display: flex;
+ padding: 1rem 0 0 0;
+`;
+
+const Button = styled(ActionButton)`
+ margin: 0 1rem 0 0;
+`;
+const StyledBackBtnIcon = styled.div`
+ padding: 0 0 3rem 0;
+ width: 4rem;
+`;
+
+export const ManageProductions = () => {
+ const [showDeleteDoneMessage, setShowDeleteDoneMessage] =
+ useState(false);
+ const [verifyRemove, setVerifyRemove] = useState(false);
+ const [removeId, setRemoveId] = useState(null);
+ const {
+ control,
+ reset,
+ formState,
+ formState: { errors, isSubmitSuccessful },
+ register,
+ handleSubmit,
+ } = useForm();
+
+ const productionId = useWatch({ name: "productionId", control });
+
+ const { onChange, onBlur, name, ref } = register("productionId", {
+ required: "Production ID is required",
+ min: 1,
+ });
+
+ const { error: productionFetchError, production } = useFetchProduction(
+ parseInt(productionId, 10)
+ );
+
+ const {
+ loading,
+ error: productionDeleteError,
+ successfullDelete,
+ } = useDeleteProduction(removeId);
+
+ useEffect(() => {
+ if (formState.isSubmitSuccessful) {
+ reset({
+ productionId: "",
+ });
+ setVerifyRemove(false);
+ }
+ }, [formState.isSubmitSuccessful, isSubmitSuccessful, reset]);
+
+ useEffect(() => {
+ if (successfullDelete) {
+ setVerifyRemove(false);
+ setShowDeleteDoneMessage(true);
+ }
+ }, [successfullDelete]);
+
+ const onSubmit: SubmitHandler = (value) => {
+ if (loading) return;
+
+ setRemoveId(parseInt(value.productionId, 10));
+ };
+ // TODO return button
+
+ return (
+
+
+
+
+ Remove Production
+
+ Production ID
+ {
+ setShowDeleteDoneMessage(false);
+ onChange(ev);
+ }}
+ name={name}
+ ref={ref}
+ onBlur={onBlur}
+ type="number"
+ autoComplete="off"
+ placeholder="Production ID"
+ />
+
+ {productionFetchError && (
+
+ The production ID could not be fetched. {productionFetchError.name}{" "}
+ {productionFetchError.message}.
+
+ )}
+ {productionDeleteError && (
+
+ The production ID could not be deleted. {productionDeleteError.name}{" "}
+ {productionDeleteError.message}.
+
+ )}
+
+ {production ? (
+ <>
+ Production name: {production.name}
+ {!verifyRemove && (
+ setVerifyRemove(true)}
+ >
+ Remove
+ {loading && }
+
+ )}
+ {verifyRemove && (
+
+ Are you sure?
+
+
+
+
+
+ )}
+ >
+ ) : (
+
+ Please enter a production id
+
+ )}
+ {showDeleteDoneMessage && (
+
+ The production {production?.name} has been removed
+
+ )}
+
+ );
+};
diff --git a/src/components/manage-productions/use-delete-production.ts b/src/components/manage-productions/use-delete-production.ts
new file mode 100644
index 00000000..d08d379b
--- /dev/null
+++ b/src/components/manage-productions/use-delete-production.ts
@@ -0,0 +1,57 @@
+import { useEffect, useState } from "react";
+import { API } from "../../api/api";
+import { useGlobalState } from "../../global-state/context-provider";
+
+type TUseDeleteProduction = (id: number | null) => {
+ loading: boolean;
+ error: Error | null;
+ successfullDelete: boolean;
+};
+
+export const useDeleteProduction: TUseDeleteProduction = (id) => {
+ const [successfullDelete, setSuccessfullDelete] = useState(false);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const [, dispatch] = useGlobalState();
+
+ useEffect(() => {
+ let aborted = false;
+ setError(null);
+ setSuccessfullDelete(false);
+ setLoading(true);
+ if (id) {
+ API.deleteProduction(id)
+ .then(() => {
+ if (aborted) return;
+
+ setSuccessfullDelete(true);
+ setLoading(false);
+ setError(null);
+ })
+ .catch((err) => {
+ dispatch({
+ type: "ERROR",
+ payload:
+ err instanceof Error
+ ? err
+ : new Error("Failed to delete production"),
+ });
+ setError(err);
+ setLoading(false);
+ });
+ } else {
+ setLoading(false);
+ }
+
+ return () => {
+ aborted = true;
+ };
+ }, [dispatch, id]);
+
+ return {
+ loading,
+ error,
+ successfullDelete,
+ };
+};
diff --git a/src/components/navigate-to-root-button/navigate-to-root-button.tsx b/src/components/navigate-to-root-button/navigate-to-root-button.tsx
new file mode 100644
index 00000000..f0a58929
--- /dev/null
+++ b/src/components/navigate-to-root-button/navigate-to-root-button.tsx
@@ -0,0 +1,19 @@
+import styled from "@emotion/styled";
+import { useNavigate } from "react-router-dom";
+import { BackArrow } from "../../assets/icons/icon";
+import { ActionButton } from "../landing-page/form-elements";
+
+const StyledBackBtn = styled(ActionButton)`
+ padding: 0;
+ margin: 0;
+`;
+
+export const NavigateToRootButton = () => {
+ const navigate = useNavigate();
+
+ return (
+ navigate("/")}>
+
+
+ );
+};