added chapter view, fixes, added ratings, added chapter updates/deletions
This commit is contained in:
parent
029b7e9353
commit
4801cf3a13
34 changed files with 1345 additions and 191 deletions
97
app/client/components/BookChapterCard.tsx
Normal file
97
app/client/components/BookChapterCard.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { gql } from '@apollo/client';
|
||||
import { Box, Button, Card, Heading, Icon, Stack, Text } from '@chakra-ui/react';
|
||||
import { BookChapterDeleteMutation } from '@generated/types';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { bookFragment } from '../fragments';
|
||||
import { useToastMutation } from '../hooks/useToastMutation';
|
||||
import { bookChapterListQuery } from '../pages/work/book/$bookId/overview';
|
||||
|
||||
export function BookChapterCard({
|
||||
bookId,
|
||||
chapterId,
|
||||
title,
|
||||
isAuthor
|
||||
}: {
|
||||
readonly bookId: string;
|
||||
readonly chapterId: string;
|
||||
readonly title: string;
|
||||
readonly isAuthor: boolean;
|
||||
}) {
|
||||
const { update: chapterDelete } = useToastMutation<BookChapterDeleteMutation>({
|
||||
mutation,
|
||||
successMsg: 'Chapter deleted successfully',
|
||||
refetchQueries: [bookChapterListQuery]
|
||||
});
|
||||
return (
|
||||
<Card.Root width="100%" variant="elevated">
|
||||
<Card.Body paddingX={4} paddingY={2}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
<Text as="div" minWidth={0}>
|
||||
<Link to={`/work/book/${bookId}/chapter/${chapterId}/view`}>
|
||||
<Heading
|
||||
size="sm"
|
||||
color="colorPalette.400"
|
||||
_hover={{
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'colorPalette.700'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Button
|
||||
color="red.500"
|
||||
variant="ghost"
|
||||
padding={0}
|
||||
_hover={{
|
||||
backgroundColor: 'bg.error'
|
||||
}}
|
||||
onClick={async () => chapterDelete({ variables: { id: chapterId } })}
|
||||
>
|
||||
<Icon fontSize="xl">
|
||||
<i className="nf nf-md-delete" />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Link to={`/work/book/${bookId}/chapter/${chapterId}/edit`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
padding={0}
|
||||
_hover={{
|
||||
backgroundColor: 'bg.warning'
|
||||
}}
|
||||
>
|
||||
<Icon color="yellow.500" fontSize="xl">
|
||||
<i className="nf nf-fa-edit" />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link to={`/work/book/${bookId}/chapter/${chapterId}/view`}>
|
||||
<Button variant="ghost" padding={0}>
|
||||
<Icon fontSize="xl">
|
||||
<i className="nf nf-fa-angle_right" />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
|
||||
const mutation = gql`
|
||||
mutation BookChapterDelete($id: ID!) {
|
||||
bookChapterDelete(id: $id) {
|
||||
...Book
|
||||
}
|
||||
}
|
||||
${bookFragment}
|
||||
`;
|
||||
121
app/client/components/BookChapterForm.tsx
Normal file
121
app/client/components/BookChapterForm.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { Button } from '@app/client/components/ui/button';
|
||||
import { Box, Input, Stack, Textarea } from '@chakra-ui/react';
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Descendant } from 'slate';
|
||||
import * as v from 'valibot';
|
||||
import { EditorSlate } from './EditorSlate';
|
||||
import { Field } from './ui/field';
|
||||
|
||||
interface BookChapterEditInitialValues {
|
||||
readonly title: string;
|
||||
readonly foreword?: string;
|
||||
readonly footnote?: string;
|
||||
readonly content?: Descendant[];
|
||||
}
|
||||
|
||||
interface BookChapterInput {
|
||||
readonly title: string;
|
||||
readonly foreword?: string;
|
||||
readonly footnote?: string;
|
||||
readonly content: Descendant[];
|
||||
}
|
||||
|
||||
interface BookChapterFormValues {
|
||||
readonly title: string;
|
||||
readonly foreword: string;
|
||||
readonly footnote: string;
|
||||
}
|
||||
|
||||
export function BookChapterForm({
|
||||
bookId,
|
||||
initialValues,
|
||||
onChapterSubmit
|
||||
}: {
|
||||
readonly bookId: string;
|
||||
readonly initialValues?: BookChapterEditInitialValues | null;
|
||||
readonly onChapterSubmit: (chapter: BookChapterInput) => Promise<void>;
|
||||
}) {
|
||||
const [content, setContent] = useState<Descendant[]>([]);
|
||||
|
||||
const schema = v.pipe(
|
||||
v.object({
|
||||
title: v.pipe(v.string(), v.nonEmpty('Title is required'), v.maxLength(60)),
|
||||
foreword: v.pipe(v.string()),
|
||||
footnote: v.pipe(v.string())
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<BookChapterFormValues>({
|
||||
defaultValues: {
|
||||
title: initialValues?.title,
|
||||
foreword: initialValues?.foreword,
|
||||
footnote: initialValues?.footnote
|
||||
},
|
||||
resolver: valibotResolver(schema)
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async values =>
|
||||
onChapterSubmit({
|
||||
title: values.title,
|
||||
foreword: values.foreword,
|
||||
footnote: values.footnote,
|
||||
content
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack justifyContent="center" alignItems="center" minHeight="88vh">
|
||||
<Stack maxWidth="840px" minHeight="80vh" justifyContent="center" alignItems="center">
|
||||
<Stack width="100%" direction="row" justifyContent="space-between" alignItems="start">
|
||||
<Box>
|
||||
<Field invalid={errors.title != null} errorText={errors.title?.message}>
|
||||
<Input {...register('title')} placeholder="Chapter title" />
|
||||
</Field>
|
||||
</Box>
|
||||
<Stack direction="row">
|
||||
<Button onClick={async () => onSubmit()}>Submit</Button>
|
||||
<Link to={`/work/book/${bookId}/overview`}>
|
||||
<Button>Go back</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Field invalid={errors.foreword != null} errorText={errors.foreword?.message}>
|
||||
<Textarea
|
||||
color="fg.muted"
|
||||
variant="subtle"
|
||||
placeholder="Foreword"
|
||||
resize="none"
|
||||
maxLength={500}
|
||||
{...register('foreword')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<EditorSlate
|
||||
initialValue={initialValues?.content}
|
||||
onChange={values => setContent(values)}
|
||||
/>
|
||||
|
||||
<Field invalid={errors.footnote != null} errorText={errors.footnote?.message}>
|
||||
<Textarea
|
||||
color="fg.muted"
|
||||
variant="subtle"
|
||||
placeholder="Footnote"
|
||||
resize="none"
|
||||
maxLength={500}
|
||||
{...register('footnote')}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,11 +12,11 @@ import { PaginatedListSkeleton } from './PaginatedListSkeleton';
|
|||
export function BookSearchList() {
|
||||
const [workingSearchValue, setWorkingSearchValue] = useState<string>('');
|
||||
const [workingsListPage, setWorkingsListPage] = useState<number>(1);
|
||||
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(10);
|
||||
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(8);
|
||||
const startIndex = (workingsListPage - 1) * workingsListPageSize;
|
||||
|
||||
const { data } = usePaginatedQuery<BooksListQuery>({
|
||||
query,
|
||||
query: bookListQuery,
|
||||
input: {
|
||||
filter: {
|
||||
terms: workingSearchValue,
|
||||
|
|
@ -51,7 +51,7 @@ export function BookSearchList() {
|
|||
);
|
||||
}
|
||||
|
||||
const query = gql`
|
||||
export const bookListQuery = gql`
|
||||
query BooksList($input: BookManyInput!) {
|
||||
bookMany(input: $input) {
|
||||
items {
|
||||
|
|
|
|||
|
|
@ -20,21 +20,25 @@ import {
|
|||
} from '../types';
|
||||
import { Prose } from './ui/prose';
|
||||
|
||||
export function EditorSlate() {
|
||||
export function EditorSlate({
|
||||
initialValue,
|
||||
onChange,
|
||||
readOnly
|
||||
}: {
|
||||
readonly initialValue?: Descendant[];
|
||||
readonly onChange?: (values: Descendant[]) => void;
|
||||
readonly readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Card.Root
|
||||
variant={'subtle'}
|
||||
width="100%"
|
||||
maxWidth="840px"
|
||||
minHeight="100%"
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
background={readOnly ? 'none' : undefined}
|
||||
>
|
||||
{/* <Card.Header>
|
||||
<Heading textAlign="center">Editor</Heading>
|
||||
</Card.Header> */}
|
||||
<Card.Body
|
||||
width="100%"
|
||||
css={{
|
||||
|
|
@ -44,7 +48,20 @@ export function EditorSlate() {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<EditorViewport />
|
||||
<EditorViewport
|
||||
initialValue={
|
||||
initialValue ?? [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
onChange={values => {
|
||||
if (onChange != null) onChange(values);
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</>
|
||||
|
|
@ -65,91 +82,63 @@ type AlignType = (typeof TEXT_ALIGN_TYPES)[number];
|
|||
type ListType = (typeof LIST_TYPES)[number];
|
||||
type CustomElementFormat = CustomElementType | AlignType | ListType;
|
||||
|
||||
function EditorViewport() {
|
||||
function EditorViewport({
|
||||
initialValue,
|
||||
onChange,
|
||||
readOnly
|
||||
}: {
|
||||
readonly initialValue: Descendant[];
|
||||
readonly onChange: (values: Descendant[]) => void;
|
||||
readonly readOnly?: boolean;
|
||||
}) {
|
||||
const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []);
|
||||
const renderLeaf = useCallback((props: RenderLeafProps) => <Leaf {...props} />, []);
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
|
||||
|
||||
const initialValue: Descendant[] = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'This is editable ' },
|
||||
{ text: 'rich', bold: true },
|
||||
{ text: ' text, ' },
|
||||
{ text: 'much', italic: true },
|
||||
{ text: ' better than a ' },
|
||||
{ text: '<textarea>', code: true },
|
||||
{ text: '!' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: "Since it's rich text, you can do things like turn a selection of text "
|
||||
},
|
||||
{ text: 'bold', bold: true },
|
||||
{
|
||||
text: ', or add a semantically rendered block quote in the middle of the page, like this:'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'block-quote',
|
||||
children: [{ text: 'A wise quote.' }]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
align: 'center',
|
||||
children: [{ text: 'Try it out for yourself!' }]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Wrap justifyContent="center" alignItems="center" gap={1}>
|
||||
<MarkButton format="bold" icon="nf nf-oct-bold" />
|
||||
<MarkButton format="italic" icon="nf nf-oct-italic" />
|
||||
<MarkButton format="underline" icon="nf nf-md-format_underline" />
|
||||
<MarkButton format="code" icon="nf nf-oct-code" />
|
||||
<BlockButton format="heading-one" icon="nf nf-fa-heading" text="1" />
|
||||
<BlockButton format="heading-two" icon="nf nf-fa-heading" text="2" />
|
||||
<BlockButton format="block-quote" icon="nf nf-oct-quote" />
|
||||
<BlockButton format="numbered-list" icon="nf nf-fa-list_ol" />
|
||||
<BlockButton format="bulleted-list" icon="nf nf-fa-list_ul" />
|
||||
<BlockButton format="left" icon="nf nf-fa-align_left" />
|
||||
<BlockButton format="center" icon="nf nf-fa-align_center" />
|
||||
<BlockButton format="right" icon="nf nf-fa-align_right" />
|
||||
<BlockButton format="justify" icon="nf nf-fa-align_justify" />
|
||||
</Wrap>
|
||||
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
|
||||
{!readOnly && (
|
||||
<Wrap justifyContent="center" alignItems="center" gap={1}>
|
||||
<MarkButton format="bold" icon="nf nf-oct-bold" />
|
||||
<MarkButton format="italic" icon="nf nf-oct-italic" />
|
||||
<MarkButton format="underline" icon="nf nf-md-format_underline" />
|
||||
<MarkButton format="code" icon="nf nf-oct-code" />
|
||||
<BlockButton format="heading-one" icon="nf nf-fa-heading" text="1" />
|
||||
<BlockButton format="heading-two" icon="nf nf-fa-heading" text="2" />
|
||||
<BlockButton format="block-quote" icon="nf nf-oct-quote" />
|
||||
<BlockButton format="numbered-list" icon="nf nf-fa-list_ol" />
|
||||
<BlockButton format="bulleted-list" icon="nf nf-fa-list_ul" />
|
||||
<BlockButton format="left" icon="nf nf-fa-align_left" />
|
||||
<BlockButton format="center" icon="nf nf-fa-align_center" />
|
||||
<BlockButton format="right" icon="nf nf-fa-align_right" />
|
||||
<BlockButton format="justify" icon="nf nf-fa-align_justify" />
|
||||
</Wrap>
|
||||
)}
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth="100%"
|
||||
height="100%"
|
||||
paddingX={3}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
>
|
||||
<Prose
|
||||
width="100%"
|
||||
minWidth="100%"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Prose width="100%" minWidth="100%" height="100%" display="flex" flex={1}>
|
||||
<Editable
|
||||
className="slate-editor"
|
||||
style={{
|
||||
outline: 'none',
|
||||
minWidth: '100%',
|
||||
maxWidth: 'unset',
|
||||
maxHeight: '75vh',
|
||||
maxHeight: readOnly ? undefined : '60vh',
|
||||
height: '100%',
|
||||
padding: 0,
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
readOnly={readOnly}
|
||||
placeholder=""
|
||||
spellCheck
|
||||
autoFocus
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Badge, Box, Card, Heading, Stack, Text } from '@chakra-ui/react';
|
|||
import { Link } from '@tanstack/react-router';
|
||||
import { toFormattedDate } from '../helpers';
|
||||
import { BookOne } from '../interfaces';
|
||||
import { IconBadge } from './IconBadge';
|
||||
import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge';
|
||||
import { WorkingRatingBadge } from './WorkingRatingBadge';
|
||||
import { WorkingReviewsBadge } from './WorkingReviewsTotalBadge';
|
||||
|
|
@ -25,7 +24,7 @@ export function WorkingListCard({
|
|||
<Stack>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Text as="div" minWidth={0}>
|
||||
<Link to={`/work/book/${id}`}>
|
||||
<Link to={`/work/book/${id}/overview`}>
|
||||
<Heading
|
||||
color="colorPalette.400"
|
||||
_hover={{
|
||||
|
|
|
|||
|
|
@ -3,15 +3,21 @@ import {
|
|||
DefaultContext,
|
||||
DocumentNode,
|
||||
FetchResult,
|
||||
InternalRefetchQueriesInclude,
|
||||
MaybeMasked,
|
||||
MutationFunctionOptions,
|
||||
OperationVariables,
|
||||
Unmasked,
|
||||
useMutation
|
||||
} from '@apollo/client';
|
||||
import { toaster } from '../components/ui/toaster';
|
||||
|
||||
interface UseToastMutationProps {
|
||||
interface UseToastMutationProps<T> {
|
||||
mutation: DocumentNode;
|
||||
refetchQueries?:
|
||||
| InternalRefetchQueriesInclude
|
||||
| ((result: FetchResult<Unmasked<NoInfer<T>>>) => InternalRefetchQueriesInclude)
|
||||
| undefined;
|
||||
successMsg?: string;
|
||||
}
|
||||
|
||||
|
|
@ -23,13 +29,14 @@ interface UseToastMutationReturn<T> {
|
|||
) => Promise<FetchResult<MaybeMasked<T>>>;
|
||||
}
|
||||
|
||||
export function useToastMutation<T>(props: UseToastMutationProps): UseToastMutationReturn<T> {
|
||||
export function useToastMutation<T>(props: UseToastMutationProps<T>): UseToastMutationReturn<T> {
|
||||
const [update] = useMutation<T>(props.mutation, {
|
||||
onCompleted: () => toaster.create({ description: props.successMsg, type: 'success' }),
|
||||
onError: error => {
|
||||
toaster.create({ description: `${error.message}`, type: 'error' });
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
refetchQueries: props.refetchQueries
|
||||
});
|
||||
|
||||
return { update };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
|
||||
import { createRootRouteWithContext, Outlet, Link } from '@tanstack/react-router';
|
||||
import { Icon, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
|
||||
import { createRootRouteWithContext, Link, Outlet, useNavigate } from '@tanstack/react-router';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import React from 'react';
|
||||
import { Avatar } from '../components/ui/avatar';
|
||||
import { Button } from '../components/ui/button';
|
||||
|
|
@ -29,9 +30,17 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
component: () => {
|
||||
const { logout } = useAuth();
|
||||
const identity = useIdentity();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack padding={3}>
|
||||
<Stack
|
||||
onKeyDown={e => {
|
||||
if (isHotkey('mod+k', e)) {
|
||||
navigate({ to: '/' });
|
||||
}
|
||||
}}
|
||||
padding={3}
|
||||
>
|
||||
<Stack flexDirection="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack flexDirection="row" justifyContent="center" alignItems="center" gap={10}>
|
||||
<Link to="/">
|
||||
|
|
@ -44,6 +53,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
</Text>
|
||||
</Stack>
|
||||
</Link>
|
||||
|
||||
{/* <Stack hideBelow="md"> */}
|
||||
{/* <Link to="/about"> */}
|
||||
{/* <Icon color="colorPalette.500" fontSize="md"> */}
|
||||
|
|
@ -85,19 +95,29 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||
</Stack>
|
||||
|
||||
<Stack flexDirection="row" alignItems="center" gap={4}>
|
||||
<InputGroup
|
||||
hideBelow="md"
|
||||
flex={1}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
startElement={
|
||||
<Icon color="colorPalette.500">
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
gap={6}
|
||||
border="sm"
|
||||
borderColor="border"
|
||||
borderRadius="md"
|
||||
onClick={() => navigate({ to: '/' })}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Icon size="sm" color="colorPalette.500">
|
||||
<i className="nf nf-oct-search" />
|
||||
</Icon>
|
||||
}
|
||||
endElement={<Kbd>⌘K</Kbd>}
|
||||
>
|
||||
<Input height={8} placeholder="Search works..." />
|
||||
</InputGroup>
|
||||
|
||||
<Text fontSize="sm" color="fg.subtle">
|
||||
Search works...{' '}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Kbd>⌘K</Kbd>
|
||||
</Stack>
|
||||
|
||||
<ColorModeButton />
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ export const Route = createFileRoute('/editor')({
|
|||
function RouteComponent() {
|
||||
return (
|
||||
<Stack justifyContent="center" alignItems="center" minHeight="88vh">
|
||||
<EditorSlate />
|
||||
<Stack maxWidth="840px" justifyContent="center" alignItems="center" minHeight="88vh">
|
||||
<EditorSlate />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { gql } from '@apollo/client';
|
||||
import { bookListQuery } from '@app/client/components/BookSearchList';
|
||||
import { useToastMutation } from '@app/client/hooks/useToastMutation';
|
||||
import { Heading, Input, Stack, Textarea } from '@chakra-ui/react';
|
||||
import { Button } from '@client/components/ui/button';
|
||||
|
|
@ -34,7 +35,8 @@ function NewBookForm() {
|
|||
const navigate = useNavigate();
|
||||
const { update: bookCreate } = useToastMutation<CreateNewBookMutation>({
|
||||
mutation: bookCreateMutation,
|
||||
successMsg: 'Book created successfully!'
|
||||
successMsg: 'Book created successfully!',
|
||||
refetchQueries: [bookListQuery]
|
||||
});
|
||||
|
||||
const schema = v.pipe(
|
||||
|
|
@ -64,7 +66,7 @@ function NewBookForm() {
|
|||
});
|
||||
if (data != null)
|
||||
navigate({
|
||||
to: `/work/book/${data.bookCreate.id}`
|
||||
to: `/work/book/${data.bookCreate.id}/overview`
|
||||
});
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Heading, Input, Stack, Text } from '@chakra-ui/react';
|
|||
import { Button } from '@client/components/ui/button';
|
||||
import { Field } from '@client/components/ui/field';
|
||||
import { PasswordInput } from '@client/components/ui/password-input';
|
||||
import { Session } from '@generated/types';
|
||||
import { AuthSignUpMutation } from '@generated/types';
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot';
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
|
@ -42,7 +42,7 @@ interface FormValues {
|
|||
|
||||
function SignUpForm() {
|
||||
const navigate = useNavigate();
|
||||
const { update: signUp } = useToastMutation<Session>({
|
||||
const { update: signUp } = useToastMutation<AuthSignUpMutation>({
|
||||
mutation,
|
||||
successMsg: 'Account created successfully'
|
||||
});
|
||||
|
|
|
|||
104
app/client/pages/work/book/$bookId/chapter/$chapterId/edit.tsx
Normal file
104
app/client/pages/work/book/$bookId/chapter/$chapterId/edit.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { gql, useQuery } from '@apollo/client';
|
||||
import { BookChapterForm } from '@app/client/components/BookChapterForm';
|
||||
import { useIdentity } from '@app/client/hooks/identity';
|
||||
import { useToastMutation } from '@app/client/hooks/useToastMutation';
|
||||
import { Skeleton } from '@chakra-ui/react';
|
||||
import { BookChapterEditQuery, BookChapterNewCreateMutation } from '@generated/types';
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/work/book/$bookId/chapter/$chapterId/edit')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.isAuthenticated) throw redirect({ to: '/' });
|
||||
}
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { bookId, chapterId } = Route.useParams();
|
||||
|
||||
const identity = useIdentity();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data } = useQuery<BookChapterEditQuery>(query, {
|
||||
variables: { id: chapterId }
|
||||
});
|
||||
|
||||
const { update: chapterUpdate } = useToastMutation<BookChapterNewCreateMutation>({
|
||||
mutation,
|
||||
successMsg: 'Chapter updated successfully'
|
||||
});
|
||||
|
||||
if (data == null) return <Skeleton />;
|
||||
if (identity?.data.user.id !== data.bookChapterOne.book.author.id)
|
||||
throw redirect({ to: `/work/book/${bookId}/overview` });
|
||||
|
||||
return (
|
||||
<BookChapterForm
|
||||
bookId={bookId}
|
||||
initialValues={{
|
||||
title: data.bookChapterOne.title,
|
||||
foreword: data.bookChapterOne.foreword != null ? data.bookChapterOne.foreword : undefined,
|
||||
footnote: data.bookChapterOne.footnote != null ? data.bookChapterOne.footnote : undefined,
|
||||
content: data.bookChapterOne.content
|
||||
}}
|
||||
onChapterSubmit={async chapter => {
|
||||
await chapterUpdate({
|
||||
variables: {
|
||||
input: {
|
||||
id: chapterId,
|
||||
...chapter
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
navigate({ to: `/work/book/${bookId}/overview` });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const query = gql`
|
||||
query BookChapterEdit($id: ID!) {
|
||||
bookChapterOne(id: $id) {
|
||||
id
|
||||
title
|
||||
foreword
|
||||
footnote
|
||||
content
|
||||
previousChapterId
|
||||
nextChapterId
|
||||
book {
|
||||
id
|
||||
updatedAt
|
||||
wordsTotal
|
||||
chaptersTotal
|
||||
author {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mutation = gql`
|
||||
mutation BookChapterEditUpdate($input: BookChapterUpdateInput!) {
|
||||
bookChapterUpdate(input: $input) {
|
||||
id
|
||||
title
|
||||
foreword
|
||||
footnote
|
||||
content
|
||||
previousChapterId
|
||||
nextChapterId
|
||||
book {
|
||||
id
|
||||
updatedAt
|
||||
wordsTotal
|
||||
chaptersTotal
|
||||
author {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
160
app/client/pages/work/book/$bookId/chapter/$chapterId/view.tsx
Normal file
160
app/client/pages/work/book/$bookId/chapter/$chapterId/view.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { gql, useQuery } from '@apollo/client';
|
||||
import { EditorSlate } from '@app/client/components/EditorSlate';
|
||||
import { bookChapterFragment } from '@app/client/fragments';
|
||||
import { Button, Heading, Icon, Skeleton, Stack, Text } from '@chakra-ui/react';
|
||||
import { BookChapterViewQuery, BookChapterViewNavigationQuery } from '@generated/types';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { Descendant } from 'slate';
|
||||
|
||||
export const Route = createFileRoute('/work/book/$bookId/chapter/$chapterId/view')({
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { bookId, chapterId } = Route.useParams();
|
||||
|
||||
const { data } = useQuery<BookChapterViewQuery>(bookChapterQuery, {
|
||||
variables: { id: chapterId }
|
||||
});
|
||||
|
||||
const { data: navigationData } = useQuery<BookChapterViewNavigationQuery>(
|
||||
bookChapterNavigationQuery,
|
||||
{
|
||||
variables: { id: chapterId }
|
||||
}
|
||||
);
|
||||
|
||||
if (data == null || navigationData == null) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<BookChapterView
|
||||
bookId={bookId}
|
||||
chapter={{ ...data.bookChapterOne, ...navigationData.bookChapterOne }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface BookChapterViewChapter {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
readonly foreword?: string | null;
|
||||
readonly footnote?: string | null;
|
||||
readonly content: Descendant[];
|
||||
readonly sequenceNumber: number;
|
||||
readonly reviewsTotal: number;
|
||||
readonly previousChapterId?: string | null;
|
||||
readonly nextChapterId?: string | null;
|
||||
readonly book: {
|
||||
readonly title: string;
|
||||
};
|
||||
}
|
||||
|
||||
function BookChapterView({
|
||||
bookId,
|
||||
chapter
|
||||
}: {
|
||||
readonly bookId: string;
|
||||
readonly chapter: BookChapterViewChapter;
|
||||
}) {
|
||||
return (
|
||||
<Stack
|
||||
maxWidth="840px"
|
||||
width="100%"
|
||||
minHeight="80vh"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Heading as="div" maxWidth="100%">
|
||||
<Text textAlign="center" minWidth={0}>
|
||||
{chapter.book.title}
|
||||
</Text>
|
||||
<Text textAlign="center">-</Text>
|
||||
<Text textAlign="center" minWidth={0}>
|
||||
{chapter.title}
|
||||
</Text>
|
||||
</Heading>
|
||||
<EditorSlate initialValue={chapter.content} readOnly />
|
||||
<ChapterNavigation
|
||||
bookId={bookId}
|
||||
previousChapterId={chapter.previousChapterId}
|
||||
nextChapterId={chapter.nextChapterId}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ChapterNavigation({
|
||||
bookId,
|
||||
previousChapterId,
|
||||
nextChapterId
|
||||
}: {
|
||||
readonly bookId: string;
|
||||
readonly previousChapterId?: string | null;
|
||||
readonly nextChapterId?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<Stack direction="row" gap={0}>
|
||||
{previousChapterId && (
|
||||
<Link to={`/work/book/${bookId}/chapter/${previousChapterId}/view`}>
|
||||
<Button variant="ghost" borderRightRadius={0}>
|
||||
<Icon>
|
||||
<i className="nf nf-md-page_previous" />
|
||||
</Icon>
|
||||
<Text>Prev</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link to={`/work/book/${bookId}/overview`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
borderLeftRadius={previousChapterId ? 0 : undefined}
|
||||
borderRightRadius={nextChapterId ? 0 : undefined}
|
||||
>
|
||||
<Icon>
|
||||
<i className="nf nf-fa-table_list" />
|
||||
</Icon>
|
||||
<Text>Contents</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
{nextChapterId && (
|
||||
<Link to={`/work/book/${bookId}/chapter/${nextChapterId}/view`}>
|
||||
<Button variant="ghost" borderLeftRadius={0}>
|
||||
<Text>Next</Text>
|
||||
<Icon>
|
||||
<i className="nf nf-md-page_next" />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const bookChapterQuery = gql`
|
||||
query BookChapterView($id: ID!) {
|
||||
bookChapterOne(id: $id) {
|
||||
id
|
||||
content
|
||||
...BookChapter
|
||||
book {
|
||||
title
|
||||
chaptersTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
${bookChapterFragment}
|
||||
`;
|
||||
|
||||
export const bookChapterNavigationQuery = gql`
|
||||
query BookChapterViewNavigation($id: ID!) {
|
||||
bookChapterOne(id: $id) {
|
||||
id
|
||||
previousChapterId
|
||||
nextChapterId
|
||||
}
|
||||
}
|
||||
`;
|
||||
89
app/client/pages/work/book/$bookId/chapter/new.tsx
Normal file
89
app/client/pages/work/book/$bookId/chapter/new.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { gql, useQuery } from '@apollo/client';
|
||||
import { BookChapterForm } from '@app/client/components/BookChapterForm';
|
||||
import { useIdentity } from '@app/client/hooks/identity';
|
||||
import { useToastMutation } from '@app/client/hooks/useToastMutation';
|
||||
import { Skeleton } from '@chakra-ui/react';
|
||||
import { BookChapterNewCreateMutation, BookChapterNewQuery } from '@generated/types';
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
||||
import { bookChapterListQuery } from '../overview';
|
||||
import { bookChapterNavigationQuery } from './$chapterId/view';
|
||||
|
||||
export const Route = createFileRoute('/work/book/$bookId/chapter/new')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.isAuthenticated) throw redirect({ to: '/' });
|
||||
}
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { bookId: id } = Route.useParams();
|
||||
|
||||
const identity = useIdentity();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data } = useQuery<BookChapterNewQuery>(query, {
|
||||
variables: { id }
|
||||
});
|
||||
|
||||
const { update: chapterCreate } = useToastMutation<BookChapterNewCreateMutation>({
|
||||
mutation,
|
||||
successMsg: 'Chapter created successfully',
|
||||
refetchQueries: [bookChapterListQuery, bookChapterNavigationQuery]
|
||||
});
|
||||
|
||||
if (data == null) return <Skeleton />;
|
||||
if (identity?.data.user.id !== data.bookOne.author.id)
|
||||
throw redirect({ to: `/work/book/${id}/overview` });
|
||||
|
||||
return (
|
||||
<BookChapterForm
|
||||
bookId={id}
|
||||
onChapterSubmit={async chapter => {
|
||||
await chapterCreate({
|
||||
variables: {
|
||||
input: {
|
||||
bookId: id,
|
||||
...chapter
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
navigate({ to: `/work/book/${id}/overview` });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const query = gql`
|
||||
query BookChapterNew($id: ID!) {
|
||||
bookOne(id: $id) {
|
||||
id
|
||||
author {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mutation = gql`
|
||||
mutation BookChapterNewCreate($input: BookChapterCreateInput!) {
|
||||
bookChapterCreate(input: $input) {
|
||||
id
|
||||
title
|
||||
foreword
|
||||
footnote
|
||||
content
|
||||
previousChapterId
|
||||
nextChapterId
|
||||
book {
|
||||
id
|
||||
updatedAt
|
||||
wordsTotal
|
||||
chaptersTotal
|
||||
author {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,59 +1,56 @@
|
|||
import { gql, useQuery } from '@apollo/client';
|
||||
import { BookChapterCard } from '@app/client/components/BookChapterCard';
|
||||
import { PaginatedListSkeleton } from '@app/client/components/PaginatedListSkeleton';
|
||||
import { Pagination } from '@app/client/components/Pagination';
|
||||
import { Button } from '@app/client/components/ui/button';
|
||||
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 { IdentityContext, useIdentity } from '@app/client/hooks/identity';
|
||||
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 { Box, Heading, HStack, Icon, Skeleton, 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')({
|
||||
export const Route = createFileRoute('/work/book/$bookId/overview')({
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { bookId: id } = Route.useParams();
|
||||
const { data: bookData } = useQuery<BookOverviewQuery>(bookQuery, { variables: { id } });
|
||||
const { data: bookData } = useQuery<BookOverviewQuery>(bookQuery, {
|
||||
variables: { id }
|
||||
});
|
||||
|
||||
const identity = useIdentity();
|
||||
|
||||
return (
|
||||
<Stack width="100%" justifyContent="center" alignItems="center" padding={3}>
|
||||
{bookData != null ? <BookOverview {...bookData.bookOne} /> : <BookOverviewSkeleton />}
|
||||
{bookData != null ? (
|
||||
<BookOverview identity={identity} book={bookData.bookOne} />
|
||||
) : (
|
||||
<BookOverviewSkeleton />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function BookOverview({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
author,
|
||||
progressStatus,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
wordsTotal,
|
||||
chaptersTotal,
|
||||
reviewsTotal,
|
||||
ratingTotal
|
||||
}: BookOne) {
|
||||
identity,
|
||||
book
|
||||
}: {
|
||||
readonly identity: IdentityContext | null;
|
||||
readonly book: 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}
|
||||
{book.title}
|
||||
</Heading>
|
||||
</Stack>
|
||||
|
||||
|
|
@ -62,7 +59,7 @@ function BookOverview({
|
|||
<Link>
|
||||
<Text fontWeight="bold">by:</Text>
|
||||
</Link>
|
||||
<Link to={`/profile/${author.username}`}>
|
||||
<Link to={`/profile/${book.author.username}`}>
|
||||
<Text
|
||||
color="colorPalette.400"
|
||||
_hover={{
|
||||
|
|
@ -70,46 +67,58 @@ function BookOverview({
|
|||
textDecorationColor: 'colorPalette.700'
|
||||
}}
|
||||
>
|
||||
{author.username}
|
||||
{book.author.username}
|
||||
</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row">
|
||||
<Text fontWeight="bold">created at:</Text>
|
||||
<Text>{toFormattedDate({ value: createdAt })}</Text>
|
||||
<Text>{toFormattedDate({ value: book.createdAt })}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row">
|
||||
<Text fontWeight="bold">last updated:</Text>
|
||||
<Text>{toFormattedDate({ value: updatedAt })}</Text>
|
||||
<Text>{toFormattedDate({ value: book.updatedAt })}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row">
|
||||
<Text fontWeight="bold">chapters:</Text>
|
||||
<Text>{chaptersTotal}</Text>
|
||||
<Text>{book.chaptersTotal}</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row">
|
||||
<Text fontWeight="bold">words:</Text>
|
||||
<Text>{wordsTotal}</Text>
|
||||
<Text>{book.wordsTotal}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" justifyContent="left" alignItems="center">
|
||||
<WorkingRatingBadge ratingTotal={ratingTotal} />
|
||||
<WorkingReviewsBadge reviewsTotal={reviewsTotal} />
|
||||
<WorkingProgressStatusBadge progressStatus={progressStatus} />
|
||||
<WorkingRatingBadge ratingTotal={book.ratingTotal} />
|
||||
<WorkingReviewsBadge reviewsTotal={book.reviewsTotal} />
|
||||
<WorkingProgressStatusBadge progressStatus={book.progressStatus} />
|
||||
</Stack>
|
||||
|
||||
<Text>{description}</Text>
|
||||
<Text>{book.description}</Text>
|
||||
|
||||
<BookChapterList id={id} />
|
||||
<BookChapterList
|
||||
id={book.id}
|
||||
identity={identity}
|
||||
isAuthor={identity?.data.id === book.author.id}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function BookChapterList({ id }: { readonly id: string }) {
|
||||
function BookChapterList({
|
||||
id,
|
||||
identity,
|
||||
isAuthor
|
||||
}: {
|
||||
readonly id: string;
|
||||
readonly identity: IdentityContext | null;
|
||||
readonly isAuthor: boolean;
|
||||
}) {
|
||||
const [chaptersListPage, setChaptersListPage] = useState<number>(1);
|
||||
const [chaptersListPageSize, setChaptersListPageSize] = useState<number>(10);
|
||||
const startIndex = (chaptersListPage - 1) * chaptersListPageSize;
|
||||
|
|
@ -129,7 +138,33 @@ function BookChapterList({ id }: { readonly id: string }) {
|
|||
<Stack>
|
||||
{data != null ? (
|
||||
<>
|
||||
<Stack>...chapters</Stack>
|
||||
<Stack>
|
||||
{data.bookChapterMany.items.length > 0 ? (
|
||||
<>
|
||||
{data.bookChapterMany.items.map(item => (
|
||||
<Stack key={`bookChapterList-${item.id}`}>
|
||||
<BookChapterCard
|
||||
bookId={id}
|
||||
chapterId={item.id}
|
||||
title={item.title}
|
||||
isAuthor={isAuthor}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Stack justifyContent="center" alignItems="center" pt={8}>
|
||||
<Text>No chapters here yet!</Text>
|
||||
{isAuthor && (
|
||||
<Link to={`/work/book/${id}/chapter/new`}>
|
||||
<Button variant="outline" size="sm">
|
||||
Create new chapter
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
{data.bookChapterMany.total > chaptersListPageSize && (
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<Pagination
|
||||
|
|
@ -141,6 +176,33 @@ function BookChapterList({ id }: { readonly id: string }) {
|
|||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack direction="row">
|
||||
<>
|
||||
{isAuthor && data.bookChapterMany.items.length !== 0 && (
|
||||
<Box>
|
||||
<Link to={`/work/book/${id}/chapter/new`}>
|
||||
<Button size="sm">Create new chapter</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
{identity != null && !isAuthor && (
|
||||
<>
|
||||
<Button size="sm" backgroundColor="green.500" disabled>
|
||||
<Icon>
|
||||
<i className="nf nf-oct-thumbsup" />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button size="sm" backgroundColor="red.500">
|
||||
<Icon>
|
||||
<i className="nf nf-oct-thumbsdown" />
|
||||
</Icon>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<PaginatedListSkeleton disableSearch />
|
||||
|
|
@ -154,7 +216,6 @@ function BookOverviewSkeleton() {
|
|||
<Stack width="80%" maxWidth="840px">
|
||||
<Stack gap="6" maxWidth="100%">
|
||||
<HStack width="full">
|
||||
<SkeletonCircle size="10" />
|
||||
<SkeletonText noOfLines={5} />
|
||||
</HStack>
|
||||
<Skeleton height="200px" />
|
||||
|
|
@ -172,7 +233,7 @@ const bookQuery = gql`
|
|||
${bookFragment}
|
||||
`;
|
||||
|
||||
const bookChapterListQuery = gql`
|
||||
export const bookChapterListQuery = gql`
|
||||
query BookChapterList($input: BookChapterManyInput!) {
|
||||
bookChapterMany(input: $input) {
|
||||
items {
|
||||
|
|
@ -18,7 +18,10 @@ import { Route as AboutImport } from './pages/about'
|
|||
import { Route as IndexImport } from './pages/index'
|
||||
import { Route as ProfileUsernameImport } from './pages/profile/$username'
|
||||
import { Route as NewBookImport } from './pages/new/book'
|
||||
import { Route as WorkBookBookIdImport } from './pages/work/book/$bookId'
|
||||
import { Route as WorkBookBookIdOverviewImport } from './pages/work/book/$bookId/overview'
|
||||
import { Route as WorkBookBookIdChapterNewImport } from './pages/work/book/$bookId/chapter/new'
|
||||
import { Route as WorkBookBookIdChapterChapterIdViewImport } from './pages/work/book/$bookId/chapter/$chapterId/view'
|
||||
import { Route as WorkBookBookIdChapterChapterIdEditImport } from './pages/work/book/$bookId/chapter/$chapterId/edit'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
|
|
@ -64,12 +67,32 @@ const NewBookRoute = NewBookImport.update({
|
|||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const WorkBookBookIdRoute = WorkBookBookIdImport.update({
|
||||
id: '/work/book/$bookId',
|
||||
path: '/work/book/$bookId',
|
||||
const WorkBookBookIdOverviewRoute = WorkBookBookIdOverviewImport.update({
|
||||
id: '/work/book/$bookId/overview',
|
||||
path: '/work/book/$bookId/overview',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const WorkBookBookIdChapterNewRoute = WorkBookBookIdChapterNewImport.update({
|
||||
id: '/work/book/$bookId/chapter/new',
|
||||
path: '/work/book/$bookId/chapter/new',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const WorkBookBookIdChapterChapterIdViewRoute =
|
||||
WorkBookBookIdChapterChapterIdViewImport.update({
|
||||
id: '/work/book/$bookId/chapter/$chapterId/view',
|
||||
path: '/work/book/$bookId/chapter/$chapterId/view',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const WorkBookBookIdChapterChapterIdEditRoute =
|
||||
WorkBookBookIdChapterChapterIdEditImport.update({
|
||||
id: '/work/book/$bookId/chapter/$chapterId/edit',
|
||||
path: '/work/book/$bookId/chapter/$chapterId/edit',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -123,11 +146,32 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof ProfileUsernameImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/work/book/$bookId': {
|
||||
id: '/work/book/$bookId'
|
||||
path: '/work/book/$bookId'
|
||||
fullPath: '/work/book/$bookId'
|
||||
preLoaderRoute: typeof WorkBookBookIdImport
|
||||
'/work/book/$bookId/overview': {
|
||||
id: '/work/book/$bookId/overview'
|
||||
path: '/work/book/$bookId/overview'
|
||||
fullPath: '/work/book/$bookId/overview'
|
||||
preLoaderRoute: typeof WorkBookBookIdOverviewImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/work/book/$bookId/chapter/new': {
|
||||
id: '/work/book/$bookId/chapter/new'
|
||||
path: '/work/book/$bookId/chapter/new'
|
||||
fullPath: '/work/book/$bookId/chapter/new'
|
||||
preLoaderRoute: typeof WorkBookBookIdChapterNewImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/work/book/$bookId/chapter/$chapterId/edit': {
|
||||
id: '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
path: '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
fullPath: '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
preLoaderRoute: typeof WorkBookBookIdChapterChapterIdEditImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/work/book/$bookId/chapter/$chapterId/view': {
|
||||
id: '/work/book/$bookId/chapter/$chapterId/view'
|
||||
path: '/work/book/$bookId/chapter/$chapterId/view'
|
||||
fullPath: '/work/book/$bookId/chapter/$chapterId/view'
|
||||
preLoaderRoute: typeof WorkBookBookIdChapterChapterIdViewImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +187,10 @@ export interface FileRoutesByFullPath {
|
|||
'/register': typeof RegisterRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
'/work/book/$bookId': typeof WorkBookBookIdRoute
|
||||
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRoute
|
||||
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
|
|
@ -154,7 +201,10 @@ export interface FileRoutesByTo {
|
|||
'/register': typeof RegisterRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
'/work/book/$bookId': typeof WorkBookBookIdRoute
|
||||
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRoute
|
||||
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
|
|
@ -166,7 +216,10 @@ export interface FileRoutesById {
|
|||
'/register': typeof RegisterRoute
|
||||
'/new/book': typeof NewBookRoute
|
||||
'/profile/$username': typeof ProfileUsernameRoute
|
||||
'/work/book/$bookId': typeof WorkBookBookIdRoute
|
||||
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRoute
|
||||
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
|
||||
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
|
|
@ -179,7 +232,10 @@ export interface FileRouteTypes {
|
|||
| '/register'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
| '/work/book/$bookId'
|
||||
| '/work/book/$bookId/overview'
|
||||
| '/work/book/$bookId/chapter/new'
|
||||
| '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
| '/work/book/$bookId/chapter/$chapterId/view'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
|
|
@ -189,7 +245,10 @@ export interface FileRouteTypes {
|
|||
| '/register'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
| '/work/book/$bookId'
|
||||
| '/work/book/$bookId/overview'
|
||||
| '/work/book/$bookId/chapter/new'
|
||||
| '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
| '/work/book/$bookId/chapter/$chapterId/view'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
|
|
@ -199,7 +258,10 @@ export interface FileRouteTypes {
|
|||
| '/register'
|
||||
| '/new/book'
|
||||
| '/profile/$username'
|
||||
| '/work/book/$bookId'
|
||||
| '/work/book/$bookId/overview'
|
||||
| '/work/book/$bookId/chapter/new'
|
||||
| '/work/book/$bookId/chapter/$chapterId/edit'
|
||||
| '/work/book/$bookId/chapter/$chapterId/view'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +273,10 @@ export interface RootRouteChildren {
|
|||
RegisterRoute: typeof RegisterRoute
|
||||
NewBookRoute: typeof NewBookRoute
|
||||
ProfileUsernameRoute: typeof ProfileUsernameRoute
|
||||
WorkBookBookIdRoute: typeof WorkBookBookIdRoute
|
||||
WorkBookBookIdOverviewRoute: typeof WorkBookBookIdOverviewRoute
|
||||
WorkBookBookIdChapterNewRoute: typeof WorkBookBookIdChapterNewRoute
|
||||
WorkBookBookIdChapterChapterIdEditRoute: typeof WorkBookBookIdChapterChapterIdEditRoute
|
||||
WorkBookBookIdChapterChapterIdViewRoute: typeof WorkBookBookIdChapterChapterIdViewRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
|
|
@ -222,7 +287,12 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
RegisterRoute: RegisterRoute,
|
||||
NewBookRoute: NewBookRoute,
|
||||
ProfileUsernameRoute: ProfileUsernameRoute,
|
||||
WorkBookBookIdRoute: WorkBookBookIdRoute,
|
||||
WorkBookBookIdOverviewRoute: WorkBookBookIdOverviewRoute,
|
||||
WorkBookBookIdChapterNewRoute: WorkBookBookIdChapterNewRoute,
|
||||
WorkBookBookIdChapterChapterIdEditRoute:
|
||||
WorkBookBookIdChapterChapterIdEditRoute,
|
||||
WorkBookBookIdChapterChapterIdViewRoute:
|
||||
WorkBookBookIdChapterChapterIdViewRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
|
|
@ -242,7 +312,10 @@ export const routeTree = rootRoute
|
|||
"/register",
|
||||
"/new/book",
|
||||
"/profile/$username",
|
||||
"/work/book/$bookId"
|
||||
"/work/book/$bookId/overview",
|
||||
"/work/book/$bookId/chapter/new",
|
||||
"/work/book/$bookId/chapter/$chapterId/edit",
|
||||
"/work/book/$bookId/chapter/$chapterId/view"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
|
|
@ -266,8 +339,17 @@ export const routeTree = rootRoute
|
|||
"/profile/$username": {
|
||||
"filePath": "profile/$username.tsx"
|
||||
},
|
||||
"/work/book/$bookId": {
|
||||
"filePath": "work/book/$bookId.tsx"
|
||||
"/work/book/$bookId/overview": {
|
||||
"filePath": "work/book/$bookId/overview.tsx"
|
||||
},
|
||||
"/work/book/$bookId/chapter/new": {
|
||||
"filePath": "work/book/$bookId/chapter/new.tsx"
|
||||
},
|
||||
"/work/book/$bookId/chapter/$chapterId/edit": {
|
||||
"filePath": "work/book/$bookId/chapter/$chapterId/edit.tsx"
|
||||
},
|
||||
"/work/book/$bookId/chapter/$chapterId/view": {
|
||||
"filePath": "work/book/$bookId/chapter/$chapterId/view.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue