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