refactor, added slate, added editor, added book
This commit is contained in:
parent
bc250e73bb
commit
029b7e9353
33 changed files with 714 additions and 496 deletions
|
|
@ -1,15 +1,5 @@
|
|||
import {
|
||||
Link as ChakraLink,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
Kbd,
|
||||
Menu,
|
||||
Separator,
|
||||
Stack,
|
||||
Text
|
||||
} from '@chakra-ui/react';
|
||||
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router';
|
||||
import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
|
||||
import { createRootRouteWithContext, Outlet, Link } from '@tanstack/react-router';
|
||||
import React from 'react';
|
||||
import { Avatar } from '../components/ui/avatar';
|
||||
import { Button } from '../components/ui/button';
|
||||
|
|
@ -18,7 +8,6 @@ import { Tooltip } from '../components/ui/tooltip';
|
|||
import { useAuth } from '../hooks/auth';
|
||||
import { useIdentity } from '../hooks/identity';
|
||||
import { appName } from '../main';
|
||||
// import { Link as ChakraLink } from '../components/ui/link';
|
||||
|
||||
const TanStackRouterDevtools =
|
||||
process.env.NODE_ENV === 'production'
|
||||
|
|
@ -67,12 +56,28 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
<>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Link to="/editor">
|
||||
<ChakraLink>editor</ChakraLink>
|
||||
<Text
|
||||
color="colorPalette.400"
|
||||
_hover={{
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'colorPalette.700'
|
||||
}}
|
||||
>
|
||||
editor
|
||||
</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Link to="/new/book">
|
||||
<ChakraLink>create</ChakraLink>
|
||||
<Text
|
||||
color="colorPalette.400"
|
||||
_hover={{
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'colorPalette.700'
|
||||
}}
|
||||
>
|
||||
create
|
||||
</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
</>
|
||||
|
|
@ -99,10 +104,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
{identity != null ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger css={{ '&:hover': { cursor: 'pointer' } }}>
|
||||
<Avatar
|
||||
size="sm"
|
||||
// src="https://avatars.githubusercontent.com/u/40203084?v=4"
|
||||
/>
|
||||
<Avatar size="sm" />
|
||||
</Menu.Trigger>
|
||||
<Menu.Positioner mt={2}>
|
||||
<Menu.Content backgroundColor={'bg.subtle'}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
// `;
|
||||
|
|
@ -15,49 +15,3 @@ function RouteComponent() {
|
|||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// const workingData1: WorkingCardProps = {
|
||||
// id: '1',
|
||||
// title: 'Worth the Candle',
|
||||
// description: `A teenager struggling after the death of his best friend finds himself in a fantasy
|
||||
// world - one which seems to be an amalgamation of every Dungeons and Dragons campaign
|
||||
// they ever played together. Now he's stuck trying to find the answers to why he's there
|
||||
// and what this world is trying to say. The most terrifying answer might be that this
|
||||
// world is an expression of the person he was back on Earth.`,
|
||||
// author: {
|
||||
// id: '1',
|
||||
// createdAt: '12.8.2025',
|
||||
// updatedAt: '4.9.2025',
|
||||
// username: 'AlexanderWhales'
|
||||
// },
|
||||
// progressStatus: WorkingProgressStatus.Finished,
|
||||
// updatedAt: '20.10.2025',
|
||||
// chaptersTotal: 10,
|
||||
// wordsTotal: 34000,
|
||||
// ratingTotal: 10,
|
||||
// reviewsTotal: 3
|
||||
// };
|
||||
//
|
||||
// const workingData2: WorkingCardProps = {
|
||||
// id: '2',
|
||||
// title: 'Test',
|
||||
// description: `Description.`,
|
||||
// author: {
|
||||
// id: '1',
|
||||
// createdAt: '12.8.2025',
|
||||
// updatedAt: '4.9.2025',
|
||||
// username: 'JohnSmith'
|
||||
// },
|
||||
// progressStatus: WorkingProgressStatus.InProgress,
|
||||
// updatedAt: '20.10.2025',
|
||||
// chaptersTotal: 10,
|
||||
// wordsTotal: 1200,
|
||||
// ratingTotal: -10,
|
||||
// reviewsTotal: 0
|
||||
// };
|
||||
//
|
||||
// const mockWorkingData = {
|
||||
// total: 4,
|
||||
// items: [workingData1, workingData2, { ...workingData1, id: '3' }, { ...workingData2, id: '4' }]
|
||||
// };
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { gql } from '@apollo/client';
|
||||
import { useToastMutation } from '@app/client/hooks/useToastMutation';
|
||||
import { Heading, Input, Stack, Textarea } from '@chakra-ui/react';
|
||||
import { Button } from '@client/components/ui/button';
|
||||
import { Field } from '@client/components/ui/field';
|
||||
import { CreateNewBookMutation } from '@generated/types';
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as v from 'valibot';
|
||||
|
||||
|
|
@ -28,10 +31,16 @@ interface FormValues {
|
|||
}
|
||||
|
||||
function NewBookForm() {
|
||||
const navigate = useNavigate();
|
||||
const { update: bookCreate } = useToastMutation<CreateNewBookMutation>({
|
||||
mutation: bookCreateMutation,
|
||||
successMsg: 'Book created successfully!'
|
||||
});
|
||||
|
||||
const schema = v.pipe(
|
||||
v.object({
|
||||
title: v.pipe(v.string(), v.nonEmpty('Title is required')),
|
||||
description: v.pipe(v.string(), v.nonEmpty('Password is required'))
|
||||
title: v.pipe(v.string(), v.nonEmpty('Title is required'), v.maxLength(60)),
|
||||
description: v.pipe(v.string(), v.nonEmpty('Description is required'), v.maxLength(900))
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -42,7 +51,23 @@ function NewBookForm() {
|
|||
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
||||
|
||||
return (
|
||||
<form style={{ width: '80%' }} onSubmit={handleSubmit(async values => {})}>
|
||||
<form
|
||||
style={{ width: '80%', maxWidth: '840px' }}
|
||||
onSubmit={handleSubmit(async values => {
|
||||
const { data } = await bookCreate({
|
||||
variables: {
|
||||
input: {
|
||||
title: values.title,
|
||||
description: values.description
|
||||
}
|
||||
}
|
||||
});
|
||||
if (data != null)
|
||||
navigate({
|
||||
to: `/work/book/${data.bookCreate.id}`
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<Field label="Title" invalid={errors.title != null} errorText={errors.title?.message}>
|
||||
<Input {...register('title')} />
|
||||
|
|
@ -52,7 +77,7 @@ function NewBookForm() {
|
|||
invalid={errors.description != null}
|
||||
errorText={errors.description?.message}
|
||||
>
|
||||
<Textarea width="100%" lineHeight={4} {...register('description')} />
|
||||
<Textarea width="100%" height={'sm'} {...register('description')} />
|
||||
</Field>
|
||||
<Button type="submit">Create new book</Button>
|
||||
</Stack>
|
||||
|
|
@ -60,38 +85,12 @@ function NewBookForm() {
|
|||
);
|
||||
}
|
||||
|
||||
// function LoginForm() {
|
||||
// const schema = v.pipe(
|
||||
// v.object({
|
||||
// email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
|
||||
// password: v.pipe(v.string(), v.nonEmpty('Password is required'))
|
||||
// })
|
||||
// );
|
||||
//
|
||||
// const {
|
||||
// register,
|
||||
// handleSubmit,
|
||||
// formState: { errors }
|
||||
// } = useForm<FormValues>({ resolver: valibotResolver(schema) });
|
||||
//
|
||||
// const { login } = useAuth();
|
||||
//
|
||||
// return (
|
||||
// <form
|
||||
// >
|
||||
// <Stack>
|
||||
// <Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}>
|
||||
// <Input {...register('email')} />
|
||||
// </Field>
|
||||
// <Field
|
||||
// label="Password"
|
||||
// invalid={errors.password != null}
|
||||
// errorText={errors.password?.message}
|
||||
// >
|
||||
// <PasswordInput {...register('password')} />
|
||||
// </Field>
|
||||
// <Button type="submit">Sign in</Button>
|
||||
// </Stack>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
const bookCreateMutation = gql`
|
||||
mutation CreateNewBook($input: BookCreateInput!) {
|
||||
bookCreate(input: $input) {
|
||||
id
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
185
app/client/pages/work/book/$bookId.tsx
Normal file
185
app/client/pages/work/book/$bookId.tsx
Normal 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}
|
||||
`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue