added resolvers, updated model, updated navbar
This commit is contained in:
parent
a8375b99c2
commit
1d40d7e15f
23 changed files with 439 additions and 301 deletions
84
app/client/components/BookSearchList.tsx
Normal file
84
app/client/components/BookSearchList.tsx
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
27
app/client/components/IconBadge.tsx
Normal file
27
app/client/components/IconBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,28 @@
|
|||
import { List } from '@chakra-ui/react';
|
||||
import { Card, List, Text } from '@chakra-ui/react';
|
||||
import { WorkingCardProps, WorkingListCard } from './WorkingListCard';
|
||||
|
||||
export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) {
|
||||
return (
|
||||
<List.Root variant="plain" gap={4}>
|
||||
{workingsList.map(workingData => (
|
||||
{workingsList.length != 0 ? (
|
||||
workingsList.map(workingData => (
|
||||
<List.Item key={workingData.id}>
|
||||
<WorkingListCard {...workingData} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { ReactNode } from 'react';
|
||||
import { IconBadge } from './IconBadge';
|
||||
import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge';
|
||||
import { WorkingRatingBadge } from './WorkingRatingBadge';
|
||||
|
||||
|
|
@ -88,28 +88,3 @@ export function WorkingListCard({
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
import { Button, Icon, Input, InputGroup, Stack } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function WorkingListSearch({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (value: string) => void;
|
||||
}) {
|
||||
export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => void }) {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
return (
|
||||
<form
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit(value);
|
||||
onSubmit(searchValue);
|
||||
}}
|
||||
>
|
||||
<Stack direction="row">
|
||||
|
|
@ -26,8 +20,8 @@ export function WorkingListSearch({
|
|||
}
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={event => onChange(event.currentTarget.value)}
|
||||
value={searchValue}
|
||||
onChange={event => setSearchValue(event.currentTarget.value)}
|
||||
placeholder="Search works..."
|
||||
/>
|
||||
</InputGroup>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ export function WorkingProgressStatusBadge({
|
|||
function toWorkingProgressStatusIcon(value: WorkingProgressStatus): string {
|
||||
if (value === WorkingProgressStatus.InProgress) return 'nf nf-fa-clock';
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BadgeProps, Text } from '@chakra-ui/react';
|
||||
import { IconBadge } from './WorkingListCard';
|
||||
import { IconBadge } from './IconBadge';
|
||||
|
||||
interface WorkingRatingParams {
|
||||
rating: string;
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ export function assertUnreachable(value: never): never {
|
|||
export function toBgColorByProgressStatus(value: WorkingProgressStatus): string {
|
||||
if (value === WorkingProgressStatus.InProgress) return 'yellow.500';
|
||||
if (value === WorkingProgressStatus.Frozen) return 'blue.500';
|
||||
if (value === WorkingProgressStatus.Finished) return 'green.500';
|
||||
if (value === WorkingProgressStatus.Complete) return 'green.500';
|
||||
assertUnreachable(value);
|
||||
}
|
||||
|
||||
export function toFgColorByProgressStatus(value: WorkingProgressStatus): string {
|
||||
if (value === WorkingProgressStatus.InProgress) return 'fg.warning';
|
||||
if (value === WorkingProgressStatus.Frozen) return 'fg.info';
|
||||
if (value === WorkingProgressStatus.Finished) return 'fg.success';
|
||||
if (value === WorkingProgressStatus.Complete) return 'fg.success';
|
||||
assertUnreachable(value);
|
||||
}
|
||||
|
||||
export function toWorkingProgressStatusLabel(value: WorkingProgressStatus): string {
|
||||
if (value === WorkingProgressStatus.InProgress) return 'In progress';
|
||||
if (value === WorkingProgressStatus.Frozen) return 'Frozen';
|
||||
if (value === WorkingProgressStatus.Finished) return 'Finished';
|
||||
if (value === WorkingProgressStatus.Complete) return 'Complete';
|
||||
assertUnreachable(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 React from 'react';
|
||||
import { Avatar } from '../components/ui/avatar';
|
||||
|
|
@ -8,6 +18,7 @@ import { Tooltip } from '../components/ui/tooltip';
|
|||
import { useAuth } from '../hooks/auth';
|
||||
import { useIdentity } from '../hooks/identity';
|
||||
import { appName } from '../main';
|
||||
// import { Link as ChakraLink } from '../components/ui/link';
|
||||
|
||||
const TanStackRouterDevtools =
|
||||
process.env.NODE_ENV === 'production'
|
||||
|
|
@ -33,7 +44,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
return (
|
||||
<Stack padding={3}>
|
||||
<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="/">
|
||||
<Stack direction="row" justifyContent="center" alignItems="center" gap={1}>
|
||||
<Icon fontSize={24} color="colorPalette.500">
|
||||
|
|
@ -51,6 +62,21 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
{/* </Icon> */}
|
||||
{/* </Link> */}
|
||||
{/* </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 flexDirection="row" alignItems="center" gap={4}>
|
||||
|
|
|
|||
|
|
@ -1,105 +1,21 @@
|
|||
import { gql } from '@apollo/client';
|
||||
import { Skeleton, Stack } from '@chakra-ui/react';
|
||||
import { WorkingListQuery } from '@generated/types';
|
||||
import { Stack } from '@chakra-ui/react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { Pagination } from '../components/Pagination';
|
||||
import { WorkingList } from '../components/WorkingList';
|
||||
import { WorkingListSearch } from '../components/WorkingListSearch';
|
||||
import { usePaginatedQuery } from '../hooks/usePaginatedQuery';
|
||||
import { BookSearchList } from '../components/BookSearchList';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: 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 (
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<Stack width="90%">
|
||||
{data != null ? (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
<BookSearchList />
|
||||
</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 = {
|
||||
// id: '1',
|
||||
// title: 'Worth the Candle',
|
||||
|
|
|
|||
66
app/client/pages/new/book.tsx
Normal file
66
app/client/pages/new/book.tsx
Normal 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>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -15,11 +15,11 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||
import { Route as rootRoute } from './pages/__root'
|
||||
import { Route as RegisterImport } from './pages/register'
|
||||
import { Route as LoginImport } from './pages/login'
|
||||
import { Route as ExampleImport } from './pages/example'
|
||||
import { Route as EditorImport } from './pages/editor'
|
||||
import { Route as AboutImport } from './pages/about'
|
||||
import { Route as IndexImport } from './pages/index'
|
||||
import { Route as ProfileUsernameImport } from './pages/profile/$username'
|
||||
import { Route as NewBookImport } from './pages/new/book'
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
|
|
@ -45,12 +45,6 @@ const LoginRoute = LoginImport.update({
|
|||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ExampleRoute = ExampleImport.update({
|
||||
id: '/example',
|
||||
path: '/example',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const EditorRoute = EditorImport.update({
|
||||
id: '/editor',
|
||||
path: '/editor',
|
||||
|
|
@ -75,6 +69,12 @@ const ProfileUsernameRoute = ProfileUsernameImport.update({
|
|||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const NewBookRoute = NewBookImport.update({
|
||||
id: '/new/book',
|
||||
path: '/new/book',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -100,13 +100,6 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof EditorImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/example': {
|
||||
id: '/example'
|
||||
path: '/example'
|
||||
fullPath: '/example'
|
||||
preLoaderRoute: typeof ExampleImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
|
|
@ -128,6 +121,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof BookLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/new/book': {
|
||||
id: '/new/book'
|
||||
path: '/new/book'
|
||||
fullPath: '/new/book'
|
||||
preLoaderRoute: typeof NewBookImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/profile/$username': {
|
||||
id: '/profile/$username'
|
||||
path: '/profile/$username'
|
||||
|
|
@ -144,10 +144,10 @@ export interface FileRoutesByFullPath {
|
|||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/editor': typeof EditorRoute
|
||||
'/example': typeof ExampleRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/book': typeof BookLazyRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +155,10 @@ export interface FileRoutesByTo {
|
|||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/editor': typeof EditorRoute
|
||||
'/example': typeof ExampleRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/book': typeof BookLazyRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
}
|
||||
|
||||
|
|
@ -167,10 +167,10 @@ export interface FileRoutesById {
|
|||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/editor': typeof EditorRoute
|
||||
'/example': typeof ExampleRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/register': typeof RegisterRoute
|
||||
'/book': typeof BookLazyRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
}
|
||||
|
||||
|
|
@ -180,30 +180,30 @@ export interface FileRouteTypes {
|
|||
| '/'
|
||||
| '/about'
|
||||
| '/editor'
|
||||
| '/example'
|
||||
| '/login'
|
||||
| '/register'
|
||||
| '/book'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/editor'
|
||||
| '/example'
|
||||
| '/login'
|
||||
| '/register'
|
||||
| '/book'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/about'
|
||||
| '/editor'
|
||||
| '/example'
|
||||
| '/login'
|
||||
| '/register'
|
||||
| '/book'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
|
@ -212,10 +212,10 @@ export interface RootRouteChildren {
|
|||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
EditorRoute: typeof EditorRoute
|
||||
ExampleRoute: typeof ExampleRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
RegisterRoute: typeof RegisterRoute
|
||||
BookLazyRoute: typeof BookLazyRoute
|
||||
NewBookRoute: typeof NewBookRoute
|
||||
ProfileUsernameRoute: typeof ProfileUsernameRoute
|
||||
}
|
||||
|
||||
|
|
@ -223,10 +223,10 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
EditorRoute: EditorRoute,
|
||||
ExampleRoute: ExampleRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
RegisterRoute: RegisterRoute,
|
||||
BookLazyRoute: BookLazyRoute,
|
||||
NewBookRoute: NewBookRoute,
|
||||
ProfileUsernameRoute: ProfileUsernameRoute,
|
||||
}
|
||||
|
||||
|
|
@ -243,10 +243,10 @@ export const routeTree = rootRoute
|
|||
"/",
|
||||
"/about",
|
||||
"/editor",
|
||||
"/example",
|
||||
"/login",
|
||||
"/register",
|
||||
"/book",
|
||||
"/new/book",
|
||||
"/profile/$username"
|
||||
]
|
||||
},
|
||||
|
|
@ -259,9 +259,6 @@ export const routeTree = rootRoute
|
|||
"/editor": {
|
||||
"filePath": "editor.tsx"
|
||||
},
|
||||
"/example": {
|
||||
"filePath": "example.tsx"
|
||||
},
|
||||
"/login": {
|
||||
"filePath": "login.tsx"
|
||||
},
|
||||
|
|
@ -271,6 +268,9 @@ export const routeTree = rootRoute
|
|||
"/book": {
|
||||
"filePath": "book.lazy.tsx"
|
||||
},
|
||||
"/new/book": {
|
||||
"filePath": "new/book.tsx"
|
||||
},
|
||||
"/profile/$username": {
|
||||
"filePath": "profile/$username.tsx"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
import { MutationResolvers, QueryResolvers } from '@generated/types';
|
||||
|
||||
/* Query */
|
||||
import { BookOneResolver } from './resolvers/bookOneResolver';
|
||||
import { BooksManyResolver } from './resolvers/booksManyResolver';
|
||||
import { IdentityResolver } from './resolvers/identityResolver';
|
||||
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 */
|
||||
import { LoginResolver } from './resolvers/auth/login';
|
||||
|
|
@ -13,11 +15,12 @@ import { LogoutResolver } from './resolvers/auth/logout';
|
|||
import { SignUpResolver } from './resolvers/auth/signUp';
|
||||
|
||||
export const Query: QueryResolvers = {
|
||||
booksMany: BooksManyResolver,
|
||||
bookOne: BookOneResolver,
|
||||
bookMany: BookManyResolver,
|
||||
bookChapterOne: BookChapterOneResolver,
|
||||
bookChapterMany: BookChapterManyResolver,
|
||||
identity: IdentityResolver,
|
||||
userOne: UserOneResolver
|
||||
// workingMany:
|
||||
};
|
||||
|
||||
export const Mutation: MutationResolvers = {
|
||||
|
|
|
|||
25
app/server/graphql/resolvers/bookChapterMany.ts
Normal file
25
app/server/graphql/resolvers/bookChapterMany.ts
Normal 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
|
||||
};
|
||||
}
|
||||
17
app/server/graphql/resolvers/bookChapterOne.ts
Normal file
17
app/server/graphql/resolvers/bookChapterOne.ts
Normal 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);
|
||||
}
|
||||
29
app/server/graphql/resolvers/bookManyResolver.ts
Normal file
29
app/server/graphql/resolvers/bookManyResolver.ts
Normal 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
|
||||
};
|
||||
}
|
||||
17
app/server/graphql/resolvers/bookOneResolver.ts
Normal file
17
app/server/graphql/resolvers/bookOneResolver.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { prisma } from '@app/server/prisma/client';
|
|||
import { BookChapter as BookChapterSchema } from '@generated/types';
|
||||
import { BookChapter } from '@prisma/client';
|
||||
|
||||
export async function toBookChapterMapping(model: BookChapter): Promise<BookChapterSchema> {
|
||||
export async function toBookChapterSchema(model: BookChapter): Promise<BookChapterSchema> {
|
||||
return {
|
||||
id: model.id,
|
||||
title: model.title,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,28 @@
|
|||
import { assertUnreachable } from '@app/client/helpers';
|
||||
import { call } from '@app/common';
|
||||
import { prisma } from '@app/server/prisma/client';
|
||||
import { Book as BookSchema } from '@generated/types';
|
||||
import { Book } from '@prisma/client';
|
||||
import {
|
||||
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> {
|
||||
return {
|
||||
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(),
|
||||
updatedAt: model.updatedAt.toISOString(),
|
||||
progressStatus: call(() => {
|
||||
ensureWorkingProgressStatus(model.progressStatus);
|
||||
return model.progressStatus;
|
||||
}),
|
||||
wordsTotal: model.wordsTotal,
|
||||
chaptersTotal: await call(async () => {
|
||||
const chaptersCount = await prisma.bookChapter.count({
|
||||
|
|
@ -17,6 +31,32 @@ export async function toBookSchema(model: Book): Promise<BookSchema> {
|
|||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -3,7 +3,11 @@ scalar Void
|
|||
type Query {
|
||||
identity: Identity!
|
||||
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 {
|
||||
|
|
@ -48,14 +52,8 @@ input PaginationFilterInput {
|
|||
size: Int
|
||||
}
|
||||
|
||||
input WorkingManyInput {
|
||||
input BookManyInput {
|
||||
filter: PaginationFilterInput!
|
||||
type: WorkingType!
|
||||
}
|
||||
|
||||
enum WorkingType {
|
||||
book
|
||||
notepad
|
||||
}
|
||||
|
||||
enum WorkingProgressStatus {
|
||||
|
|
@ -64,33 +62,23 @@ enum WorkingProgressStatus {
|
|||
complete
|
||||
}
|
||||
|
||||
type WorkingManyOutput {
|
||||
items: [Working!]!
|
||||
type BookManyOutput {
|
||||
items: [Book!]!
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type Working {
|
||||
type Book {
|
||||
id: ID!
|
||||
title: String!
|
||||
description: String!
|
||||
author: User!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
type: WorkingType!
|
||||
book: Book
|
||||
notepad: Notepad
|
||||
progressStatus: WorkingProgressStatus!
|
||||
ratingTotal: Int!
|
||||
reviewsTotal: Int!
|
||||
}
|
||||
|
||||
type Book {
|
||||
id: ID!
|
||||
workingId: ID!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
wordsTotal: Int!
|
||||
chaptersTotal: Int!
|
||||
ratingTotal: Int!
|
||||
reviewsTotal: Int!
|
||||
}
|
||||
|
||||
type BookChapter {
|
||||
|
|
@ -104,6 +92,7 @@ type BookChapter {
|
|||
}
|
||||
|
||||
input BookChapterManyInput {
|
||||
bookId: String!
|
||||
filter: PaginationFilterInput!
|
||||
}
|
||||
|
||||
|
|
@ -112,10 +101,6 @@ type BookChapterManyOutput {
|
|||
total: Int!
|
||||
}
|
||||
|
||||
type Notepad {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Review {
|
||||
id: ID!
|
||||
author: User!
|
||||
|
|
@ -124,6 +109,8 @@ type Review {
|
|||
}
|
||||
|
||||
input ReviewManyInput {
|
||||
bookId: String
|
||||
bookChapterId: String
|
||||
filter: PaginationFilterInput!
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,37 +21,12 @@ model User {
|
|||
|
||||
isDarkMode Boolean @default(true)
|
||||
|
||||
workings Working[]
|
||||
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?
|
||||
books Book[]
|
||||
|
||||
reviews Review[]
|
||||
ratings Rating[]
|
||||
}
|
||||
|
||||
enum WorkingType {
|
||||
book
|
||||
notepad
|
||||
}
|
||||
|
||||
enum WorkingProgressStatus {
|
||||
inProgress
|
||||
frozen
|
||||
|
|
@ -64,12 +39,18 @@ model Book {
|
|||
createdAt DateTime @default(now())
|
||||
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)
|
||||
|
||||
working Working @relation(fields: [workingId], references: [id])
|
||||
workingId String @unique @db.VarChar(255)
|
||||
|
||||
chapters BookChapter[]
|
||||
|
||||
reviews Review[]
|
||||
ratings Rating[]
|
||||
}
|
||||
|
||||
model BookChapter {
|
||||
|
|
@ -82,6 +63,8 @@ model BookChapter {
|
|||
foreword String @db.MediumText
|
||||
footnote String @db.MediumText
|
||||
|
||||
wordsTotal Int @default(0)
|
||||
|
||||
book Book @relation(fields: [bookId], references: [id])
|
||||
bookId String @db.VarChar(255)
|
||||
|
||||
|
|
@ -100,11 +83,11 @@ model Review {
|
|||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.VarChar(255)
|
||||
|
||||
working Working? @relation(fields: [workingId], references: [id])
|
||||
workingId String? @db.VarChar(255)
|
||||
|
||||
bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id])
|
||||
bookChapterId String? @db.VarChar(255)
|
||||
|
||||
book Book? @relation(fields: [bookId], references: [id])
|
||||
bookId String?
|
||||
}
|
||||
|
||||
enum RatingType {
|
||||
|
|
@ -123,9 +106,9 @@ model Rating {
|
|||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.VarChar(255)
|
||||
|
||||
working Working? @relation(fields: [workingId], references: [id])
|
||||
workingId String? @db.VarChar(255)
|
||||
|
||||
bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id])
|
||||
bookChapterId String? @db.VarChar(255)
|
||||
|
||||
book Book? @relation(fields: [bookId], references: [id])
|
||||
bookId String?
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue