Compare commits

...

3 Commits

Author SHA1 Message Date
Sylver fc0071165f feat: use skeleton loaders on some important pages 2024-02-11 17:06:49 +08:00
Sylver ef18709612 fix: forgot that was necessary 2024-02-11 12:59:27 +08:00
Sylver faae08470d fix: url parse errors 2024-02-11 12:58:29 +08:00
14 changed files with 238 additions and 118 deletions

View File

@ -4,6 +4,7 @@ export default {
overwrite: true,
schema: '../api/src/schema.gql',
documents: ['src/**/*.tsx'],
errorsOnly: true,
generates: {
'src/@generated/': {
preset: 'client',

View File

@ -13,7 +13,7 @@
"build": "tsc --noEmit && rm -rf ./dist/* && vavite build && tsup && rm -rf ./dist/server",
"generate": "graphql-codegen --config codegen.ts",
"start": "node ./dist/index.js",
"watch": "concurrently \"vavite serve\" \"pnpm generate --watch --errors-only\""
"watch": "concurrently \"vavite serve\" \"pnpm generate --watch\""
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.3.0",

View File

@ -19,6 +19,9 @@ export enum ButtonStyle {
Disabled = 'bg-dark-300 hover:bg-dark-400 cursor-not-allowed',
}
export const BASE_BUTTON_CLASSES =
'flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]';
export const Button = forwardRef<any, ButtonProps>(
(
{
@ -38,11 +41,7 @@ export const Button = forwardRef<any, ButtonProps>(
if (disabled) style = ButtonStyle.Disabled;
const onClickWrap = disabled || loading ? undefined : onClick;
const onKeyDownWrap = disabled || loading ? undefined : onKeyDown;
const classes = clsx(
'flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]',
className,
style,
);
const classes = clsx(BASE_BUTTON_CLASSES, className, style);
return (
<As

View File

@ -41,12 +41,7 @@ export function InputContainer<T extends InputChildPropsBase>({
const formik = useContext<FormikContextType<any>>(FormikContext);
const errorMessage = !!(formik && id && formik.touched[id]) && (formik.errors[id] as string);
if (errorMessage) style = InputStyle.Error;
const childClasses = clsx(
'w-full h-full px-3 py-2 rounded outline-none border transition duration-75',
maxHeight && 'max-h-[calc(2.65em-2px)]',
style,
className
);
const childClasses = clsx(BASE_INPUT_CLASSES, maxHeight && BASE_INPUT_MAX_HEIGHT, className, style);
if (formik) {
if (!id) {
@ -74,3 +69,6 @@ export function InputContainer<T extends InputChildPropsBase>({
</Fragment>
);
}
export const BASE_INPUT_CLASSES = 'w-full h-full px-3 py-2 rounded outline-none border transition duration-75';
export const BASE_INPUT_MAX_HEIGHT = 'max-h-[calc(2.65em-2px)]';

View File

@ -0,0 +1,59 @@
import clsx from 'clsx';
import { FC, Fragment, ReactNode, memo } from 'react';
import { BASE_BUTTON_CLASSES } from './button';
import { BASE_INPUT_CLASSES, BASE_INPUT_MAX_HEIGHT } from './input/container';
interface SkeletonProps {
className?: string;
}
export const Skeleton = memo<SkeletonProps>(({ className }) => {
const hasHeight = !className?.includes(' h-');
return <div className={clsx('animate-pulse bg-gray-800 rounded-md', className, hasHeight && 'h-4')} />;
});
interface SkeletonListProps {
count: number;
children: ReactNode;
className?: string;
as?: FC | FC<{ className?: string }>;
}
export const SkeletonList = memo<SkeletonListProps>(({ count, children, className, as }) => {
const Component = as || 'div';
return (
<Component className={className}>
{Array.from({ length: count }).map((_, i) => (
<Fragment key={i}>{children}</Fragment>
))}
</Component>
);
});
interface InputSkeletonProps {
maxHeight?: boolean;
className?: string;
}
export const InputSkeleton = memo<InputSkeletonProps>(({ maxHeight = true, className }) => {
const classes = clsx(BASE_INPUT_CLASSES, maxHeight && BASE_INPUT_MAX_HEIGHT, className, 'border-transparent');
return <Skeleton className={classes} />;
});
export const ButtonSkeleton = memo<SkeletonProps>(({ className }) => {
return <Skeleton className={clsx(BASE_BUTTON_CLASSES, className, 'min-h-[2.65em] min-w-[20em]')} />;
});
export const SkeletonWrap = memo<{
show: boolean;
children: ReactNode;
}>(({ show, children }) => {
if (!show) return children;
return (
<span className="relative">
<Skeleton className="absolute inset-0 h-full" />
<span className="opacity-0">{children}</span>
</span>
);
});

View File

@ -4,7 +4,7 @@ import { FiDownload } from 'react-icons/fi';
import { RegularUserFragment } from '../../@generated/graphql';
import { Container } from '../../components/container';
import { Section } from '../../components/section';
import { Spinner } from '../../components/spinner';
import { Skeleton, SkeletonList, SkeletonWrap } from '../../components/skeleton';
import { Toggle } from '../../components/toggle';
import { downloadFile } from '../../helpers/download.helper';
import { generateConfig } from '../../helpers/generate-config.helper';
@ -12,7 +12,7 @@ import { useConfig } from '../../hooks/useConfig';
import { CustomisationOption } from './customisation-option';
export interface ConfigGeneratorProps {
user: RegularUserFragment & { token: string };
user?: RegularUserFragment & { token: string };
}
export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
@ -23,7 +23,7 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
const downloadable = !!selectedHosts[0];
const download = () => {
if (!downloadable) return;
if (!downloadable || !user) return;
const { name, content } = generateConfig({
direct: !embedded,
hosts: selectedHosts,
@ -39,19 +39,16 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
<Section>
<Container className="flex flex-col justify-between dots selection:bg-purple-600 py-8 px-0">
<div className="w-full flex-grow">
{!config.data && (
<div className="flex items-center justify-center w-full h-full py-10">
<Spinner className="w-auto" />
</div>
)}
{config.data && (
<Fragment>
<Fragment>
<SkeletonWrap show={!config.data}>
<div className="font-bold text-xl">Config Generator</div>
<p className="text-sm text-gray-400">
Pick the hosts you want with the options you think will suit you best. These options are saved in the
config file and are not persisted between sessions. Changing them will not affect existing config files.
</p>
<div className="flex flex-col gap-2 mt-6">
</SkeletonWrap>
<div className="flex flex-col gap-2 mt-6">
<SkeletonWrap show={!config.data}>
<CustomisationOption
title="Direct Links"
description="Embedded links are recommended and will embed the image in the site with additional metadata and functionality like syntax highlighting. Direct links will return links that take you straight to the image, which may have better compatibility with some services."
@ -72,6 +69,8 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
]}
/>
</CustomisationOption>
</SkeletonWrap>
<SkeletonWrap show={!config.data}>
<CustomisationOption
title="Paste Shortcut"
description="Whether to redirect text file uploads to the pastes endpoint"
@ -92,10 +91,12 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
]}
/>
</CustomisationOption>
</div>
<div className="mt-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2">
{config.data.hosts.map((host) => {
</SkeletonWrap>
</div>
<div className="mt-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2">
{config.data &&
config.data.hosts.map((host) => {
const isSelected = selectedHosts.includes(host.normalised);
const classes = clsx(
'rounded px-2 py-1 truncate transition border border-transparent',
@ -116,14 +117,18 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
}
}}
>
{host.normalised.replace('{{username}}', user.username)}
{user ? host.normalised.replace('{{username}}', user.username) : host.normalised}
</button>
);
})}
</div>
{!config.data && (
<SkeletonList count={6} as={Fragment}>
<Skeleton className="h-8" />
</SkeletonList>
)}
</div>
</Fragment>
)}
</div>
</Fragment>
</div>
<button
type="submit"
@ -133,7 +138,9 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
downloadable ? 'text-purple-400 hover:underline' : 'text-gray-700 cursor-not-allowed',
)}
>
download config <FiDownload className="h-3.5 w-3.5" />
<SkeletonWrap show={!config.data}>
download config <FiDownload className="h-3.5 w-3.5" />
</SkeletonWrap>
</button>
</Container>
</Section>

View File

@ -3,6 +3,7 @@ import { FiFileMinus, FiTrash } from 'react-icons/fi';
import { graphql } from '../../../@generated';
import { FileCardFragment } from '../../../@generated/graphql';
import { Link } from '../../../components/link';
import { Skeleton } from '../../../components/skeleton';
import { useConfig } from '../../../hooks/useConfig';
import { MissingPreview } from '../missing-preview';
@ -78,3 +79,5 @@ export const FileCard = memo<{ file: FileCardFragment }>(({ file }) => {
</Link>
);
});
export const FileCardSkeleton = () => <Skeleton className="h-44" />;

View File

@ -5,14 +5,12 @@ import { graphql } from '../../@generated';
import { Breadcrumbs } from '../../components/breadcrumbs';
import { Card } from '../../components/card';
import { Error } from '../../components/error';
import { PageLoader } from '../../components/page-loader';
import { SkeletonList } from '../../components/skeleton';
import { Toggle } from '../../components/toggle';
import { useQueryState } from '../../hooks/useQueryState';
import { FileCard } from './cards/file-card';
import { FileCard, FileCardSkeleton } from './cards/file-card';
import { PasteCard } from './cards/paste-card';
const PER_PAGE = 24;
const GetFilesQuery = graphql(`
query GetFiles($after: String) {
user {
@ -89,7 +87,11 @@ export const FileList: FC = () => {
</div>
</div>
<div className="pb-5">
{!source.data && <PageLoader />}
{!source.data && (
<SkeletonList count={12} className="grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-6">
<FileCardSkeleton />
</SkeletonList>
)}
{filter === 'files' && (
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-6">
{files.data?.user.files.edges.map(({ node }) => <FileCard key={node.id} file={node} />)}

View File

@ -69,23 +69,22 @@ export const useLogoutUser = () => {
};
export const useUserRedirect = (
data: RegularUserFragment | null | undefined,
loading: boolean,
query: { data: { user: RegularUserFragment } | null | undefined; loading: boolean; called: boolean },
redirect: boolean | undefined,
) => {
useEffect(() => {
if (!data && !loading && redirect) {
if (!query.data && !query.loading && query.called && redirect) {
navigate(`/login?to=${window.location.href}`);
}
}, [redirect, data, loading]);
}, [redirect, query.data, query.loading, query.called]);
};
export const useUser = <T extends TypedDocumentNode<GetUserQuery, any>>(redirect?: boolean, query?: T) => {
const { login, otpRequired } = useLoginUser();
const { logout } = useLogoutUser();
const { data, loading, error } = useQuery((query || UserQuery) as T);
const { data, loading, called, error } = useQuery((query || UserQuery) as T);
useUserRedirect(data?.user, loading, redirect);
useUserRedirect({ data, loading, called }, redirect);
return {
data: data?.user as RegularUserFragment | null | undefined,

View File

@ -1,5 +1,5 @@
import { useMutation, useQuery } from '@apollo/client';
import { FC } from 'react';
import { FC, Fragment } from 'react';
import { graphql } from '../../../@generated';
import { GetUserDocument } from '../../../@generated/graphql';
import { Breadcrumbs } from '../../../components/breadcrumbs';
@ -7,12 +7,11 @@ import { Button } from '../../../components/button';
import { Container } from '../../../components/container';
import { Input } from '../../../components/input/input';
import { OtpInput } from '../../../components/input/otp';
import { PageLoader } from '../../../components/page-loader';
import { ButtonSkeleton, InputSkeleton, Skeleton } from '../../../components/skeleton';
import { Title } from '../../../components/title';
import { ConfigGenerator } from '../../../containers/config-generator/config-generator';
import { navigate } from '../../../helpers/routing';
import { useAsync } from '../../../hooks/useAsync';
import { useConfig } from '../../../hooks/useConfig';
import { useLogoutUser, useUserRedirect } from '../../../hooks/useUser';
const RefreshToken = graphql(`
@ -40,9 +39,8 @@ const UserQueryWithToken = graphql(`
`);
export const Page: FC = () => {
const { logout } = useLogoutUser();
const user = useQuery(UserQueryWithToken);
const config = useConfig();
const { logout } = useLogoutUser();
const [refreshMutation] = useMutation(RefreshToken);
const [refresh, refreshing] = useAsync(async () => {
// eslint-disable-next-line no-alert
@ -52,16 +50,12 @@ export const Page: FC = () => {
await logout();
});
useUserRedirect(user.data?.user, user.loading, true);
useUserRedirect(user, true);
const [disableOTP, disableOTPMut] = useMutation(DisableOtp, {
refetchQueries: [{ query: GetUserDocument }],
});
if (!user.data || !config.data) {
return <PageLoader title="Preferences" />;
}
return (
<Container>
<Title>Preferences</Title>
@ -70,38 +64,66 @@ export const Page: FC = () => {
</Breadcrumbs>
<div className="grid grid-cols-2 gap-4">
<div className="left col-span-full md:col-span-1">
<div className="font-bold text-xl">Upload Token</div>
<p className="text-sm mt-2 text-gray-400">
This token is used when uploading files.{' '}
<button type="button" className="text-purple-400 hover:underline" onClick={refresh} disabled={refreshing}>
Click here
</button>{' '}
to reset your token and invalidate all existing ShareX configurations.
</p>
{user.data && (
<Fragment>
<div className="font-bold text-xl">Upload Token</div>
<p className="text-sm mt-2 text-gray-400">
This token is used when uploading files.{' '}
<button
type="button"
className="text-purple-400 hover:underline"
onClick={refresh}
disabled={refreshing}
>
Click here
</button>{' '}
to reset your token and invalidate all existing ShareX configurations.
</p>
</Fragment>
)}
{!user.data && (
<Fragment>
<Skeleton className="w-1/2 mb-1" />
<Skeleton className="w-3/4" />
</Fragment>
)}
</div>
<div className="right flex items-center col-span-full md:col-span-1">
<Input
readOnly
value={user.data.user.token}
onFocus={(event) => {
event.target.select();
}}
/>
{user.data && (
<Input
readOnly
value={user.data.user.token}
onFocus={(event) => {
event.target.select();
}}
/>
)}
{!user.data && <InputSkeleton />}
</div>
</div>
<div className="mt-10">
<ConfigGenerator user={user.data.user} />
<ConfigGenerator user={user.data?.user} />
</div>
<div className="grid grid-cols-2 gap-4 mt-8">
<div className="left col-span-full md:col-span-1">
<div className="font-bold text-xl">2-factor Authentication</div>
<p className="text-sm mt-2 text-gray-400">
2-factor authentication is currently {user.data.user.otpEnabled ? 'enabled' : 'disabled'}.{' '}
{user.data.user.otpEnabled ? `Enter an authenticator code to disable it.` : 'Click to setup.'}
</p>
{user.data && (
<Fragment>
<div className="font-bold text-xl">2-factor Authentication</div>
<p className="text-sm mt-2 text-gray-400">
2-factor authentication is currently {user.data.user.otpEnabled ? 'enabled' : 'disabled'}.{' '}
{user.data.user.otpEnabled ? `Enter an authenticator code to disable it.` : 'Click to setup.'}
</p>
</Fragment>
)}
{!user.data && (
<Fragment>
<Skeleton className="w-1/2 mb-1" />
<Skeleton className="w-3/4" />
</Fragment>
)}
</div>
<div className="right flex items-center col-span-full md:col-span-1">
{user.data.user.otpEnabled && (
{user.data && user.data.user.otpEnabled && (
<OtpInput
loading={disableOTPMut.loading}
onCode={(otpCode) => {
@ -111,11 +133,12 @@ export const Page: FC = () => {
}}
/>
)}
{!user.data.user.otpEnabled && (
{user.data && !user.data.user.otpEnabled && (
<Button className="w-auto ml-auto" onClick={() => navigate(`/dashboard/mfa`)}>
Enable 2FA
</Button>
)}
{!user.data && <ButtonSkeleton className="ml-auto" />}
</div>
</div>
</Container>

View File

@ -2,13 +2,13 @@ import { useMutation, useQuery } from '@apollo/client';
import clsx from 'clsx';
import copyToClipboard from 'copy-to-clipboard';
import type { FC, ReactNode } from 'react';
import { useState } from 'react';
import { Fragment, useState } from 'react';
import { FiDownload, FiShare, FiTrash } from 'react-icons/fi';
import { graphql } from '../../../@generated';
import { Container } from '../../../components/container';
import { Embed } from '../../../components/embed/embed';
import { Error } from '../../../components/error';
import { PageLoader } from '../../../components/page-loader';
import { Skeleton, SkeletonList } from '../../../components/skeleton';
import { Spinner } from '../../../components/spinner';
import { Title } from '../../../components/title';
import { useToasts } from '../../../components/toast';
@ -114,46 +114,59 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
return <Error error={file.error} />;
}
if (!file.data) {
return <PageLoader />;
}
const canDelete = file.data.file.isOwner || deleteKey;
const canDelete = file.data?.file.isOwner || deleteKey;
return (
<Container className="mt-5 md-2 md:mb-5">
<Title>{file.data.file.displayName}</Title>
{file.data && <Title>{file.data.file.displayName}</Title>}
<div className="grid grid-cols-1 gap-4 md:grid-cols-6 pb-1">
<div className="flex items-end col-span-5 overflow-hidden whitespace-nowrap pb-1">
<h1 className="mr-2 text-xl font-bold md:text-4xl md:break-all">{file.data.file.displayName}</h1>
<span className="text-xs text-gray-500">{file.data.file.sizeFormatted}</span>
{file.data && (
<Fragment>
<h1 className="mr-2 text-xl font-bold md:text-4xl md:break-all">{file.data.file.displayName}</h1>
<span className="text-xs text-gray-500">{file.data.file.sizeFormatted}</span>
</Fragment>
)}
{!file.data && <Skeleton className="w-1/2 h-8" />}
</div>
<div className="col-span-5">
<Embed
data={{
type: file.data.file.type,
paths: file.data.file.paths,
size: file.data.file.size,
displayName: file.data.file.displayName,
height: file.data.file.metadata?.height,
width: file.data.file.metadata?.width,
textContent: file.data.file.textContent,
}}
/>
{file.data && (
<Embed
data={{
type: file.data.file.type,
paths: file.data.file.paths,
size: file.data.file.size,
displayName: file.data.file.displayName,
height: file.data.file.metadata?.height,
width: file.data.file.metadata?.width,
textContent: file.data.file.textContent,
}}
/>
)}
{!file.data && <Skeleton className="w-full h-[50dvh]" />}
</div>
<div className="flex md:flex-col">
<div className="flex text-sm gap-3 text-gray-500 cursor-pointer md:flex-col">
<FileOption onClick={copyLink}>
<FiShare className="h-4 mr-1" /> Copy link
</FileOption>
<FileOption onClick={downloadFile}>
<FiDownload className="h-4 mr-1" /> Download
</FileOption>
{canDelete && (
<FileOption onClick={deleteFile} className="text-red-400 hover:text-red-500">
<FiTrash className="h-4 mr-1" />
{deletingFile ? <Spinner size="small" /> : confirm ? 'Are you sure?' : 'Delete'}
</FileOption>
{file.data && (
<Fragment>
<FileOption onClick={copyLink}>
<FiShare className="h-4 mr-1" /> Copy link
</FileOption>
<FileOption onClick={downloadFile}>
<FiDownload className="h-4 mr-1" /> Download
</FileOption>
{canDelete && (
<FileOption onClick={deleteFile} className="text-red-400 hover:text-red-500">
<FiTrash className="h-4 mr-1" />
{deletingFile ? <Spinner size="small" /> : confirm ? 'Are you sure?' : 'Delete'}
</FileOption>
)}
</Fragment>
)}
{!file.data && (
<SkeletonList count={3} className="space-y-2">
<Skeleton className="w-1/2" />
</SkeletonList>
)}
</div>
</div>

View File

@ -11,6 +11,7 @@ import { renderPage } from 'vike/server';
import { PageContext } from 'vike/types';
import { REWRITES } from './rewrites';
import { fileURLToPath } from 'url';
import url from 'url';
const fileDir = dirname(fileURLToPath(import.meta.url));
const staticDir = process.env.STATIC_DIR?.replace('{{FILE_DIR}}', fileDir) || resolve('dist/client');
@ -39,7 +40,8 @@ async function startServer() {
const instance = Fastify({
rewriteUrl: (request) => {
if (!request.url) throw new Error('No url');
const { pathname } = new URL(request.url, 'http://localhost');
const { pathname } = url.parse(request.url);
if (!pathname) return request.url;
// if discord tries to request the html of an image, redirect it straight to the image
// this means the image is embedded, not the opengraph data for the image.

View File

@ -1434,7 +1434,7 @@ packages:
'@fastify/reply-from': 9.7.0
fast-querystring: 1.1.2
fastify-plugin: 4.5.1
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1836,7 +1836,7 @@ packages:
graphql-ws: 5.14.3(graphql@16.8.1)
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -1871,7 +1871,7 @@ packages:
graphql: 16.8.1
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -2123,7 +2123,7 @@ packages:
isomorphic-ws: 5.0.0(ws@8.16.0)
tslib: 2.6.2
value-or-promise: 1.0.12
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
transitivePeerDependencies:
- '@types/node'
- bufferutil
@ -7333,7 +7333,7 @@ packages:
peerDependencies:
ws: '*'
dependencies:
ws: 8.16.0(utf-8-validate@6.0.3)
ws: 8.16.0
dev: true
/istextorbinary@9.5.0:
@ -8476,6 +8476,7 @@ packages:
/node-gyp-build@4.8.0:
resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
hasBin: true
dev: false
/node-html-parser@6.1.12:
resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==}
@ -10952,6 +10953,7 @@ packages:
requiresBuild: true
dependencies:
node-gyp-build: 4.8.0
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -11353,6 +11355,19 @@ packages:
utf-8-validate: 6.0.3
dev: false
/ws@8.16.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: true
/ws@8.16.0(utf-8-validate@6.0.3):
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
@ -11366,6 +11381,7 @@ packages:
optional: true
dependencies:
utf-8-validate: 6.0.3
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}

View File

@ -1,15 +1,13 @@
#!/bin/sh
# Define a cleanup function
cleanup() {
pkill -P $$
}
# Trap signals and errors
trap cleanup EXIT HUP INT QUIT PIPE TERM ERR
cd packages/api && npm run start &
cd packages/web && HOST=0.0.0.0 npm run start &
(cd packages/api && npm run start) &
(cd packages/web && HOST=0.0.0.0 npm run start) &
wait -n
exit $?