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

View file

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

View file

@ -1,7 +1,8 @@
import { Card, List, Text } from '@chakra-ui/react'; 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 ( return (
<List.Root variant="plain" gap={4}> <List.Root variant="plain" gap={4}>
{workingsList.length !== 0 ? ( {workingsList.length !== 0 ? (

View file

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

View file

@ -12,7 +12,7 @@ export function WorkingProgressStatusBadge({
progressStatus: WorkingProgressStatus; progressStatus: WorkingProgressStatus;
}) { }) {
return ( return (
<Box flex={1} justifyContent="center" alignItems="center"> <Box>
<Badge <Badge
variant="solid" variant="solid"
backgroundColor={toBgColorByProgressStatus(progressStatus)} 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'; import { IconBadge } from './IconBadge';
interface WorkingRatingParams { interface WorkingRatingParams {
@ -21,8 +21,10 @@ export function WorkingRatingBadge({ ratingTotal }: { ratingTotal: number }) {
iconClass: 'nf nf-fa-angle_down' iconClass: 'nf nf-fa-angle_down'
}; };
return ( return (
<IconBadge backgroundColor={ratingParams.color} iconClass={ratingParams.iconClass}> <Box>
<Text>{ratingParams.rating}</Text> <IconBadge backgroundColor={ratingParams.color} iconClass={ratingParams.iconClass}>
</IconBadge> <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 { Link as ChakraLink } from '@chakra-ui/react';
import { createLink, LinkComponent } from "@tanstack/react-router"; import { createLink, LinkComponent } from '@tanstack/react-router';
import * as React from "react"; import * as React from 'react';
interface ChakraLinkProps interface ChakraLinkProps extends Omit<React.ComponentPropsWithoutRef<typeof ChakraLink>, 'href'> {}
extends Omit<React.ComponentPropsWithoutRef<typeof ChakraLink>, "href"> {}
const ChakraLinkComponent = React.forwardRef< const ChakraLinkComponent = React.forwardRef<HTMLAnchorElement, ChakraLinkProps>((props, ref) => {
HTMLAnchorElement,
ChakraLinkProps
>((props, ref) => {
return <ChakraLink ref={ref} {...props} />; return <ChakraLink ref={ref} {...props} />;
}); });
const CreatedLinkComponent = createLink(ChakraLinkComponent); const CreatedLinkComponent = createLink(ChakraLinkComponent);
export const Link: LinkComponent<typeof ChakraLinkComponent> = (props) => { export const Link: LinkComponent<typeof ChakraLinkComponent> = props => {
return ( return (
<CreatedLinkComponent <CreatedLinkComponent
textDecoration={"underline"} textDecoration={'underline'}
_hover={{ textDecoration: "none" }} _hover={{ textDecoration: 'none' }}
_focus={{ textDecoration: "none" }} _focus={{ textDecoration: 'none' }}
preload={"intent"} preload={'intent'}
{...props} {...props}
/> />
); );

View file

@ -7,10 +7,34 @@ export const sessionFragment = gql`
} }
`; `;
// refreshToken { export const bookFragment = gql`
// token fragment Book on Book {
// maxAge id
// secure title
// httpOnly description
// sameSite 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 { WorkingProgressStatus } from '@generated/types';
import dayjs from 'dayjs';
export function assertUnreachable(value: never): never { export function assertUnreachable(value: never): never {
throw new Error(`Unhandled scenario: ${value}`); 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 { export function toBgColorByProgressStatus(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'yellow.500'; if (value === WorkingProgressStatus.InProgress) return 'yellow.500';
if (value === WorkingProgressStatus.Frozen) return 'blue.500'; if (value === WorkingProgressStatus.Frozen) return 'blue.500';

View file

@ -10,6 +10,7 @@ import { DocumentNode } from 'graphql';
interface UsePaginatedQueryProps { interface UsePaginatedQueryProps {
query: DocumentNode; query: DocumentNode;
input: { input: {
[key: string]: any; // for the custom inputs
filter: { filter: {
terms?: string | null; terms?: string | null;
offset?: number | 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 { import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
Link as ChakraLink, import { createRootRouteWithContext, Outlet, Link } from '@tanstack/react-router';
Icon,
Input,
InputGroup,
Kbd,
Menu,
Separator,
Stack,
Text
} from '@chakra-ui/react';
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router';
import React from 'react'; import React from 'react';
import { Avatar } from '../components/ui/avatar'; import { Avatar } from '../components/ui/avatar';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
@ -18,7 +8,6 @@ import { Tooltip } from '../components/ui/tooltip';
import { useAuth } from '../hooks/auth'; import { useAuth } from '../hooks/auth';
import { useIdentity } from '../hooks/identity'; import { useIdentity } from '../hooks/identity';
import { appName } from '../main'; import { appName } from '../main';
// import { Link as ChakraLink } from '../components/ui/link';
const TanStackRouterDevtools = const TanStackRouterDevtools =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
@ -67,12 +56,28 @@ export const Route = createRootRouteWithContext<RouterContext>()({
<> <>
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<Link to="/editor"> <Link to="/editor">
<ChakraLink>editor</ChakraLink> <Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
editor
</Text>
</Link> </Link>
</Stack> </Stack>
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<Link to="/new/book"> <Link to="/new/book">
<ChakraLink>create</ChakraLink> <Text
color="colorPalette.400"
_hover={{
textDecoration: 'underline',
textDecorationColor: 'colorPalette.700'
}}
>
create
</Text>
</Link> </Link>
</Stack> </Stack>
</> </>
@ -99,10 +104,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
{identity != null ? ( {identity != null ? (
<Menu.Root> <Menu.Root>
<Menu.Trigger css={{ '&:hover': { cursor: 'pointer' } }}> <Menu.Trigger css={{ '&:hover': { cursor: 'pointer' } }}>
<Avatar <Avatar size="sm" />
size="sm"
// src="https://avatars.githubusercontent.com/u/40203084?v=4"
/>
</Menu.Trigger> </Menu.Trigger>
<Menu.Positioner mt={2}> <Menu.Positioner mt={2}>
<Menu.Content backgroundColor={'bg.subtle'}> <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> </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 { Heading, Input, Stack, Textarea } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button'; import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field'; import { Field } from '@client/components/ui/field';
import { CreateNewBookMutation } from '@generated/types';
import { valibotResolver } from '@hookform/resolvers/valibot'; 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 { useForm } from 'react-hook-form';
import * as v from 'valibot'; import * as v from 'valibot';
@ -28,10 +31,16 @@ interface FormValues {
} }
function NewBookForm() { function NewBookForm() {
const navigate = useNavigate();
const { update: bookCreate } = useToastMutation<CreateNewBookMutation>({
mutation: bookCreateMutation,
successMsg: 'Book created successfully!'
});
const schema = v.pipe( const schema = v.pipe(
v.object({ v.object({
title: v.pipe(v.string(), v.nonEmpty('Title is required')), title: v.pipe(v.string(), v.nonEmpty('Title is required'), v.maxLength(60)),
description: v.pipe(v.string(), v.nonEmpty('Password is required')) description: v.pipe(v.string(), v.nonEmpty('Description is required'), v.maxLength(900))
}) })
); );
@ -42,7 +51,23 @@ function NewBookForm() {
} = useForm<FormValues>({ resolver: valibotResolver(schema) }); } = useForm<FormValues>({ resolver: valibotResolver(schema) });
return ( 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> <Stack>
<Field label="Title" invalid={errors.title != null} errorText={errors.title?.message}> <Field label="Title" invalid={errors.title != null} errorText={errors.title?.message}>
<Input {...register('title')} /> <Input {...register('title')} />
@ -52,7 +77,7 @@ function NewBookForm() {
invalid={errors.description != null} invalid={errors.description != null}
errorText={errors.description?.message} errorText={errors.description?.message}
> >
<Textarea width="100%" lineHeight={4} {...register('description')} /> <Textarea width="100%" height={'sm'} {...register('description')} />
</Field> </Field>
<Button type="submit">Create new book</Button> <Button type="submit">Create new book</Button>
</Stack> </Stack>
@ -60,38 +85,12 @@ function NewBookForm() {
); );
} }
// function LoginForm() { const bookCreateMutation = gql`
// const schema = v.pipe( mutation CreateNewBook($input: BookCreateInput!) {
// v.object({ bookCreate(input: $input) {
// email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')), id
// password: v.pipe(v.string(), v.nonEmpty('Password is required')) title
// }) description
// ); }
// }
// 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>
// );
// }

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. // 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. // 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 Routes
import { Route as rootRoute } from './pages/__root' 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 IndexImport } from './pages/index'
import { Route as ProfileUsernameImport } from './pages/profile/$username' import { Route as ProfileUsernameImport } from './pages/profile/$username'
import { Route as NewBookImport } from './pages/new/book' import { Route as NewBookImport } from './pages/new/book'
import { Route as WorkBookBookIdImport } from './pages/work/book/$bookId'
// Create Virtual Routes
const BookLazyImport = createFileRoute('/book')()
// Create/Update Routes // 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({ const RegisterRoute = RegisterImport.update({
id: '/register', id: '/register',
path: '/register', path: '/register',
@ -75,6 +64,12 @@ const NewBookRoute = NewBookImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const WorkBookBookIdRoute = WorkBookBookIdImport.update({
id: '/work/book/$bookId',
path: '/work/book/$bookId',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@ -114,13 +109,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RegisterImport preLoaderRoute: typeof RegisterImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/book': {
id: '/book'
path: '/book'
fullPath: '/book'
preLoaderRoute: typeof BookLazyImport
parentRoute: typeof rootRoute
}
'/new/book': { '/new/book': {
id: '/new/book' id: '/new/book'
path: '/new/book' path: '/new/book'
@ -135,6 +123,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProfileUsernameImport preLoaderRoute: typeof ProfileUsernameImport
parentRoute: typeof rootRoute 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 '/editor': typeof EditorRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute '/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
@ -157,9 +152,9 @@ export interface FileRoutesByTo {
'/editor': typeof EditorRoute '/editor': typeof EditorRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute '/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@ -169,9 +164,9 @@ export interface FileRoutesById {
'/editor': typeof EditorRoute '/editor': typeof EditorRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute '/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
'/work/book/$bookId': typeof WorkBookBookIdRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@ -182,9 +177,9 @@ export interface FileRouteTypes {
| '/editor' | '/editor'
| '/login' | '/login'
| '/register' | '/register'
| '/book'
| '/new/book' | '/new/book'
| '/profile/$username' | '/profile/$username'
| '/work/book/$bookId'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@ -192,9 +187,9 @@ export interface FileRouteTypes {
| '/editor' | '/editor'
| '/login' | '/login'
| '/register' | '/register'
| '/book'
| '/new/book' | '/new/book'
| '/profile/$username' | '/profile/$username'
| '/work/book/$bookId'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@ -202,9 +197,9 @@ export interface FileRouteTypes {
| '/editor' | '/editor'
| '/login' | '/login'
| '/register' | '/register'
| '/book'
| '/new/book' | '/new/book'
| '/profile/$username' | '/profile/$username'
| '/work/book/$bookId'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@ -214,9 +209,9 @@ export interface RootRouteChildren {
EditorRoute: typeof EditorRoute EditorRoute: typeof EditorRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute RegisterRoute: typeof RegisterRoute
BookLazyRoute: typeof BookLazyRoute
NewBookRoute: typeof NewBookRoute NewBookRoute: typeof NewBookRoute
ProfileUsernameRoute: typeof ProfileUsernameRoute ProfileUsernameRoute: typeof ProfileUsernameRoute
WorkBookBookIdRoute: typeof WorkBookBookIdRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
@ -225,9 +220,9 @@ const rootRouteChildren: RootRouteChildren = {
EditorRoute: EditorRoute, EditorRoute: EditorRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute, RegisterRoute: RegisterRoute,
BookLazyRoute: BookLazyRoute,
NewBookRoute: NewBookRoute, NewBookRoute: NewBookRoute,
ProfileUsernameRoute: ProfileUsernameRoute, ProfileUsernameRoute: ProfileUsernameRoute,
WorkBookBookIdRoute: WorkBookBookIdRoute,
} }
export const routeTree = rootRoute export const routeTree = rootRoute
@ -245,9 +240,9 @@ export const routeTree = rootRoute
"/editor", "/editor",
"/login", "/login",
"/register", "/register",
"/book",
"/new/book", "/new/book",
"/profile/$username" "/profile/$username",
"/work/book/$bookId"
] ]
}, },
"/": { "/": {
@ -265,14 +260,14 @@ export const routeTree = rootRoute
"/register": { "/register": {
"filePath": "register.tsx" "filePath": "register.tsx"
}, },
"/book": {
"filePath": "book.lazy.tsx"
},
"/new/book": { "/new/book": {
"filePath": "new/book.tsx" "filePath": "new/book.tsx"
}, },
"/profile/$username": { "/profile/$username": {
"filePath": "profile/$username.tsx" "filePath": "profile/$username.tsx"
},
"/work/book/$bookId": {
"filePath": "work/book/$bookId.tsx"
} }
} }
} }

View file

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

View file

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

View file

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