diff --git a/examples/tiptap-collab-editing/.eslintrc.js b/examples/tiptap-collab-editing/.eslintrc.js new file mode 100644 index 00000000000..ced78085f86 --- /dev/null +++ b/examples/tiptap-collab-editing/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], +}; diff --git a/examples/tiptap-collab-editing/.gitignore b/examples/tiptap-collab-editing/.gitignore new file mode 100644 index 00000000000..3f7bf98da3e --- /dev/null +++ b/examples/tiptap-collab-editing/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/examples/tiptap-collab-editing/README.md b/examples/tiptap-collab-editing/README.md new file mode 100644 index 00000000000..52e1a4f9970 --- /dev/null +++ b/examples/tiptap-collab-editing/README.md @@ -0,0 +1,24 @@ +# Real time collaborative editing using tiptap and webrtc + +this is an example of using tiptap,yjs and webrtc to do real time collaborative editing across device and browsers + +## Relevent files + +``` +app +├── routes +│ └── editor.tsx // tiptab editor route, open it in different browser to see the changes get sync +└── utils + └── webrtc.client.tsx //init the webrtc, ydoc and collaboration plugin on client +``` + +## Preview + +Open this example on [CodeSandbox](https://codesandbox.com): + +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/remix/tree/main/examples/tiptap-collab-editing) + +## Related Links + +- [Collaborative editing – Tiptap Editor](https://tiptap.dev/guide/collaborative-editing#show-other-cursors) +- [Remix | Module Constraints](https://remix.run/docs/en/v1/guides/constraints#document-guard) diff --git a/examples/tiptap-collab-editing/app/entry.client.tsx b/examples/tiptap-collab-editing/app/entry.client.tsx new file mode 100644 index 00000000000..3eec1fd0a02 --- /dev/null +++ b/examples/tiptap-collab-editing/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { RemixBrowser } from "@remix-run/react"; +import { hydrate } from "react-dom"; + +hydrate(, document); diff --git a/examples/tiptap-collab-editing/app/entry.server.tsx b/examples/tiptap-collab-editing/app/entry.server.tsx new file mode 100644 index 00000000000..5afa18235cc --- /dev/null +++ b/examples/tiptap-collab-editing/app/entry.server.tsx @@ -0,0 +1,21 @@ +import type { EntryContext } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToString } from "react-dom/server"; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + status: responseStatusCode, + headers: responseHeaders, + }); +} diff --git a/examples/tiptap-collab-editing/app/root.tsx b/examples/tiptap-collab-editing/app/root.tsx new file mode 100644 index 00000000000..927a0f745df --- /dev/null +++ b/examples/tiptap-collab-editing/app/root.tsx @@ -0,0 +1,32 @@ +import type { MetaFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export const meta: MetaFunction = () => ({ + charset: "utf-8", + title: "New Remix App", + viewport: "width=device-width,initial-scale=1", +}); + +export default function App() { + return ( + + + + + + + + + + + + + ); +} diff --git a/examples/tiptap-collab-editing/app/routes/editor.tsx b/examples/tiptap-collab-editing/app/routes/editor.tsx new file mode 100644 index 00000000000..4a7ec8d0880 --- /dev/null +++ b/examples/tiptap-collab-editing/app/routes/editor.tsx @@ -0,0 +1,32 @@ +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { Collaboration, ydoc, WebrtcProvider } from "~/utils/webrtc.client"; + +if (typeof document !== "undefined") { + //join the room remix example + new WebrtcProvider("room-remix-example", ydoc); +} +export default () => { + let editor; + if (typeof document !== "undefined") { + editor = useEditor({ + extensions: [ + StarterKit.configure({ + history: false, + }), + Collaboration.configure({ + document: ydoc, + }), + ], + }); + } + + return ( +
+

Open in two browsers and see the editing in sync

+
+ {editor && } +
+
+ ); +}; diff --git a/examples/tiptap-collab-editing/app/utils/webrtc.client.tsx b/examples/tiptap-collab-editing/app/utils/webrtc.client.tsx new file mode 100644 index 00000000000..bad914b518b --- /dev/null +++ b/examples/tiptap-collab-editing/app/utils/webrtc.client.tsx @@ -0,0 +1,6 @@ +import Collaboration from "@tiptap/extension-collaboration"; +import * as Y from "yjs"; +import { WebrtcProvider } from "y-webrtc"; +const ydoc = new Y.Doc(); + +export { Collaboration, WebrtcProvider, ydoc }; diff --git a/examples/tiptap-collab-editing/package.json b/examples/tiptap-collab-editing/package.json new file mode 100644 index 00000000000..6e2a4d80bdc --- /dev/null +++ b/examples/tiptap-collab-editing/package.json @@ -0,0 +1,35 @@ +{ + "name": "remix-example-template", + "private": true, + "description": "", + "license": "", + "sideEffects": false, + "scripts": { + "build": "remix build", + "dev": "remix dev", + "start": "remix-serve build" + }, + "dependencies": { + "@remix-run/node": "1.3.5", + "@remix-run/react": "1.3.5", + "@remix-run/serve": "1.3.5", + "@tiptap/extension-collaboration": "^2.0.0-beta.33", + "@tiptap/react": "^2.0.0-beta.108", + "@tiptap/starter-kit": "^2.0.0-beta.183", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "y-webrtc": "^10.2.2", + "yjs": "^13.5.34" + }, + "devDependencies": { + "@remix-run/dev": "1.3.5", + "@remix-run/eslint-config": "1.3.5", + "@types/react": "^17.0.39", + "@types/react-dom": "^17.0.13", + "eslint": "^8.10.0", + "typescript": "^4.6.2" + }, + "engines": { + "node": ">=14" + } +} diff --git a/examples/tiptap-collab-editing/public/favicon.ico b/examples/tiptap-collab-editing/public/favicon.ico new file mode 100644 index 00000000000..8830cf6821b Binary files /dev/null and b/examples/tiptap-collab-editing/public/favicon.ico differ diff --git a/examples/tiptap-collab-editing/remix.config.js b/examples/tiptap-collab-editing/remix.config.js new file mode 100644 index 00000000000..1755894599a --- /dev/null +++ b/examples/tiptap-collab-editing/remix.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: [".*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", +}; diff --git a/examples/tiptap-collab-editing/remix.env.d.ts b/examples/tiptap-collab-editing/remix.env.d.ts new file mode 100644 index 00000000000..72e2affe311 --- /dev/null +++ b/examples/tiptap-collab-editing/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/tiptap-collab-editing/sandbox.config.json b/examples/tiptap-collab-editing/sandbox.config.json new file mode 100644 index 00000000000..4363d87a30d --- /dev/null +++ b/examples/tiptap-collab-editing/sandbox.config.json @@ -0,0 +1,6 @@ +{ + "hardReloadOnChange": true, + "container": { + "port": 3000 + } +} diff --git a/examples/tiptap-collab-editing/tsconfig.json b/examples/tiptap-collab-editing/tsconfig.json new file mode 100644 index 00000000000..749e4b959e8 --- /dev/null +++ b/examples/tiptap-collab-editing/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +}