added slate editor, fixes

This commit is contained in:
lnn0q 2025-05-19 09:42:15 +03:00
parent 1d40d7e15f
commit bc250e73bb
12 changed files with 828 additions and 16 deletions

View file

@ -36,7 +36,7 @@ export function BookSearchList() {
<WorkingList workingsList={[...data.bookMany.items]} /> <WorkingList workingsList={[...data.bookMany.items]} />
{data.bookMany.items.length != 0 && ( {data.bookMany.items.length !== 0 && (
<Pagination <Pagination
total={data.bookMany.total} total={data.bookMany.total}
page={workingsListPage} page={workingsListPage}

View file

@ -0,0 +1,362 @@
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 { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import {
Editable,
RenderElementProps,
RenderLeafProps,
Slate,
useSlate,
withReact
} from 'slate-react';
import {
CustomEditor,
CustomElement,
CustomElementType,
CustomElementWithAlign,
CustomTextKey
} from '../types';
import { Prose } from './ui/prose';
export function EditorSlate() {
return (
<>
<Card.Root
variant={'subtle'}
width="100%"
maxWidth="840px"
minHeight="100%"
flex={1}
justifyContent="center"
alignItems="center"
>
{/* <Card.Header>
<Heading textAlign="center">Editor</Heading>
</Card.Header> */}
<Card.Body
width="100%"
css={{
'& *': {
fontSize: 'revert',
fontWeight: 'revert'
}
}}
>
<RichTextExample />
</Card.Body>
</Card.Root>
</>
);
}
const HOTKEYS: Record<string, CustomTextKey> = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
'mod+`': 'code'
};
const LIST_TYPES = ['numbered-list', 'bulleted-list'] as const;
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify'] as const;
type AlignType = (typeof TEXT_ALIGN_TYPES)[number];
type ListType = (typeof LIST_TYPES)[number];
type CustomElementFormat = CustomElementType | AlignType | ListType;
const RichTextExample = () => {
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>
<Stack width="100%" display="flex" justifyContent="center" alignItems="center">
<Prose
width="100%"
height="100%"
maxWidth="unset"
maxHeight="unset"
display="flex"
justifyContent="center"
alignItems="center"
>
<Editable
className="slate-editor"
style={{
outline: 'none',
minWidth: '100%'
}}
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder=""
spellCheck
autoFocus
onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event as any)) {
event.preventDefault();
const mark = HOTKEYS[hotkey];
toggleMark(editor, mark);
}
}
}}
/>
</Prose>
</Stack>
</Slate>
);
};
const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => {
const isActive = isBlockActive(editor, format, isAlignType(format) ? 'align' : 'type');
const isList = isListType(format);
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
isListType(n.type) &&
!isAlignType(format),
split: true
});
let newProperties: Partial<SlateElement>;
if (isAlignType(format)) {
newProperties = {
align: isActive ? undefined : format
};
} else {
newProperties = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format
};
}
Transforms.setNodes<SlateElement>(editor, newProperties);
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
};
const isBlockActive = (
editor: CustomEditor,
format: CustomElementFormat,
blockType: 'type' | 'align' = 'type'
) => {
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: n => {
if (!Editor.isEditor(n) && SlateElement.isElement(n)) {
if (blockType === 'align' && isAlignElement(n)) {
return n.align === format;
}
return n.type === format;
}
return false;
}
})
);
return !!match;
};
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
const Element = ({ attributes, children, element }: RenderElementProps) => {
const style: React.CSSProperties = {};
if (isAlignElement(element)) {
style.textAlign = element.align as AlignType;
}
switch (element.type) {
case 'block-quote':
return (
<blockquote style={style} {...attributes}>
{children}
</blockquote>
);
case 'bulleted-list':
return (
<ul style={style} {...attributes}>
{children}
</ul>
);
case 'heading-one':
return (
<h1 style={style} {...attributes}>
{children}
</h1>
);
case 'heading-two':
return (
<h2 style={style} {...attributes}>
{children}
</h2>
);
case 'list-item':
return (
<li style={style} {...attributes}>
{children}
</li>
);
case 'numbered-list':
return (
<ol style={style} {...attributes}>
{children}
</ol>
);
default:
return (
<p style={style} {...attributes}>
{children}
</p>
);
}
};
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
if (leaf.code) {
children = <code>{children}</code>;
}
if (leaf.italic) {
children = <em style={{ fontStyle: 'italic' }}>{children}</em>;
}
if (leaf.underline) {
children = <u>{children}</u>;
}
return <span {...attributes}>{children}</span>;
};
interface BlockButtonProps {
format: CustomElementFormat;
icon: string;
text?: string;
}
const BlockButton = ({ format, icon, text }: BlockButtonProps) => {
const editor = useSlate();
return (
<Button
variant={isBlockActive(editor, format) ? 'solid' : 'ghost'}
onMouseDown={(event: MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
toggleBlock(editor, format);
}}
gap={0.5}
>
<i className={icon} />
<Stack alignItems="end">
<Text fontSize="0.8rem">{text}</Text>
</Stack>
</Button>
);
};
interface MarkButtonProps {
format: CustomTextKey;
icon: string;
text?: string;
}
const MarkButton = ({ format, icon, text }: MarkButtonProps) => {
const editor = useSlate();
return (
<Button
variant={isMarkActive(editor, format) ? 'solid' : 'ghost'}
onMouseDown={(event: MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
toggleMark(editor, format);
}}
>
<i className={icon} />
{text}
</Button>
);
};
const isAlignType = (format: CustomElementFormat): format is AlignType => {
return TEXT_ALIGN_TYPES.includes(format as AlignType);
};
const isListType = (format: CustomElementFormat): format is ListType => {
return LIST_TYPES.includes(format as ListType);
};
const isAlignElement = (element: CustomElement): element is CustomElementWithAlign => {
return 'align' in element;
};

View file

@ -0,0 +1,56 @@
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.
<>
<Card.Root
variant={'subtle'}
width="100%"
maxWidth="840px"
minHeight="sm"
justifyContent="center"
alignItems="center"
>
<Card.Header>
<Heading textAlign="center">Editor</Heading>
</Card.Header>
<Card.Body
width="90%"
css={{
'& *': {
fontSize: 'revert',
fontWeight: 'revert'
}
}}
>
<Slate editor={editor} initialValue={initialValue}>
<Editable />
</Slate>
</Card.Body>
</Card.Root>
</>
);
}

View file

@ -4,7 +4,7 @@ import { WorkingCardProps, WorkingListCard } from './WorkingListCard';
export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) { export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) {
return ( return (
<List.Root variant="plain" gap={4}> <List.Root variant="plain" gap={4}>
{workingsList.length != 0 ? ( {workingsList.length !== 0 ? (
workingsList.map(workingData => ( workingsList.map(workingData => (
<List.Item key={workingData.id}> <List.Item key={workingData.id}>
<WorkingListCard {...workingData} /> <WorkingListCard {...workingData} />

View file

@ -1,6 +1,6 @@
import { Stack } from '@chakra-ui/react'; import { Stack } from '@chakra-ui/react';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { Editor } from '../components/Editor'; import { EditorSlate } from '../components/EditorSlate';
export const Route = createFileRoute('/editor')({ export const Route = createFileRoute('/editor')({
component: RouteComponent component: RouteComponent
@ -8,8 +8,8 @@ export const Route = createFileRoute('/editor')({
function RouteComponent() { function RouteComponent() {
return ( return (
<Stack justifyContent="center" alignItems="center"> <Stack justifyContent="center" alignItems="center" minHeight="88vh">
<Editor /> <EditorSlate />
</Stack> </Stack>
); );
} }

View file

@ -1,4 +1,4 @@
import { Heading, Input, Stack } from '@chakra-ui/react'; import { Heading, Input, Stack, Textarea } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button'; import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field'; import { Field } from '@client/components/ui/field';
import { valibotResolver } from '@hookform/resolvers/valibot'; import { valibotResolver } from '@hookform/resolvers/valibot';
@ -15,13 +15,51 @@ export const Route = createFileRoute('/new/book')({
function RouteComponent() { function RouteComponent() {
return ( return (
<Stack flexDirection={'column'} justifyContent={'center'} alignItems={'center'}> <Stack width="100%" flexDirection={'column'} justifyContent={'center'} alignItems={'center'}>
<Heading>New book</Heading> <Heading>New book</Heading>
<form></form> <NewBookForm />
</Stack> </Stack>
); );
} }
interface FormValues {
title: string;
description: string;
}
function NewBookForm() {
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'))
})
);
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormValues>({ resolver: valibotResolver(schema) });
return (
<form style={{ width: '80%' }} onSubmit={handleSubmit(async values => {})}>
<Stack>
<Field label="Title" invalid={errors.title != null} errorText={errors.title?.message}>
<Input {...register('title')} />
</Field>
<Field
label="Description"
invalid={errors.description != null}
errorText={errors.description?.message}
>
<Textarea width="100%" lineHeight={4} {...register('description')} />
</Field>
<Button type="submit">Create new book</Button>
</Stack>
</form>
);
}
// function LoginForm() { // function LoginForm() {
// const schema = v.pipe( // const schema = v.pipe(
// v.object({ // v.object({
@ -40,13 +78,6 @@ function RouteComponent() {
// //
// return ( // return (
// <form // <form
// onSubmit={handleSubmit(async values => {
// const session = await login({
// email: values.email,
// password: values.password
// });
// if (session != null) window.history.back();
// })}
// > // >
// <Stack> // <Stack>
// <Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}> // <Field label="Email" invalid={errors.email != null} errorText={errors.email?.message}>

196
app/client/types.d.ts vendored Normal file
View file

@ -0,0 +1,196 @@
import { Descendant, BaseEditor, BaseRange, Range, Element } from 'slate';
import { ReactEditor } from 'slate-react';
import { HistoryEditor } from 'slate-history';
export type BlockQuoteElement = {
type: 'block-quote';
align?: string;
children: Descendant[];
};
export type BulletedListElement = {
type: 'bulleted-list';
align?: string;
children: Descendant[];
};
export type CheckListItemElement = {
type: 'check-list-item';
checked: boolean;
children: Descendant[];
};
export type EditableVoidElement = {
type: 'editable-void';
children: EmptyText[];
};
export type HeadingElement = {
type: 'heading-one';
align?: string;
children: Descendant[];
};
export type HeadingTwoElement = {
type: 'heading-two';
align?: string;
children: Descendant[];
};
export type HeadingThreeElement = {
type: 'heading-three';
align?: string;
children: Descendant[];
};
export type HeadingFourElement = {
type: 'heading-four';
align?: string;
children: Descendant[];
};
export type HeadingFiveElement = {
type: 'heading-five';
align?: string;
children: Descendant[];
};
export type HeadingSixElement = {
type: 'heading-six';
align?: string;
children: Descendant[];
};
export type ImageElement = {
type: 'image';
url: string;
children: EmptyText[];
};
export type LinkElement = { type: 'link'; url: string; children: Descendant[] };
export type ButtonElement = { type: 'button'; children: Descendant[] };
export type BadgeElement = { type: 'badge'; children: Descendant[] };
export type ListItemElement = { type: 'list-item'; children: Descendant[] };
export type NumberedListItemElement = {
type: 'numbered-list';
children: Descendant[];
};
export type MentionElement = {
type: 'mention';
character: string;
children: CustomText[];
};
export type ParagraphElement = {
type: 'paragraph';
align?: string;
children: Descendant[];
};
export type TableElement = { type: 'table'; children: TableRow[] };
export type TableCellElement = { type: 'table-cell'; children: CustomText[] };
export type TableRowElement = { type: 'table-row'; children: TableCell[] };
export type TitleElement = { type: 'title'; children: Descendant[] };
export type VideoElement = { type: 'video'; url: string; children: EmptyText[] };
export type CodeBlockElement = {
type: 'code-block';
language: string;
children: Descendant[];
};
export type CodeLineElement = {
type: 'code-line';
children: Descendant[];
};
export type CustomElementWithAlign =
| ParagraphElement
| HeadingElement
| HeadingTwoElement
| HeadingThreeElement
| HeadingFourElement
| HeadingFiveElement
| HeadingSixElement
| BlockQuoteElement
| BulletedListElement;
type CustomElement =
| BlockQuoteElement
| BulletedListElement
| CheckListItemElement
| EditableVoidElement
| HeadingElement
| HeadingTwoElement
| HeadingThreeElement
| HeadingFourElement
| HeadingFiveElement
| HeadingSixElement
| ImageElement
| LinkElement
| ButtonElement
| BadgeElement
| ListItemElement
| NumberedListItemElement
| MentionElement
| ParagraphElement
| TableElement
| TableRowElement
| TableCellElement
| TitleElement
| VideoElement
| CodeBlockElement
| CodeLineElement;
export type CustomElementType = CustomElement['type'];
export type CustomText = {
bold?: boolean;
italic?: boolean;
code?: boolean;
underline?: boolean;
strikethrough?: boolean;
// MARKDOWN PREVIEW SPECIFIC LEAF
underlined?: boolean;
title?: boolean;
list?: boolean;
hr?: boolean;
blockquote?: boolean;
text: string;
};
export type CustomTextKey = keyof Omit<CustomText, 'text'>;
export type EmptyText = {
text: string;
};
export type RenderElementPropsFor<T> = RenderElementProps & {
element: T;
};
export type CustomEditor = BaseEditor &
ReactEditor &
HistoryEditor & {
nodeToDecorations?: Map<Element, Range[]>;
};
declare module 'slate' {
interface CustomTypes {
Editor: CustomEditor;
Element: CustomElement;
Text: CustomText;
Range: BaseRange & {
[key: string]: unknown;
};
}
}

View file

@ -11,6 +11,7 @@ export async function toBookChapterSchema(model: BookChapter): Promise<BookChapt
updatedAt: model.createdAt.toISOString(), updatedAt: model.createdAt.toISOString(),
foreword: model.foreword, foreword: model.foreword,
footnote: model.footnote, footnote: model.footnote,
sequenceNumber: model.sequenceNumber,
reviewsTotal: await call(async () => { reviewsTotal: await call(async () => {
const reviewsCount = await prisma.review.count({ const reviewsCount = await prisma.review.count({
where: { where: {

View file

@ -52,6 +52,18 @@ input PaginationFilterInput {
size: Int size: Int
} }
input BookCreateInput {
title: String!
description: String!
}
input BookChapterCreateInput {
title: String!
content: String!
foreword: String
footnote: String
}
input BookManyInput { input BookManyInput {
filter: PaginationFilterInput! filter: PaginationFilterInput!
} }
@ -88,6 +100,7 @@ type BookChapter {
updatedAt: String! updatedAt: String!
foreword: String! foreword: String!
footnote: String! footnote: String!
sequenceNumber: Int!
reviewsTotal: Int! reviewsTotal: Int!
} }

View file

@ -59,6 +59,8 @@ model BookChapter {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
sequenceNumber Int @default(1)
title String @db.VarChar(255) title String @db.VarChar(255)
foreword String @db.MediumText foreword String @db.MediumText
footnote String @db.MediumText footnote String @db.MediumText

148
package-lock.json generated
View file

@ -31,12 +31,16 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"is-hotkey": "^0.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"slate": "^0.114.0",
"slate-history": "^0.113.1",
"slate-react": "^0.114.2",
"tsx": "^4.3.0", "tsx": "^4.3.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@ -56,6 +60,7 @@
"@types/cookie-parser": "^1.4.8", "@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/is-hotkey": "^0.1.10",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.9.3", "@types/node": "^20.9.3",
"@types/react": "^18.0.38", "@types/react": "^18.0.38",
@ -5068,6 +5073,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@kamilkisiela/fast-url-parser": { "node_modules/@kamilkisiela/fast-url-parser": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz",
@ -5950,6 +5961,13 @@
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/is-hotkey": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz",
"integrity": "sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/js-yaml": { "node_modules/@types/js-yaml": {
"version": "4.0.9", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
@ -8492,6 +8510,12 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -9060,6 +9084,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/direction": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
"integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==",
"license": "MIT",
"bin": {
"direction": "cli.js"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -11103,6 +11140,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "3.7.6", "version": "3.7.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
@ -11460,6 +11507,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-hotkey": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
},
"node_modules/is-interactive": { "node_modules/is-interactive": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
@ -11549,6 +11602,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@ -12076,7 +12138,6 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
@ -14037,6 +14098,15 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/scuid": { "node_modules/scuid": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
@ -14374,6 +14444,82 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/slate": {
"version": "0.114.0",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.114.0.tgz",
"integrity": "sha512-r3KHl22433DlN5BpLAlL4b3D8ItoGKAkj91YT6GhP39XuLoBT+YFd9ObKuL/okgiPb5lbwnW+71fM45hHceN9w==",
"license": "MIT",
"dependencies": {
"immer": "^10.0.3",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
},
"node_modules/slate-dom": {
"version": "0.114.0",
"resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.114.0.tgz",
"integrity": "sha512-3LWIfiDPNQSY+SCPsvMTErCkx2gXTViLoWISisw6uM+unwiOkEF9ZmpHp88/SSmcq6k3P4aIquehUNeNUlkdiA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"direction": "^1.0.4",
"is-hotkey": "^0.2.0",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0",
"tiny-invariant": "1.3.1"
},
"peerDependencies": {
"slate": ">=0.99.0"
}
},
"node_modules/slate-dom/node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"license": "MIT",
"peer": true
},
"node_modules/slate-history": {
"version": "0.113.1",
"resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz",
"integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==",
"license": "MIT",
"dependencies": {
"is-plain-object": "^5.0.0"
},
"peerDependencies": {
"slate": ">=0.65.3"
}
},
"node_modules/slate-react": {
"version": "0.114.2",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.114.2.tgz",
"integrity": "sha512-yqJnX1/7A30szl9BxW3qX99MZy6mM6VtUi1rXTy0JpRMTKv3rduo0WOxqcX90tpt0ke2pzHGbrLLr1buIN4vrw==",
"license": "MIT",
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"direction": "^1.0.4",
"is-hotkey": "^0.2.0",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0",
"tiny-invariant": "1.3.1"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"slate": ">=0.114.0",
"slate-dom": ">=0.110.2"
}
},
"node_modules/slate-react/node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"license": "MIT"
},
"node_modules/slice-ansi": { "node_modules/slice-ansi": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",

View file

@ -37,12 +37,16 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"is-hotkey": "^0.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"slate": "^0.114.0",
"slate-history": "^0.113.1",
"slate-react": "^0.114.2",
"tsx": "^4.3.0", "tsx": "^4.3.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@ -62,6 +66,7 @@
"@types/cookie-parser": "^1.4.8", "@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/is-hotkey": "^0.1.10",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.9.3", "@types/node": "^20.9.3",
"@types/react": "^18.0.38", "@types/react": "^18.0.38",