added resolvers, updated model, updated navbar

This commit is contained in:
lnn0q 2025-05-17 21:41:07 +03:00
parent a8375b99c2
commit 1d40d7e15f
23 changed files with 439 additions and 301 deletions

View file

@ -0,0 +1,84 @@
import { gql } from '@apollo/client';
import { Skeleton, 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 { usePaginatedQuery } from '../hooks/usePaginatedQuery';
export function BookSearchList() {
const [workingSearchValue, setWorkingSearchValue] = useState<string>('');
const [workingsListPage, setWorkingsListPage] = useState<number>(1);
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(2);
const startIndex = (workingsListPage - 1) * workingsListPageSize;
const { data } = usePaginatedQuery<BooksListQuery>({
query,
input: {
filter: {
terms: workingSearchValue,
offset: startIndex,
size: workingsListPageSize
}
}
});
return (
<>
{data != null ? (
<>
<WorkingListSearch
onSubmit={value => {
setWorkingSearchValue(value);
}}
/>
<WorkingList workingsList={[...data.bookMany.items]} />
{data.bookMany.items.length != 0 && (
<Pagination
total={data.bookMany.total}
page={workingsListPage}
setPage={setWorkingsListPage}
pageSize={workingsListPageSize}
/>
)}
</>
) : (
<>
<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} />
</>
)}
</>
);
}
const query = gql`
query BooksList($input: BookManyInput!) {
bookMany(input: $input) {
items {
id
title
description
author {
id
username
}
updatedAt
progressStatus
ratingTotal
reviewsTotal
wordsTotal
chaptersTotal
}
total
}
}
`;

View file

@ -0,0 +1,27 @@
import { Badge, BadgeProps, Box, Icon, Stack } from '@chakra-ui/react';
import { ReactNode } from 'react';
export function IconBadge({
children,
iconClass,
backgroundColor,
size = 'lg'
}: {
children?: ReactNode;
iconClass: string;
backgroundColor?: BadgeProps['backgroundColor'];
size?: 'sm' | 'md' | 'lg' | 'xs';
}) {
return (
<Box flex={1} direction="row" justifyContent="center" alignItems="center">
<Badge variant="solid" size={size} backgroundColor={backgroundColor}>
<Stack direction="row">
<Icon fontSize="xl">
<i className={iconClass} />
</Icon>
{children}
</Stack>
</Badge>
</Box>
);
}

View file

@ -1,14 +1,28 @@
import { List } from '@chakra-ui/react'; import { Card, List, Text } from '@chakra-ui/react';
import { WorkingCardProps, WorkingListCard } from './WorkingListCard'; import { WorkingCardProps, WorkingListCard } from './WorkingListCard';
export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) { export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) {
return ( return (
<List.Root variant="plain" gap={4}> <List.Root variant="plain" gap={4}>
{workingsList.map(workingData => ( {workingsList.length != 0 ? (
workingsList.map(workingData => (
<List.Item key={workingData.id}> <List.Item key={workingData.id}>
<WorkingListCard {...workingData} /> <WorkingListCard {...workingData} />
</List.Item> </List.Item>
))} ))
) : (
<List.Item>
<Card.Root
variant="outline"
width="100%"
padding={4}
justifyContent="center"
alignItems={'center'}
>
<Text fontSize="md">Nothing is here yet!</Text>
</Card.Root>
</List.Item>
)}
</List.Root> </List.Root>
); );
} }

View file

@ -1,6 +1,6 @@
import { Badge, BadgeProps, Box, Card, Heading, Icon, Link, Stack, Text } from '@chakra-ui/react'; import { Badge, Box, Card, Heading, Link, Stack, Text } from '@chakra-ui/react';
import { WorkingProgressStatus } from '@generated/types'; import { WorkingProgressStatus } from '@generated/types';
import { ReactNode } from 'react'; import { IconBadge } from './IconBadge';
import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge'; import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge';
import { WorkingRatingBadge } from './WorkingRatingBadge'; import { WorkingRatingBadge } from './WorkingRatingBadge';
@ -88,28 +88,3 @@ export function WorkingListCard({
</Card.Root> </Card.Root>
); );
} }
export function IconBadge({
children,
iconClass,
backgroundColor,
size = 'lg'
}: {
children?: ReactNode;
iconClass: string;
backgroundColor?: BadgeProps['backgroundColor'];
size?: 'sm' | 'md' | 'lg' | 'xs';
}) {
return (
<Box flex={1} direction="row" justifyContent="center" alignItems="center">
<Badge variant="solid" size={size} backgroundColor={backgroundColor}>
<Stack direction="row">
<Icon fontSize="xl">
<i className={iconClass} />
</Icon>
{children}
</Stack>
</Badge>
</Box>
);
}

View file

@ -1,19 +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';
export function WorkingListSearch({ export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => void }) {
value, const [searchValue, setSearchValue] = useState<string>('');
onChange,
onSubmit
}: {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
}) {
return ( return (
<form <form
onSubmit={event => { onSubmit={event => {
event.preventDefault(); event.preventDefault();
onSubmit(value); onSubmit(searchValue);
}} }}
> >
<Stack direction="row"> <Stack direction="row">
@ -26,8 +20,8 @@ export function WorkingListSearch({
} }
> >
<Input <Input
value={value} value={searchValue}
onChange={event => onChange(event.currentTarget.value)} onChange={event => setSearchValue(event.currentTarget.value)}
placeholder="Search works..." placeholder="Search works..."
/> />
</InputGroup> </InputGroup>

View file

@ -32,6 +32,6 @@ export function WorkingProgressStatusBadge({
function toWorkingProgressStatusIcon(value: WorkingProgressStatus): string { function toWorkingProgressStatusIcon(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'nf nf-fa-clock'; if (value === WorkingProgressStatus.InProgress) return 'nf nf-fa-clock';
if (value === WorkingProgressStatus.Frozen) return 'nf nf-fa-snowflake'; if (value === WorkingProgressStatus.Frozen) return 'nf nf-fa-snowflake';
if (value === WorkingProgressStatus.Finished) return 'nf nf-fa-circle_check'; if (value === WorkingProgressStatus.Complete) return 'nf nf-fa-circle_check';
assertUnreachable(value); assertUnreachable(value);
} }

View file

@ -1,5 +1,5 @@
import { BadgeProps, Text } from '@chakra-ui/react'; import { BadgeProps, Text } from '@chakra-ui/react';
import { IconBadge } from './WorkingListCard'; import { IconBadge } from './IconBadge';
interface WorkingRatingParams { interface WorkingRatingParams {
rating: string; rating: string;

View file

@ -7,20 +7,20 @@ export function assertUnreachable(value: never): never {
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';
if (value === WorkingProgressStatus.Finished) return 'green.500'; if (value === WorkingProgressStatus.Complete) return 'green.500';
assertUnreachable(value); assertUnreachable(value);
} }
export function toFgColorByProgressStatus(value: WorkingProgressStatus): string { export function toFgColorByProgressStatus(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'fg.warning'; if (value === WorkingProgressStatus.InProgress) return 'fg.warning';
if (value === WorkingProgressStatus.Frozen) return 'fg.info'; if (value === WorkingProgressStatus.Frozen) return 'fg.info';
if (value === WorkingProgressStatus.Finished) return 'fg.success'; if (value === WorkingProgressStatus.Complete) return 'fg.success';
assertUnreachable(value); assertUnreachable(value);
} }
export function toWorkingProgressStatusLabel(value: WorkingProgressStatus): string { export function toWorkingProgressStatusLabel(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'In progress'; if (value === WorkingProgressStatus.InProgress) return 'In progress';
if (value === WorkingProgressStatus.Frozen) return 'Frozen'; if (value === WorkingProgressStatus.Frozen) return 'Frozen';
if (value === WorkingProgressStatus.Finished) return 'Finished'; if (value === WorkingProgressStatus.Complete) return 'Complete';
assertUnreachable(value); assertUnreachable(value);
} }

View file

@ -1,4 +1,14 @@
import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react'; 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 { 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';
@ -8,6 +18,7 @@ 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'
@ -33,7 +44,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
return ( return (
<Stack padding={3}> <Stack padding={3}>
<Stack flexDirection="row" justifyContent="space-between" alignItems="center"> <Stack flexDirection="row" justifyContent="space-between" alignItems="center">
<Stack flexDirection="row" justifyContent="center" alignItems="center" gap={4}> <Stack flexDirection="row" justifyContent="center" alignItems="center" gap={10}>
<Link to="/"> <Link to="/">
<Stack direction="row" justifyContent="center" alignItems="center" gap={1}> <Stack direction="row" justifyContent="center" alignItems="center" gap={1}>
<Icon fontSize={24} color="colorPalette.500"> <Icon fontSize={24} color="colorPalette.500">
@ -51,6 +62,21 @@ export const Route = createRootRouteWithContext<RouterContext>()({
{/* </Icon> */} {/* </Icon> */}
{/* </Link> */} {/* </Link> */}
{/* </Stack> */} {/* </Stack> */}
{identity != null && (
<>
<Stack direction="row" alignItems="center">
<Link to="/editor">
<ChakraLink>editor</ChakraLink>
</Link>
</Stack>
<Stack direction="row" alignItems="center">
<Link to="/new/book">
<ChakraLink>create</ChakraLink>
</Link>
</Stack>
</>
)}
</Stack> </Stack>
<Stack flexDirection="row" alignItems="center" gap={4}> <Stack flexDirection="row" alignItems="center" gap={4}>

View file

@ -1,105 +1,21 @@
import { gql } from '@apollo/client'; import { Stack } from '@chakra-ui/react';
import { Skeleton, Stack } from '@chakra-ui/react';
import { WorkingListQuery } from '@generated/types';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react'; import { BookSearchList } from '../components/BookSearchList';
import { Pagination } from '../components/Pagination';
import { WorkingList } from '../components/WorkingList';
import { WorkingListSearch } from '../components/WorkingListSearch';
import { usePaginatedQuery } from '../hooks/usePaginatedQuery';
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: RouteComponent component: RouteComponent
}); });
function RouteComponent() { function RouteComponent() {
const [workingSearchValue, setWorkingSearchValue] = useState<string>('');
const [workingsListPage, setWorkingsListPage] = useState<number>(1);
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(2);
const startIndex = (workingsListPage - 1) * workingsListPageSize;
const { data } = usePaginatedQuery<WorkingListQuery>({
query,
input: {
filter: {
terms: workingSearchValue,
offset: startIndex,
size: workingsListPageSize
}
}
});
return ( return (
<Stack justifyContent="center" alignItems="center"> <Stack justifyContent="center" alignItems="center">
<Stack width="90%"> <Stack width="90%">
{data != null ? ( <BookSearchList />
<>
<WorkingListSearch
value={workingSearchValue}
onSubmit={value => {
alert(value);
}}
onChange={value => setWorkingSearchValue(value)}
/>
<WorkingList
workingsList={data.workingMany.items.map(item => ({
...item,
chaptersTotal: item.chapters.total,
reviewsTotal: item.reviews.total
}))}
/>
<Pagination
total={data.workingMany.total}
page={workingsListPage}
setPage={setWorkingsListPage}
pageSize={workingsListPageSize}
/>
</>
) : (
<>
<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} />
</>
)}
</Stack> </Stack>
</Stack> </Stack>
); );
} }
const query = gql`
query WorkingList($input: WorkingManyInput!) {
workingMany(input: $input) {
items {
id
type
title
description
author {
id
username
}
updatedAt
progressStatus
ratingTotal
reviewsTotal
book {
id
chaptersTotal
wordsTotal
}
}
total
}
}
`;
// const workingData1: WorkingCardProps = { // const workingData1: WorkingCardProps = {
// id: '1', // id: '1',
// title: 'Worth the Candle', // title: 'Worth the Candle',

View file

@ -0,0 +1,66 @@
import { Heading, Input, Stack } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field';
import { valibotResolver } from '@hookform/resolvers/valibot';
import { createFileRoute, redirect } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import * as v from 'valibot';
export const Route = createFileRoute('/new/book')({
component: RouteComponent,
beforeLoad: ({ context }) => {
if (!context.isAuthenticated) throw redirect({ to: '/' });
}
});
function RouteComponent() {
return (
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
<Heading>New book</Heading>
<form></form>
</Stack>
);
}
// 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
// onSubmit={handleSubmit(async values => {
// const session = await login({
// email: values.email,
// password: values.password
// });
// if (session != null) window.history.back();
// })}
// >
// <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

@ -15,11 +15,11 @@ import { createFileRoute } from '@tanstack/react-router'
import { Route as rootRoute } from './pages/__root' import { Route as rootRoute } from './pages/__root'
import { Route as RegisterImport } from './pages/register' import { Route as RegisterImport } from './pages/register'
import { Route as LoginImport } from './pages/login' import { Route as LoginImport } from './pages/login'
import { Route as ExampleImport } from './pages/example'
import { Route as EditorImport } from './pages/editor' import { Route as EditorImport } from './pages/editor'
import { Route as AboutImport } from './pages/about' 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'
// Create Virtual Routes // Create Virtual Routes
@ -45,12 +45,6 @@ const LoginRoute = LoginImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const ExampleRoute = ExampleImport.update({
id: '/example',
path: '/example',
getParentRoute: () => rootRoute,
} as any)
const EditorRoute = EditorImport.update({ const EditorRoute = EditorImport.update({
id: '/editor', id: '/editor',
path: '/editor', path: '/editor',
@ -75,6 +69,12 @@ const ProfileUsernameRoute = ProfileUsernameImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const NewBookRoute = NewBookImport.update({
id: '/new/book',
path: '/new/book',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@ -100,13 +100,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof EditorImport preLoaderRoute: typeof EditorImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/example': {
id: '/example'
path: '/example'
fullPath: '/example'
preLoaderRoute: typeof ExampleImport
parentRoute: typeof rootRoute
}
'/login': { '/login': {
id: '/login' id: '/login'
path: '/login' path: '/login'
@ -128,6 +121,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BookLazyImport preLoaderRoute: typeof BookLazyImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/new/book': {
id: '/new/book'
path: '/new/book'
fullPath: '/new/book'
preLoaderRoute: typeof NewBookImport
parentRoute: typeof rootRoute
}
'/profile/$username': { '/profile/$username': {
id: '/profile/$username' id: '/profile/$username'
path: '/profile/$username' path: '/profile/$username'
@ -144,10 +144,10 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/editor': typeof EditorRoute '/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute '/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
} }
@ -155,10 +155,10 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/editor': typeof EditorRoute '/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute '/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
} }
@ -167,10 +167,10 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/editor': typeof EditorRoute '/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute '/book': typeof BookLazyRoute
'/new/book': typeof NewBookRoute
'/profile/$username': typeof ProfileUsernameRoute '/profile/$username': typeof ProfileUsernameRoute
} }
@ -180,30 +180,30 @@ export interface FileRouteTypes {
| '/' | '/'
| '/about' | '/about'
| '/editor' | '/editor'
| '/example'
| '/login' | '/login'
| '/register' | '/register'
| '/book' | '/book'
| '/new/book'
| '/profile/$username' | '/profile/$username'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/about' | '/about'
| '/editor' | '/editor'
| '/example'
| '/login' | '/login'
| '/register' | '/register'
| '/book' | '/book'
| '/new/book'
| '/profile/$username' | '/profile/$username'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/about' | '/about'
| '/editor' | '/editor'
| '/example'
| '/login' | '/login'
| '/register' | '/register'
| '/book' | '/book'
| '/new/book'
| '/profile/$username' | '/profile/$username'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@ -212,10 +212,10 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute AboutRoute: typeof AboutRoute
EditorRoute: typeof EditorRoute EditorRoute: typeof EditorRoute
ExampleRoute: typeof ExampleRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute RegisterRoute: typeof RegisterRoute
BookLazyRoute: typeof BookLazyRoute BookLazyRoute: typeof BookLazyRoute
NewBookRoute: typeof NewBookRoute
ProfileUsernameRoute: typeof ProfileUsernameRoute ProfileUsernameRoute: typeof ProfileUsernameRoute
} }
@ -223,10 +223,10 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute, AboutRoute: AboutRoute,
EditorRoute: EditorRoute, EditorRoute: EditorRoute,
ExampleRoute: ExampleRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute, RegisterRoute: RegisterRoute,
BookLazyRoute: BookLazyRoute, BookLazyRoute: BookLazyRoute,
NewBookRoute: NewBookRoute,
ProfileUsernameRoute: ProfileUsernameRoute, ProfileUsernameRoute: ProfileUsernameRoute,
} }
@ -243,10 +243,10 @@ export const routeTree = rootRoute
"/", "/",
"/about", "/about",
"/editor", "/editor",
"/example",
"/login", "/login",
"/register", "/register",
"/book", "/book",
"/new/book",
"/profile/$username" "/profile/$username"
] ]
}, },
@ -259,9 +259,6 @@ export const routeTree = rootRoute
"/editor": { "/editor": {
"filePath": "editor.tsx" "filePath": "editor.tsx"
}, },
"/example": {
"filePath": "example.tsx"
},
"/login": { "/login": {
"filePath": "login.tsx" "filePath": "login.tsx"
}, },
@ -271,6 +268,9 @@ export const routeTree = rootRoute
"/book": { "/book": {
"filePath": "book.lazy.tsx" "filePath": "book.lazy.tsx"
}, },
"/new/book": {
"filePath": "new/book.tsx"
},
"/profile/$username": { "/profile/$username": {
"filePath": "profile/$username.tsx" "filePath": "profile/$username.tsx"
} }

View file

@ -2,10 +2,12 @@
import { MutationResolvers, QueryResolvers } from '@generated/types'; import { MutationResolvers, QueryResolvers } from '@generated/types';
/* Query */ /* Query */
import { BookOneResolver } from './resolvers/bookOneResolver';
import { BooksManyResolver } from './resolvers/booksManyResolver';
import { IdentityResolver } from './resolvers/identityResolver'; import { IdentityResolver } from './resolvers/identityResolver';
import { UserOneResolver } from './resolvers/userOneResolver'; import { UserOneResolver } from './resolvers/userOneResolver';
import { BookChapterManyResolver } from './resolvers/bookChapterMany';
import { BookChapterOneResolver } from './resolvers/bookChapterOne';
import { BookManyResolver } from './resolvers/bookManyResolver';
import { BookOneResolver } from './resolvers/bookOneResolver';
/* Mutation */ /* Mutation */
import { LoginResolver } from './resolvers/auth/login'; import { LoginResolver } from './resolvers/auth/login';
@ -13,11 +15,12 @@ import { LogoutResolver } from './resolvers/auth/logout';
import { SignUpResolver } from './resolvers/auth/signUp'; import { SignUpResolver } from './resolvers/auth/signUp';
export const Query: QueryResolvers = { export const Query: QueryResolvers = {
booksMany: BooksManyResolver,
bookOne: BookOneResolver, bookOne: BookOneResolver,
bookMany: BookManyResolver,
bookChapterOne: BookChapterOneResolver,
bookChapterMany: BookChapterManyResolver,
identity: IdentityResolver, identity: IdentityResolver,
userOne: UserOneResolver userOne: UserOneResolver
// workingMany:
}; };
export const Mutation: MutationResolvers = { export const Mutation: MutationResolvers = {

View file

@ -0,0 +1,25 @@
import { IdentityContext } from '@app/server/identity';
import { prisma } from '@app/server/prisma/client';
import { BookChapterManyOutput, QueryBookChapterManyArgs } from '@generated/types';
import { toBookChapterSchema } from './mappers/bookChapterMapper';
export async function BookChapterManyResolver(
_: any,
{ input }: QueryBookChapterManyArgs,
{ identity }: IdentityContext
): Promise<BookChapterManyOutput> {
const { bookId, filter } = input;
const items = await prisma.bookChapter.findMany({
where: {
bookId
},
skip: filter.offset ?? 0,
take: filter.size ?? 10
});
return {
items: await Promise.all(items.map(value => toBookChapterSchema(value))),
total: items.length
};
}

View file

@ -0,0 +1,17 @@
import { IdentityContext } from '@app/server/identity';
import { prisma } from '@app/server/prisma/client';
import { BookChapter, QueryBookChapterOneArgs } from '@generated/types';
import { toBookChapterSchema } from './mappers/bookChapterMapper';
export async function BookChapterOneResolver(
_: any,
{ id }: QueryBookChapterOneArgs,
{ identity }: IdentityContext
): Promise<BookChapter> {
const model = await prisma.bookChapter.findUniqueOrThrow({
where: {
id
}
});
return toBookChapterSchema(model);
}

View file

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

View file

@ -0,0 +1,17 @@
import { IdentityContext } from '@app/server/identity';
import { prisma } from '@app/server/prisma/client';
import { Book, QueryBookOneArgs } from '@generated/types';
import { toBookSchema } from './mappers/bookMapper';
export async function BookOneResolver(
_: any,
{ id }: QueryBookOneArgs,
{ identity }: IdentityContext
): Promise<Book> {
const model = await prisma.book.findUniqueOrThrow({
where: {
id
}
});
return toBookSchema(model);
}

View file

@ -3,7 +3,7 @@ import { prisma } from '@app/server/prisma/client';
import { BookChapter as BookChapterSchema } from '@generated/types'; import { BookChapter as BookChapterSchema } from '@generated/types';
import { BookChapter } from '@prisma/client'; import { BookChapter } from '@prisma/client';
export async function toBookChapterMapping(model: BookChapter): Promise<BookChapterSchema> { export async function toBookChapterSchema(model: BookChapter): Promise<BookChapterSchema> {
return { return {
id: model.id, id: model.id,
title: model.title, title: model.title,

View file

@ -1,14 +1,28 @@
import { assertUnreachable } from '@app/client/helpers';
import { call } from '@app/common'; import { call } from '@app/common';
import { prisma } from '@app/server/prisma/client'; import { prisma } from '@app/server/prisma/client';
import { Book as BookSchema } from '@generated/types'; import {
import { Book } from '@prisma/client'; Book as BookSchema,
WorkingProgressStatus as WorkingProgressStatusSchema
} from '@generated/types';
import { Book, WorkingProgressStatus } from '@prisma/client';
import { toUserSchema } from './userMapper';
export async function toBookSchema(model: Book): Promise<BookSchema> { export async function toBookSchema(model: Book): Promise<BookSchema> {
return { return {
id: model.id, id: model.id,
workingId: model.workingId, title: model.title,
description: model.description,
author: await call(async () => {
const userModel = await prisma.user.findUniqueOrThrow({ where: { id: model.authorId } });
return toUserSchema(userModel);
}),
createdAt: model.createdAt.toISOString(), createdAt: model.createdAt.toISOString(),
updatedAt: model.updatedAt.toISOString(), updatedAt: model.updatedAt.toISOString(),
progressStatus: call(() => {
ensureWorkingProgressStatus(model.progressStatus);
return model.progressStatus;
}),
wordsTotal: model.wordsTotal, wordsTotal: model.wordsTotal,
chaptersTotal: await call(async () => { chaptersTotal: await call(async () => {
const chaptersCount = await prisma.bookChapter.count({ const chaptersCount = await prisma.bookChapter.count({
@ -17,6 +31,32 @@ export async function toBookSchema(model: Book): Promise<BookSchema> {
} }
}); });
return chaptersCount; return chaptersCount;
}),
// todo: ratings implementation
ratingTotal: await call(async () => {
const ratingsCount = await prisma.rating.count({
where: {
bookId: model.id
}
});
return ratingsCount;
}),
reviewsTotal: await call(async () => {
const reviewsCount = await prisma.review.count({
where: {
bookId: model.id
}
});
return reviewsCount;
}) })
}; };
} }
function ensureWorkingProgressStatus(
value: WorkingProgressStatus
): asserts value is WorkingProgressStatusSchema {
if (value === WorkingProgressStatus.inProgress) return;
if (value === WorkingProgressStatus.frozen) return;
if (value === WorkingProgressStatus.complete) return;
assertUnreachable(value);
}

View file

@ -1,65 +0,0 @@
import { assertUnreachable } from '@app/client/helpers';
import { call } from '@app/common';
import { prisma } from '@app/server/prisma/client';
import {
WorkingProgressStatus as WorkingProgressStatusSchema,
Working as WorkingSchema,
WorkingType as WorkingTypeSchema
} from '@generated/types';
import { Working, WorkingProgressStatus, WorkingType } from '@prisma/client';
import { toUserSchema } from './userMapper';
export async function toWorkingSchema(model: Working): Promise<WorkingSchema> {
return {
id: model.id,
title: model.title,
description: model.description,
author: await call(async () => {
const userModel = await prisma.user.findUniqueOrThrow({ where: { id: model.authorId } });
return toUserSchema(userModel);
}),
createdAt: model.createdAt.toISOString(),
updatedAt: model.updatedAt.toISOString(),
type: call(() => {
ensureWorkingType(model.type);
return model.type;
}),
progressStatus: call(() => {
ensureWorkingProgressStatus(model.progressStatus);
return model.progressStatus;
}),
//todo: ratings implementation
//todo: book/notes implementation
ratingTotal: await call(async () => {
const ratingsCount = await prisma.rating.count({
where: {
workingId: model.id
}
});
return ratingsCount;
}),
reviewsTotal: await call(async () => {
const reviewsCount = await prisma.review.count({
where: {
workingId: model.id
}
});
return reviewsCount;
})
};
}
function ensureWorkingType(value: WorkingType): asserts value is WorkingTypeSchema {
if (value == WorkingType.book) return;
if (value == WorkingType.notepad) return;
assertUnreachable(value);
}
function ensureWorkingProgressStatus(
value: WorkingProgressStatus
): asserts value is WorkingProgressStatusSchema {
if (value == WorkingProgressStatus.inProgress) return;
if (value == WorkingProgressStatus.frozen) return;
if (value == WorkingProgressStatus.complete) return;
assertUnreachable(value);
}

View file

@ -3,7 +3,11 @@ scalar Void
type Query { type Query {
identity: Identity! identity: Identity!
userOne(id: ID!): User! userOne(id: ID!): User!
workingMany(input: WorkingManyInput!): WorkingManyOutput! bookOne(id: ID!): Book!
bookMany(input: BookManyInput!): BookManyOutput!
bookChapterOne(id: ID!): BookChapter!
bookChapterMany(input: BookChapterManyInput!): BookChapterManyOutput!
reviewMany(input: ReviewManyInput!): ReviewManyOutput!
} }
type Mutation { type Mutation {
@ -48,14 +52,8 @@ input PaginationFilterInput {
size: Int size: Int
} }
input WorkingManyInput { input BookManyInput {
filter: PaginationFilterInput! filter: PaginationFilterInput!
type: WorkingType!
}
enum WorkingType {
book
notepad
} }
enum WorkingProgressStatus { enum WorkingProgressStatus {
@ -64,33 +62,23 @@ enum WorkingProgressStatus {
complete complete
} }
type WorkingManyOutput { type BookManyOutput {
items: [Working!]! items: [Book!]!
total: Int! total: Int!
} }
type Working { type Book {
id: ID! id: ID!
title: String! title: String!
description: String! description: String!
author: User! author: User!
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
type: WorkingType!
book: Book
notepad: Notepad
progressStatus: WorkingProgressStatus! progressStatus: WorkingProgressStatus!
ratingTotal: Int!
reviewsTotal: Int!
}
type Book {
id: ID!
workingId: ID!
createdAt: String!
updatedAt: String!
wordsTotal: Int! wordsTotal: Int!
chaptersTotal: Int! chaptersTotal: Int!
ratingTotal: Int!
reviewsTotal: Int!
} }
type BookChapter { type BookChapter {
@ -104,6 +92,7 @@ type BookChapter {
} }
input BookChapterManyInput { input BookChapterManyInput {
bookId: String!
filter: PaginationFilterInput! filter: PaginationFilterInput!
} }
@ -112,10 +101,6 @@ type BookChapterManyOutput {
total: Int! total: Int!
} }
type Notepad {
id: ID!
}
type Review { type Review {
id: ID! id: ID!
author: User! author: User!
@ -124,6 +109,8 @@ type Review {
} }
input ReviewManyInput { input ReviewManyInput {
bookId: String
bookChapterId: String
filter: PaginationFilterInput! filter: PaginationFilterInput!
} }

View file

@ -21,37 +21,12 @@ model User {
isDarkMode Boolean @default(true) isDarkMode Boolean @default(true)
workings Working[] books Book[]
reviews Review[]
ratings Rating[]
}
model Working {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
description String @db.MediumText
progressStatus WorkingProgressStatus @default(inProgress)
type WorkingType
author User @relation(fields: [authorId], references: [id])
authorId String
book Book?
reviews Review[] reviews Review[]
ratings Rating[] ratings Rating[]
} }
enum WorkingType {
book
notepad
}
enum WorkingProgressStatus { enum WorkingProgressStatus {
inProgress inProgress
frozen frozen
@ -64,12 +39,18 @@ model Book {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
title String @db.VarChar(255)
description String @db.MediumText
progressStatus WorkingProgressStatus @default(inProgress)
author User @relation(fields: [authorId], references: [id])
authorId String
wordsTotal Int @default(0) wordsTotal Int @default(0)
working Working @relation(fields: [workingId], references: [id])
workingId String @unique @db.VarChar(255)
chapters BookChapter[] chapters BookChapter[]
reviews Review[]
ratings Rating[]
} }
model BookChapter { model BookChapter {
@ -82,6 +63,8 @@ model BookChapter {
foreword String @db.MediumText foreword String @db.MediumText
footnote String @db.MediumText footnote String @db.MediumText
wordsTotal Int @default(0)
book Book @relation(fields: [bookId], references: [id]) book Book @relation(fields: [bookId], references: [id])
bookId String @db.VarChar(255) bookId String @db.VarChar(255)
@ -100,11 +83,11 @@ model Review {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String @db.VarChar(255) userId String @db.VarChar(255)
working Working? @relation(fields: [workingId], references: [id])
workingId String? @db.VarChar(255)
bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id]) bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id])
bookChapterId String? @db.VarChar(255) bookChapterId String? @db.VarChar(255)
book Book? @relation(fields: [bookId], references: [id])
bookId String?
} }
enum RatingType { enum RatingType {
@ -123,9 +106,9 @@ model Rating {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String @db.VarChar(255) userId String @db.VarChar(255)
working Working? @relation(fields: [workingId], references: [id])
workingId String? @db.VarChar(255)
bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id]) bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id])
bookChapterId String? @db.VarChar(255) bookChapterId String? @db.VarChar(255)
book Book? @relation(fields: [bookId], references: [id])
bookId String?
} }