[frontend]: add stream keys page
This commit is contained in:
parent
8018d21011
commit
4c0e463aec
@ -112,7 +112,24 @@ export default [
|
||||
"@typescript-eslint/no-misused-promises": ["error", {
|
||||
"checksVoidReturn": false
|
||||
}
|
||||
]
|
||||
],
|
||||
'@typescript-eslint/naming-convention': ['error',
|
||||
{
|
||||
selector: 'enum',
|
||||
format: ['UPPER_CASE', 'PascalCase'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
|
||||
},
|
||||
{
|
||||
selector: 'function',
|
||||
format: ['camelCase', 'PascalCase'],
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: ['PascalCase'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -55,6 +55,7 @@
|
||||
"@next/eslint-plugin-next": "15.4.5",
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"@tailwindcss/postcss": "4.1.11",
|
||||
"@types/apollo-upload-client": "18.0.0",
|
||||
"@types/node": "22.17.0",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react-dom": "19.1.7",
|
||||
|
||||
20
frontend/src/app/(site)/dashboard/keys/page.tsx
Normal file
20
frontend/src/app/(site)/dashboard/keys/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { KeysSettings } from '@/components/features/keys/settings/KeysSettings';
|
||||
import { NO_INDEX_PAGE } from '@/libs/constants/seo.constants';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations('dashboard.keys.header');
|
||||
|
||||
return {
|
||||
title: t('heading'),
|
||||
description: t('description'),
|
||||
...NO_INDEX_PAGE,
|
||||
};
|
||||
}
|
||||
|
||||
const KeysSettingsPage = () => <KeysSettings />;
|
||||
|
||||
export default KeysSettingsPage;
|
||||
@ -0,0 +1,173 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@/components/ui/common/Button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/common/Dialog';
|
||||
|
||||
export const InstructionModal = () => {
|
||||
const t = useTranslations('dashboard.keys.instructionModal');
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
{t('trigger')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-h-[80vh] max-w-[800px] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{t('heading')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="text-sm text-muted-foreground">
|
||||
{t('description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('step1Title')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('step1Description')}
|
||||
</p>
|
||||
|
||||
<ol className="list-inside list-decimal space-y-2 pl-4">
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('downloadObs')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('downloadObsDescription')}
|
||||
|
||||
{' '}
|
||||
|
||||
<a
|
||||
className="text-blue-500 underline hover:text-blue-700"
|
||||
href="https://obsproject.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('obsLinkText')}
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('copyKeys')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('copyKeysDescription')}
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 className="mt-4 text-lg font-semibold">
|
||||
{t('step2Title')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('step2Description')}
|
||||
</p>
|
||||
|
||||
<ol className="list-inside list-decimal space-y-2 pl-4">
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('openObs')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('openObsDescription')}
|
||||
</li>
|
||||
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('openStreamSettings')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('openStreamSettingsDescription')}
|
||||
</li>
|
||||
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('enterDetails')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('enterDetailsDescription')}
|
||||
</li>
|
||||
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('saveSettings')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('saveSettingsDescription')}
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 className="mt-4 text-lg font-semibold">
|
||||
{t('step3Title')}
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('step3Description')}
|
||||
</p>
|
||||
|
||||
<ol className="list-inside list-decimal space-y-2 pl-4">
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('startStream')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('startStreamDescription')}
|
||||
</li>
|
||||
|
||||
<li className="text-sm text-muted-foreground">
|
||||
<strong>
|
||||
{t('monitorStream')}
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
|
||||
{t('monitorStreamDescription')}
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
{t('congrats')}
|
||||
</p>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
{t('close')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Heading } from '@/components/ui/elements/Heading';
|
||||
import { ToggleCardSkeleton } from '@/components/ui/elements/ToggleCard';
|
||||
import { useCurrent } from '@/hooks/useCurrent';
|
||||
|
||||
import { CreateIngressForm } from './forms/CreateIngressForm';
|
||||
import { StreamKey } from './forms/StreamKey';
|
||||
import { StreamURL } from './forms/StreamURL';
|
||||
import { InstructionModal } from './InstructionModal';
|
||||
|
||||
export const KeysSettings = () => {
|
||||
const t = useTranslations('dashboard.keys.header');
|
||||
|
||||
const { user, isLoadingProfile } = useCurrent();
|
||||
|
||||
return (
|
||||
<div className="lg:px-10">
|
||||
<div className="block items-center justify-between space-y-3 lg:flex lg:space-y-0">
|
||||
<Heading
|
||||
description={t('description')}
|
||||
size="lg"
|
||||
title={t('heading')}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-x-4">
|
||||
<InstructionModal />
|
||||
|
||||
<CreateIngressForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-6">
|
||||
{isLoadingProfile
|
||||
? Array.from({ length: 2 }).map((_, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<ToggleCardSkeleton key={index} />
|
||||
))
|
||||
: (
|
||||
<>
|
||||
<StreamURL value={user?.stream?.serverUrl} />
|
||||
|
||||
<StreamKey value={user?.stream?.key} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '@/components/ui/common/Button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/common/Dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from '@/components/ui/common/Form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/common/Select';
|
||||
import { useCreateIngressMutation } from '@/graphql/generated/output';
|
||||
import { useCurrent } from '@/hooks/useCurrent';
|
||||
import {
|
||||
IngressType,
|
||||
type TypeCreateIngressSchema,
|
||||
createIngressSchema,
|
||||
} from '@/schemas/stream/create-ingress.schema';
|
||||
|
||||
export const CreateIngressForm = () => {
|
||||
const t = useTranslations('dashboard.keys.createModal');
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { refetch } = useCurrent();
|
||||
|
||||
const form = useForm<TypeCreateIngressSchema>({
|
||||
resolver: zodResolver(createIngressSchema),
|
||||
defaultValues: {
|
||||
ingressType: IngressType.RTMP,
|
||||
},
|
||||
});
|
||||
|
||||
const [create, { loading: isLoadingCreate }] = useCreateIngressMutation({
|
||||
onCompleted() {
|
||||
setIsOpen(false);
|
||||
void refetch();
|
||||
toast.success(t('successMessage'));
|
||||
},
|
||||
onError() {
|
||||
toast.error(t('errorMessage'));
|
||||
},
|
||||
});
|
||||
|
||||
const { isValid } = form.formState;
|
||||
|
||||
function onSubmit(data: TypeCreateIngressSchema) {
|
||||
void create({ variables: { ingressType: data.ingressType } });
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
{t('trigger')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('heading')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ingressType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('ingressTypeLabel')}
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
defaultValue={field.value.toString()}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
'ingressTypePlaceholder',
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
disabled={isLoadingCreate}
|
||||
value={IngressType.RTMP.toString()}
|
||||
>
|
||||
RTMP
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem
|
||||
disabled={isLoadingCreate}
|
||||
value={IngressType.WHIP.toString()}
|
||||
>
|
||||
WHIP
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
{t('ingressTypeDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button disabled={!isValid || isLoadingCreate}>
|
||||
{t('submitButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/common/Button';
|
||||
import { Input } from '@/components/ui/common/Input';
|
||||
import { CardContainer } from '@/components/ui/elements/CardContainer';
|
||||
import { CopyButton } from '@/components/ui/elements/CopyButton';
|
||||
|
||||
type StreamKeyProps = {
|
||||
value?: string | null;
|
||||
};
|
||||
|
||||
export const StreamKey = ({ value }: StreamKeyProps) => {
|
||||
const t = useTranslations('dashboard.keys.key');
|
||||
|
||||
const [isShow, setIsShow] = useState(false);
|
||||
|
||||
const Icon = isShow ? Eye : EyeOff;
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
isRightContentFull
|
||||
heading={t('heading')}
|
||||
rightContent={(
|
||||
<div className="flex w-full items-center gap-x-4">
|
||||
<Input
|
||||
disabled
|
||||
placeholder={t('heading')}
|
||||
type={isShow ? 'text' : 'password'}
|
||||
value={value ?? ''}
|
||||
/>
|
||||
|
||||
<CopyButton value={value} />
|
||||
|
||||
<Button
|
||||
size="lgIcon"
|
||||
variant="ghost"
|
||||
onClick={() => { setIsShow(!isShow); }}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Input } from '@/components/ui/common/Input';
|
||||
import { CardContainer } from '@/components/ui/elements/CardContainer';
|
||||
import { CopyButton } from '@/components/ui/elements/CopyButton';
|
||||
|
||||
type StreamURLProps = {
|
||||
value?: string | null;
|
||||
};
|
||||
|
||||
export const StreamURL = ({ value }: StreamURLProps) => {
|
||||
const t = useTranslations('dashboard.keys.url');
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
isRightContentFull
|
||||
heading={t('heading')}
|
||||
rightContent={(
|
||||
<div className="flex w-full items-center gap-x-4">
|
||||
<Input
|
||||
disabled
|
||||
placeholder={t('heading')}
|
||||
value={value ?? ''}
|
||||
/>
|
||||
|
||||
<CopyButton value={value} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
40
frontend/src/components/ui/elements/CopyButton.tsx
Normal file
40
frontend/src/components/ui/elements/CopyButton.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { Button } from '../common/Button';
|
||||
|
||||
type CopyButtonProps = {
|
||||
value?: string | null;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value }: CopyButtonProps) => {
|
||||
const t = useTranslations('components.copyButton');
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
function onCopy() {
|
||||
if (!value) return;
|
||||
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(value);
|
||||
toast.success(t('successMessage'));
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const Icon = isCopied ? Check : Copy;
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!value || isCopied}
|
||||
size="lgIcon"
|
||||
variant="ghost"
|
||||
onClick={() => { onCopy(); }}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@ -643,6 +643,13 @@ export type VerifyAccountMutationVariables = Exact<{
|
||||
|
||||
export type VerifyAccountMutation = { __typename?: 'Mutation', verifyAccount: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', isEmailVerified: boolean } | null } };
|
||||
|
||||
export type CreateIngressMutationVariables = Exact<{
|
||||
ingressType: Scalars['Float']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIngressMutation = { __typename?: 'Mutation', createIngress: boolean };
|
||||
|
||||
export type ChangeEmailMutationVariables = Exact<{
|
||||
data: ChangeEmailInput;
|
||||
}>;
|
||||
@ -761,7 +768,7 @@ export type FindUnreadNotificationsCountQuery = { __typename?: 'Query', findUnre
|
||||
export type FindProfileQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type FindProfileQuery = { __typename?: 'Query', findProfile: { __typename?: 'UserModel', avatar?: string | null, bio?: string | null, createdAt: any, email: string, id: string, name: string, updatedAt: any, isEmailVerified: boolean, isTotpEnabled: boolean, isVerified: boolean, displayName: string, socialLink: Array<{ __typename?: 'SocialLinkModel', title: string, url: string, position: number }>, notificationSettings?: { __typename?: 'NotificationSettingsModel', siteNotifications: boolean, telegramNotifications: boolean } | null } };
|
||||
export type FindProfileQuery = { __typename?: 'Query', findProfile: { __typename?: 'UserModel', avatar?: string | null, bio?: string | null, createdAt: any, email: string, id: string, name: string, updatedAt: any, isEmailVerified: boolean, isTotpEnabled: boolean, isVerified: boolean, displayName: string, socialLink: Array<{ __typename?: 'SocialLinkModel', title: string, url: string, position: number }>, notificationSettings?: { __typename?: 'NotificationSettingsModel', siteNotifications: boolean, telegramNotifications: boolean } | null, stream: { __typename?: 'StreamModel', serverUrl?: string | null, key?: string | null } } };
|
||||
|
||||
export type FindSessionsByUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -1046,6 +1053,37 @@ export function useVerifyAccountMutation(baseOptions?: Apollo.MutationHookOption
|
||||
export type VerifyAccountMutationHookResult = ReturnType<typeof useVerifyAccountMutation>;
|
||||
export type VerifyAccountMutationResult = Apollo.MutationResult<VerifyAccountMutation>;
|
||||
export type VerifyAccountMutationOptions = Apollo.BaseMutationOptions<VerifyAccountMutation, VerifyAccountMutationVariables>;
|
||||
export const CreateIngressDocument = gql`
|
||||
mutation CreateIngress($ingressType: Float!) {
|
||||
createIngress(ingressType: $ingressType)
|
||||
}
|
||||
`;
|
||||
export type CreateIngressMutationFn = Apollo.MutationFunction<CreateIngressMutation, CreateIngressMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateIngressMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateIngressMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateIngressMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [createIngressMutation, { data, loading, error }] = useCreateIngressMutation({
|
||||
* variables: {
|
||||
* ingressType: // value for 'ingressType'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateIngressMutation(baseOptions?: Apollo.MutationHookOptions<CreateIngressMutation, CreateIngressMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateIngressMutation, CreateIngressMutationVariables>(CreateIngressDocument, options);
|
||||
}
|
||||
export type CreateIngressMutationHookResult = ReturnType<typeof useCreateIngressMutation>;
|
||||
export type CreateIngressMutationResult = Apollo.MutationResult<CreateIngressMutation>;
|
||||
export type CreateIngressMutationOptions = Apollo.BaseMutationOptions<CreateIngressMutation, CreateIngressMutationVariables>;
|
||||
export const ChangeEmailDocument = gql`
|
||||
mutation ChangeEmail($data: ChangeEmailInput!) {
|
||||
changeEmail(data: $data) {
|
||||
@ -1716,6 +1754,10 @@ export const FindProfileDocument = gql`
|
||||
siteNotifications
|
||||
telegramNotifications
|
||||
}
|
||||
stream {
|
||||
serverUrl
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
mutation CreateIngress($ingressType: Float!) {
|
||||
createIngress(ingressType: $ingressType)
|
||||
}
|
||||
@ -20,5 +20,9 @@ query FindProfile {
|
||||
siteNotifications
|
||||
telegramNotifications
|
||||
}
|
||||
stream {
|
||||
serverUrl
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,9 +8,9 @@ import { SERVER_URL, WEBSOCKET_URL } from './constants/url.constants';
|
||||
const httpLink = createUploadLink({
|
||||
uri: SERVER_URL,
|
||||
credentials: 'include',
|
||||
// headers: {
|
||||
// 'apollo-require-preflight': 'true',
|
||||
// },
|
||||
headers: {
|
||||
'apollo-require-preflight': 'true',
|
||||
},
|
||||
});
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
|
||||
12
frontend/src/schemas/stream/create-ingress.schema.ts
Normal file
12
frontend/src/schemas/stream/create-ingress.schema.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum IngressType {
|
||||
RTMP = 0,
|
||||
WHIP = 1,
|
||||
}
|
||||
|
||||
export const createIngressSchema = z.object({
|
||||
ingressType: z.enum(IngressType),
|
||||
});
|
||||
|
||||
export type TypeCreateIngressSchema = z.infer<typeof createIngressSchema>;
|
||||
@ -15,7 +15,7 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@apollo/client@3.13.9":
|
||||
"@apollo/client@3.13.9", "@apollo/client@^3.8.0":
|
||||
version "3.13.9"
|
||||
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.13.9.tgz#311e0df1734aaa5598e01a292bb7982df49cc062"
|
||||
integrity sha512-RStSzQfL1XwL6/NWd7W8avhGQYTgPCtJ+qHkkTTSj9Upp3VVm6Oppv81YWdXG1FgEpDPW4hvCrTUELdcC4inCQ==
|
||||
@ -2213,11 +2213,25 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/apollo-upload-client@18.0.0":
|
||||
version "18.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/apollo-upload-client/-/apollo-upload-client-18.0.0.tgz#1f7e2ff0c0e6508bc1bd19b2340339c7abd117c3"
|
||||
integrity sha512-cMgITNemktxasqvp6jiPj15dv84n3FTMvMoYBP1+xonDS+0l6JygIJrj2LJh85rShRzTOOkrElrAsCXXARa3KA==
|
||||
dependencies:
|
||||
"@apollo/client" "^3.8.0"
|
||||
"@types/extract-files" "*"
|
||||
graphql "14 - 16"
|
||||
|
||||
"@types/estree@^1.0.6":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
|
||||
"@types/extract-files@*":
|
||||
version "13.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/extract-files/-/extract-files-13.0.1.tgz#3ec057a3fa25f778245a76a17271d23b71ee31d7"
|
||||
integrity sha512-/fRbzc2lAd7jDJSSnxWiUyXWjdUZZ4HbISLJzVgt1AvrdOa7U49YRPcvuCUywkmURZ7uwJOheDjx19itbQ5KvA==
|
||||
|
||||
"@types/js-yaml@^4.0.0":
|
||||
version "4.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
|
||||
@ -4271,7 +4285,7 @@ graphql-ws@^6.0.6:
|
||||
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-6.0.6.tgz#e9c9ff85f4ddb5bbe6faa2721c0d27e6c11746d2"
|
||||
integrity sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==
|
||||
|
||||
graphql@16.11.0:
|
||||
"graphql@14 - 16", graphql@16.11.0:
|
||||
version "16.11.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633"
|
||||
integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user