diff --git a/src/App.tsx b/src/App.tsx index 802151a6..92d946a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,10 +17,10 @@ import { isValidBrowser } from "./bowser.ts"; import { DisplayContainerHeader } from "./components/landing-page/display-container-header.tsx"; import { NavigateToRootButton } from "./components/navigate-to-root-button/navigate-to-root-button.tsx"; import { CallsPage } from "./components/calls-page/calls-page.tsx"; -import { CreateProductionPage } from "./components/create-production/create-production-page.tsx"; import { Header } from "./components/header.tsx"; import { useLocalUserSettings } from "./hooks/use-local-user-settings.ts"; import { ManageProductionsPage } from "./components/manage-productions-page/manage-productions-page.tsx"; +import { CreateProductionPage } from "./components/create-production/create-production-page.tsx"; const DisplayBoxPositioningContainer = styled(FlexContainer)` justify-content: center; @@ -126,16 +126,16 @@ const App = () => { /> setApiError(true)} - /> - } + element={} errorElement={} /> } + element={ + setApiError(true)} + /> + } errorElement={} /> => - handleFetchRequest( + fetchProduction: (id: number): Promise => + handleFetchRequest( fetch(`${API_URL}production/${id}`, { method: "GET", headers: { diff --git a/src/components/accessing-local-storage/access-local-storage.ts b/src/components/accessing-local-storage/access-local-storage.ts index 0be06b5f..fb264600 100644 --- a/src/components/accessing-local-storage/access-local-storage.ts +++ b/src/components/accessing-local-storage/access-local-storage.ts @@ -1,4 +1,5 @@ -import { createStorage, StorageType } from "@martinstark/storage-ts"; +import { createStorage, StorageTS, StorageType } from "@martinstark/storage-ts"; +import { useCallback, useEffect, useState } from "react"; type Schema = { username: string; @@ -6,27 +7,42 @@ type Schema = { audiooutput?: string; }; -// Create a store of the desired type. If it is not available, -// in-memory storage will be used as a fallback. -const store = createStorage({ - type: StorageType.LOCAL, - prefix: "id", - silent: true, -}); - export function useStorage() { + const [store, setStore] = useState>(); + + useEffect(() => { + // Create a store of the desired type. If it is not available, + // in-memory storage will be used as a fallback. + setStore( + createStorage({ + type: StorageType.LOCAL, + prefix: "id", + silent: true, + }) + ); + }, []); + type Key = keyof Schema; - const readFromStorage = (key: keyof Schema): Schema[Key] | null => { - return store.read(key); - }; + const readFromStorage = useCallback( + (key: keyof Schema): Schema[Key] | null => { + return store?.read(key); + }, + [store] + ); - const writeToStorage = (key: keyof Schema, value: Schema[Key]): void => { - store.write(key, value); - }; + const writeToStorage = useCallback( + (key: keyof Schema, value: Schema[Key]): void => { + store?.write(key, value); + }, + [store] + ); - const clearStorage = (key: keyof Schema) => { - store.delete(key); - }; + const removeFromStorage = useCallback( + (key: keyof Schema) => { + store?.delete(key); + }, + [store] + ); - return { readFromStorage, writeToStorage, clearStorage }; + return { readFromStorage, writeToStorage, removeFromStorage }; } diff --git a/src/components/create-production/create-production-page.tsx b/src/components/create-production/create-production-page.tsx index 053e98dc..c5b74396 100644 --- a/src/components/create-production/create-production-page.tsx +++ b/src/components/create-production/create-production-page.tsx @@ -1,19 +1,340 @@ -import { useEffect } from "react"; -import { useGlobalState } from "../../global-state/context-provider"; -import { CreateProduction } from "./create-production"; +import { + Controller, + SubmitHandler, + useFieldArray, + useForm, +} from "react-hook-form"; +import { useEffect, useState } from "react"; +import styled from "@emotion/styled"; +import { ErrorMessage } from "@hookform/error-message"; +import { DisplayContainerHeader } from "../landing-page/display-container-header.tsx"; +import { + DecorativeLabel, + FormInput, + FormLabel, + StyledWarningMessage, + PrimaryButton, + SecondaryButton, +} from "../landing-page/form-elements.tsx"; +import { API } from "../../api/api.ts"; +import { useGlobalState } from "../../global-state/context-provider.tsx"; +import { Spinner } from "../loader/loader.tsx"; +import { FlexContainer } from "../generic-components.ts"; +import { RemoveLineButton } from "../remove-line-button/remove-line-button.tsx"; +import { useFetchProduction } from "../landing-page/use-fetch-production.ts"; +import { darkText, errorColour } from "../../css-helpers/defaults.ts"; +import { NavigateToRootButton } from "../navigate-to-root-button/navigate-to-root-button.tsx"; +import { ResponsiveFormContainer } from "../user-settings/user-settings.tsx"; +import { isMobile } from "../../bowser.ts"; +import { Checkbox } from "../checkbox/checkbox.tsx"; -export const CreateProductionPage = ({ - setApiError, -}: { - setApiError: () => void; -}) => { - const [{ apiError }] = useGlobalState(); +type FormValues = { + productionName: string; + defaultLine: string; + defaultLineProgramOutput: boolean; + lines: { name: string; programOutputLine?: boolean }[]; +}; + +const HeaderWrapper = styled.div` + display: flex; + margin-bottom: 2rem; + align-items: center; + h2 { + margin: 0; + margin-left: 1rem; + } +`; + +export const ListItemWrapper = styled.div` + position: relative; +`; + +const ButtonWrapper = styled.div` + margin: 0 1rem 1rem 0; + :last-of-type { + margin: 0 0 1rem; + } +`; + +const ProductionConfirmation = styled.div` + background: #91fa8c; + padding: 1rem; + margin-bottom: 1rem; + border-radius: 0.5rem; + border: 1px solid #b2ffa1; + color: #1a1a1a; +`; + +const CopyToClipboardWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const CopyConfirmation = styled.p` + padding-left: 1rem; +`; + +const FetchErrorMessage = styled.div` + background: ${errorColour}; + color: ${darkText}; + padding: 0.5rem; + margin: 1rem 0; +`; + +const CheckboxWrapper = styled.div` + margin-bottom: 3rem; +`; +export const CreateProductionPage = () => { + const [, dispatch] = useGlobalState(); + const [createdProductionId, setCreatedProductionId] = useState( + null + ); + const [loading, setLoading] = useState(false); + const [copiedUrl, setCopiedUrl] = useState(false); + const { + formState: { errors }, + control, + register, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + productionName: "", + defaultLine: "", + defaultLineProgramOutput: false, + lines: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "lines", + rules: { + minLength: 1, + }, + }); + + const { error: productionFetchError, production } = useFetchProduction( + createdProductionId ? parseInt(createdProductionId, 10) : null + ); + + const onSubmit: SubmitHandler = (value) => { + setLoading(true); + API.createProduction({ + name: value.productionName, + lines: [ + { + name: value.defaultLine, + programOutputLine: value.defaultLineProgramOutput, + }, + ...value.lines, + ], + }) + .then((v) => { + setCreatedProductionId(v.productionId); + setLoading(false); + }) + .catch((error) => { + dispatch({ + type: "ERROR", + payload: error, + }); + setLoading(false); + }); + }; + + // Reset form values when created production id changes useEffect(() => { - if (apiError) { - setApiError(); + if (createdProductionId) { + reset({ + productionName: "", + defaultLine: "", + lines: [], + }); + dispatch({ + type: "PRODUCTION_UPDATED", + }); + } + }, [createdProductionId, dispatch, reset]); + + useEffect(() => { + let timeout: number | null = null; + if (copiedUrl) { + timeout = window.setTimeout(() => { + setCopiedUrl(false); + }, 1500); + } + return () => { + if (timeout !== null) { + window.clearTimeout(timeout); + } + }; + }, [copiedUrl]); + + const handleCopyProdUrlsToClipboard = (input: string[]) => { + if (input !== null) { + navigator.clipboard + .writeText(input.join("\n")) + .then(() => { + setCopiedUrl(true); + console.log("Text copied to clipboard"); + }) + .catch((err) => { + console.error("Failed to copy text: ", err); + }); } - }, [apiError, setApiError]); + }; - return ; + return ( + + + + Create Production + + + Production Name + + + + + Line + + ( + + ) => + field.onChange(e.target.checked) + } + /> + + )} + /> + + + {fields.map((field, index) => ( +
+ + Line + + + ( + + ) => + controllerField.onChange(e.target.checked) + } + /> + + )} + /> + {index === fields.length - 1 && ( + remove(index)} + /> + )} + + + +
+ ))} + + + append({ name: "" })}> + Add Line + + + + + Create Production + {loading && } + + + + {createdProductionId !== null && ( + <> + + The production ID is: {createdProductionId.toString()} + + {!productionFetchError && production && ( + + + handleCopyProdUrlsToClipboard( + production.lines.map((item) => { + return ` ${item.name}: ${window.location.origin}/production-calls/production/${production.productionId}/line/${item.id}`; + }) + ) + } + disabled={copiedUrl} + > + Copy Links + + {copiedUrl && Copied} + + )} + {productionFetchError && ( + + The production information could not be fetched, not able to copy + to clipboard. + + )} + + )} +
+ ); }; diff --git a/src/components/create-production/create-production.tsx b/src/components/create-production/create-production.tsx deleted file mode 100644 index e64641eb..00000000 --- a/src/components/create-production/create-production.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import { - Controller, - SubmitHandler, - useFieldArray, - useForm, -} from "react-hook-form"; -import { useEffect, useState } from "react"; -import styled from "@emotion/styled"; -import { ErrorMessage } from "@hookform/error-message"; -import { DisplayContainerHeader } from "../landing-page/display-container-header.tsx"; -import { - DecorativeLabel, - FormInput, - FormLabel, - StyledWarningMessage, - PrimaryButton, - SecondaryButton, -} from "../landing-page/form-elements.tsx"; -import { API } from "../../api/api.ts"; -import { useGlobalState } from "../../global-state/context-provider.tsx"; -import { Spinner } from "../loader/loader.tsx"; -import { FlexContainer } from "../generic-components.ts"; -import { RemoveLineButton } from "../remove-line-button/remove-line-button.tsx"; -import { useFetchProduction } from "../landing-page/use-fetch-production.ts"; -import { darkText, errorColour } from "../../css-helpers/defaults.ts"; -import { NavigateToRootButton } from "../navigate-to-root-button/navigate-to-root-button.tsx"; -import { ResponsiveFormContainer } from "../user-settings/user-settings.tsx"; -import { isMobile } from "../../bowser.ts"; -import { Checkbox } from "../checkbox/checkbox.tsx"; - -type FormValues = { - productionName: string; - defaultLine: string; - defaultLineProgramOutput: boolean; - lines: { name: string; programOutputLine?: boolean }[]; -}; - -const HeaderWrapper = styled.div` - display: flex; - margin-bottom: 2rem; - align-items: center; - h2 { - margin: 0; - margin-left: 1rem; - } -`; - -export const ListItemWrapper = styled.div` - position: relative; -`; - -const ButtonWrapper = styled.div` - margin: 0 1rem 1rem 0; - :last-of-type { - margin: 0 0 1rem; - } -`; - -const ProductionConfirmation = styled.div` - background: #91fa8c; - padding: 1rem; - margin-bottom: 1rem; - border-radius: 0.5rem; - border: 1px solid #b2ffa1; - color: #1a1a1a; -`; - -const CopyToClipboardWrapper = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const CopyConfirmation = styled.p` - padding-left: 1rem; -`; - -const FetchErrorMessage = styled.div` - background: ${errorColour}; - color: ${darkText}; - padding: 0.5rem; - margin: 1rem 0; -`; - -const CheckboxWrapper = styled.div` - margin-bottom: 3rem; -`; - -export const CreateProduction = () => { - const [, dispatch] = useGlobalState(); - const [createdProductionId, setCreatedProductionId] = useState( - null - ); - const [loading, setLoading] = useState(false); - const [copiedUrl, setCopiedUrl] = useState(false); - const { - formState: { errors }, - control, - register, - handleSubmit, - reset, - } = useForm({ - defaultValues: { - productionName: "", - defaultLine: "", - defaultLineProgramOutput: false, - lines: [], - }, - }); - - const { fields, append, remove } = useFieldArray({ - control, - name: "lines", - rules: { - minLength: 1, - }, - }); - - const { error: productionFetchError, production } = useFetchProduction( - createdProductionId ? parseInt(createdProductionId, 10) : null - ); - - const onSubmit: SubmitHandler = (value) => { - setLoading(true); - API.createProduction({ - name: value.productionName, - lines: [ - { - name: value.defaultLine, - programOutputLine: value.defaultLineProgramOutput, - }, - ...value.lines, - ], - }) - .then((v) => { - setCreatedProductionId(v.productionId); - setLoading(false); - }) - .catch((error) => { - dispatch({ - type: "ERROR", - payload: error, - }); - setLoading(false); - }); - }; - - // Reset form values when created production id changes - useEffect(() => { - if (createdProductionId) { - reset({ - productionName: "", - defaultLine: "", - lines: [], - }); - dispatch({ - type: "PRODUCTION_UPDATED", - }); - } - }, [createdProductionId, dispatch, reset]); - - useEffect(() => { - let timeout: number | null = null; - if (copiedUrl) { - timeout = window.setTimeout(() => { - setCopiedUrl(false); - }, 1500); - } - return () => { - if (timeout !== null) { - window.clearTimeout(timeout); - } - }; - }, [copiedUrl]); - - const handleCopyProdUrlsToClipboard = (input: string[]) => { - if (input !== null) { - navigator.clipboard - .writeText(input.join("\n")) - .then(() => { - setCopiedUrl(true); - console.log("Text copied to clipboard"); - }) - .catch((err) => { - console.error("Failed to copy text: ", err); - }); - } - }; - - return ( - - - - Create Production - - - Production Name - - - - - Line - - ( - - ) => - field.onChange(e.target.checked) - } - /> - - )} - /> - - - {fields.map((field, index) => ( -
- - Line - - - ( - - ) => - controllerField.onChange(e.target.checked) - } - /> - - )} - /> - {index === fields.length - 1 && ( - remove(index)} - /> - )} - - - -
- ))} - - - append({ name: "" })}> - Add Line - - - - - Create Production - {loading && } - - - - {createdProductionId !== null && ( - <> - - The production ID is: {createdProductionId.toString()} - - {!productionFetchError && production && ( - - - handleCopyProdUrlsToClipboard( - production.lines.map((item) => { - return ` ${item.name}: ${window.location.origin}/production-calls/production/${production.productionId}/line/${item.id}`; - }) - ) - } - disabled={copiedUrl} - > - Copy Links - - {copiedUrl && Copied} - - )} - {productionFetchError && ( - - The production information could not be fetched, not able to copy - to clipboard. - - )} - - )} -
- ); -}; diff --git a/src/components/landing-page/join-production.tsx b/src/components/landing-page/join-production.tsx index cd6d50d2..e29bfe82 100644 --- a/src/components/landing-page/join-production.tsx +++ b/src/components/landing-page/join-production.tsx @@ -1,4 +1,4 @@ -import { SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useForm, useWatch } from "react-hook-form"; import { ErrorMessage } from "@hookform/error-message"; import { useEffect, useState } from "react"; import styled from "@emotion/styled"; @@ -120,7 +120,7 @@ export const JoinProduction = ({ reset, setValue, getValues, - watch, + control, } = useForm({ defaultValues: { productionId: @@ -145,16 +145,17 @@ export const JoinProduction = ({ useNavigateToProduction(joinProductionOptions); + // this will update whenever lineId changes + const selectedLineId = useWatch({ name: "lineId", control }); + useEffect(() => { - const selectedLineId = watch("lineId"); if (production) { const selectedLine = production.lines.find( (line) => line.id.toString() === selectedLineId ); setIsProgramOutputLine(!!selectedLine?.programOutputLine); } - // eslint-disable-next-line - }, [production, watch("lineId")]); + }, [production, selectedLineId]); // Update selected line id when a new production is fetched useEffect(() => { diff --git a/src/components/manage-productions-page/manage-productions-page.tsx b/src/components/manage-productions-page/manage-productions-page.tsx index 23c2a702..4047ca73 100644 --- a/src/components/manage-productions-page/manage-productions-page.tsx +++ b/src/components/manage-productions-page/manage-productions-page.tsx @@ -5,8 +5,12 @@ import { useRefreshAnimation } from "../landing-page/use-refresh-animation"; import { ProductionsList } from "../production-list/productions-list"; import { PageHeader } from "../page-layout/page-header"; -export const ManageProductionsPage = () => { - const [{ reloadProductionList }] = useGlobalState(); +export const ManageProductionsPage = ({ + setApiError, +}: { + setApiError: () => void; +}) => { + const [{ apiError, reloadProductionList }] = useGlobalState(); const { productions, doInitialLoad, error, setIntervalLoad } = useFetchProductionList({ limit: "30", @@ -18,6 +22,12 @@ export const ManageProductionsPage = () => { doInitialLoad, }); + useEffect(() => { + if (apiError) { + setApiError(); + } + }, [apiError, setApiError]); + useEffect(() => { const interval = window.setInterval(() => { setIntervalLoad(true); diff --git a/src/components/production-line/select-devices.tsx b/src/components/production-line/select-devices.tsx index a8594899..867a912f 100644 --- a/src/components/production-line/select-devices.tsx +++ b/src/components/production-line/select-devices.tsx @@ -1,4 +1,4 @@ -import { useForm, SubmitHandler } from "react-hook-form"; +import { useForm, SubmitHandler, useWatch } from "react-hook-form"; import { useParams } from "react-router-dom"; import { isBrowserFirefox, isMobile } from "../../bowser"; import { @@ -37,7 +37,7 @@ export const SelectDevices = ({ formState: { isValid, isDirty }, register, handleSubmit, - watch, + control, } = useForm({ defaultValues: { username: "", @@ -53,11 +53,14 @@ export const SelectDevices = ({ }); // Watch all form values - const watchedValues = watch(); + const watchedValues = useWatch({ + name: ["audioinput", "audiooutput"], + control, + }); const audioInputTheSame = - joinProductionOptions?.audioinput === watchedValues.audioinput; + joinProductionOptions?.audioinput === watchedValues[0]; const audioOutputTheSame = - joinProductionOptions?.audiooutput === watchedValues.audiooutput; + joinProductionOptions?.audiooutput === watchedValues[1]; const audioNotChanged = audioInputTheSame && audioOutputTheSame; // Reset connection and re-connect to production-line diff --git a/src/components/production-list/manage-production-buttons.tsx b/src/components/production-list/manage-production-buttons.tsx index 601ea992..d6c4bea0 100644 --- a/src/components/production-list/manage-production-buttons.tsx +++ b/src/components/production-list/manage-production-buttons.tsx @@ -3,7 +3,7 @@ import { ErrorMessage } from "@hookform/error-message"; import { useForm, Controller, SubmitHandler } from "react-hook-form"; import { FC, useEffect, useState } from "react"; import { RemoveIcon } from "../../assets/icons/icon"; -import { ListItemWrapper } from "../create-production/create-production"; +import { ListItemWrapper } from "../create-production/create-production-page"; import { FormInput, FormLabel, diff --git a/src/hooks/use-local-user-settings.ts b/src/hooks/use-local-user-settings.ts index bc80fd67..df3a9e74 100644 --- a/src/hooks/use-local-user-settings.ts +++ b/src/hooks/use-local-user-settings.ts @@ -12,7 +12,7 @@ export const useLocalUserSettings = ({ devices, dispatch, }: TUseLocalUserSettings) => { - const { readFromStorage, clearStorage } = useStorage(); + const { readFromStorage, removeFromStorage } = useStorage(); useEffect(() => { if (devices.input || devices.output) { const storedAudioInput = readFromStorage("audioinput"); @@ -27,9 +27,9 @@ export const useLocalUserSettings = ({ (device) => device.deviceId === storedAudioOutput )?.deviceId; - if (!foundInputDevice) clearStorage("audioinput"); + if (!foundInputDevice) removeFromStorage("audioinput"); - if (!foundOutputDevice) clearStorage("audiooutput"); + if (!foundOutputDevice) removeFromStorage("audiooutput"); const payload = { username: readFromStorage("username") || "", @@ -42,6 +42,5 @@ export const useLocalUserSettings = ({ payload, }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [devices, dispatch]); + }, [devices, dispatch, readFromStorage, removeFromStorage]); };