* replace next with vite
* disable strictmode (it interferes with collaboration in dev mode)
This commit is contained in:
Philipinho
2023-10-20 17:12:08 +01:00
parent a86991e3d7
commit 9b682c8af5
96 changed files with 645 additions and 505 deletions

View File

@@ -0,0 +1,161 @@
import '@/features/editor/styles/editor.css';
import { HocuspocusProvider } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { EditorContent, useEditor } from '@tiptap/react';
import { StarterKit } from '@tiptap/starter-kit';
import { Placeholder } from '@tiptap/extension-placeholder';
import { Collaboration } from '@tiptap/extension-collaboration';
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor';
import { useEffect, useLayoutEffect, useState } from 'react';
import { useAtom } from 'jotai';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom';
import useCollaborationUrl from '@/features/editor/hooks/use-collaboration-url';
import { IndexeddbPersistence } from 'y-indexeddb';
import { RichTextEditor } from '@mantine/tiptap';
import { TextAlign } from '@tiptap/extension-text-align';
import { Highlight } from '@tiptap/extension-highlight';
import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Link } from '@tiptap/extension-link';
import { Underline } from '@tiptap/extension-underline';
interface EditorProps {
pageId: string,
token?: string,
}
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D'];
const getRandomElement = list => list[Math.floor(Math.random() * list.length)];
const getRandomColor = () => getRandomElement(colors);
export default function Editor({ pageId }: EditorProps) {
const [token] = useAtom(authTokensAtom);
const collaborationURL = useCollaborationUrl();
const [provider, setProvider] = useState<any>();
const [yDoc] = useState(() => new Y.Doc());
useEffect(() => {
if (token) {
const indexeddbProvider = new IndexeddbPersistence(pageId, yDoc)
const provider = new HocuspocusProvider({
url: collaborationURL,
name: pageId,
document: yDoc,
token: token.accessToken,
});
setProvider(provider);
return () => {
provider.destroy();
setProvider(null);
indexeddbProvider.destroy();
};
}
}, [pageId, token]);
if (!provider) {
return null;
}
return (
<TiptapEditor ydoc={yDoc} provider={provider} />
);
}
interface TiptapEditorProps {
ydoc: Y.Doc,
provider: HocuspocusProvider
}
function TiptapEditor({ ydoc, provider }: TiptapEditorProps) {
const [currentUser] = useAtom(currentUserAtom);
const extensions = [
StarterKit.configure({
history: false,
}),
Placeholder.configure({
placeholder: 'Write here',
}),
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider,
}),
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
];
const editor = useEditor({
extensions: extensions,
});
useEffect(() => {
if (editor && currentUser.user) {
editor.chain().focus().updateUser({ ...currentUser.user, color: getRandomColor() }).run();
}
}, [editor, currentUser.user]);
useEffect(() => {
provider.on('status', event => {
console.log(event);
});
}, [provider]);
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}

View File

@@ -0,0 +1,17 @@
const useCollaborationURL = (): string => {
const PATH = "/collaboration";
if (import.meta.env.VITE_COLLABORATION_URL) {
return import.meta.env.VITE_COLLABORATION_URL + PATH;
}
const API_URL = import.meta.env.VITE_BACKEND_API_URL;
if (!API_URL) {
throw new Error("Backend API URL is not defined");
}
const wsProtocol = API_URL.startsWith('https') ? 'wss' : 'ws';
return `${wsProtocol}://${API_URL.split('://')[1]}${PATH}`;
};
export default useCollaborationURL;

View File

@@ -0,0 +1,199 @@
/* Basic editor styles */
.tiptap {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
mark {
background-color: #faf594;
}
img {
height: auto;
max-width: 100%;
}
hr {
margin: 1rem 0;
}
blockquote {
border-left: 2px solid rgba(#0d0d0d, 0.1);
padding-left: 1rem;
}
hr {
border: none;
border-top: 2px solid rgba(#0d0d0d, 0.1);
margin: 2rem 0;
}
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
align-items: center;
display: flex;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
}
}
.editor {
background-color: #fff;
border: 2px solid #0d0d0d;
border-radius: 0.75rem;
box-shadow: 5px 5px #000;
color: #0d0d0d;
display: flex;
flex-direction: column;
max-height: 26rem;
&__header {
align-items: center;
border-bottom: 2px solid #0d0d0d;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
display: flex;
justify-content: space-between;
padding: 0.5rem 1rem;
}
&__users {
color: rgba(#000, 0.8);
display: flex;
font-size: 0.85rem;
gap: 1rem;
}
&__content {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
padding: 1.25rem 1rem;
-webkit-overflow-scrolling: touch;
}
/* Some information about the status */
&__status {
align-items: center;
border-radius: 5px;
display: flex;
&::before {
background: rgba(#0d0d0d, 0.5);
border-radius: 50%;
content: " ";
display: inline-block;
flex: 0 0 auto;
height: 0.5rem;
margin-right: 0.5rem;
width: 0.5rem;
}
&--connecting::before {
background: #616161;
}
&--connected::before {
background: #b9f18d;
}
}
&__name button {
appearance: none;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
font: inherit;
line-height: normal;
margin: 0;
padding: 0;
overflow: visible;
width: auto;
}
}
/* Give a remote user a caret */
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}
/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 0.75rem;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;
}
.dots {
display: flex;
gap: 6px;
}
.dot {
background: #000;
border-radius: 100%;
height: 0.625rem;
width: 0.625rem;
}