refactor, added slate, added editor, added book

This commit is contained in:
lnn0q 2025-05-28 23:25:46 +03:00
parent bc250e73bb
commit 029b7e9353
33 changed files with 714 additions and 496 deletions

View file

@ -1,16 +1,18 @@
import { gql } from '@apollo/client';
import { Skeleton, Stack } from '@chakra-ui/react';
import { Stack } from '@chakra-ui/react';
import { BooksListQuery } from '@generated/types';
import { useState } from 'react';
import { Pagination } from '../components/Pagination';
import { WorkingList } from '../components/WorkingList';
import { WorkingListSearch } from '../components/WorkingListSearch';
import { ListSearch } from '../components/ListSearch';
import { bookFragment } from '../fragments';
import { usePaginatedQuery } from '../hooks/usePaginatedQuery';
import { PaginatedListSkeleton } from './PaginatedListSkeleton';
export function BookSearchList() {
const [workingSearchValue, setWorkingSearchValue] = useState<string>('');
const [workingsListPage, setWorkingsListPage] = useState<number>(1);
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(2);
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(10);
const startIndex = (workingsListPage - 1) * workingsListPageSize;
const { data } = usePaginatedQuery<BooksListQuery>({
@ -26,35 +28,24 @@ export function BookSearchList() {
return (
<>
<ListSearch placeholder="Search works..." onSubmit={value => setWorkingSearchValue(value)} />
{data != null ? (
<>
<WorkingListSearch
onSubmit={value => {
setWorkingSearchValue(value);
}}
/>
<WorkingList workingsList={[...data.bookMany.items]} />
{data.bookMany.items.length !== 0 && (
{data.bookMany.total > workingsListPageSize && (
<Stack justifyContent="center" alignItems="center">
<Pagination
total={data.bookMany.total}
page={workingsListPage}
setPage={setWorkingsListPage}
pageSize={workingsListPageSize}
setPageSize={setWorkingsListPageSize}
/>
</Stack>
)}
</>
) : (
<>
<Stack direction="row" justifyContent="center" alignItems="center" gap={2}>
<Skeleton height={10} width="80%" />
<Skeleton height={10} width="calc(20% - 6px)" />
</Stack>
<Skeleton height={20} />
<Skeleton height={20} />
<Skeleton height={20} />
</>
<PaginatedListSkeleton />
)}
</>
);
@ -64,21 +55,10 @@ const query = gql`
query BooksList($input: BookManyInput!) {
bookMany(input: $input) {
items {
id
title
description
author {
id
username
}
updatedAt
progressStatus
ratingTotal
reviewsTotal
wordsTotal
chaptersTotal
...Book
}
total
}
}
${bookFragment}
`;

View file

@ -1,6 +1,6 @@
import { Button, Card, Stack, Text, Wrap } from '@chakra-ui/react';
import isHotkey from 'is-hotkey';
import React, { KeyboardEvent, MouseEvent, useCallback, useMemo, useState } from 'react';
import React, { KeyboardEvent, MouseEvent, useCallback, useMemo } from 'react';
import { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import {
@ -44,7 +44,7 @@ export function EditorSlate() {
}
}}
>
<RichTextExample />
<EditorViewport />
</Card.Body>
</Card.Root>
</>
@ -65,7 +65,7 @@ type AlignType = (typeof TEXT_ALIGN_TYPES)[number];
type ListType = (typeof LIST_TYPES)[number];
type CustomElementFormat = CustomElementType | AlignType | ListType;
const RichTextExample = () => {
function EditorViewport() {
const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []);
const renderLeaf = useCallback((props: RenderLeafProps) => <Leaf {...props} />, []);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
@ -123,12 +123,17 @@ const RichTextExample = () => {
<BlockButton format="right" icon="nf nf-fa-align_right" />
<BlockButton format="justify" icon="nf nf-fa-align_justify" />
</Wrap>
<Stack width="100%" display="flex" justifyContent="center" alignItems="center">
<Stack
width="100%"
maxWidth="100%"
paddingX={3}
display="flex"
justifyContent="center"
alignItems="center"
>
<Prose
width="100%"
height="100%"
maxWidth="unset"
maxHeight="unset"
minWidth="100%"
display="flex"
justifyContent="center"
alignItems="center"
@ -137,7 +142,11 @@ const RichTextExample = () => {
className="slate-editor"
style={{
outline: 'none',
minWidth: '100%'
minWidth: '100%',
maxWidth: 'unset',
maxHeight: '75vh',
padding: 0,
overflowY: 'auto'
}}
renderElement={renderElement}
renderLeaf={renderLeaf}
@ -158,9 +167,9 @@ const RichTextExample = () => {
</Stack>
</Slate>
);
};
}
const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => {
function toggleBlock(editor: CustomEditor, format: CustomElementFormat) {
const isActive = isBlockActive(editor, format, isAlignType(format) ? 'align' : 'type');
const isList = isListType(format);
@ -188,9 +197,9 @@ const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
}
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
function toggleMark(editor: CustomEditor, format: CustomTextKey) {
const isActive = isMarkActive(editor, format);
if (isActive) {
@ -198,13 +207,13 @@ const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
} else {
Editor.addMark(editor, format, true);
}
};
}
const isBlockActive = (
function isBlockActive(
editor: CustomEditor,
format: CustomElementFormat,
blockType: 'type' | 'align' = 'type'
) => {
) {
const { selection } = editor;
if (!selection) return false;
@ -223,15 +232,15 @@ const isBlockActive = (
})
);
return !!match;
};
return Boolean(match);
}
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
function isMarkActive(editor: CustomEditor, format: CustomTextKey) {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
}
const Element = ({ attributes, children, element }: RenderElementProps) => {
function Element({ attributes, children, element }: RenderElementProps) {
const style: React.CSSProperties = {};
if (isAlignElement(element)) {
style.textAlign = element.align as AlignType;
@ -280,9 +289,9 @@ const Element = ({ attributes, children, element }: RenderElementProps) => {
</p>
);
}
};
}
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
function Leaf({ attributes, children, leaf }: RenderLeafProps) {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
@ -300,7 +309,7 @@ const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
}
return <span {...attributes}>{children}</span>;
};
}
interface BlockButtonProps {
format: CustomElementFormat;
@ -308,7 +317,7 @@ interface BlockButtonProps {
text?: string;
}
const BlockButton = ({ format, icon, text }: BlockButtonProps) => {
function BlockButton({ format, icon, text }: BlockButtonProps) {
const editor = useSlate();
return (
<Button
@ -325,7 +334,7 @@ const BlockButton = ({ format, icon, text }: BlockButtonProps) => {
</Stack>
</Button>
);
};
}
interface MarkButtonProps {
format: CustomTextKey;
@ -333,7 +342,7 @@ interface MarkButtonProps {
text?: string;
}
const MarkButton = ({ format, icon, text }: MarkButtonProps) => {
function MarkButton({ format, icon, text }: MarkButtonProps) {
const editor = useSlate();
return (
<Button
@ -347,16 +356,16 @@ const MarkButton = ({ format, icon, text }: MarkButtonProps) => {
{text}
</Button>
);
};
}
const isAlignType = (format: CustomElementFormat): format is AlignType => {
function isAlignType(format: CustomElementFormat): format is AlignType {
return TEXT_ALIGN_TYPES.includes(format as AlignType);
};
}
const isListType = (format: CustomElementFormat): format is ListType => {
function isListType(format: CustomElementFormat): format is ListType {
return LIST_TYPES.includes(format as ListType);
};
}
const isAlignElement = (element: CustomElement): element is CustomElementWithAlign => {
function isAlignElement(element: CustomElement): element is CustomElementWithAlign {
return 'align' in element;
};
}

View file

@ -1,56 +0,0 @@
import { Card, Heading } from '@chakra-ui/react';
import { useState } from 'react';
import { BaseEditor, createEditor, Descendant } from 'slate';
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
type CustomElement = { type: 'paragraph'; children: CustomText[] };
type CustomText = { text: string };
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
export function EditorSlate() {
const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [{ text: 'A line of text in a paragraph.' }]
}
];
const [editor] = useState(() => withReact(createEditor()));
return (
// Add the editable component inside the context.
<>
<Card.Root
variant={'subtle'}
width="100%"
maxWidth="840px"
minHeight="sm"
justifyContent="center"
alignItems="center"
>
<Card.Header>
<Heading textAlign="center">Editor</Heading>
</Card.Header>
<Card.Body
width="90%"
css={{
'& *': {
fontSize: 'revert',
fontWeight: 'revert'
}
}}
>
<Slate editor={editor} initialValue={initialValue}>
<Editable />
</Slate>
</Card.Body>
</Card.Root>
</>
);
}

View file

@ -0,0 +1,25 @@
import { Text } from '@chakra-ui/react';
import { LinkProps as LinkExtended, Link as TanstackLink } from '@tanstack/react-router';
import { ReactNode } from 'react';
interface LinkProps {
children: ReactNode;
to: LinkExtended['to'];
link?: LinkExtended;
}
export function Link(props: LinkProps) {
return (
<TanstackLink {...props}>
<Text
color="colorPalette.500"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
{props.children}
</Text>
</TanstackLink>
);
}

View file

@ -1,7 +1,13 @@
import { Button, Icon, Input, InputGroup, Stack } from '@chakra-ui/react';
import { useState } from 'react';
export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => void }) {
export function ListSearch({
placeholder,
onSubmit
}: {
readonly placeholder?: string;
readonly onSubmit: (value: string) => void;
}) {
const [searchValue, setSearchValue] = useState<string>('');
return (
<form
@ -22,7 +28,8 @@ export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => v
<Input
value={searchValue}
onChange={event => setSearchValue(event.currentTarget.value)}
placeholder="Search works..."
onSubmit={event => event.preventDefault()}
placeholder={placeholder}
/>
</InputGroup>
<Button type="submit" border="none">

View file

@ -0,0 +1,17 @@
import { Skeleton, Stack } from '@chakra-ui/react';
export function PaginatedListSkeleton({ disableSearch }: { readonly disableSearch?: boolean }) {
return (
<>
{!disableSearch && (
<Stack direction="row" justifyContent="center" alignItems="center" gap={2}>
<Skeleton height={10} width="80%" />
<Skeleton height={10} width="calc(20% - 6px)" />
</Stack>
)}
<Skeleton height={20} />
<Skeleton height={20} />
<Skeleton height={20} />
</>
);
}

View file

@ -11,6 +11,7 @@ export function Pagination({
page: number;
setPage: (value: number) => void;
pageSize?: number;
setPageSize?: (value: number) => void;
}) {
return (
<ChakraPagination.Root

View file

@ -1,7 +1,8 @@
import { Card, List, Text } from '@chakra-ui/react';
import { WorkingCardProps, WorkingListCard } from './WorkingListCard';
import { WorkingListCard } from './WorkingListCard';
import { BookOne } from '../interfaces';
export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) {
export function WorkingList({ workingsList }: { workingsList: BookOne[] }) {
return (
<List.Root variant="plain" gap={4}>
{workingsList.length !== 0 ? (

View file

@ -1,23 +1,14 @@
import { Badge, Box, Card, Heading, Link, Stack, Text } from '@chakra-ui/react';
import { WorkingProgressStatus } from '@generated/types';
import { Badge, Box, Card, Heading, Stack, Text } from '@chakra-ui/react';
import { Link } from '@tanstack/react-router';
import { toFormattedDate } from '../helpers';
import { BookOne } from '../interfaces';
import { IconBadge } from './IconBadge';
import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge';
import { WorkingRatingBadge } from './WorkingRatingBadge';
export interface WorkingCardProps {
id: string;
title: string;
description: string;
author: { id: string; username: string };
progressStatus: WorkingProgressStatus;
updatedAt: string;
chaptersTotal: number;
wordsTotal: number;
ratingTotal: number;
reviewsTotal: number;
}
import { WorkingReviewsBadge } from './WorkingReviewsTotalBadge';
export function WorkingListCard({
id,
title,
description,
author,
@ -27,29 +18,47 @@ export function WorkingListCard({
wordsTotal,
ratingTotal,
reviewsTotal
}: WorkingCardProps) {
}: BookOne) {
return (
<Card.Root width="100%" variant="elevated">
<Card.Body>
<Stack>
<Stack direction="row" justifyContent="space-between">
<Link>
<Heading size="lg">{title}</Heading>
<Text as="div" minWidth={0}>
<Link to={`/work/book/${id}`}>
<Heading
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
{title}
</Heading>
</Link>
</Text>
<Stack direction="row" justifyContent="center" alignItems="center">
<WorkingRatingBadge ratingTotal={ratingTotal} />
<IconBadge backgroundColor="orange.400" iconClass="nf nf-oct-comment">
<Text>{reviewsTotal}</Text>
</IconBadge>
<WorkingReviewsBadge reviewsTotal={reviewsTotal} />
<WorkingProgressStatusBadge progressStatus={progressStatus} />
</Stack>
</Stack>
<Stack>
<Stack direction="row">
<Text>by:</Text>
<Link>{author.username}</Link>
<Link>by:</Link>
<Link to={`/profile/${author.username}`}>
<Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
{author.username}
</Text>
</Link>
</Stack>
<Stack direction="row">
@ -57,7 +66,7 @@ export function WorkingListCard({
<Badge size="md" variant="solid">
<Stack direction="row">
<Text>last updated:</Text>
<Text>{updatedAt}</Text>
<Text>{toFormattedDate({ value: updatedAt })}</Text>
</Stack>
</Badge>
</Box>

View file

@ -12,7 +12,7 @@ export function WorkingProgressStatusBadge({
progressStatus: WorkingProgressStatus;
}) {
return (
<Box flex={1} justifyContent="center" alignItems="center">
<Box>
<Badge
variant="solid"
backgroundColor={toBgColorByProgressStatus(progressStatus)}

View file

@ -1,4 +1,4 @@
import { BadgeProps, Text } from '@chakra-ui/react';
import { BadgeProps, Box, Text } from '@chakra-ui/react';
import { IconBadge } from './IconBadge';
interface WorkingRatingParams {
@ -21,8 +21,10 @@ export function WorkingRatingBadge({ ratingTotal }: { ratingTotal: number }) {
iconClass: 'nf nf-fa-angle_down'
};
return (
<Box>
<IconBadge backgroundColor={ratingParams.color} iconClass={ratingParams.iconClass}>
<Text>{ratingParams.rating}</Text>
</IconBadge>
</Box>
);
}

View file

@ -0,0 +1,12 @@
import { Box, Text } from '@chakra-ui/react';
import { IconBadge } from './IconBadge';
export function WorkingReviewsBadge({ reviewsTotal }: { readonly reviewsTotal: number }) {
return (
<Box>
<IconBadge backgroundColor="orange.400" iconClass="nf nf-oct-comment">
<Text>{reviewsTotal}</Text>
</IconBadge>
</Box>
);
}

View file

@ -1,26 +1,22 @@
import { Link as ChakraLink } from "@chakra-ui/react";
import { createLink, LinkComponent } from "@tanstack/react-router";
import * as React from "react";
import { Link as ChakraLink } from '@chakra-ui/react';
import { createLink, LinkComponent } from '@tanstack/react-router';
import * as React from 'react';
interface ChakraLinkProps
extends Omit<React.ComponentPropsWithoutRef<typeof ChakraLink>, "href"> {}
interface ChakraLinkProps extends Omit<React.ComponentPropsWithoutRef<typeof ChakraLink>, 'href'> {}
const ChakraLinkComponent = React.forwardRef<
HTMLAnchorElement,
ChakraLinkProps
>((props, ref) => {
const ChakraLinkComponent = React.forwardRef<HTMLAnchorElement, ChakraLinkProps>((props, ref) => {
return <ChakraLink ref={ref} {...props} />;
});
const CreatedLinkComponent = createLink(ChakraLinkComponent);
export const Link: LinkComponent<typeof ChakraLinkComponent> = (props) => {
export const Link: LinkComponent<typeof ChakraLinkComponent> = props => {
return (
<CreatedLinkComponent
textDecoration={"underline"}
_hover={{ textDecoration: "none" }}
_focus={{ textDecoration: "none" }}
preload={"intent"}
textDecoration={'underline'}
_hover={{ textDecoration: 'none' }}
_focus={{ textDecoration: 'none' }}
preload={'intent'}
{...props}
/>
);

View file

@ -7,10 +7,34 @@ export const sessionFragment = gql`
}
`;
// refreshToken {
// token
// maxAge
// secure
// httpOnly
// sameSite
// }
export const bookFragment = gql`
fragment Book on Book {
id
title
description
author {
id
username
}
createdAt
updatedAt
progressStatus
wordsTotal
chaptersTotal
ratingTotal
reviewsTotal
}
`;
export const bookChapterFragment = gql`
fragment BookChapter on BookChapter {
id
title
createdAt
updatedAt
foreword
footnote
sequenceNumber
reviewsTotal
}
`;

View file

@ -1,9 +1,16 @@
import { WorkingProgressStatus } from '@generated/types';
import dayjs from 'dayjs';
export function assertUnreachable(value: never): never {
throw new Error(`Unhandled scenario: ${value}`);
}
export function toFormattedDate({ value }: { value: string }): string {
const date = new Date(value);
const day = dayjs(date).format('DD.MM.YYYY');
return day;
}
export function toBgColorByProgressStatus(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'yellow.500';
if (value === WorkingProgressStatus.Frozen) return 'blue.500';

View file

@ -10,6 +10,7 @@ import { DocumentNode } from 'graphql';
interface UsePaginatedQueryProps {
query: DocumentNode;
input: {
[key: string]: any; // for the custom inputs
filter: {
terms?: string | null;
offset?: number | null;

18
app/client/interfaces.tsx Normal file
View file

@ -0,0 +1,18 @@
import { WorkingProgressStatus } from '@generated/types';
export interface BookOne {
readonly id: string;
readonly title: string;
readonly description: string;
readonly author: {
readonly id: string;
readonly username: string;
};
readonly createdAt: string;
readonly updatedAt: string;
readonly progressStatus: WorkingProgressStatus;
readonly wordsTotal: number;
readonly chaptersTotal: number;
readonly ratingTotal: number;
readonly reviewsTotal: number;
}

View file

@ -1,15 +1,5 @@
import {
Link as ChakraLink,
Icon,
Input,
InputGroup,
Kbd,
Menu,
Separator,
Stack,
Text
} from '@chakra-ui/react';
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router';
import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
import { createRootRouteWithContext, Outlet, Link } from '@tanstack/react-router';
import React from 'react';
import { Avatar } from '../components/ui/avatar';
import { Button } from '../components/ui/button';
@ -18,7 +8,6 @@ import { Tooltip } from '../components/ui/tooltip';
import { useAuth } from '../hooks/auth';
import { useIdentity } from '../hooks/identity';
import { appName } from '../main';
// import { Link as ChakraLink } from '../components/ui/link';
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
@ -67,12 +56,28 @@ export const Route = createRootRouteWithContext<RouterContext>()({
<>
<Stack direction="row" alignItems="center">
<Link to="/editor">
<ChakraLink>editor</ChakraLink>
<Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
editor
</Text>
</Link>
</Stack>
<Stack direction="row" alignItems="center">
<Link to="/new/book">
<ChakraLink>create</ChakraLink>
<Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
create
</Text>
</Link>
</Stack>
</>
@ -99,10 +104,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
{identity != null ? (
<Menu.Root>
<Menu.Trigger css={{ '&:hover': { cursor: 'pointer' } }}>
<Avatar
size="sm"
// src="https://avatars.githubusercontent.com/u/40203084?v=4"
/>
<Avatar size="sm" />
</Menu.Trigger>
<Menu.Positioner mt={2}>
<Menu.Content backgroundColor={'bg.subtle'}>

View file

@ -1,20 +0,0 @@
// import { gql } from '@apollo/client';
import { createLazyFileRoute } from '@tanstack/react-router';
export const Route = createLazyFileRoute('/book')({
component: RouteComponent
});
function RouteComponent() {
return <div>"/book"</div>;
}
// const getBookQuery = gql`
// query BookOne {
// booksMany {
// id
// author
// title
// }
// }
// `;

View file

@ -15,49 +15,3 @@ function RouteComponent() {
</Stack>
);
}
// const workingData1: WorkingCardProps = {
// id: '1',
// title: 'Worth the Candle',
// description: `A teenager struggling after the death of his best friend finds himself in a fantasy
// world - one which seems to be an amalgamation of every Dungeons and Dragons campaign
// they ever played together. Now he's stuck trying to find the answers to why he's there
// and what this world is trying to say. The most terrifying answer might be that this
// world is an expression of the person he was back on Earth.`,
// author: {
// id: '1',
// createdAt: '12.8.2025',
// updatedAt: '4.9.2025',
// username: 'AlexanderWhales'
// },
// progressStatus: WorkingProgressStatus.Finished,
// updatedAt: '20.10.2025',
// chaptersTotal: 10,
// wordsTotal: 34000,
// ratingTotal: 10,
// reviewsTotal: 3
// };
//
// const workingData2: WorkingCardProps = {
// id: '2',
// title: 'Test',
// description: `Description.`,
// author: {
// id: '1',
// createdAt: '12.8.2025',
// updatedAt: '4.9.2025',
// username: 'JohnSmith'
// },
// progressStatus: WorkingProgressStatus.InProgress,
// updatedAt: '20.10.2025',
// chaptersTotal: 10,
// wordsTotal: 1200,
// ratingTotal: -10,
// reviewsTotal: 0
// };
//
// const mockWorkingData = {
// total: 4,
// items: [workingData1, workingData2, { ...workingData1, id: '3' }, { ...workingData2, id: '4' }]
// };
//

View file

@ -1,8 +1,11 @@
import { gql } from '@apollo/client';
import { useToastMutation } from '@app/client/hooks/useToastMutation';
import { Heading, Input, Stack, Textarea } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field';
import { CreateNewBookMutation } from '@generated/types';
import { valibotResolver } from '@hookform/resolvers/valibot';
import { createFileRoute, redirect } from '@tanstack/react-router';
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import * as v from 'valibot';
@ -28,10 +31,16 @@ interface FormValues {
}
function NewBookForm() {
const navigate = useNavigate();
const { update: bookCreate } = useToastMutation<CreateNewBookMutation>({
mutation: bookCreateMutation,
successMsg: 'Book created successfully!'
});
const schema = v.pipe(
v.object({
title: v.pipe(v.string(), v.nonEmpty('Title is required')),
description: v.pipe(v.string(), v.nonEmpty('Password is required'))
title: v.pipe(v.string(), v.nonEmpty('Title is required'), v.maxLength(60)),
description: v.pipe(v.string(), v.nonEmpty('Description is required'), v.maxLength(900))
})
);
@ -42,7 +51,23 @@ function NewBookForm() {
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
return (
<form style={{ width: '80%' }} onSubmit={handleSubmit(async values => {})}>
<form
style={{ width: '80%', maxWidth: '840px' }}
onSubmit={handleSubmit(async values => {
const { data } = await bookCreate({
variables: {
input: {
title: values.title,
description: values.description
}
}
});
if (data != null)
navigate({
to: `/work/book/${data.bookCreate.id}`
});
})}
>
<Stack>
<Field label="Title" invalid={errors.title != null} errorText={errors.title?.message}>
<Input {...register('title')} />
@ -52,7 +77,7 @@ function NewBookForm() {
invalid={errors.description != null}
errorText={errors.description?.message}
>
<Textarea width="100%" lineHeight={4} {...register('description')} />
<Textarea width="100%" height={'sm'} {...register('description')} />
</Field>
<Button type="submit">Create new book</Button>
</Stack>
@ -60,38 +85,12 @@ function NewBookForm() {
);
}
// function LoginForm() {
// const schema = v.pipe(
// v.object({
// email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
// password: v.pipe(v.string(), v.nonEmpty('Password is required'))
// })
// );
//
// const {
// register,
// handleSubmit,
// formState: { errors }
// } = useForm<FormValues>({ resolver: valibotResolver(schema) });
//
// const { login } = useAuth();
//
// return (
// <form
// >
// <Stack>
// <Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}>
// <Input {...register('email')} />
// </Field>
// <Field
// label="Password"
// invalid={errors.password != null}
// errorText={errors.password?.message}
// >
// <PasswordInput {...register('password')} />
// </Field>
// <Button type="submit">Sign in</Button>
// </Stack>
// </form>
// );
// }
const bookCreateMutation = gql`
mutation CreateNewBook($input: BookCreateInput!) {
bookCreate(input: $input) {
id
title
description
}
}
`;

View file

@ -0,0 +1,185 @@
import { gql, useQuery } from '@apollo/client';
import { PaginatedListSkeleton } from '@app/client/components/PaginatedListSkeleton';
import { Pagination } from '@app/client/components/Pagination';
import { WorkingProgressStatusBadge } from '@app/client/components/WorkingProgressStatusBadge';
import { WorkingRatingBadge } from '@app/client/components/WorkingRatingBadge';
import { WorkingReviewsBadge } from '@app/client/components/WorkingReviewsTotalBadge';
import { bookChapterFragment, bookFragment } from '@app/client/fragments';
import { toFormattedDate } from '@app/client/helpers';
import { usePaginatedQuery } from '@app/client/hooks/usePaginatedQuery';
import { BookOne } from '@app/client/interfaces';
import {
Heading,
HStack,
Skeleton,
SkeletonCircle,
SkeletonText,
Stack,
Text
} from '@chakra-ui/react';
import { BookChapterListQuery, BookOverviewQuery } from '@generated/types';
import { createFileRoute, Link } from '@tanstack/react-router';
import { useState } from 'react';
export const Route = createFileRoute('/work/book/$bookId')({
component: RouteComponent
});
function RouteComponent() {
const { bookId: id } = Route.useParams();
const { data: bookData } = useQuery<BookOverviewQuery>(bookQuery, { variables: { id } });
return (
<Stack width="100%" justifyContent="center" alignItems="center" padding={3}>
{bookData != null ? <BookOverview {...bookData.bookOne} /> : <BookOverviewSkeleton />}
</Stack>
);
}
function BookOverview({
id,
title,
description,
author,
progressStatus,
createdAt,
updatedAt,
wordsTotal,
chaptersTotal,
reviewsTotal,
ratingTotal
}: BookOne) {
return (
<Stack width="80%" maxWidth="840px" gap={3}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Heading color="colorPalette.400" size="2xl" overflow="auto">
{title}
</Heading>
</Stack>
<Stack direction="column" gap={1}>
<Stack direction="row">
<Link>
<Text fontWeight="bold">by:</Text>
</Link>
<Link to={`/profile/${author.username}`}>
<Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
{author.username}
</Text>
</Link>
</Stack>
<Stack direction="row">
<Text fontWeight="bold">created at:</Text>
<Text>{toFormattedDate({ value: createdAt })}</Text>
</Stack>
<Stack direction="row">
<Text fontWeight="bold">last updated:</Text>
<Text>{toFormattedDate({ value: updatedAt })}</Text>
</Stack>
<Stack direction="row">
<Text fontWeight="bold">chapters:</Text>
<Text>{chaptersTotal}</Text>
</Stack>
<Stack direction="row">
<Text fontWeight="bold">words:</Text>
<Text>{wordsTotal}</Text>
</Stack>
</Stack>
<Stack direction="row" justifyContent="left" alignItems="center">
<WorkingRatingBadge ratingTotal={ratingTotal} />
<WorkingReviewsBadge reviewsTotal={reviewsTotal} />
<WorkingProgressStatusBadge progressStatus={progressStatus} />
</Stack>
<Text>{description}</Text>
<BookChapterList id={id} />
</Stack>
);
}
function BookChapterList({ id }: { readonly id: string }) {
const [chaptersListPage, setChaptersListPage] = useState<number>(1);
const [chaptersListPageSize, setChaptersListPageSize] = useState<number>(10);
const startIndex = (chaptersListPage - 1) * chaptersListPageSize;
const { data } = usePaginatedQuery<BookChapterListQuery>({
query: bookChapterListQuery,
input: {
bookId: id,
filter: {
offset: startIndex,
size: chaptersListPageSize
}
}
});
return (
<Stack>
{data != null ? (
<>
<Stack>...chapters</Stack>
{data.bookChapterMany.total > chaptersListPageSize && (
<Stack justifyContent="center" alignItems="center">
<Pagination
total={data.bookChapterMany.total}
page={chaptersListPage}
setPage={setChaptersListPage}
pageSize={chaptersListPageSize}
setPageSize={setChaptersListPageSize}
/>
</Stack>
)}
</>
) : (
<PaginatedListSkeleton disableSearch />
)}
</Stack>
);
}
function BookOverviewSkeleton() {
return (
<Stack width="80%" maxWidth="840px">
<Stack gap="6" maxWidth="100%">
<HStack width="full">
<SkeletonCircle size="10" />
<SkeletonText noOfLines={5} />
</HStack>
<Skeleton height="200px" />
</Stack>
</Stack>
);
}
const bookQuery = gql`
query BookOverview($id: ID!) {
bookOne(id: $id) {
...Book
}
}
${bookFragment}
`;
const bookChapterListQuery = gql`
query BookChapterList($input: BookChapterManyInput!) {
bookChapterMany(input: $input) {
items {
...BookChapter
}
total
}
}
${bookChapterFragment}
`;

View file

@ -8,8 +8,6 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './pages/__root'
@ -20,19 +18,10 @@ import { Route as AboutImport } from './pages/about'
import { Route as IndexImport } from './pages/index'
import { Route as ProfileUsernameImport } from './pages/profile/$username'
import { Route as NewBookImport } from './pages/new/book'
// Create Virtual Routes
const BookLazyImport = createFileRoute('/book')()
import { Route as WorkBookBookIdImport } from './pages/work/book/$bookId'
// Create/Update Routes
const BookLazyRoute = BookLazyImport.update({
id: '/book',
path: '/book',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./pages/book.lazy').then((d) => d.Route))
const RegisterRoute = RegisterImport.update({
id: '/register',
path: '/register',
@ -75,6 +64,12 @@ const NewBookRoute = NewBookImport.update({
getParentRoute: () => rootRoute,
} as any)
const WorkBookBookIdRoute = WorkBookBookIdImport.update({
id: '/work/book/$bookId',
path: '/work/book/$bookId',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@ -114,13 +109,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RegisterImport
parentRoute: typeof rootRoute
}
'/book': {
id: '/book'
path: '/book'
fullPath: '/book'
preLoaderRoute: typeof BookLazyImport
parentRoute: typeof rootRoute
}
'/new/book': {
id: '/new/book'
path: '/new/book'
@ -135,6 +123,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileUsernameImport
parentRoute: typeof rootRoute
}
'/work/book/$bookId': {
id: '/work/book/$bookId'
path: '/work/book/$bookId'
fullPath: '/work/book/$bookId'
preLoaderRoute: typeof WorkBookBookIdImport
parentRoute: typeof rootRoute
}
}
}
@ -146,9 +141,9 @@ export interface FileRoutesByFullPath {
'/editor': typeof EditorRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
}
export interface FileRoutesByTo {
@ -157,9 +152,9 @@ export interface FileRoutesByTo {
'/editor': typeof EditorRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
}
export interface FileRoutesById {
@ -169,9 +164,9 @@ export interface FileRoutesById {
'/editor': typeof EditorRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
}
export interface FileRouteTypes {
@ -182,9 +177,9 @@ export interface FileRouteTypes {
| '/editor'
| '/login'
| '/register'
| '/book'
| '/new/book'
| '/profile/$username'
| '/work/book/$bookId'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@ -192,9 +187,9 @@ export interface FileRouteTypes {
| '/editor'
| '/login'
| '/register'
| '/book'
| '/new/book'
| '/profile/$username'
| '/work/book/$bookId'
id:
| '__root__'
| '/'
@ -202,9 +197,9 @@ export interface FileRouteTypes {
| '/editor'
| '/login'
| '/register'
| '/book'
| '/new/book'
| '/profile/$username'
| '/work/book/$bookId'
fileRoutesById: FileRoutesById
}
@ -214,9 +209,9 @@ export interface RootRouteChildren {
EditorRoute: typeof EditorRoute
LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute
BookLazyRoute: typeof BookLazyRoute
NewBookRoute: typeof NewBookRoute
ProfileUsernameRoute: typeof ProfileUsernameRoute
WorkBookBookIdRoute: typeof WorkBookBookIdRoute
}
const rootRouteChildren: RootRouteChildren = {
@ -225,9 +220,9 @@ const rootRouteChildren: RootRouteChildren = {
EditorRoute: EditorRoute,
LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute,
BookLazyRoute: BookLazyRoute,
NewBookRoute: NewBookRoute,
ProfileUsernameRoute: ProfileUsernameRoute,
WorkBookBookIdRoute: WorkBookBookIdRoute,
}
export const routeTree = rootRoute
@ -245,9 +240,9 @@ export const routeTree = rootRoute
"/editor",
"/login",
"/register",
"/book",
"/new/book",
"/profile/$username"
"/profile/$username",
"/work/book/$bookId"
]
},
"/": {
@ -265,14 +260,14 @@ export const routeTree = rootRoute
"/register": {
"filePath": "register.tsx"
},
"/book": {
"filePath": "book.lazy.tsx"
},
"/new/book": {
"filePath": "new/book.tsx"
},
"/profile/$username": {
"filePath": "profile/$username.tsx"
},
"/work/book/$bookId": {
"filePath": "work/book/$bookId.tsx"
}
}
}

View file

@ -2,17 +2,18 @@
import { MutationResolvers, QueryResolvers } from '@generated/types';
/* Query */
import { IdentityResolver } from './resolvers/identityResolver';
import { UserOneResolver } from './resolvers/userOneResolver';
import { BookChapterManyResolver } from './resolvers/bookChapterMany';
import { BookChapterOneResolver } from './resolvers/bookChapterOne';
import { BookChapterManyResolver } from './resolvers/bookChapterManyResolver';
import { BookChapterOneResolver } from './resolvers/bookChapterOneResolver';
import { BookManyResolver } from './resolvers/bookManyResolver';
import { BookOneResolver } from './resolvers/bookOneResolver';
import { IdentityResolver } from './resolvers/identityResolver';
import { UserOneResolver } from './resolvers/userOneResolver';
/* Mutation */
import { LoginResolver } from './resolvers/auth/login';
import { LogoutResolver } from './resolvers/auth/logout';
import { SignUpResolver } from './resolvers/auth/signUp';
import { BookCreateResolver } from './resolvers/bookCreateResolver';
export const Query: QueryResolvers = {
bookOne: BookOneResolver,
@ -26,7 +27,8 @@ export const Query: QueryResolvers = {
export const Mutation: MutationResolvers = {
signUp: SignUpResolver,
login: LoginResolver,
logout: LogoutResolver
logout: LogoutResolver,
bookCreate: BookCreateResolver
};
export const Subscription = {};

View file

@ -1,6 +1,7 @@
import { IdentityContext } from '@app/server/identity';
import { prisma } from '@app/server/prisma/client';
import { BookChapterManyOutput, QueryBookChapterManyArgs } from '@generated/types';
import { Prisma } from '@prisma/client';
import { toBookChapterSchema } from './mappers/bookChapterMapper';
export async function BookChapterManyResolver(
@ -9,11 +10,14 @@ export async function BookChapterManyResolver(
{ identity }: IdentityContext
): Promise<BookChapterManyOutput> {
const { bookId, filter } = input;
const terms = filter.terms?.toLowerCase();
const bookChapterWhere: Prisma.BookChapterWhereInput = {
AND: [{ bookId }, { OR: terms != null ? [{ title: { contains: terms } }] : undefined }]
};
const items = await prisma.bookChapter.findMany({
where: {
bookId
},
where: bookChapterWhere,
skip: filter.offset ?? 0,
take: filter.size ?? 10
});

View file

@ -0,0 +1,27 @@
import { ensureIdentity } from '@app/server/helpers';
import { IdentityContext } from '@app/server/identity';
import { prisma, uuid } from '@app/server/prisma/client';
import { Book, MutationBookCreateArgs } from '@generated/types';
import { toBookSchema } from './mappers/bookMapper';
export async function BookCreateResolver(
_: any,
{ input }: MutationBookCreateArgs,
{ identity }: IdentityContext
): Promise<Book> {
ensureIdentity(identity);
console.log(input.title.length);
if (input.title.length > 60) throw new Error('Title is too long.');
if (input.description.length > 900) throw new Error('Description is too long.');
const model = await prisma.book.create({
data: {
id: uuid(),
title: input.title,
description: input.description,
authorId: identity.id
}
});
return toBookSchema(model);
}

View file

@ -1,6 +1,7 @@
import { IdentityContext } from '@app/server/identity';
import { prisma } from '@app/server/prisma/client';
import { BookManyOutput, QueryBookManyArgs } from '@generated/types';
import { Prisma } from '@prisma/client';
import { toBookSchema } from './mappers/bookMapper';
export async function BookManyResolver(
@ -11,19 +12,19 @@ export async function BookManyResolver(
const { filter } = input;
const terms = filter.terms?.toLowerCase();
const items = await prisma.book.findMany({
where: {
const bookWhere: Prisma.BookWhereInput = {
OR:
terms != null
? [{ title: { contains: terms } }, { author: { username: terms } }]
: undefined
},
terms != null ? [{ title: { contains: terms } }, { author: { username: terms } }] : undefined
};
const items = await prisma.book.findMany({
where: bookWhere,
skip: filter.offset ?? 0,
take: filter.size ?? 10
});
return {
items: await Promise.all(items.map(value => toBookSchema(value))),
total: items.length
total: await prisma.book.count({ where: bookWhere })
};
}

View file

@ -14,6 +14,7 @@ type Mutation {
signUp(input: SignUpInput!): Void
login(input: LoginInput!): Session!
logout: Void
bookCreate(input: BookCreateInput!): Book!
}
input SignUpInput {

7
app/server/helpers.ts Normal file
View file

@ -0,0 +1,7 @@
import { IdentityContext } from './identity';
export function ensureIdentity(
identity: IdentityContext['identity']
): asserts identity is NonNullable<IdentityContext['identity']> {
if (identity == null) throw new Error('Unauthorized.');
}

289
package-lock.json generated
View file

@ -28,6 +28,7 @@
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"graphql": "^16.9.0",
@ -2512,9 +2513,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
@ -2528,9 +2529,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
@ -2544,9 +2545,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
@ -2560,9 +2561,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
@ -2576,9 +2577,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
@ -2592,9 +2593,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
@ -2608,9 +2609,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
@ -2624,9 +2625,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
@ -2640,9 +2641,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
@ -2656,9 +2657,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
@ -2672,9 +2673,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
@ -2688,9 +2689,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
@ -2704,9 +2705,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
@ -2720,9 +2721,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
@ -2736,9 +2737,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
@ -2752,9 +2753,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
@ -2768,9 +2769,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
@ -2784,9 +2785,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
@ -2800,9 +2801,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
@ -2816,9 +2817,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
@ -2832,9 +2833,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
@ -2848,9 +2849,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
@ -2864,9 +2865,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
@ -2880,9 +2881,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
@ -2896,9 +2897,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
@ -3712,13 +3713,13 @@
}
},
"node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/fetch": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.7.tgz",
"integrity": "sha512-sL31zX8BqZovZc38ovBFmKEfao9AzZ/24sWSHKNhDhcnzIO/PYAX2xF6vYtgU9hinrEGlvScTTyKSMynHGdfEA==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz",
"integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@whatwg-node/node-fetch": "^0.7.19",
"@whatwg-node/node-fetch": "^0.7.21",
"urlpattern-polyfill": "^10.0.0"
},
"engines": {
@ -3726,9 +3727,9 @@
}
},
"node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/node-fetch": {
"version": "0.7.20",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.20.tgz",
"integrity": "sha512-2LI5HkZcdAODb6umb7MUxkWYNUi3j6apgQvIzp+X3w3AVPdvVsshvjaMo4gwLeFtfs+cyMNOLTm9Hhm/TXq6DA==",
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz",
"integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4021,13 +4022,13 @@
}
},
"node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/fetch": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.7.tgz",
"integrity": "sha512-sL31zX8BqZovZc38ovBFmKEfao9AzZ/24sWSHKNhDhcnzIO/PYAX2xF6vYtgU9hinrEGlvScTTyKSMynHGdfEA==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz",
"integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@whatwg-node/node-fetch": "^0.7.19",
"@whatwg-node/node-fetch": "^0.7.21",
"urlpattern-polyfill": "^10.0.0"
},
"engines": {
@ -4035,9 +4036,9 @@
}
},
"node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/node-fetch": {
"version": "0.7.20",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.20.tgz",
"integrity": "sha512-2LI5HkZcdAODb6umb7MUxkWYNUi3j6apgQvIzp+X3w3AVPdvVsshvjaMo4gwLeFtfs+cyMNOLTm9Hhm/TXq6DA==",
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz",
"integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4222,13 +4223,13 @@
}
},
"node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/fetch": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.7.tgz",
"integrity": "sha512-sL31zX8BqZovZc38ovBFmKEfao9AzZ/24sWSHKNhDhcnzIO/PYAX2xF6vYtgU9hinrEGlvScTTyKSMynHGdfEA==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz",
"integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@whatwg-node/node-fetch": "^0.7.19",
"@whatwg-node/node-fetch": "^0.7.21",
"urlpattern-polyfill": "^10.0.0"
},
"engines": {
@ -4236,9 +4237,9 @@
}
},
"node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/node-fetch": {
"version": "0.7.20",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.20.tgz",
"integrity": "sha512-2LI5HkZcdAODb6umb7MUxkWYNUi3j6apgQvIzp+X3w3AVPdvVsshvjaMo4gwLeFtfs+cyMNOLTm9Hhm/TXq6DA==",
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz",
"integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4589,13 +4590,13 @@
}
},
"node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/fetch": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.7.tgz",
"integrity": "sha512-sL31zX8BqZovZc38ovBFmKEfao9AzZ/24sWSHKNhDhcnzIO/PYAX2xF6vYtgU9hinrEGlvScTTyKSMynHGdfEA==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz",
"integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@whatwg-node/node-fetch": "^0.7.19",
"@whatwg-node/node-fetch": "^0.7.21",
"urlpattern-polyfill": "^10.0.0"
},
"engines": {
@ -4603,9 +4604,9 @@
}
},
"node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/node-fetch": {
"version": "0.7.20",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.20.tgz",
"integrity": "sha512-2LI5HkZcdAODb6umb7MUxkWYNUi3j6apgQvIzp+X3w3AVPdvVsshvjaMo4gwLeFtfs+cyMNOLTm9Hhm/TXq6DA==",
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz",
"integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4733,13 +4734,13 @@
}
},
"node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/fetch": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.7.tgz",
"integrity": "sha512-sL31zX8BqZovZc38ovBFmKEfao9AzZ/24sWSHKNhDhcnzIO/PYAX2xF6vYtgU9hinrEGlvScTTyKSMynHGdfEA==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz",
"integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@whatwg-node/node-fetch": "^0.7.19",
"@whatwg-node/node-fetch": "^0.7.21",
"urlpattern-polyfill": "^10.0.0"
},
"engines": {
@ -4747,9 +4748,9 @@
}
},
"node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/node-fetch": {
"version": "0.7.20",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.20.tgz",
"integrity": "sha512-2LI5HkZcdAODb6umb7MUxkWYNUi3j6apgQvIzp+X3w3AVPdvVsshvjaMo4gwLeFtfs+cyMNOLTm9Hhm/TXq6DA==",
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz",
"integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -8800,6 +8801,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@ -9389,9 +9396,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -9401,31 +9408,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4",
"@esbuild/android-arm": "0.25.4",
"@esbuild/android-arm64": "0.25.4",
"@esbuild/android-x64": "0.25.4",
"@esbuild/darwin-arm64": "0.25.4",
"@esbuild/darwin-x64": "0.25.4",
"@esbuild/freebsd-arm64": "0.25.4",
"@esbuild/freebsd-x64": "0.25.4",
"@esbuild/linux-arm": "0.25.4",
"@esbuild/linux-arm64": "0.25.4",
"@esbuild/linux-ia32": "0.25.4",
"@esbuild/linux-loong64": "0.25.4",
"@esbuild/linux-mips64el": "0.25.4",
"@esbuild/linux-ppc64": "0.25.4",
"@esbuild/linux-riscv64": "0.25.4",
"@esbuild/linux-s390x": "0.25.4",
"@esbuild/linux-x64": "0.25.4",
"@esbuild/netbsd-arm64": "0.25.4",
"@esbuild/netbsd-x64": "0.25.4",
"@esbuild/openbsd-arm64": "0.25.4",
"@esbuild/openbsd-x64": "0.25.4",
"@esbuild/sunos-x64": "0.25.4",
"@esbuild/win32-arm64": "0.25.4",
"@esbuild/win32-ia32": "0.25.4",
"@esbuild/win32-x64": "0.25.4"
"@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/esbuild-register": {
@ -11863,9 +11870,9 @@
}
},
"node_modules/jackspeak": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"

View file

@ -34,6 +34,7 @@
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"graphql": "^16.9.0",