From 029b7e9353aaeb990ce2ed156c63af41d6793748 Mon Sep 17 00:00:00 2001 From: lnn0q Date: Wed, 28 May 2025 23:25:46 +0300 Subject: [PATCH] refactor, added slate, added editor, added book --- app/client/components/BookSearchList.tsx | 58 ++-- app/client/components/EditorSlate.tsx | 75 +++-- app/client/components/EditorTestSlate.tsx | 56 ---- app/client/components/Link.tsx | 25 ++ .../{WorkingListSearch.tsx => ListSearch.tsx} | 11 +- .../components/PaginatedListSkeleton.tsx | 17 ++ app/client/components/Pagination.tsx | 1 + app/client/components/WorkingList.tsx | 5 +- app/client/components/WorkingListCard.tsx | 59 ++-- .../components/WorkingProgressStatusBadge.tsx | 2 +- app/client/components/WorkingRatingBadge.tsx | 10 +- .../components/WorkingReviewsTotalBadge.tsx | 12 + app/client/components/ui/link.tsx | 24 +- app/client/fragments.tsx | 38 ++- app/client/helpers.tsx | 7 + app/client/hooks/usePaginatedQuery.tsx | 1 + app/client/interfaces.tsx | 18 ++ app/client/pages/__root.tsx | 40 +-- app/client/pages/book.lazy.tsx | 20 -- app/client/pages/index.tsx | 46 --- app/client/pages/new/book.tsx | 79 +++-- app/client/pages/work/book/$bookId.tsx | 185 +++++++++++ app/client/routeTree.gen.ts | 59 ++-- app/server/graphql/graphql.ts | 12 +- .../resolvers/bookChapterCreateResolver.ts | 0 ...pterMany.ts => bookChapterManyResolver.ts} | 10 +- ...hapterOne.ts => bookChapterOneResolver.ts} | 0 .../graphql/resolvers/bookCreateResolver.ts | 27 ++ .../graphql/resolvers/bookManyResolver.ts | 15 +- app/server/graphql/schema.graphql | 1 + app/server/helpers.ts | 7 + package-lock.json | 289 +++++++++--------- package.json | 1 + 33 files changed, 714 insertions(+), 496 deletions(-) delete mode 100644 app/client/components/EditorTestSlate.tsx create mode 100644 app/client/components/Link.tsx rename app/client/components/{WorkingListSearch.tsx => ListSearch.tsx} (77%) create mode 100644 app/client/components/PaginatedListSkeleton.tsx create mode 100644 app/client/components/WorkingReviewsTotalBadge.tsx create mode 100644 app/client/interfaces.tsx delete mode 100644 app/client/pages/book.lazy.tsx create mode 100644 app/client/pages/work/book/$bookId.tsx create mode 100644 app/server/graphql/resolvers/bookChapterCreateResolver.ts rename app/server/graphql/resolvers/{bookChapterMany.ts => bookChapterManyResolver.ts} (71%) rename app/server/graphql/resolvers/{bookChapterOne.ts => bookChapterOneResolver.ts} (100%) create mode 100644 app/server/graphql/resolvers/bookCreateResolver.ts create mode 100644 app/server/helpers.ts diff --git a/app/client/components/BookSearchList.tsx b/app/client/components/BookSearchList.tsx index 0f6d881..ab67d40 100644 --- a/app/client/components/BookSearchList.tsx +++ b/app/client/components/BookSearchList.tsx @@ -1,16 +1,18 @@ import { gql } from '@apollo/client'; -import { Skeleton, Stack } from '@chakra-ui/react'; +import { 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 { ListSearch } from '../components/ListSearch'; +import { bookFragment } from '../fragments'; import { usePaginatedQuery } from '../hooks/usePaginatedQuery'; +import { PaginatedListSkeleton } from './PaginatedListSkeleton'; export function BookSearchList() { const [workingSearchValue, setWorkingSearchValue] = useState(''); const [workingsListPage, setWorkingsListPage] = useState(1); - const [workingsListPageSize, setWorkingsListPageSize] = useState(2); + const [workingsListPageSize, setWorkingsListPageSize] = useState(10); const startIndex = (workingsListPage - 1) * workingsListPageSize; const { data } = usePaginatedQuery({ @@ -26,35 +28,24 @@ export function BookSearchList() { return ( <> + setWorkingSearchValue(value)} /> {data != null ? ( <> - { - setWorkingSearchValue(value); - }} - /> - - - {data.bookMany.items.length !== 0 && ( - + {data.bookMany.total > workingsListPageSize && ( + + + )} ) : ( - <> - - - - - - - - + )} ); @@ -64,21 +55,10 @@ const query = gql` query BooksList($input: BookManyInput!) { bookMany(input: $input) { items { - id - title - description - author { - id - username - } - updatedAt - progressStatus - ratingTotal - reviewsTotal - wordsTotal - chaptersTotal + ...Book } total } } + ${bookFragment} `; diff --git a/app/client/components/EditorSlate.tsx b/app/client/components/EditorSlate.tsx index 01a6e00..2a0a14a 100644 --- a/app/client/components/EditorSlate.tsx +++ b/app/client/components/EditorSlate.tsx @@ -1,6 +1,6 @@ import { Button, Card, Stack, Text, Wrap } from '@chakra-ui/react'; import isHotkey from 'is-hotkey'; -import React, { KeyboardEvent, MouseEvent, useCallback, useMemo, useState } from 'react'; +import React, { KeyboardEvent, MouseEvent, useCallback, useMemo } from 'react'; import { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate'; import { withHistory } from 'slate-history'; import { @@ -44,7 +44,7 @@ export function EditorSlate() { } }} > - + @@ -65,7 +65,7 @@ type AlignType = (typeof TEXT_ALIGN_TYPES)[number]; type ListType = (typeof LIST_TYPES)[number]; type CustomElementFormat = CustomElementType | AlignType | ListType; -const RichTextExample = () => { +function EditorViewport() { const renderElement = useCallback((props: RenderElementProps) => , []); const renderLeaf = useCallback((props: RenderLeafProps) => , []); const editor = useMemo(() => withHistory(withReact(createEditor())), []); @@ -123,12 +123,17 @@ const RichTextExample = () => { - + { className="slate-editor" style={{ outline: 'none', - minWidth: '100%' + minWidth: '100%', + maxWidth: 'unset', + maxHeight: '75vh', + padding: 0, + overflowY: 'auto' }} renderElement={renderElement} renderLeaf={renderLeaf} @@ -158,9 +167,9 @@ const RichTextExample = () => { ); -}; +} -const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => { +function toggleBlock(editor: CustomEditor, format: CustomElementFormat) { const isActive = isBlockActive(editor, format, isAlignType(format) ? 'align' : 'type'); const isList = isListType(format); @@ -188,9 +197,9 @@ const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => { const block = { type: format, children: [] }; Transforms.wrapNodes(editor, block); } -}; +} -const toggleMark = (editor: CustomEditor, format: CustomTextKey) => { +function toggleMark(editor: CustomEditor, format: CustomTextKey) { const isActive = isMarkActive(editor, format); if (isActive) { @@ -198,13 +207,13 @@ const toggleMark = (editor: CustomEditor, format: CustomTextKey) => { } else { Editor.addMark(editor, format, true); } -}; +} -const isBlockActive = ( +function isBlockActive( editor: CustomEditor, format: CustomElementFormat, blockType: 'type' | 'align' = 'type' -) => { +) { const { selection } = editor; if (!selection) return false; @@ -223,15 +232,15 @@ const isBlockActive = ( }) ); - return !!match; -}; + return Boolean(match); +} -const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => { +function isMarkActive(editor: CustomEditor, format: CustomTextKey) { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; -}; +} -const Element = ({ attributes, children, element }: RenderElementProps) => { +function Element({ attributes, children, element }: RenderElementProps) { const style: React.CSSProperties = {}; if (isAlignElement(element)) { style.textAlign = element.align as AlignType; @@ -280,9 +289,9 @@ const Element = ({ attributes, children, element }: RenderElementProps) => {

); } -}; +} -const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => { +function Leaf({ attributes, children, leaf }: RenderLeafProps) { if (leaf.bold) { children = {children}; } @@ -300,7 +309,7 @@ const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => { } return {children}; -}; +} interface BlockButtonProps { format: CustomElementFormat; @@ -308,7 +317,7 @@ interface BlockButtonProps { text?: string; } -const BlockButton = ({ format, icon, text }: BlockButtonProps) => { +function BlockButton({ format, icon, text }: BlockButtonProps) { const editor = useSlate(); return ( ); -}; +} interface MarkButtonProps { format: CustomTextKey; @@ -333,7 +342,7 @@ interface MarkButtonProps { text?: string; } -const MarkButton = ({ format, icon, text }: MarkButtonProps) => { +function MarkButton({ format, icon, text }: MarkButtonProps) { const editor = useSlate(); return ( ); -}; +} -const isAlignType = (format: CustomElementFormat): format is AlignType => { +function isAlignType(format: CustomElementFormat): format is AlignType { return TEXT_ALIGN_TYPES.includes(format as AlignType); -}; +} -const isListType = (format: CustomElementFormat): format is ListType => { +function isListType(format: CustomElementFormat): format is ListType { return LIST_TYPES.includes(format as ListType); -}; +} -const isAlignElement = (element: CustomElement): element is CustomElementWithAlign => { +function isAlignElement(element: CustomElement): element is CustomElementWithAlign { return 'align' in element; -}; +} diff --git a/app/client/components/EditorTestSlate.tsx b/app/client/components/EditorTestSlate.tsx deleted file mode 100644 index 3129685..0000000 --- a/app/client/components/EditorTestSlate.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Card, Heading } from '@chakra-ui/react'; -import { useState } from 'react'; -import { BaseEditor, createEditor, Descendant } from 'slate'; -import { Editable, ReactEditor, Slate, withReact } from 'slate-react'; - -type CustomElement = { type: 'paragraph'; children: CustomText[] }; -type CustomText = { text: string }; - -declare module 'slate' { - interface CustomTypes { - Editor: BaseEditor & ReactEditor; - Element: CustomElement; - Text: CustomText; - } -} - -export function EditorSlate() { - const initialValue: Descendant[] = [ - { - type: 'paragraph', - children: [{ text: 'A line of text in a paragraph.' }] - } - ]; - - const [editor] = useState(() => withReact(createEditor())); - return ( - // Add the editable component inside the context. - <> - - - Editor - - - - - - - - - ); -} diff --git a/app/client/components/Link.tsx b/app/client/components/Link.tsx new file mode 100644 index 0000000..5f262f1 --- /dev/null +++ b/app/client/components/Link.tsx @@ -0,0 +1,25 @@ +import { Text } from '@chakra-ui/react'; +import { LinkProps as LinkExtended, Link as TanstackLink } from '@tanstack/react-router'; +import { ReactNode } from 'react'; + +interface LinkProps { + children: ReactNode; + to: LinkExtended['to']; + link?: LinkExtended; +} + +export function Link(props: LinkProps) { + return ( + + + {props.children} + + + ); +} diff --git a/app/client/components/WorkingListSearch.tsx b/app/client/components/ListSearch.tsx similarity index 77% rename from app/client/components/WorkingListSearch.tsx rename to app/client/components/ListSearch.tsx index 4a85652..5442397 100644 --- a/app/client/components/WorkingListSearch.tsx +++ b/app/client/components/ListSearch.tsx @@ -1,7 +1,13 @@ import { Button, Icon, Input, InputGroup, Stack } from '@chakra-ui/react'; import { useState } from 'react'; -export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => void }) { +export function ListSearch({ + placeholder, + onSubmit +}: { + readonly placeholder?: string; + readonly onSubmit: (value: string) => void; +}) { const [searchValue, setSearchValue] = useState(''); return (
v setSearchValue(event.currentTarget.value)} - placeholder="Search works..." + onSubmit={event => event.preventDefault()} + placeholder={placeholder} />