Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ module.exports = {
{ allowConstantExport: true },
],
"no-console": "off",
"no-underscore-dangle": "off",
},
};
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ManageProductionsPage } from "./components/manage-productions-page/mana
import { CreateProductionPage } from "./components/create-production/create-production-page.tsx";
import { useSetupTokenRefresh } from "./hooks/use-reauth.tsx";
import { TUserSettings } from "./components/user-settings/types";
import { IngestsPage } from "./components/ingests-page/ingests-page.tsx";

const DisplayBoxPositioningContainer = styled(FlexContainer)`
justify-content: center;
Expand Down Expand Up @@ -150,6 +151,13 @@ const AppContent = ({
}
errorElement={<ErrorPage />}
/>
<Route
path="/ingests"
element={
<IngestsPage setApiError={() => setApiError(true)} />
}
errorElement={<ErrorPage />}
/>
<Route
path="/production-calls/production/:productionId/line/:lineId"
element={<CallsPage />}
Expand Down
93 changes: 93 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,43 @@ export type TBasicProductionResponse = {
lines: TLine[];
};

export type TAudioDevice = {
name: string;
maxInputChannels: number;
maxOutputChannels: number;
defaultSampleRate: number;
defaultLowInputLatency: number;
defaultLowOutputLatency: number;
defaultHighInputLatency: number;
defaultHighOutputLatency: number;
isInput: boolean;
isOutput: boolean;
hostApiName: string;
label?: string;
Copy link
Contributor

@malmen237 malmen237 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label?: string;
or
label: string;

};

Comment on lines +35 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type TAudioDevice = {
name: string;
maxInputChannels: number;
maxOutputChannels: number;
defaultSampleRate: number;
defaultLowInputLatency: number;
defaultLowOutputLatency: number;
defaultHighInputLatency: number;
defaultHighOutputLatency: number;
isInput: boolean;
isOutput: boolean;
hostApiName: string;
label?: string;
};
export type TAudioDevice = {
name: string;
maxChannels: number;
defaultSampleRate: number;
defaultLowLatency: number;
defaultHighLatency: number;
type: "input" | "output";
hostApiName: string;
label?: string;
};

export type TSavedIngest = {
_id: string;
label: string;
ipAddress: string;
deviceOutput: TAudioDevice[];
deviceInput: TAudioDevice[];
};

export type TEditIngest = {
_id: string;
label?: string;
deviceOutput?: TAudioDevice;
deviceInput?: TAudioDevice;
};

export type TListIngestResponse = {
ingests: TSavedIngest[];
offset: 0;
limit: 0;
totalItems: 0;
};

export type TListProductionsResponse = {
productions: TBasicProductionResponse[];
offset: 0;
Expand Down Expand Up @@ -291,4 +328,60 @@ export const API = {
})
);
},
fetchIngestList: (): Promise<TListIngestResponse> =>
handleFetchRequest<TListIngestResponse>(
fetch(`${API_URL}ingest`, {
method: "GET",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
createIngest: async (data: { label: string; ipAddress: string }) =>
handleFetchRequest<boolean>(
fetch(`${API_URL}ingest/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
body: JSON.stringify({
label: data.label,
ipAddress: data.ipAddress,
}),
})
),
fetchIngest: (id: number): Promise<TSavedIngest> =>
handleFetchRequest<TSavedIngest>(
fetch(`${API_URL}ingest/${id}`, {
method: "GET",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
updateIngest: async (data: TEditIngest) =>
handleFetchRequest<TEditIngest>(
fetch(`${API_URL}ingest/${data._id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
body: JSON.stringify({
label: data.label,
deviceOutput: data.deviceOutput,
deviceInput: data.deviceInput,
}),
})
),
deleteIngest: async (id: string): Promise<string> =>
handleFetchRequest<string>(
fetch(`${API_URL}ingest/${id}`, {
method: "DELETE",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
};
2 changes: 1 addition & 1 deletion src/components/audio-feed-modal/audio-feed-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from "@emotion/styled";
import { Modal } from "../modal/modal";
import { PrimaryButton } from "../landing-page/form-elements";
import { PrimaryButton } from "../form-elements/form-elements";
import { Checkbox } from "../checkbox/checkbox";

const ContentWrapper = styled.div`
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls-page/connect-to-ws-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useGlobalState } from "../../global-state/context-provider";
import { useWebSocket } from "../../hooks/use-websocket";
import { useWebsocketActions } from "../../hooks/use-websocket-actions";
import { useWebsocketReconnect } from "../../hooks/use-websocket-reconnect";
import { PrimaryButton } from "../landing-page/form-elements";
import { PrimaryButton } from "../form-elements/form-elements";
import { Spinner } from "../loader/loader";
import { ConnectToWsModal } from "./connect-to-ws-modal";

Expand Down
2 changes: 1 addition & 1 deletion src/components/calls-page/connect-to-ws-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
FormInput,
PrimaryButton,
SecondaryButton,
} from "../landing-page/form-elements";
} from "../form-elements/form-elements";
import { Modal } from "../modal/modal";

const ButtonWrapper = styled.div`
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls-page/header-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled from "@emotion/styled";
import { MicMuted, MicUnmuted } from "../../assets/icons/icon";
import { isMobile, isTablet } from "../../bowser";
import { PrimaryButton, SecondaryButton } from "../landing-page/form-elements";
import { PrimaryButton, SecondaryButton } from "../form-elements/form-elements";
import { ConnectToWSButton } from "./connect-to-ws-button";
import { useGlobalMuteToggle } from "./use-global-mute-toggle";

Expand Down
2 changes: 1 addition & 1 deletion src/components/copy-button/copy-all-links-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from "@emotion/styled";
import { PrimaryButton } from "../landing-page/form-elements";
import { PrimaryButton } from "../form-elements/form-elements";
import { useCopyLinks } from "./use-copy-links";
import { TProduction } from "../production-line/types";
import { CheckIcon } from "../../assets/icons/icon";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrimaryButton, SecondaryButton } from "../landing-page/form-elements";
import { PrimaryButton, SecondaryButton } from "../form-elements/form-elements";
import { Spinner } from "../loader/loader";
import { ButtonContainer, ButtonWrapper } from "./create-production-components";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "react-hook-form";
import { useEffect, useState } from "react";
import { DisplayContainerHeader } from "../landing-page/display-container-header.tsx";
import { FormInput } from "../landing-page/form-elements.tsx";
import { FormInput } from "../form-elements/form-elements.tsx";
import { useGlobalState } from "../../global-state/context-provider.tsx";
import {
ListItemWrapper,
Expand Down
25 changes: 25 additions & 0 deletions src/components/delete-button/delete-button-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from "@emotion/styled";
import { SecondaryButton } from "../form-elements/form-elements";

export const ButtonsWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin: 1rem 0 1rem 0;
`;

export const DeleteButton = styled(SecondaryButton)`
display: flex;
align-items: center;
background: #d15c5c;
color: white;

&:disabled {
background: #ab5252;
}
`;

export const SpinnerWrapper = styled.div`
position: relative;
width: 2rem;
height: 2rem;
`;
2 changes: 1 addition & 1 deletion src/components/display-box.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from "@emotion/styled";
import { PrimaryButton } from "./landing-page/form-elements";
import { PrimaryButton } from "./form-elements/form-elements";

const borderRadius = 0.5;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export const FormInput = styled.input`

&.edit-name {
margin: 0;

&.device-label {
font-size: 1.2rem;
}
}
`;

Expand All @@ -52,6 +56,12 @@ export const FormSelect = styled.select`
border-radius: 0.5rem;
background: #32383b;
color: white;

&.ingest {
display: flex;
align-items: center;
margin: 0 1rem 0 0;
}
`;

export const FormLabel = styled.label`
Expand Down
2 changes: 1 addition & 1 deletion src/components/generic-components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from "@emotion/styled";
import { FormContainer } from "./landing-page/form-elements";
import { FormContainer } from "./form-elements/form-elements";
import { isMobile } from "../bowser";

// Screen size breakpoints based on width
Expand Down
92 changes: 92 additions & 0 deletions src/components/ingests-page/add-ingest-modal/add-ingest-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { SubmitHandler, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import { useSubmitOnEnter } from "../../../hooks/use-submit-form-enter-press";
import { ButtonWrapper } from "../../generic-components";
import { FormInput } from "../../form-elements/form-elements";
import { FormItem } from "../../user-settings-form/form-item";
import { FormWrapper, SubmitButton } from "../ingest-components";
import { useCreateIngest } from "./use-create-ingest";
import { Spinner } from "../../loader/loader";
import { SpinnerWrapper } from "../../delete-button/delete-button-components";

type FormValues = {
ingestLabel: string;
ipAddress: string;
};

type AddIngestFormProps = {
onSave?: () => void;
};

export const AddIngestForm = ({ onSave }: AddIngestFormProps) => {
const [createIngest, setCreateIngest] = useState<FormValues | null>(null);
const {
formState: { errors, isValid },
register,
handleSubmit,
} = useForm<FormValues>({
resetOptions: {
keepDirtyValues: true, // user-interacted input will be retained
keepErrors: true, // input errors will be retained with value update
},
});

const { loading, success } = useCreateIngest({ createIngest });

useEffect(() => {
if (success) {
setCreateIngest(null);
if (onSave) onSave();
}
}, [success, onSave]);

const onSubmit: SubmitHandler<FormValues> = (data) => {
setCreateIngest(data);
};

useSubmitOnEnter<FormValues>({
handleSubmit,
submitHandler: onSubmit,
shouldSubmitOnEnter: true,
});

return (
<FormWrapper>
<FormItem label="Name" fieldName="ingestLabel" errors={errors}>
<FormInput
// eslint-disable-next-line
{...register(`ingestLabel`, {
required: "Ingest name is required",
minLength: 1,
})}
placeholder="Name for Ingest"
/>
</FormItem>
<FormItem label="Server IP Address" fieldName="ipAddress" errors={errors}>
<FormInput
// eslint-disable-next-line
{...register(`ipAddress`, {
required: "IP address is required",
minLength: 1,
})}
placeholder="192.168.1.1"
/>
</FormItem>
<ButtonWrapper>
<SubmitButton
type="button"
disabled={!isValid}
onClick={handleSubmit(onSubmit)}
shouldSubmitOnEnter
>
Add Ingest
{loading && (
<SpinnerWrapper>
<Spinner className="production-list" />
</SpinnerWrapper>
)}
</SubmitButton>
</ButtonWrapper>
</FormWrapper>
);
};
21 changes: 21 additions & 0 deletions src/components/ingests-page/add-ingest-modal/ingest-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FC } from "react";
import { DisplayContainerHeader } from "../../landing-page/display-container-header";
import { ResponsiveFormContainer } from "../../generic-components";
import { AddIngestForm } from "./add-ingest-form";

interface IngestFormModalProps {
className?: string;
onSave?: () => void;
}

export const IngestFormModal: FC<IngestFormModalProps> = (props) => {
const { className, onSave } = props;

return (
<ResponsiveFormContainer className={className}>
<DisplayContainerHeader>Add New Ingest</DisplayContainerHeader>

<AddIngestForm onSave={onSave} />
</ResponsiveFormContainer>
);
};
24 changes: 24 additions & 0 deletions src/components/ingests-page/add-ingest-modal/use-create-ingest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { API } from "../../../api/api";
import { useRequest } from "../../../hooks/use-request";

type FormValues = {
ingestLabel: string;
ipAddress: string;
};

export const useCreateIngest = ({
createIngest,
}: {
createIngest: FormValues | null;
}) => {
return useRequest<{ label: string; ipAddress: string }, boolean>({
params: createIngest
? {
label: createIngest.ingestLabel,
ipAddress: createIngest.ipAddress,
}
: null,
apiCall: API.createIngest,
errorMessage: (i) => `Failed to create ingest: ${i.label}`,
});
};
Loading