diff --git a/.env b/.env index 7a1bb0c..60382a5 100644 --- a/.env +++ b/.env @@ -1,10 +1,10 @@ # Common Environment Variables -NEXT_PUBLIC_YORKIE_VERSION='0.5.5' -NEXT_PUBLIC_YORKIE_JS_VERSION='0.5.5' +NEXT_PUBLIC_YORKIE_VERSION='0.5.6' +NEXT_PUBLIC_YORKIE_JS_VERSION='0.5.6' NEXT_PUBLIC_YORKIE_IOS_VERSION='0.4.24' NEXT_PUBLIC_YORKIE_ANDROID_VERSION='0.4.24' NEXT_PUBLIC_DASHBOARD_PATH='/dashboard' -NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.5/yorkie-js-sdk.js' +NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.6/yorkie-js-sdk.js' NEXT_PUBLIC_STATUS_URL='https://stats.uptimerobot.com/otOOggryMR' # Development Environment Variables diff --git a/examples/nextjs-scheduler/fileInfo.ts b/examples/nextjs-scheduler/fileInfo.ts index 8364e70..5e49fda 100644 --- a/examples/nextjs-scheduler/fileInfo.ts +++ b/examples/nextjs-scheduler/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"nextjs-scheduler","path":"/","children":[{"isFile":false,"name":"app","path":"/app","children":[{"isFile":false,"name":"utils","path":"/app/utils","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"handlePeers.ts","path":"/app/utils/handlePeers.ts","content":"import { Indexable } from 'yorkie-js-sdk';\n\nconst randomPeers = [\n 'Alice',\n 'Bob',\n 'Carol',\n 'Chuck',\n 'Dave',\n 'Erin',\n 'Frank',\n 'Grace',\n 'Ivan',\n 'Justin',\n 'Matilda',\n 'Oscar',\n 'Steve',\n 'Victor',\n 'Zoe',\n];\n\n/**\n * display each peer's name\n */\nexport function displayPeers(\n peers: Array<{ clientID: string; presence: Indexable }>,\n) {\n const users = [];\n for (const { presence } of peers) {\n users.push(presence.userName);\n }\n\n return users;\n}\n\n/**\n * create random name of anonymous peer\n */\nexport function createRandomPeers() {\n const index = Math.floor(Math.random() * randomPeers.length);\n\n return randomPeers[index];\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"parseDate.ts","path":"/app/utils/parseDate.ts","content":"/**\n * transform date format to DD-MM-YYYY\n */\nexport function parseDate(date: Date) {\n let [month, day, year] = date.toLocaleDateString('en').split('/');\n\n month = Number(month) > 9 ? month : '0' + month;\n day = Number(day) > 9 ? day : '0' + day;\n year = year.slice(2);\n\n return `${day}-${month}-${year}`;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"types.ts","path":"/app/utils/types.ts","content":"export interface ENVtypes {\n url: string;\n apiKey: string;\n}\n\nexport interface ContentTypes {\n date: string;\n text: string;\n}\n\nexport interface EditorPropsTypes {\n content: Array;\n actions: { [name: string]: any };\n}\n\nexport type ChangeEventHandler = (\n event: React.ChangeEvent,\n) => void;\n\ntype ValuePiece = Date | any;\n\nexport type CalendarValue = ValuePiece | [ValuePiece, ValuePiece];\n"}]},{"isFile":false,"name":"styles","path":"/app/styles","children":[{"isFile":true,"isOpen":false,"language":"css","name":"calendar.css","path":"/app/styles/calendar.css","content":"/* custom css code */\n\n.react-calendar {\n width: 350px;\n max-width: 100%;\n background: white;\n border: 1px solid #a0a096;\n font-family: Arial, Helvetica, sans-serif;\n line-height: 1.125em;\n}\n\n.react-calendar--doubleView {\n width: 700px;\n}\n\n.react-calendar--doubleView .react-calendar__viewContainer {\n display: flex;\n margin: -0.5em;\n}\n\n.react-calendar--doubleView .react-calendar__viewContainer > * {\n width: 50%;\n margin: 0.5em;\n}\n\n.react-calendar,\n.react-calendar *,\n.react-calendar *:before,\n.react-calendar *:after {\n -moz-box-sizing: border-box;\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.react-calendar button {\n margin: 0;\n border: 0;\n outline: none;\n}\n\n.react-calendar button:enabled:hover {\n cursor: pointer;\n}\n\n.react-calendar__navigation {\n display: flex;\n height: 44px;\n margin-bottom: 1em;\n}\n\n.react-calendar__navigation button {\n min-width: 44px;\n background: none;\n}\n\n.react-calendar__navigation button:disabled {\n background-color: #f0f0f0;\n}\n\n.react-calendar__navigation button:enabled:hover,\n.react-calendar__navigation button:enabled:focus {\n background-color: #e6e6e6;\n}\n\n.react-calendar__month-view__weekdays {\n text-align: center;\n text-transform: uppercase;\n font-weight: bold;\n font-size: 0.75em;\n}\n\n.react-calendar__month-view__weekdays__weekday {\n padding: 0.5em;\n}\n\n.react-calendar__month-view__weekNumbers .react-calendar__tile {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n font-weight: bold;\n}\n\n.react-calendar__month-view__days__day--weekend {\n color: #d10000;\n}\n\n.react-calendar__month-view__days__day--neighboringMonth {\n color: #757575;\n}\n\n.react-calendar__year-view .react-calendar__tile,\n.react-calendar__decade-view .react-calendar__tile,\n.react-calendar__century-view .react-calendar__tile {\n padding: 2em 0.5em;\n}\n\n.react-calendar__tile {\n max-width: 100%;\n padding: 10px 6.6667px;\n background: none;\n text-align: center;\n line-height: 16px;\n}\n\n.react-calendar__tile:disabled {\n background-color: #f0f0f0;\n}\n\n.react-calendar__tile:enabled:hover,\n.react-calendar__tile:enabled:focus {\n background-color: #e6e6e6;\n}\n\n.react-calendar__tile--now {\n background: #ffff76;\n}\n\n.react-calendar__tile--now:enabled:hover,\n.react-calendar__tile--now:enabled:focus {\n background: #ffffa9;\n}\n\n.react-calendar__tile--hasActive {\n background: #76baff;\n}\n\n.react-calendar__tile--hasActive:enabled:hover,\n.react-calendar__tile--hasActive:enabled:focus {\n background: #a9d4ff;\n}\n\n.react-calendar__tile--active {\n background: #006edc;\n color: white;\n}\n\n.highlight {\n background-color: #00887a;\n color: #f0f3f5;\n}\n\n.react-calendar__tile--active:enabled:hover,\n.react-calendar__tile--active:enabled:focus {\n background: #1087ff;\n}\n\n.react-calendar--selectRange .react-calendar__tile--hover {\n background-color: #e6e6e6;\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"globals.css","path":"/app/styles/globals.css","content":"body {\n display: flex;\n padding: 1rem;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n font-size: 17px;\n color: #2f2f2f;\n background-color: #cccccc;\n}\n\ninput {\n width: 22rem;\n height: 3.5rem;\n outline: none;\n margin-left: 1rem;\n border: none;\n font-size: 20px;\n}\n\ntextarea {\n resize: none;\n outline: none;\n font-size: 17px;\n}\n\n.button {\n font-size: 17px;\n cursor: pointer;\n border: none;\n padding: 1rem 2rem 1rem 2rem;\n color: #f0f3f5;\n background-color: #00887a;\n}\n.button:hover {\n background-color: #00557a;\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"page.module.css","path":"/app/styles/page.module.css","content":".main {\n width: 340px;\n}\n\n.textArea {\n width: 100%;\n height: 8rem;\n}\n\n.memo {\n width: 100%;\n min-height: 1rem;\n border-top: 1px solid #2f2f2f;\n border-bottom: 1px solid #2f2f2f;\n word-wrap: break-word;\n}\n\n.inputForm_editor {\n margin-top: 3rem;\n}\n"}]},{"isFile":true,"isOpen":false,"language":"tsx","name":"Scheduler.tsx","path":"/app/Scheduler.tsx","content":"'use client';\n\nimport React, { useState } from 'react';\nimport './styles/calendar.css';\nimport styles from './styles/page.module.css';\n\nimport { EditorPropsTypes, CalendarValue } from './utils/types';\nimport { parseDate } from './utils/parseDate';\nimport Calendar from 'react-calendar';\n\n/**\n * handle calendar component\n */\nexport default function Scheduler(props: EditorPropsTypes) {\n const { content, actions } = props;\n const [date, onChange] = useState(new Date());\n const [text, setText] = useState('Enter text here!');\n\n const currentDate = date ? parseDate(new Date(date.toString())) : '';\n\n const eventHandler = (event: string) => {\n let flag = false;\n switch (event) {\n case 'PUSH':\n flag = false;\n content.forEach((item) => {\n if (item.date === currentDate) {\n flag = !flag;\n return 0;\n }\n });\n\n flag\n ? actions.updateContent(currentDate, text)\n : actions.addContent(currentDate, text);\n\n setText('Enter text here!');\n break;\n case 'DELETE':\n actions.deleteContent(currentDate);\n break;\n }\n };\n\n return (\n
\n
\n \n date.toLocaleString('en', { day: 'numeric' })\n }\n tileClassName={({ date }) =>\n content.find((item) => item.date === parseDate(date))\n ? 'highlight'\n : ''\n }\n />\n

selected day : {currentDate}

\n
\n {content.map((item, i: number) => {\n if (item.date === currentDate) {\n return

{item.text}

;\n }\n })}\n
\n
\n

input form

\n ) =>\n setText(e.target.value)\n }\n />\n
\n \n \n
\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/app/favicon.ico","content":""},{"isFile":true,"isOpen":false,"language":"tsx","name":"layout.tsx","path":"/app/layout.tsx","content":"import './styles/globals.css';\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n title: 'Next.js react-calendar example',\n description: 'example of yorkie-js-sdk with next.js & react-calendar',\n icons: {\n icon: './favicon.ico',\n },\n};\n\n/**\n * default root layout of service\n */\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"not-found.tsx","path":"/app/not-found.tsx","content":"/**\n * 404-not found\n */\nexport default function notFound() {\n return

404 not found

;\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"page.tsx","path":"/app/page.tsx","content":"/**\n * yorkie-js-sdk must be loaded on client-side\n */\n'use client';\n\nimport styles from './styles/page.module.css';\nimport React, { useEffect, useState } from 'react';\n\nimport { ContentTypes, ENVtypes } from './utils/types';\nimport { displayPeers, createRandomPeers } from './utils/handlePeers';\nimport { parseDate } from './utils/parseDate';\nimport yorkie, { Document, JSONArray, DocEventType } from 'yorkie-js-sdk';\nimport Scheduler from './Scheduler';\n\n// parseDate() value's format = \"DD-MM-YYYY\"\nconst defaultContent: JSONArray = [\n {\n date: parseDate(new Date()).replace(/^\\d{2}/, '01'),\n text: 'payday',\n },\n {\n date: parseDate(new Date()).replace(/^\\d{2}/, '17'),\n text: \"Garry's birthday\",\n },\n];\n\nconst ENV: ENVtypes = {\n url: process.env.NEXT_PUBLIC_YORKIE_API_ADDR!,\n apiKey: process.env.NEXT_PUBLIC_YORKIE_API_KEY!,\n};\n\nconst documentKey = `next.js-Scheduler-${parseDate(new Date())}`;\n\n/**\n * main page\n */\nexport default function Editor() {\n const [peers, setPeers] = useState>([]);\n const [content, setContent] = useState>(defaultContent);\n\n // create Yorkie Document with useState value\n const [doc] = useState }>>(\n () =>\n new yorkie.Document<{ content: JSONArray }>(documentKey),\n );\n\n const actions = {\n // push new content to Yorkie's database\n addContent(date: string, text: string) {\n doc.update((root) => {\n root.content.push({ date, text });\n });\n },\n\n // delete selected content at Yorkie's database\n deleteContent(date: string) {\n doc.update((root) => {\n let target;\n for (const item of root.content) {\n if (item.date === date) {\n target = item as any;\n break;\n }\n }\n\n if (target) {\n root.content.deleteByID!(target.getID());\n }\n });\n },\n\n // edit selected content at Yorkie's database\n updateContent(date: string, text: string) {\n doc.update((root) => {\n let target;\n for (const item of root.content) {\n if (item.date === date) {\n target = item;\n break;\n }\n }\n\n if (target) {\n target.text = text;\n }\n });\n },\n };\n\n useEffect(() => {\n // create Yorkie Client at client-side\n const client = new yorkie.Client(ENV.url, {\n apiKey: ENV.apiKey,\n });\n\n // subscribe document event of \"PresenceChanged\"(=\"peers-changed\")\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n setPeers(displayPeers(doc.getPresences()));\n }\n });\n\n /**\n * `attachDoc` is a helper function to attach the document into the client.\n */\n async function attachDoc(\n doc: Document<{ content: JSONArray }>,\n callback: (props: any) => void,\n ) {\n // 01. activate client\n await client.activate();\n // 02. attach the document into the client with presence\n await client.attach(doc, {\n initialPresence: {\n userName: createRandomPeers(),\n },\n });\n\n // 03. create default content if not exists.\n doc.update((root) => {\n if (!root.content) {\n root.content = defaultContent;\n }\n }, 'create default content if not exists');\n\n // 04. subscribe doc's change event from local and remote.\n doc.subscribe(() => {\n callback(doc.getRoot().content);\n });\n\n // 05. set content to the attached document.\n callback(doc.getRoot().content);\n }\n\n attachDoc(doc, (content) => setContent(content));\n }, []);\n\n return (\n
\n

\n peers : [\n {peers.map((man: string, i: number) => {\n return {man}, ;\n })}{' '}\n ]\n

\n \n
\n );\n}\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"NEXT_PUBLIC_YORKIE_API_ADDR='http://localhost:8080'\nNEXT_PUBLIC_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n extends: ['next', 'plugin:prettier/recommended'],\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\n '@next/next/no-html-link-for-pages': 'off',\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Next.js scheduler Example\n\n

\n \n \"Live\n \n

\n\n\"Next.js\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm nextjs-scheduler dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"next.config.js","path":"/next.config.js","content":"/** @type {import('next').NextConfig} */\nconst nextConfig = {\n output: 'export',\n distDir: 'dist',\n basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',\n assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '',\n reactStrictMode: false,\n};\n\nmodule.exports = nextConfig;\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"nextjs-scheduler\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev -p 5174\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"14.1.3\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.2.0\",\n \"@types/react-dom\": \"18.2.0\",\n \"eslint-config-next\": \"^14.2.5\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"prettier\": \"^3.3.3\",\n \"typescript\": \"5.3.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": false,\n \"forceConsistentCasingInFileNames\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"plugins\": [\n {\n \"name\": \"next\"\n }\n ],\n \"paths\": {\n \"@/*\": [\"./*\"],\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"],\n \"react\": [\"./node_modules/@types/react\"]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\"node_modules\"]\n}\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"nextjs-scheduler","path":"/","children":[{"isFile":false,"name":"app","path":"/app","children":[{"isFile":false,"name":"utils","path":"/app/utils","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"handlePeers.ts","path":"/app/utils/handlePeers.ts","content":"import { Indexable } from 'yorkie-js-sdk';\n\nconst randomPeers = [\n 'Alice',\n 'Bob',\n 'Carol',\n 'Chuck',\n 'Dave',\n 'Erin',\n 'Frank',\n 'Grace',\n 'Ivan',\n 'Justin',\n 'Matilda',\n 'Oscar',\n 'Steve',\n 'Victor',\n 'Zoe',\n];\n\n/**\n * display each peer's name\n */\nexport function displayPeers(\n peers: Array<{ clientID: string; presence: Indexable }>,\n) {\n const users = [];\n for (const { presence } of peers) {\n users.push(presence.userName);\n }\n\n return users;\n}\n\n/**\n * create random name of anonymous peer\n */\nexport function createRandomPeers() {\n const index = Math.floor(Math.random() * randomPeers.length);\n\n return randomPeers[index];\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"parseDate.ts","path":"/app/utils/parseDate.ts","content":"/**\n * transform date format to DD-MM-YYYY\n */\nexport function parseDate(date: Date) {\n let [month, day, year] = date.toLocaleDateString('en').split('/');\n\n month = Number(month) > 9 ? month : '0' + month;\n day = Number(day) > 9 ? day : '0' + day;\n year = year.slice(2);\n\n return `${day}-${month}-${year}`;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"types.ts","path":"/app/utils/types.ts","content":"export interface ENVtypes {\n url: string;\n apiKey: string;\n}\n\nexport interface ContentTypes {\n date: string;\n text: string;\n}\n\nexport interface EditorPropsTypes {\n content: Array;\n actions: { [name: string]: any };\n}\n\nexport type ChangeEventHandler = (\n event: React.ChangeEvent,\n) => void;\n\ntype ValuePiece = Date | any;\n\nexport type CalendarValue = ValuePiece | [ValuePiece, ValuePiece];\n"}]},{"isFile":false,"name":"styles","path":"/app/styles","children":[{"isFile":true,"isOpen":false,"language":"css","name":"calendar.css","path":"/app/styles/calendar.css","content":"/* custom css code */\n\n.react-calendar {\n width: 350px;\n max-width: 100%;\n background: white;\n border: 1px solid #a0a096;\n font-family: Arial, Helvetica, sans-serif;\n line-height: 1.125em;\n}\n\n.react-calendar--doubleView {\n width: 700px;\n}\n\n.react-calendar--doubleView .react-calendar__viewContainer {\n display: flex;\n margin: -0.5em;\n}\n\n.react-calendar--doubleView .react-calendar__viewContainer > * {\n width: 50%;\n margin: 0.5em;\n}\n\n.react-calendar,\n.react-calendar *,\n.react-calendar *:before,\n.react-calendar *:after {\n -moz-box-sizing: border-box;\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.react-calendar button {\n margin: 0;\n border: 0;\n outline: none;\n}\n\n.react-calendar button:enabled:hover {\n cursor: pointer;\n}\n\n.react-calendar__navigation {\n display: flex;\n height: 44px;\n margin-bottom: 1em;\n}\n\n.react-calendar__navigation button {\n min-width: 44px;\n background: none;\n}\n\n.react-calendar__navigation button:disabled {\n background-color: #f0f0f0;\n}\n\n.react-calendar__navigation button:enabled:hover,\n.react-calendar__navigation button:enabled:focus {\n background-color: #e6e6e6;\n}\n\n.react-calendar__month-view__weekdays {\n text-align: center;\n text-transform: uppercase;\n font-weight: bold;\n font-size: 0.75em;\n}\n\n.react-calendar__month-view__weekdays__weekday {\n padding: 0.5em;\n}\n\n.react-calendar__month-view__weekNumbers .react-calendar__tile {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.75em;\n font-weight: bold;\n}\n\n.react-calendar__month-view__days__day--weekend {\n color: #d10000;\n}\n\n.react-calendar__month-view__days__day--neighboringMonth {\n color: #757575;\n}\n\n.react-calendar__year-view .react-calendar__tile,\n.react-calendar__decade-view .react-calendar__tile,\n.react-calendar__century-view .react-calendar__tile {\n padding: 2em 0.5em;\n}\n\n.react-calendar__tile {\n max-width: 100%;\n padding: 10px 6.6667px;\n background: none;\n text-align: center;\n line-height: 16px;\n}\n\n.react-calendar__tile:disabled {\n background-color: #f0f0f0;\n}\n\n.react-calendar__tile:enabled:hover,\n.react-calendar__tile:enabled:focus {\n background-color: #e6e6e6;\n}\n\n.react-calendar__tile--now {\n background: #ffff76;\n}\n\n.react-calendar__tile--now:enabled:hover,\n.react-calendar__tile--now:enabled:focus {\n background: #ffffa9;\n}\n\n.react-calendar__tile--hasActive {\n background: #76baff;\n}\n\n.react-calendar__tile--hasActive:enabled:hover,\n.react-calendar__tile--hasActive:enabled:focus {\n background: #a9d4ff;\n}\n\n.react-calendar__tile--active {\n background: #006edc;\n color: white;\n}\n\n.highlight {\n background-color: #00887a;\n color: #f0f3f5;\n}\n\n.react-calendar__tile--active:enabled:hover,\n.react-calendar__tile--active:enabled:focus {\n background: #1087ff;\n}\n\n.react-calendar--selectRange .react-calendar__tile--hover {\n background-color: #e6e6e6;\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"globals.css","path":"/app/styles/globals.css","content":"body {\n display: flex;\n padding: 1rem;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n font-size: 17px;\n color: #2f2f2f;\n background-color: #cccccc;\n}\n\ninput {\n width: 22rem;\n height: 3.5rem;\n outline: none;\n margin-left: 1rem;\n border: none;\n font-size: 20px;\n}\n\ntextarea {\n resize: none;\n outline: none;\n font-size: 17px;\n}\n\n.button {\n font-size: 17px;\n cursor: pointer;\n border: none;\n padding: 1rem 2rem 1rem 2rem;\n color: #f0f3f5;\n background-color: #00887a;\n}\n.button:hover {\n background-color: #00557a;\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"page.module.css","path":"/app/styles/page.module.css","content":".main {\n width: 340px;\n}\n\n.textArea {\n width: 100%;\n height: 8rem;\n}\n\n.memo {\n width: 100%;\n min-height: 1rem;\n border-top: 1px solid #2f2f2f;\n border-bottom: 1px solid #2f2f2f;\n word-wrap: break-word;\n}\n\n.inputForm_editor {\n margin-top: 3rem;\n}\n"}]},{"isFile":true,"isOpen":false,"language":"tsx","name":"Scheduler.tsx","path":"/app/Scheduler.tsx","content":"'use client';\n\nimport React, { useState } from 'react';\nimport './styles/calendar.css';\nimport styles from './styles/page.module.css';\n\nimport { EditorPropsTypes, CalendarValue } from './utils/types';\nimport { parseDate } from './utils/parseDate';\nimport Calendar from 'react-calendar';\n\n/**\n * handle calendar component\n */\nexport default function Scheduler(props: EditorPropsTypes) {\n const { content, actions } = props;\n const [date, onChange] = useState(new Date());\n const [text, setText] = useState('Enter text here!');\n\n const currentDate = date ? parseDate(new Date(date.toString())) : '';\n\n const eventHandler = (event: string) => {\n let flag = false;\n switch (event) {\n case 'PUSH':\n flag = false;\n content.forEach((item) => {\n if (item.date === currentDate) {\n flag = !flag;\n return 0;\n }\n });\n\n flag\n ? actions.updateContent(currentDate, text)\n : actions.addContent(currentDate, text);\n\n setText('Enter text here!');\n break;\n case 'DELETE':\n actions.deleteContent(currentDate);\n break;\n }\n };\n\n return (\n
\n
\n \n date.toLocaleString('en', { day: 'numeric' })\n }\n tileClassName={({ date }) =>\n content.find((item) => item.date === parseDate(date))\n ? 'highlight'\n : ''\n }\n />\n

selected day : {currentDate}

\n
\n {content.map((item, i: number) => {\n if (item.date === currentDate) {\n return

{item.text}

;\n }\n })}\n
\n
\n

input form

\n ) =>\n setText(e.target.value)\n }\n />\n
\n \n \n
\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/app/favicon.ico","content":""},{"isFile":true,"isOpen":false,"language":"tsx","name":"layout.tsx","path":"/app/layout.tsx","content":"import './styles/globals.css';\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n title: 'Next.js react-calendar example',\n description: 'example of yorkie-js-sdk with next.js & react-calendar',\n icons: {\n icon: './favicon.ico',\n },\n};\n\n/**\n * default root layout of service\n */\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"not-found.tsx","path":"/app/not-found.tsx","content":"/**\n * 404-not found\n */\nexport default function notFound() {\n return

404 not found

;\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"page.tsx","path":"/app/page.tsx","content":"/**\n * yorkie-js-sdk must be loaded on client-side\n */\n'use client';\n\nimport styles from './styles/page.module.css';\nimport React, { useEffect, useState } from 'react';\n\nimport { ContentTypes, ENVtypes } from './utils/types';\nimport { displayPeers, createRandomPeers } from './utils/handlePeers';\nimport { parseDate } from './utils/parseDate';\nimport yorkie, { Document, JSONArray, DocEventType } from 'yorkie-js-sdk';\nimport Scheduler from './Scheduler';\n\n// parseDate() value's format = \"DD-MM-YYYY\"\nconst defaultContent: JSONArray = [\n {\n date: parseDate(new Date()).replace(/^\\d{2}/, '01'),\n text: 'payday',\n },\n {\n date: parseDate(new Date()).replace(/^\\d{2}/, '17'),\n text: \"Garry's birthday\",\n },\n];\n\nconst ENV: ENVtypes = {\n url: process.env.NEXT_PUBLIC_YORKIE_API_ADDR!,\n apiKey: process.env.NEXT_PUBLIC_YORKIE_API_KEY!,\n};\n\nconst documentKey = `next.js-Scheduler-${parseDate(new Date())}`;\n\n/**\n * main page\n */\nexport default function Editor() {\n const [peers, setPeers] = useState>([]);\n const [content, setContent] = useState>(defaultContent);\n\n // create Yorkie Document with useState value\n const [doc] = useState }>>(\n () =>\n new yorkie.Document<{ content: JSONArray }>(documentKey),\n );\n\n const actions = {\n // push new content to Yorkie's database\n addContent(date: string, text: string) {\n doc.update((root) => {\n root.content.push({ date, text });\n });\n },\n\n // delete selected content at Yorkie's database\n deleteContent(date: string) {\n doc.update((root) => {\n let target;\n for (const item of root.content) {\n if (item.date === date) {\n target = item as any;\n break;\n }\n }\n\n if (target) {\n root.content.deleteByID!(target.getID());\n }\n });\n },\n\n // edit selected content at Yorkie's database\n updateContent(date: string, text: string) {\n doc.update((root) => {\n let target;\n for (const item of root.content) {\n if (item.date === date) {\n target = item;\n break;\n }\n }\n\n if (target) {\n target.text = text;\n }\n });\n },\n };\n\n useEffect(() => {\n // create Yorkie Client at client-side\n const client = new yorkie.Client(ENV.url, {\n apiKey: ENV.apiKey,\n });\n\n // subscribe document event of \"PresenceChanged\"(=\"peers-changed\")\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n setPeers(displayPeers(doc.getPresences()));\n }\n });\n\n /**\n * `attachDoc` is a helper function to attach the document into the client.\n */\n async function attachDoc(\n doc: Document<{ content: JSONArray }>,\n callback: (props: any) => void,\n ) {\n // 01. activate client\n await client.activate();\n // 02. attach the document into the client with presence\n await client.attach(doc, {\n initialPresence: {\n userName: createRandomPeers(),\n },\n });\n\n // 03. create default content if not exists.\n doc.update((root) => {\n if (!root.content) {\n root.content = defaultContent;\n }\n }, 'create default content if not exists');\n\n // 04. subscribe doc's change event from local and remote.\n doc.subscribe(() => {\n callback(doc.getRoot().content);\n });\n\n // 05. set content to the attached document.\n callback(doc.getRoot().content);\n }\n\n attachDoc(doc, (content) => setContent(content));\n }, []);\n\n return (\n
\n

\n peers : [\n {peers.map((man: string, i: number) => {\n return {man}, ;\n })}{' '}\n ]\n

\n \n
\n );\n}\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"NEXT_PUBLIC_YORKIE_API_ADDR='http://localhost:8080'\nNEXT_PUBLIC_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n extends: ['next', 'plugin:prettier/recommended'],\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\n '@next/next/no-html-link-for-pages': 'off',\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Next.js scheduler Example\n\n

\n \n \"Live\n \n

\n\n\"Next.js\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm nextjs-scheduler dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"next.config.js","path":"/next.config.js","content":"/** @type {import('next').NextConfig} */\nconst nextConfig = {\n output: 'export',\n distDir: 'dist',\n basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',\n assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '',\n reactStrictMode: false,\n};\n\nmodule.exports = nextConfig;\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"nextjs-scheduler\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev -p 5174\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"14.1.3\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.2.0\",\n \"@types/react-dom\": \"18.2.0\",\n \"eslint-config-next\": \"^14.2.5\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"prettier\": \"^3.3.3\",\n \"typescript\": \"5.3.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": false,\n \"forceConsistentCasingInFileNames\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"plugins\": [\n {\n \"name\": \"next\"\n }\n ],\n \"paths\": {\n \"@/*\": [\"./*\"],\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"],\n \"react\": [\"./node_modules/@types/react\"]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\"node_modules\"]\n}\n"}]} \ No newline at end of file diff --git a/examples/profile-stack/fileInfo.ts b/examples/profile-stack/fileInfo.ts index c356794..b56d4f9 100644 --- a/examples/profile-stack/fileInfo.ts +++ b/examples/profile-stack/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"profile-stack","path":"/","children":[{"isFile":false,"name":"public","path":"/public","children":[{"isFile":false,"name":"images","path":"/public/images","children":[{"isFile":true,"isOpen":false,"language":"svg","name":"profile-blue.svg","path":"/public/images/profile-blue.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-green.svg","path":"/public/images/profile-green.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-orange.svg","path":"/public/images/profile-orange.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-purple.svg","path":"/public/images/profile-purple.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-red.svg","path":"/public/images/profile-red.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-yellow.svg","path":"/public/images/profile-yellow.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"}]},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/public/favicon.ico","content":""}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Profile Stack Example\n\n

\n \n \"Live\n \n

\n\n\"Profile\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm profile-stack dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Profile Stack - Yorkie Example\n \n \n \n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"main.js","path":"/main.js","content":"import yorkie, { DocEventType } from 'yorkie-js-sdk';\nimport { getRandomName, getRandomColor } from './util.js';\n\nasync function main() {\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n const doc = new yorkie.Document('profile-stack', {\n enableDevtools: true,\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeerList(doc.getPresences(), client.getID());\n }\n });\n await client.attach(doc, {\n // set the client's name and color to presence.\n initialPresence: {\n name: getRandomName(),\n color: getRandomColor(),\n },\n });\n\n window.addEventListener('beforeunload', () => {\n client.deactivate();\n });\n}\n\nconst MAX_PEER_VIEW = 4;\nconst createPeer = (name, color, type) => {\n const $peer = document.createElement('div');\n $peer.className = 'peer';\n\n if (type === 'main') {\n $peer.innerHTML = `\n
\n \"profile\"\n
\n
${name}
\n `;\n } else if (type === 'more') {\n $peer.innerHTML = `\n \"profile\"\n ${name}\n `;\n }\n return $peer;\n};\n\nconst displayPeerList = (peers, myClientID) => {\n const peerList = peers.filter(\n ({ clientID: id, presence }) =>\n id !== myClientID && presence.name && presence.color,\n );\n const peerCount = peerList.length + 1;\n const hasMorePeers = peerCount > MAX_PEER_VIEW;\n const $peerList = document.getElementById('peerList');\n $peerList.innerHTML = '';\n const $peerMoreList = document.createElement('div');\n $peerMoreList.className = 'peer-more-list speech-bubbles';\n\n const myPresence = peers.find(\n ({ clientID: id }) => id === myClientID,\n ).presence;\n const $me = createPeer(`${myPresence.name} (me)`, myPresence.color, 'main');\n $me.classList.add('me');\n $peerList.appendChild($me);\n peerList.forEach((peer, i) => {\n const { name, color } = peer.presence;\n if (i < MAX_PEER_VIEW - 1) {\n const $peer = createPeer(name, color, 'main');\n $peerList.appendChild($peer);\n return;\n }\n const $peer = createPeer(name, color, 'more');\n $peerMoreList.appendChild($peer);\n });\n\n if (hasMorePeers) {\n const $peer = document.createElement('div');\n $peer.className = 'peer more';\n $peer.innerHTML = `\n
\n +${peerCount - MAX_PEER_VIEW}\n
\n `;\n $peer.appendChild($peerMoreList);\n $peerList.appendChild($peer);\n }\n};\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"profile-stack\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"yorkie-js-sdk\": \"^0.5.5\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/style.css","content":"* {\n margin: 0;\n padding: 0;\n}\n\nbody {\n --light-gray: #f5f3f1;\n --gray: #c2bdba;\n --black: #332e2b;\n --white: #fefdfb;\n\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n color: var(--black);\n}\n\nimg {\n vertical-align: top;\n}\n\n*::-webkit-scrollbar {\n width: 10px;\n height: 4px;\n}\n\n*::-webkit-scrollbar-thumb {\n background: var(--gray);\n border-radius: 10px;\n border: 3px solid var(--white);\n}\n\n*::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.speech-bubbles {\n padding: 16px;\n border: 1px solid var(--gray);\n border-radius: 16px;\n}\n\n.speech-bubbles:before {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -6px;\n margin-top: -8px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 6px solid transparent;\n border-right: 6px solid transparent;\n border-bottom: 8px solid var(--gray);\n}\n\n.speech-bubbles:after {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -5px;\n margin-top: -6px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n border-bottom: 7px solid var(--white);\n}\n\n#peerList {\n display: inline-flex;\n border: 1px solid var(--gray);\n border-radius: 100px;\n white-space: nowrap;\n}\n\n.peer {\n position: relative;\n margin: 12px;\n}\n\n.profile-img {\n width: 52px;\n cursor: pointer;\n}\n\n.peer.me {\n order: -1;\n}\n\n.peer .name {\n font-weight: 900;\n white-space: nowrap;\n}\n\n.peer .speech-bubbles {\n display: none;\n position: absolute;\n top: 80px;\n left: 50%;\n transform: translate(-50%);\n background: var(--white);\n}\n\n.peer:hover .speech-bubbles {\n display: block;\n}\n\n.peer.more {\n display: flex;\n justify-content: center;\n align-items: center;\n width: 52px;\n height: 52px;\n background: var(--light-gray);\n border-radius: 100%;\n font-weight: 900;\n font-size: 24px;\n cursor: pointer;\n}\n\n.peer-more-list {\n font-size: 16px;\n}\n\n.peer-more-list .peer {\n display: flex;\n align-items: center;\n margin: 0 0 12px 0;\n}\n\n.peer-more-list .peer:last-child {\n margin-bottom: 0;\n}\n\n.peer-more-list .profile-img {\n margin-right: 8px;\n width: 26px;\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"util.js","path":"/util.js","content":"const NAMES = [\n 'Ali',\n 'Beatriz',\n 'Charles',\n 'Diya',\n 'Eric',\n 'Fatima',\n 'Gabriel',\n 'Hanna',\n 'Johnson',\n 'Perry',\n 'Parker',\n 'Kelly',\n];\nexport const getRandomName = () => {\n const index = Math.floor(Math.random() * NAMES.length);\n return NAMES[index];\n};\n\nconst COLORS = ['red', 'yellow', 'orange', 'green', 'blue', 'purple'];\nexport const getRandomColor = () => {\n const index = Math.floor(Math.random() * COLORS.length);\n return COLORS[index];\n};\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"profile-stack","path":"/","children":[{"isFile":false,"name":"public","path":"/public","children":[{"isFile":false,"name":"images","path":"/public/images","children":[{"isFile":true,"isOpen":false,"language":"svg","name":"profile-blue.svg","path":"/public/images/profile-blue.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-green.svg","path":"/public/images/profile-green.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-orange.svg","path":"/public/images/profile-orange.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-purple.svg","path":"/public/images/profile-purple.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-red.svg","path":"/public/images/profile-red.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-yellow.svg","path":"/public/images/profile-yellow.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n"}]},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/public/favicon.ico","content":""}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Profile Stack Example\n\n

\n \n \"Live\n \n

\n\n\"Profile\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm profile-stack dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Profile Stack - Yorkie Example\n \n \n \n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"main.js","path":"/main.js","content":"import yorkie, { DocEventType } from 'yorkie-js-sdk';\nimport { getRandomName, getRandomColor } from './util.js';\n\nasync function main() {\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n const doc = new yorkie.Document('profile-stack', {\n enableDevtools: true,\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeerList(doc.getPresences(), client.getID());\n }\n });\n await client.attach(doc, {\n // set the client's name and color to presence.\n initialPresence: {\n name: getRandomName(),\n color: getRandomColor(),\n },\n });\n\n window.addEventListener('beforeunload', () => {\n client.deactivate();\n });\n}\n\nconst MAX_PEER_VIEW = 4;\nconst createPeer = (name, color, type) => {\n const $peer = document.createElement('div');\n $peer.className = 'peer';\n\n if (type === 'main') {\n $peer.innerHTML = `\n
\n \"profile\"\n
\n
${name}
\n `;\n } else if (type === 'more') {\n $peer.innerHTML = `\n \"profile\"\n ${name}\n `;\n }\n return $peer;\n};\n\nconst displayPeerList = (peers, myClientID) => {\n const peerList = peers.filter(\n ({ clientID: id, presence }) =>\n id !== myClientID && presence.name && presence.color,\n );\n const peerCount = peerList.length + 1;\n const hasMorePeers = peerCount > MAX_PEER_VIEW;\n const $peerList = document.getElementById('peerList');\n $peerList.innerHTML = '';\n const $peerMoreList = document.createElement('div');\n $peerMoreList.className = 'peer-more-list speech-bubbles';\n\n const myPresence = peers.find(\n ({ clientID: id }) => id === myClientID,\n ).presence;\n const $me = createPeer(`${myPresence.name} (me)`, myPresence.color, 'main');\n $me.classList.add('me');\n $peerList.appendChild($me);\n peerList.forEach((peer, i) => {\n const { name, color } = peer.presence;\n if (i < MAX_PEER_VIEW - 1) {\n const $peer = createPeer(name, color, 'main');\n $peerList.appendChild($peer);\n return;\n }\n const $peer = createPeer(name, color, 'more');\n $peerMoreList.appendChild($peer);\n });\n\n if (hasMorePeers) {\n const $peer = document.createElement('div');\n $peer.className = 'peer more';\n $peer.innerHTML = `\n
\n +${peerCount - MAX_PEER_VIEW}\n
\n `;\n $peer.appendChild($peerMoreList);\n $peerList.appendChild($peer);\n }\n};\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"profile-stack\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"yorkie-js-sdk\": \"^0.5.6\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/style.css","content":"* {\n margin: 0;\n padding: 0;\n}\n\nbody {\n --light-gray: #f5f3f1;\n --gray: #c2bdba;\n --black: #332e2b;\n --white: #fefdfb;\n\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n color: var(--black);\n}\n\nimg {\n vertical-align: top;\n}\n\n*::-webkit-scrollbar {\n width: 10px;\n height: 4px;\n}\n\n*::-webkit-scrollbar-thumb {\n background: var(--gray);\n border-radius: 10px;\n border: 3px solid var(--white);\n}\n\n*::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.speech-bubbles {\n padding: 16px;\n border: 1px solid var(--gray);\n border-radius: 16px;\n}\n\n.speech-bubbles:before {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -6px;\n margin-top: -8px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 6px solid transparent;\n border-right: 6px solid transparent;\n border-bottom: 8px solid var(--gray);\n}\n\n.speech-bubbles:after {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -5px;\n margin-top: -6px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n border-bottom: 7px solid var(--white);\n}\n\n#peerList {\n display: inline-flex;\n border: 1px solid var(--gray);\n border-radius: 100px;\n white-space: nowrap;\n}\n\n.peer {\n position: relative;\n margin: 12px;\n}\n\n.profile-img {\n width: 52px;\n cursor: pointer;\n}\n\n.peer.me {\n order: -1;\n}\n\n.peer .name {\n font-weight: 900;\n white-space: nowrap;\n}\n\n.peer .speech-bubbles {\n display: none;\n position: absolute;\n top: 80px;\n left: 50%;\n transform: translate(-50%);\n background: var(--white);\n}\n\n.peer:hover .speech-bubbles {\n display: block;\n}\n\n.peer.more {\n display: flex;\n justify-content: center;\n align-items: center;\n width: 52px;\n height: 52px;\n background: var(--light-gray);\n border-radius: 100%;\n font-weight: 900;\n font-size: 24px;\n cursor: pointer;\n}\n\n.peer-more-list {\n font-size: 16px;\n}\n\n.peer-more-list .peer {\n display: flex;\n align-items: center;\n margin: 0 0 12px 0;\n}\n\n.peer-more-list .peer:last-child {\n margin-bottom: 0;\n}\n\n.peer-more-list .profile-img {\n margin-right: 8px;\n width: 26px;\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"util.js","path":"/util.js","content":"const NAMES = [\n 'Ali',\n 'Beatriz',\n 'Charles',\n 'Diya',\n 'Eric',\n 'Fatima',\n 'Gabriel',\n 'Hanna',\n 'Johnson',\n 'Perry',\n 'Parker',\n 'Kelly',\n];\nexport const getRandomName = () => {\n const index = Math.floor(Math.random() * NAMES.length);\n return NAMES[index];\n};\n\nconst COLORS = ['red', 'yellow', 'orange', 'green', 'blue', 'purple'];\nexport const getRandomColor = () => {\n const index = Math.floor(Math.random() * COLORS.length);\n return COLORS[index];\n};\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/react-tldraw/fileInfo.ts b/examples/react-tldraw/fileInfo.ts index 45e00ab..4479e57 100644 --- a/examples/react-tldraw/fileInfo.ts +++ b/examples/react-tldraw/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"react-tldraw","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"hooks","path":"/src/hooks","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"types.ts","path":"/src/hooks/types.ts","content":"// Yorkie type for typescript\nimport type { TDAsset, TDBinding, TDShape, TDUser } from '@tldraw/tldraw';\nimport type { JSONObject } from 'yorkie-js-sdk';\nexport type Options = {\n apiKey?: string;\n syncLoopDuration: number;\n reconnectStreamDelay: number;\n};\n\nexport type YorkieDocType = {\n shapes: JSONObject>>;\n bindings: JSONObject>>;\n assets: JSONObject>>;\n};\n\nexport type YorkiePresenceType = {\n tdUser: TDUser;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"useMultiplayerState.ts","path":"/src/hooks/useMultiplayerState.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n TDUserStatus,\n TDAsset,\n TDBinding,\n TDShape,\n TDUser,\n TldrawApp,\n} from '@tldraw/tldraw';\nimport { useThrottleCallback } from '@react-hook/throttle';\nimport * as yorkie from 'yorkie-js-sdk';\nimport randomColor from 'randomcolor';\nimport { uniqueNamesGenerator, names } from 'unique-names-generator';\nimport _ from 'lodash';\n\nimport type { Options, YorkieDocType, YorkiePresenceType } from './types';\n\n// Yorkie Client declaration\nlet client: yorkie.Client;\n\n// Yorkie Document declaration\nlet doc: yorkie.Document;\n\nexport function useMultiplayerState(roomId: string) {\n const [app, setApp] = useState();\n const [loading, setLoading] = useState(true);\n\n // Callbacks --------------\n\n const onMount = useCallback(\n (app: TldrawApp) => {\n app.loadRoom(roomId);\n app.setIsLoading(true);\n app.pause();\n setApp(app);\n\n const randomName = uniqueNamesGenerator({\n dictionaries: [names],\n });\n\n // On mount, create new user\n app.updateUsers([\n {\n id: app!.currentUser!.id,\n point: [0, 0],\n color: randomColor(),\n status: TDUserStatus.Connected,\n activeShapes: [],\n selectedIds: [],\n metadata: { name: randomName }, // <-- custom metadata\n },\n ]);\n },\n [roomId],\n );\n\n // Update Yorkie doc when the app's shapes change.\n // Prevent overloading yorkie update api call by throttle\n const onChangePage = useThrottleCallback(\n (\n app: TldrawApp,\n shapes: Record,\n bindings: Record,\n ) => {\n if (!app || client === undefined || doc === undefined) return;\n\n const getUpdatedPropertyList = (\n source: T,\n target: T,\n ) => {\n return (Object.keys(source) as Array).filter(\n (key) => !_.isEqual(source[key], target[key]),\n );\n };\n\n Object.entries(shapes).forEach(([id, shape]) => {\n doc.update((root) => {\n if (!shape) {\n delete root.shapes[id];\n } else if (!root.shapes[id]) {\n root.shapes[id] = shape;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n shape,\n root.shapes[id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = shape[key];\n (root.shapes[id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n\n Object.entries(bindings).forEach(([id, binding]) => {\n doc.update((root) => {\n if (!binding) {\n delete root.bindings[id];\n } else if (!root.bindings[id]) {\n root.bindings[id] = binding;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n binding,\n root.bindings[id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = binding[key];\n (root.bindings[id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n\n // Should store app.document.assets which is global asset storage referenced by inner page assets\n // Document key for assets should be asset.id (string), not index\n Object.entries(app.assets).forEach(([, asset]) => {\n doc.update((root) => {\n if (!asset.id) {\n delete root.assets[asset.id];\n } else if (root.assets[asset.id]) {\n root.assets[asset.id] = asset;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n asset,\n root.assets[asset.id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = asset[key];\n (root.assets[asset.id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n },\n 60,\n false,\n );\n\n // Handle presence updates when the user's pointer / selection changes\n const onChangePresence = useThrottleCallback(\n (app: TldrawApp, user: TDUser) => {\n if (!app || client === undefined || !client.isActive()) return;\n\n doc.update((root, presence) => {\n presence.set({ tdUser: user });\n });\n },\n 60,\n false,\n );\n\n // Document Changes --------\n\n useEffect(() => {\n if (!app) return;\n\n // Detach & deactive yorkie client before unload\n function handleDisconnect() {\n if (client === undefined || doc === undefined) return;\n\n client.detach(doc);\n client.deactivate();\n }\n\n window.addEventListener('beforeunload', handleDisconnect);\n\n // Subscribe to changes\n function handleChanges() {\n const root = doc.getRoot();\n\n // Parse proxy object to record\n const shapeRecord: Record = JSON.parse(\n root.shapes.toJSON!(),\n );\n const bindingRecord: Record = JSON.parse(\n root.bindings.toJSON!(),\n );\n const assetRecord: Record = JSON.parse(\n root.assets.toJSON!(),\n );\n\n // Replace page content with changed(propagated) records\n app?.replacePageContent(shapeRecord, bindingRecord, assetRecord);\n }\n\n let stillAlive = true;\n\n // Setup the document's storage and subscriptions\n async function setupDocument() {\n try {\n // 01. Create client with RPCAddr and options with apiKey if provided.\n // Then activate client.\n const options: Options = {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n syncLoopDuration: 0,\n reconnectStreamDelay: 1000,\n };\n\n client = new yorkie.Client(\n import.meta.env.VITE_YORKIE_API_ADDR,\n options,\n );\n await client.activate();\n\n // 02. Create document with tldraw custom object type.\n doc = new yorkie.Document(roomId, {\n enableDevtools: true,\n });\n\n // 02-1. Subscribe peers-changed event and update tldraw users state\n doc.subscribe('my-presence', (event) => {\n if (event.type === yorkie.DocEventType.Initialized) {\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n }\n });\n doc.subscribe('others', (event) => {\n // remove leaved users\n if (event.type === yorkie.DocEventType.Unwatched) {\n app?.removeUser(event.value.presence.tdUser.id);\n }\n\n // update users\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n });\n\n // 02-2. Attach document with initialPresence.\n const option = app?.currentUser && {\n initialPresence: { tdUser: app.currentUser },\n };\n await client.attach(doc, option);\n\n // 03. Initialize document if document not exists.\n doc.update((root) => {\n if (!root.shapes) {\n root.shapes = {};\n }\n if (!root.bindings) {\n root.bindings = {};\n }\n if (!root.assets) {\n root.assets = {};\n }\n }, 'create shapes/bindings/assets object if not exists');\n\n // 04. Subscribe document event and handle changes.\n doc.subscribe((event) => {\n if (event.type === 'remote-change') {\n handleChanges();\n }\n });\n\n // 05. Sync client to sync document with other peers.\n await client.sync();\n\n if (stillAlive) {\n // Update the document with initial content\n handleChanges();\n\n // Zoom to fit the content & finish loading\n if (app) {\n app.zoomToFit();\n if (app.zoom > 1) {\n app.resetZoom();\n }\n app.setIsLoading(false);\n }\n\n setLoading(false);\n }\n } catch (e) {\n console.error(e);\n }\n }\n\n setupDocument();\n\n return () => {\n window.removeEventListener('beforeunload', handleDisconnect);\n stillAlive = false;\n };\n }, [app]);\n\n return {\n onMount,\n onChangePage,\n loading,\n onChangePresence,\n };\n}\n"}]},{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"html,\n* {\n box-sizing: border-box;\n}\n\nbody {\n overscroll-behavior: none;\n margin: 0px;\n padding: 0px;\n font-size: 1em;\n font-family: Arial, Helvetica, sans-serif;\n}\n\n.tldraw {\n position: fixed;\n top: 0px;\n left: 0px;\n right: 0px;\n bottom: 0px;\n width: 100%;\n height: 100%;\n}"},{"isFile":true,"isOpen":false,"language":"tsx","name":"App.tsx","path":"/src/App.tsx","content":"import { Tldraw, useFileSystem } from '@tldraw/tldraw';\nimport { useMultiplayerState } from './hooks/useMultiplayerState';\nimport CustomCursor from './CustomCursor';\nimport './App.css';\n\n/*\nThis demo shows how to integrate TLDraw with a multiplayer room\nvia Yorkie.\n\nWarning: Keeping images enabled for multiplayer applications\nwithout providing a storage bucket based solution will cause\nmassive base64 string to be written to the multiplayer storage.\nIt's recommended to use a storage bucket based solution, such as\nAmazon AWS S3.\n*/\n\nexport default function App() {\n const fileSystemEvents = useFileSystem();\n const { ...events } = useMultiplayerState(\n `tldraw-${(new Date()).toISOString().substring(0, 10).replace(/-/g, '')}`\n );\n const component = { Cursor: CustomCursor };\n\n return (\n
\n \n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"CustomCursor.tsx","path":"/src/CustomCursor.tsx","content":"import { CursorComponent } from '@tldraw/core';\n\n// A custom cursor component.\n// Component overrides for the tldraw renderer\nconst CustomCursor: CursorComponent<{ name: 'Anonymous' }> = ({\n color,\n metadata,\n}) => {\n return (\n \n \n \n {metadata!.name}\n \n \n );\n};\n\nexport default CustomCursor;\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"main.tsx","path":"/src/main.tsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n \n \n ,\n);\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"tldraw.d.ts","path":"/src/tldraw.d.ts","content":"import { Indexable, Json } from '@yorkie-js-sdk/src/document/document';\nimport { TDUser } from '@tldraw/tldraw';\n\ndeclare module '@tldraw/tldraw' {\n interface TDUser extends Indexable {}\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React tldraw Example\n\n

\n \n \"Live\n \n

\n\n\"React\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-tldraw dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n react-tldraw\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-tldraw\",\n \"private\": true,\n \"version\": \"0.1.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"@react-hook/throttle\": \"^2.2.0\",\n \"@tldraw/core\": \"^1.23.2\",\n \"@tldraw/tldraw\": \"1.26.3\",\n \"lodash\": \"^4.17.21\",\n \"randomcolor\": \"^0.6.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"unique-names-generator\": \"^4.7.1\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.14.198\",\n \"@types/randomcolor\": \"^0.5.5\",\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"react-tldraw","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"hooks","path":"/src/hooks","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"types.ts","path":"/src/hooks/types.ts","content":"// Yorkie type for typescript\nimport type { TDAsset, TDBinding, TDShape, TDUser } from '@tldraw/tldraw';\nimport type { JSONObject } from 'yorkie-js-sdk';\nexport type Options = {\n apiKey?: string;\n syncLoopDuration: number;\n reconnectStreamDelay: number;\n};\n\nexport type YorkieDocType = {\n shapes: JSONObject>>;\n bindings: JSONObject>>;\n assets: JSONObject>>;\n};\n\nexport type YorkiePresenceType = {\n tdUser: TDUser;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"useMultiplayerState.ts","path":"/src/hooks/useMultiplayerState.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n TDUserStatus,\n TDAsset,\n TDBinding,\n TDShape,\n TDUser,\n TldrawApp,\n} from '@tldraw/tldraw';\nimport { useThrottleCallback } from '@react-hook/throttle';\nimport * as yorkie from 'yorkie-js-sdk';\nimport randomColor from 'randomcolor';\nimport { uniqueNamesGenerator, names } from 'unique-names-generator';\nimport _ from 'lodash';\n\nimport type { Options, YorkieDocType, YorkiePresenceType } from './types';\n\n// Yorkie Client declaration\nlet client: yorkie.Client;\n\n// Yorkie Document declaration\nlet doc: yorkie.Document;\n\nexport function useMultiplayerState(roomId: string) {\n const [app, setApp] = useState();\n const [loading, setLoading] = useState(true);\n\n // Callbacks --------------\n\n const onMount = useCallback(\n (app: TldrawApp) => {\n app.loadRoom(roomId);\n app.setIsLoading(true);\n app.pause();\n setApp(app);\n\n const randomName = uniqueNamesGenerator({\n dictionaries: [names],\n });\n\n // On mount, create new user\n app.updateUsers([\n {\n id: app!.currentUser!.id,\n point: [0, 0],\n color: randomColor(),\n status: TDUserStatus.Connected,\n activeShapes: [],\n selectedIds: [],\n metadata: { name: randomName }, // <-- custom metadata\n },\n ]);\n },\n [roomId],\n );\n\n // Update Yorkie doc when the app's shapes change.\n // Prevent overloading yorkie update api call by throttle\n const onChangePage = useThrottleCallback(\n (\n app: TldrawApp,\n shapes: Record,\n bindings: Record,\n ) => {\n if (!app || client === undefined || doc === undefined) return;\n\n const getUpdatedPropertyList = (\n source: T,\n target: T,\n ) => {\n return (Object.keys(source) as Array).filter(\n (key) => !_.isEqual(source[key], target[key]),\n );\n };\n\n Object.entries(shapes).forEach(([id, shape]) => {\n doc.update((root) => {\n if (!shape) {\n delete root.shapes[id];\n } else if (!root.shapes[id]) {\n root.shapes[id] = shape;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n shape,\n root.shapes[id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = shape[key];\n (root.shapes[id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n\n Object.entries(bindings).forEach(([id, binding]) => {\n doc.update((root) => {\n if (!binding) {\n delete root.bindings[id];\n } else if (!root.bindings[id]) {\n root.bindings[id] = binding;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n binding,\n root.bindings[id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = binding[key];\n (root.bindings[id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n\n // Should store app.document.assets which is global asset storage referenced by inner page assets\n // Document key for assets should be asset.id (string), not index\n Object.entries(app.assets).forEach(([, asset]) => {\n doc.update((root) => {\n if (!asset.id) {\n delete root.assets[asset.id];\n } else if (root.assets[asset.id]) {\n root.assets[asset.id] = asset;\n } else {\n const updatedPropertyList = getUpdatedPropertyList(\n asset,\n root.assets[asset.id]!.toJS!(),\n );\n\n updatedPropertyList.forEach((key) => {\n const newValue = asset[key];\n (root.assets[asset.id][key] as typeof newValue) = newValue;\n });\n }\n });\n });\n },\n 60,\n false,\n );\n\n // Handle presence updates when the user's pointer / selection changes\n const onChangePresence = useThrottleCallback(\n (app: TldrawApp, user: TDUser) => {\n if (!app || client === undefined || !client.isActive()) return;\n\n doc.update((root, presence) => {\n presence.set({ tdUser: user });\n });\n },\n 60,\n false,\n );\n\n // Document Changes --------\n\n useEffect(() => {\n if (!app) return;\n\n // Detach & deactive yorkie client before unload\n function handleDisconnect() {\n if (client === undefined || doc === undefined) return;\n\n client.detach(doc);\n client.deactivate();\n }\n\n window.addEventListener('beforeunload', handleDisconnect);\n\n // Subscribe to changes\n function handleChanges() {\n const root = doc.getRoot();\n\n // Parse proxy object to record\n const shapeRecord: Record = JSON.parse(\n root.shapes.toJSON!(),\n );\n const bindingRecord: Record = JSON.parse(\n root.bindings.toJSON!(),\n );\n const assetRecord: Record = JSON.parse(\n root.assets.toJSON!(),\n );\n\n // Replace page content with changed(propagated) records\n app?.replacePageContent(shapeRecord, bindingRecord, assetRecord);\n }\n\n let stillAlive = true;\n\n // Setup the document's storage and subscriptions\n async function setupDocument() {\n try {\n // 01. Create client with RPCAddr and options with apiKey if provided.\n // Then activate client.\n const options: Options = {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n syncLoopDuration: 0,\n reconnectStreamDelay: 1000,\n };\n\n client = new yorkie.Client(\n import.meta.env.VITE_YORKIE_API_ADDR,\n options,\n );\n await client.activate();\n\n // 02. Create document with tldraw custom object type.\n doc = new yorkie.Document(roomId, {\n enableDevtools: true,\n });\n\n // 02-1. Subscribe peers-changed event and update tldraw users state\n doc.subscribe('my-presence', (event) => {\n if (event.type === yorkie.DocEventType.Initialized) {\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n }\n });\n doc.subscribe('others', (event) => {\n // remove leaved users\n if (event.type === yorkie.DocEventType.Unwatched) {\n app?.removeUser(event.value.presence.tdUser.id);\n }\n\n // update users\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n });\n\n // 02-2. Attach document with initialPresence.\n const option = app?.currentUser && {\n initialPresence: { tdUser: app.currentUser },\n };\n await client.attach(doc, option);\n\n // 03. Initialize document if document not exists.\n doc.update((root) => {\n if (!root.shapes) {\n root.shapes = {};\n }\n if (!root.bindings) {\n root.bindings = {};\n }\n if (!root.assets) {\n root.assets = {};\n }\n }, 'create shapes/bindings/assets object if not exists');\n\n // 04. Subscribe document event and handle changes.\n doc.subscribe((event) => {\n if (event.type === 'remote-change') {\n handleChanges();\n }\n });\n\n // 05. Sync client to sync document with other peers.\n await client.sync();\n\n if (stillAlive) {\n // Update the document with initial content\n handleChanges();\n\n // Zoom to fit the content & finish loading\n if (app) {\n app.zoomToFit();\n if (app.zoom > 1) {\n app.resetZoom();\n }\n app.setIsLoading(false);\n }\n\n setLoading(false);\n }\n } catch (e) {\n console.error(e);\n }\n }\n\n setupDocument();\n\n return () => {\n window.removeEventListener('beforeunload', handleDisconnect);\n stillAlive = false;\n };\n }, [app]);\n\n return {\n onMount,\n onChangePage,\n loading,\n onChangePresence,\n };\n}\n"}]},{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"html,\n* {\n box-sizing: border-box;\n}\n\nbody {\n overscroll-behavior: none;\n margin: 0px;\n padding: 0px;\n font-size: 1em;\n font-family: Arial, Helvetica, sans-serif;\n}\n\n.tldraw {\n position: fixed;\n top: 0px;\n left: 0px;\n right: 0px;\n bottom: 0px;\n width: 100%;\n height: 100%;\n}"},{"isFile":true,"isOpen":false,"language":"tsx","name":"App.tsx","path":"/src/App.tsx","content":"import { Tldraw, useFileSystem } from '@tldraw/tldraw';\nimport { useMultiplayerState } from './hooks/useMultiplayerState';\nimport CustomCursor from './CustomCursor';\nimport './App.css';\n\n/*\nThis demo shows how to integrate TLDraw with a multiplayer room\nvia Yorkie.\n\nWarning: Keeping images enabled for multiplayer applications\nwithout providing a storage bucket based solution will cause\nmassive base64 string to be written to the multiplayer storage.\nIt's recommended to use a storage bucket based solution, such as\nAmazon AWS S3.\n*/\n\nexport default function App() {\n const fileSystemEvents = useFileSystem();\n const { ...events } = useMultiplayerState(\n `tldraw-${(new Date()).toISOString().substring(0, 10).replace(/-/g, '')}`\n );\n const component = { Cursor: CustomCursor };\n\n return (\n
\n \n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"CustomCursor.tsx","path":"/src/CustomCursor.tsx","content":"import { CursorComponent } from '@tldraw/core';\n\n// A custom cursor component.\n// Component overrides for the tldraw renderer\nconst CustomCursor: CursorComponent<{ name: 'Anonymous' }> = ({\n color,\n metadata,\n}) => {\n return (\n \n \n \n {metadata!.name}\n \n \n );\n};\n\nexport default CustomCursor;\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"main.tsx","path":"/src/main.tsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n \n \n ,\n);\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"tldraw.d.ts","path":"/src/tldraw.d.ts","content":"import { Indexable, Json } from '@yorkie-js-sdk/src/document/document';\nimport { TDUser } from '@tldraw/tldraw';\n\ndeclare module '@tldraw/tldraw' {\n interface TDUser extends Indexable {}\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React tldraw Example\n\n

\n \n \"Live\n \n

\n\n\"React\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-tldraw dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n react-tldraw\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-tldraw\",\n \"private\": true,\n \"version\": \"0.1.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"@react-hook/throttle\": \"^2.2.0\",\n \"@tldraw/core\": \"^1.23.2\",\n \"@tldraw/tldraw\": \"1.26.3\",\n \"lodash\": \"^4.17.21\",\n \"randomcolor\": \"^0.6.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"unique-names-generator\": \"^4.7.1\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.14.198\",\n \"@types/randomcolor\": \"^0.5.5\",\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/react-todomvc/fileInfo.ts b/examples/react-todomvc/fileInfo.ts index 58bf767..3a1dd1b 100644 --- a/examples/react-todomvc/fileInfo.ts +++ b/examples/react-todomvc/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"react-todomvc","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"body {\n margin: 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n.filters li button {\n color: inherit;\n margin: 0px 3px 0px 3px;\n padding: 0px 3px 0px 3px;\n text-decoration: none;\n border: 1px solid transparent;\n border-radius: 3px;\n}\n\n.filters li button:hover {\n border-color: #DB7676;\n}\n\n.filters li button.selected {\n border-color: #CE4646;\n}"},{"isFile":true,"isOpen":false,"language":"tsx","name":"App.tsx","path":"/src/App.tsx","content":"import React, { useState, useEffect } from 'react';\nimport yorkie, { Document, JSONArray } from 'yorkie-js-sdk';\nimport 'todomvc-app-css/index.css';\n\nimport Header from './Header';\nimport MainSection from './MainSection';\nimport { Todo } from './model';\nimport './App.css';\n\nconst initialState = [\n {\n id: 0,\n text: 'Yorkie JS SDK',\n completed: false,\n },\n {\n id: 1,\n text: 'Garbage collection',\n completed: false,\n },\n {\n id: 2,\n text: 'RichText datatype',\n completed: false,\n },\n] as Array;\n\n/**\n * `App` is the root component of the application.\n */\nexport default function App() {\n const [doc] = useState }>>(\n () =>\n new yorkie.Document<{ todos: JSONArray }>(\n `react-todomvc-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n ),\n );\n const [todos, setTodos] = useState>([]);\n\n const actions = {\n addTodo: (text: string) => {\n doc?.update((root) => {\n root.todos.push({\n id:\n root.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) +\n 1,\n completed: false,\n text,\n });\n });\n },\n deleteTodo: (id: number) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo as any;\n break;\n }\n }\n if (target) {\n root.todos.deleteByID!(target.getID());\n }\n });\n },\n editTodo: (id: number, text: string) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo;\n break;\n }\n }\n if (target) {\n target.text = text;\n }\n });\n },\n completeTodo: (id: number) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo;\n break;\n }\n }\n if (target) {\n target.completed = !target.completed;\n }\n });\n },\n clearCompleted: () => {\n doc?.update((root) => {\n for (const todo of root.todos) {\n if (todo.completed) {\n const t = todo as any;\n root.todos.deleteByID!(t.getID());\n }\n }\n }, '');\n },\n };\n\n useEffect(() => {\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n\n /**\n * `attachDoc` is a helper function to attach the document into the client.\n */\n async function attachDoc(\n doc: Document<{ todos: JSONArray }>,\n callback: (todos: any) => void,\n ) {\n // 01. create client with RPCAddr then activate it.\n await client.activate();\n\n // 02. attach the document into the client.\n await client.attach(doc);\n\n // 03. create default todos if not exists.\n doc.update((root) => {\n if (!root.todos) {\n root.todos = initialState;\n }\n }, 'create default todos if not exists');\n\n // 04. subscribe change event from local and remote.\n doc.subscribe((event) => {\n callback(doc.getRoot().todos);\n });\n\n // 05. set todos the attached document.\n callback(doc.getRoot().todos);\n }\n\n attachDoc(doc, (todos) => {\n setTodos(todos);\n });\n }, []);\n\n return (\n
\n
\n \n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"Footer.tsx","path":"/src/Footer.tsx","content":"import React from 'react';\nimport classnames from 'classnames';\n\nconst FILTER_TITLES: { [name: string]: string } = {\n SHOW_ALL: 'All',\n SHOW_ACTIVE: 'Active',\n SHOW_COMPLETED: 'Completed',\n};\n\ntype MouseEventHandler =\n (e: React.MouseEvent) => void;\n\ninterface FooterProps {\n completedCount: number;\n activeCount: number;\n filter: string;\n onClearCompleted: MouseEventHandler;\n onShow: Function;\n}\n\nexport default function Footer(props: FooterProps) {\n const {\n activeCount,\n completedCount,\n filter: selectedFilter,\n onClearCompleted,\n onShow\n } = props;\n return (\n
\n \n {activeCount || 'No'}\n  {activeCount === 1 ? 'item' : 'items'} left\n \n
    \n {\n ['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map((filter) => (\n
  • \n onShow(filter)}\n >\n {FILTER_TITLES[filter]}\n \n
  • \n ))\n }\n
\n {!!completedCount && (\n \n )}\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"Header.tsx","path":"/src/Header.tsx","content":"import React from 'react';\nimport TodoTextInput from './TodoTextInput';\n\ninterface HeaderProps {\n addTodo: Function\n}\n\nexport default function Header(props: HeaderProps) {\n return (\n
\n

todos

\n {\n if (text.length !== 0) {\n props.addTodo(text);\n }\n }}\n placeholder=\"What needs to be done?\"\n />\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"MainSection.tsx","path":"/src/MainSection.tsx","content":"import React, { useState } from 'react';\nimport { Todo } from './model';\nimport TodoItem from './TodoItem';\nimport Footer from './Footer';\n\nconst TODO_FILTERS: { [name: string]: (todo: Todo) => boolean } = {\n SHOW_ALL: (todo: Todo) => true,\n SHOW_ACTIVE: (todo: Todo) => !todo.completed,\n SHOW_COMPLETED: (todo: Todo) => todo.completed,\n};\n\ntype ChangeEventHandler = (event: React.ChangeEvent) => void;\n\ninterface MainSectionProps {\n todos: Array;\n actions: { [name: string]: Function };\n}\n\nexport default function MainSection(props: MainSectionProps) {\n const [filter, setFilter] = useState('SHOW_ALL');\n const { todos, actions } = props;\n const filteredTodos = todos.filter(TODO_FILTERS[filter]);\n const completedCount = todos.reduce((count, todo) => {\n return todo.completed ? count + 1 : count;\n }, 0);\n const activeCount = todos.length - completedCount;\n if (todos.length === 0) {\n return null;\n }\n\n return (\n
\n \n
    \n {\n filteredTodos.map((todo) => (\n \n ))\n }\n
\n actions.clearCompleted()}\n onShow={setFilter}\n />\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"TodoItem.tsx","path":"/src/TodoItem.tsx","content":"import React, { useState } from 'react';\nimport classnames from 'classnames';\nimport { Todo } from './model';\nimport TodoTextInput from './TodoTextInput';\n\ninterface TodoItemProps {\n todo: Todo;\n editTodo: Function;\n deleteTodo: Function;\n completeTodo: Function;\n}\n\nexport default function TodoItem(props: TodoItemProps) {\n const [editing, setEditing] = useState(false);\n const { todo, completeTodo, editTodo, deleteTodo } = props;\n \n return (\n \n {editing ? (\n {\n if (text.length === 0) {\n deleteTodo(todo.id);\n } else {\n editTodo(todo.id, text);\n }\n setEditing(false);\n }}\n />\n ) : (\n
\n completeTodo(todo.id)}\n />\n \n
\n )}\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"TodoTextInput.tsx","path":"/src/TodoTextInput.tsx","content":"import React, { useState } from 'react';\nimport classnames from 'classnames';\n\ninterface TodoInputProps {\n onSave: Function;\n placeholder?: string;\n editing?: boolean;\n text?: string;\n newTodo?: boolean;\n}\n\nexport default function TodoTextInput(props: TodoInputProps) {\n const [text, setText] = useState(props.text || '');\n\n return (\n ) => {\n if (!props.newTodo) {\n props.onSave(e.target.value);\n }\n }}\n onChange={(e: React.ChangeEvent) => {\n setText(e.target.value);\n }}\n onKeyDown={(e: React.KeyboardEvent) => {\n const target = e.target as HTMLInputElement;\n if (e.which === 13) {\n props.onSave(target.value.trim());\n if (props.newTodo) {\n setText('');\n }\n }\n }}\n />\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"main.tsx","path":"/src/main.tsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n ,\n);\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"model.ts","path":"/src/model.ts","content":"export interface Todo {\n id: number;\n text: string;\n completed: boolean;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React TodoMVC Example\n\n

\n \n \"Live\n \n

\n\n\"React\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-todomvc dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Vite + React + TS\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-todomvc\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"classnames\": \"^2.3.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"todomvc-app-css\": \"^2.4.2\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"react-todomvc","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"body {\n margin: 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n.filters li button {\n color: inherit;\n margin: 0px 3px 0px 3px;\n padding: 0px 3px 0px 3px;\n text-decoration: none;\n border: 1px solid transparent;\n border-radius: 3px;\n}\n\n.filters li button:hover {\n border-color: #DB7676;\n}\n\n.filters li button.selected {\n border-color: #CE4646;\n}"},{"isFile":true,"isOpen":false,"language":"tsx","name":"App.tsx","path":"/src/App.tsx","content":"import React, { useState, useEffect } from 'react';\nimport yorkie, { Document, JSONArray } from 'yorkie-js-sdk';\nimport 'todomvc-app-css/index.css';\n\nimport Header from './Header';\nimport MainSection from './MainSection';\nimport { Todo } from './model';\nimport './App.css';\n\nconst initialState = [\n {\n id: 0,\n text: 'Yorkie JS SDK',\n completed: false,\n },\n {\n id: 1,\n text: 'Garbage collection',\n completed: false,\n },\n {\n id: 2,\n text: 'RichText datatype',\n completed: false,\n },\n] as Array;\n\n/**\n * `App` is the root component of the application.\n */\nexport default function App() {\n const [doc] = useState }>>(\n () =>\n new yorkie.Document<{ todos: JSONArray }>(\n `react-todomvc-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n ),\n );\n const [todos, setTodos] = useState>([]);\n\n const actions = {\n addTodo: (text: string) => {\n doc?.update((root) => {\n root.todos.push({\n id:\n root.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) +\n 1,\n completed: false,\n text,\n });\n });\n },\n deleteTodo: (id: number) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo as any;\n break;\n }\n }\n if (target) {\n root.todos.deleteByID!(target.getID());\n }\n });\n },\n editTodo: (id: number, text: string) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo;\n break;\n }\n }\n if (target) {\n target.text = text;\n }\n });\n },\n completeTodo: (id: number) => {\n doc?.update((root) => {\n let target;\n for (const todo of root.todos) {\n if (todo.id === id) {\n target = todo;\n break;\n }\n }\n if (target) {\n target.completed = !target.completed;\n }\n });\n },\n clearCompleted: () => {\n doc?.update((root) => {\n for (const todo of root.todos) {\n if (todo.completed) {\n const t = todo as any;\n root.todos.deleteByID!(t.getID());\n }\n }\n }, '');\n },\n };\n\n useEffect(() => {\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n\n /**\n * `attachDoc` is a helper function to attach the document into the client.\n */\n async function attachDoc(\n doc: Document<{ todos: JSONArray }>,\n callback: (todos: any) => void,\n ) {\n // 01. create client with RPCAddr then activate it.\n await client.activate();\n\n // 02. attach the document into the client.\n await client.attach(doc);\n\n // 03. create default todos if not exists.\n doc.update((root) => {\n if (!root.todos) {\n root.todos = initialState;\n }\n }, 'create default todos if not exists');\n\n // 04. subscribe change event from local and remote.\n doc.subscribe((event) => {\n callback(doc.getRoot().todos);\n });\n\n // 05. set todos the attached document.\n callback(doc.getRoot().todos);\n }\n\n attachDoc(doc, (todos) => {\n setTodos(todos);\n });\n }, []);\n\n return (\n
\n
\n \n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"Footer.tsx","path":"/src/Footer.tsx","content":"import React from 'react';\nimport classnames from 'classnames';\n\nconst FILTER_TITLES: { [name: string]: string } = {\n SHOW_ALL: 'All',\n SHOW_ACTIVE: 'Active',\n SHOW_COMPLETED: 'Completed',\n};\n\ntype MouseEventHandler =\n (e: React.MouseEvent) => void;\n\ninterface FooterProps {\n completedCount: number;\n activeCount: number;\n filter: string;\n onClearCompleted: MouseEventHandler;\n onShow: Function;\n}\n\nexport default function Footer(props: FooterProps) {\n const {\n activeCount,\n completedCount,\n filter: selectedFilter,\n onClearCompleted,\n onShow\n } = props;\n return (\n
\n \n {activeCount || 'No'}\n  {activeCount === 1 ? 'item' : 'items'} left\n \n
    \n {\n ['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map((filter) => (\n
  • \n onShow(filter)}\n >\n {FILTER_TITLES[filter]}\n \n
  • \n ))\n }\n
\n {!!completedCount && (\n \n )}\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"Header.tsx","path":"/src/Header.tsx","content":"import React from 'react';\nimport TodoTextInput from './TodoTextInput';\n\ninterface HeaderProps {\n addTodo: Function\n}\n\nexport default function Header(props: HeaderProps) {\n return (\n
\n

todos

\n {\n if (text.length !== 0) {\n props.addTodo(text);\n }\n }}\n placeholder=\"What needs to be done?\"\n />\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"MainSection.tsx","path":"/src/MainSection.tsx","content":"import React, { useState } from 'react';\nimport { Todo } from './model';\nimport TodoItem from './TodoItem';\nimport Footer from './Footer';\n\nconst TODO_FILTERS: { [name: string]: (todo: Todo) => boolean } = {\n SHOW_ALL: (todo: Todo) => true,\n SHOW_ACTIVE: (todo: Todo) => !todo.completed,\n SHOW_COMPLETED: (todo: Todo) => todo.completed,\n};\n\ntype ChangeEventHandler = (event: React.ChangeEvent) => void;\n\ninterface MainSectionProps {\n todos: Array;\n actions: { [name: string]: Function };\n}\n\nexport default function MainSection(props: MainSectionProps) {\n const [filter, setFilter] = useState('SHOW_ALL');\n const { todos, actions } = props;\n const filteredTodos = todos.filter(TODO_FILTERS[filter]);\n const completedCount = todos.reduce((count, todo) => {\n return todo.completed ? count + 1 : count;\n }, 0);\n const activeCount = todos.length - completedCount;\n if (todos.length === 0) {\n return null;\n }\n\n return (\n
\n \n
    \n {\n filteredTodos.map((todo) => (\n \n ))\n }\n
\n actions.clearCompleted()}\n onShow={setFilter}\n />\n
\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"TodoItem.tsx","path":"/src/TodoItem.tsx","content":"import React, { useState } from 'react';\nimport classnames from 'classnames';\nimport { Todo } from './model';\nimport TodoTextInput from './TodoTextInput';\n\ninterface TodoItemProps {\n todo: Todo;\n editTodo: Function;\n deleteTodo: Function;\n completeTodo: Function;\n}\n\nexport default function TodoItem(props: TodoItemProps) {\n const [editing, setEditing] = useState(false);\n const { todo, completeTodo, editTodo, deleteTodo } = props;\n \n return (\n \n {editing ? (\n {\n if (text.length === 0) {\n deleteTodo(todo.id);\n } else {\n editTodo(todo.id, text);\n }\n setEditing(false);\n }}\n />\n ) : (\n
\n completeTodo(todo.id)}\n />\n \n
\n )}\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"TodoTextInput.tsx","path":"/src/TodoTextInput.tsx","content":"import React, { useState } from 'react';\nimport classnames from 'classnames';\n\ninterface TodoInputProps {\n onSave: Function;\n placeholder?: string;\n editing?: boolean;\n text?: string;\n newTodo?: boolean;\n}\n\nexport default function TodoTextInput(props: TodoInputProps) {\n const [text, setText] = useState(props.text || '');\n\n return (\n ) => {\n if (!props.newTodo) {\n props.onSave(e.target.value);\n }\n }}\n onChange={(e: React.ChangeEvent) => {\n setText(e.target.value);\n }}\n onKeyDown={(e: React.KeyboardEvent) => {\n const target = e.target as HTMLInputElement;\n if (e.which === 13) {\n props.onSave(target.value.trim());\n if (props.newTodo) {\n setText('');\n }\n }\n }}\n />\n );\n}\n"},{"isFile":true,"isOpen":false,"language":"tsx","name":"main.tsx","path":"/src/main.tsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n ,\n);\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"model.ts","path":"/src/model.ts","content":"export interface Todo {\n id: number;\n text: string;\n completed: boolean;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React TodoMVC Example\n\n

\n \n \"Live\n \n

\n\n\"React\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-todomvc dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Vite + React + TS\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-todomvc\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"classnames\": \"^2.3.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"todomvc-app-css\": \"^2.4.2\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/simultaneous-cursors/fileInfo.ts b/examples/simultaneous-cursors/fileInfo.ts index b85e65c..b6d9e58 100644 --- a/examples/simultaneous-cursors/fileInfo.ts +++ b/examples/simultaneous-cursors/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"simultaneous-cursors","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"hooks","path":"/src/hooks","children":[{"isFile":true,"isOpen":false,"language":"jsx","name":"useInterval.jsx","path":"/src/hooks/useInterval.jsx","content":"import { useRef, useEffect } from 'react';\n\nexport default function useInterval(callback, delay) {\n const savedCallback = useRef(callback);\n\n useEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n useEffect(() => {\n function tick() {\n savedCallback.current();\n }\n if (delay !== null) {\n let id = setInterval(tick, delay);\n return () => clearInterval(id);\n }\n }, [delay]);\n}\n"}]},{"isFile":false,"name":"components","path":"/src/components","children":[{"isFile":true,"isOpen":false,"language":"jsx","name":"Cursor.jsx","path":"/src/components/Cursor.jsx","content":"import PenCursor from './PenCursor';\nimport FullAnimation from './FullAnimation';\n\nconst Cursor = ({ selectedCursorShape, x, y, pointerDown }) => {\n return (\n <>\n \n {(selectedCursorShape === 'heart' ||\n selectedCursorShape === 'thumbs') && (\n \n )}\n {selectedCursorShape === 'pen' && pointerDown && (\n \n )}\n \n );\n};\n\nexport default Cursor;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"CursorSelections.jsx","path":"/src/components/CursorSelections.jsx","content":"import { useState } from 'react';\n\nconst CursorSelections = ({ handleCursorShapeSelect, clientsLength }) => {\n const [selectedCursorShape, setSelectedCursorShape] = useState('cursor');\n\n const cursorShapes = ['heart', 'thumbs', 'pen', 'cursor'];\n\n return (\n
\n
\n {cursorShapes.map((shape) => (\n {\n handleCursorShapeSelect(shape);\n setSelectedCursorShape(shape);\n }}\n className={`${\n selectedCursorShape === shape\n ? 'cursor-shape-selected'\n : 'cursor-shape-not-selected'\n }`}\n src={`./icons/icon_${shape}.svg`}\n />\n ))}\n
\n\n
\n

\n {clientsLength !== 1\n ? `${clientsLength} users are here`\n : '1 user here'}\n

\n
\n
\n );\n};\n\nexport default CursorSelections;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"FullAnimation.jsx","path":"/src/components/FullAnimation.jsx","content":"import { useState } from 'react';\nimport SingleAnimation from './SingleAnimation';\nimport useInterval from '../hooks/useInterval';\n\nconst FullAnimation = ({ pointerDown, xPos, yPos, selectedCursorShape }) => {\n const [singleAnimationsArray, setSingleAnimationsArray] = useState([]);\n\n const animationBubbleRate = 100;\n\n useInterval(() => {\n setSingleAnimationsArray((singleAnimationsArray) =>\n singleAnimationsArray.filter(\n (animation) => animation.timestamp > Date.now() - 4000,\n ),\n );\n }, 1000);\n\n useInterval(() => {\n if (pointerDown) {\n setSingleAnimationsArray((singleAnimationsArray) =>\n singleAnimationsArray.concat([\n {\n point: { x: xPos, y: yPos },\n timestamp: Date.now(),\n },\n ]),\n );\n }\n }, animationBubbleRate);\n\n return (\n \n {singleAnimationsArray.map((animation) => {\n return (\n \n );\n })}\n \n );\n};\n\nexport default FullAnimation;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"PenCursor.jsx","path":"/src/components/PenCursor.jsx","content":"import React, { useRef, useEffect, useState } from 'react';\n\nclass Point {\n constructor(x, y) {\n this.x = x;\n this.y = y;\n this.lifetime = 0;\n }\n}\n\nconst PenCursor = ({ xPos, yPos }) => {\n const [allPoints, setAllPoints] = useState([]);\n const canvasRef = useRef(null);\n const [points, setPoints] = useState([]);\n\n const addPoint = (x, y) => {\n const point = new Point(x, y);\n\n points.push(point);\n setPoints(points);\n\n allPoints.push(point);\n setAllPoints(allPoints);\n };\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const ctx = canvas.getContext('2d');\n\n const animatePoints = () => {\n ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n const duration = (0.7 * (1 * 4000)) / 60;\n\n for (let i = 0; i < points.length; ++i) {\n const point = points[i];\n let lastPoint;\n\n if (points[i - 1] !== undefined) {\n lastPoint = points[i - 1];\n } else lastPoint = point;\n\n point.lifetime += 1;\n\n if (point.lifetime > duration) {\n points.shift();\n } else {\n ctx.lineWidth = 5;\n\n ctx.lineJoin = 'round';\n\n const red = 0;\n const green = 0;\n const blue = 0;\n ctx.strokeStyle = `rgb(${red},${green},${blue})`;\n\n ctx.beginPath();\n\n ctx.moveTo(lastPoint.x, lastPoint.y);\n ctx.lineTo(point.x, point.y);\n\n ctx.stroke();\n ctx.closePath();\n }\n }\n requestAnimationFrame(animatePoints);\n };\n\n animatePoints();\n }, [points]);\n\n useEffect(() => {\n addPoint(xPos, yPos);\n }, [xPos, yPos]);\n\n return (\n \n );\n};\n\nexport default PenCursor;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"SingleAnimation.jsx","path":"/src/components/SingleAnimation.jsx","content":"import styles from './SingleAnimation.module.css';\n\nexport default function SingleAnimation({\n x,\n y,\n timestamp,\n selectedCursorShape,\n}) {\n return (\n
\n \n
\n
\n \n
\n
\n
\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"SingleAnimation.module.css","path":"/src/components/SingleAnimation.module.css","content":".goUp0 {\n opacity: 0;\n animation: goUpAnimation0 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation0 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -400px);\n }\n}\n\n.goUp1 {\n opacity: 0;\n animation: goUpAnimation1 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation1 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -300px);\n }\n}\n\n.goUp2 {\n opacity: 0;\n animation: goUpAnimation2 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation2 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -200px);\n }\n}\n\n.leftRight0 {\n animation: leftRightAnimation0 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation0 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(50px, 0px);\n }\n}\n\n.leftRight1 {\n animation: leftRightAnimation1 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation1 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(100px, 0px);\n }\n}\n\n.leftRight2 {\n animation: leftRightAnimation2 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation2 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(-50px, 0px);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n }\n\n to {\n opacity: 0;\n }\n}\n"}]},{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"body {\n max-width: 100vw;\n min-height: 100vh;\n overflow-x: hidden;\n}\n\n.general-container {\n max-width: 100%;\n}\n\n.cursor-selector-container {\n user-select: none;\n\n height: 92px;\n width: 192px;\n\n position: fixed;\n bottom: 0;\n right: 0;\n\n padding: 20px;\n\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.cursor-selections-container {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.num-users-container {\n border-radius: 8px;\n background-color: rgba(27, 26, 26, 0.8);\n\n display: flex;\n justify-content: center;\n align-items: center;\n\n color: white;\n\n height: 40px;\n width: 192px;\n}\n\n.cursor-shape-selected {\n background-color: rgb(81, 76, 73);\n border-radius: 9px;\n}\n\n.cursor-shape-not-selected {\n background-color: rgb(81, 76, 73);\n opacity: 0.5;\n border-radius: 9px;\n}\n.cursor-shape-not-selected:hover {\n background-color: rgb(81, 76, 73);\n opacity: 0.7;\n border-radius: 9px;\n}\n\n.single-animation-container {\n user-select: none;\n pointer-events: none;\n position: absolute;\n left: -20px;\n top: -10px;\n}\n\n* {\n cursor: none;\n}\n\n.pen-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -10px;\n top: -30px;\n}\n.pen-cursor-canvas {\n position: fixed;\n top: 0;\n left: 0;\n}\n\n.heart-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -17px;\n top: -17px;\n}\n\n.thumbs-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -17px;\n top: -17px;\n}\n\n.cursor-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -5px;\n top: -5px;\n}\n\n.cursor-name {\n position: fixed;\n left: 35px;\n top: -10px;\n}\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"App.jsx","path":"/src/App.jsx","content":"import { useEffect, useState } from 'react';\nimport yorkie from 'yorkie-js-sdk';\nimport Cursor from './components/Cursor';\nimport CursorSelections from './components/CursorSelections';\nimport './App.css';\n\nconst client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n});\n\nconst doc = new yorkie.Document('simultaneous-cursors', {\n enableDevtools: true,\n});\n\nconst App = () => {\n const [clients, setClients] = useState([]);\n\n const handleCursorShapeSelect = (cursorShape) => {\n doc.update((root, presence) => {\n presence.set({\n cursorShape,\n });\n });\n };\n\n useEffect(() => {\n const setup = async () => {\n await client.activate();\n\n doc.subscribe('presence', (event) => {\n setClients(doc.getPresences());\n });\n\n await client.attach(doc, {\n initialPresence: {\n cursorShape: 'cursor',\n cursor: {\n xPos: 0,\n yPos: 0,\n },\n pointerDown: false,\n },\n });\n\n window.addEventListener('beforeunload', () => {\n client.deactivate();\n });\n };\n\n setup();\n\n const handlePointerUp = () => {\n doc.update((root, presence) => {\n presence.set({\n pointerDown: false,\n });\n });\n };\n const handlePointerDown = () => {\n doc.update((root, presence) => {\n presence.set({\n pointerDown: true,\n });\n });\n };\n const handleMouseMove = (event) => {\n doc.update((root, presence) => {\n presence.set({\n cursor: {\n xPos: event.clientX,\n yPos: event.clientY,\n },\n });\n });\n };\n\n window.addEventListener('mousedown', handlePointerDown);\n window.addEventListener('mouseup', handlePointerUp);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousedown', handlePointerDown);\n window.removeEventListener('mouseup', handlePointerUp);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, []);\n\n return (\n
\n {clients.map(\n ({ clientID, presence: { cursorShape, cursor, pointerDown } }) => {\n if (!cursor) return null;\n return (\n \n );\n },\n )}\n\n \n
\n );\n};\n\nexport default App;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"main.jsx","path":"/src/main.jsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App.jsx';\n\nReactDOM.createRoot(document.getElementById('root')).render();\n"}]},{"isFile":false,"name":"public","path":"/public","children":[{"isFile":false,"name":"icons","path":"/public/icons","children":[{"isFile":true,"isOpen":false,"language":"svg","name":"icon_cursor.svg","path":"/public/icons/icon_cursor.svg","content":"\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_heart.svg","path":"/public/icons/icon_heart.svg","content":"\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_pen.svg","path":"/public/icons/icon_pen.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_thumbs.svg","path":"/public/icons/icon_thumbs.svg","content":"\n\n\n\n\n\n\n\n\n\n"}]},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/public/favicon.ico","content":""}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Simultaneous-Cursors Example\n\n

\n \n \"Live\n \n

\n\n\"simultaneous-cursors\"\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm simultaneous-cursors dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm simultaneous-cursors dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Simultaneous Cursors - Yorkie Example\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"simultaneous-cursors\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"vite\": \"^5.0.12\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"simultaneous-cursors","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"hooks","path":"/src/hooks","children":[{"isFile":true,"isOpen":false,"language":"jsx","name":"useInterval.jsx","path":"/src/hooks/useInterval.jsx","content":"import { useRef, useEffect } from 'react';\n\nexport default function useInterval(callback, delay) {\n const savedCallback = useRef(callback);\n\n useEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n useEffect(() => {\n function tick() {\n savedCallback.current();\n }\n if (delay !== null) {\n let id = setInterval(tick, delay);\n return () => clearInterval(id);\n }\n }, [delay]);\n}\n"}]},{"isFile":false,"name":"components","path":"/src/components","children":[{"isFile":true,"isOpen":false,"language":"jsx","name":"Cursor.jsx","path":"/src/components/Cursor.jsx","content":"import PenCursor from './PenCursor';\nimport FullAnimation from './FullAnimation';\n\nconst Cursor = ({ selectedCursorShape, x, y, pointerDown }) => {\n return (\n <>\n \n {(selectedCursorShape === 'heart' ||\n selectedCursorShape === 'thumbs') && (\n \n )}\n {selectedCursorShape === 'pen' && pointerDown && (\n \n )}\n \n );\n};\n\nexport default Cursor;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"CursorSelections.jsx","path":"/src/components/CursorSelections.jsx","content":"import { useState } from 'react';\n\nconst CursorSelections = ({ handleCursorShapeSelect, clientsLength }) => {\n const [selectedCursorShape, setSelectedCursorShape] = useState('cursor');\n\n const cursorShapes = ['heart', 'thumbs', 'pen', 'cursor'];\n\n return (\n
\n
\n {cursorShapes.map((shape) => (\n {\n handleCursorShapeSelect(shape);\n setSelectedCursorShape(shape);\n }}\n className={`${\n selectedCursorShape === shape\n ? 'cursor-shape-selected'\n : 'cursor-shape-not-selected'\n }`}\n src={`./icons/icon_${shape}.svg`}\n />\n ))}\n
\n\n
\n

\n {clientsLength !== 1\n ? `${clientsLength} users are here`\n : '1 user here'}\n

\n
\n
\n );\n};\n\nexport default CursorSelections;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"FullAnimation.jsx","path":"/src/components/FullAnimation.jsx","content":"import { useState } from 'react';\nimport SingleAnimation from './SingleAnimation';\nimport useInterval from '../hooks/useInterval';\n\nconst FullAnimation = ({ pointerDown, xPos, yPos, selectedCursorShape }) => {\n const [singleAnimationsArray, setSingleAnimationsArray] = useState([]);\n\n const animationBubbleRate = 100;\n\n useInterval(() => {\n setSingleAnimationsArray((singleAnimationsArray) =>\n singleAnimationsArray.filter(\n (animation) => animation.timestamp > Date.now() - 4000,\n ),\n );\n }, 1000);\n\n useInterval(() => {\n if (pointerDown) {\n setSingleAnimationsArray((singleAnimationsArray) =>\n singleAnimationsArray.concat([\n {\n point: { x: xPos, y: yPos },\n timestamp: Date.now(),\n },\n ]),\n );\n }\n }, animationBubbleRate);\n\n return (\n \n {singleAnimationsArray.map((animation) => {\n return (\n \n );\n })}\n \n );\n};\n\nexport default FullAnimation;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"PenCursor.jsx","path":"/src/components/PenCursor.jsx","content":"import React, { useRef, useEffect, useState } from 'react';\n\nclass Point {\n constructor(x, y) {\n this.x = x;\n this.y = y;\n this.lifetime = 0;\n }\n}\n\nconst PenCursor = ({ xPos, yPos }) => {\n const [allPoints, setAllPoints] = useState([]);\n const canvasRef = useRef(null);\n const [points, setPoints] = useState([]);\n\n const addPoint = (x, y) => {\n const point = new Point(x, y);\n\n points.push(point);\n setPoints(points);\n\n allPoints.push(point);\n setAllPoints(allPoints);\n };\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const ctx = canvas.getContext('2d');\n\n const animatePoints = () => {\n ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n const duration = (0.7 * (1 * 4000)) / 60;\n\n for (let i = 0; i < points.length; ++i) {\n const point = points[i];\n let lastPoint;\n\n if (points[i - 1] !== undefined) {\n lastPoint = points[i - 1];\n } else lastPoint = point;\n\n point.lifetime += 1;\n\n if (point.lifetime > duration) {\n points.shift();\n } else {\n ctx.lineWidth = 5;\n\n ctx.lineJoin = 'round';\n\n const red = 0;\n const green = 0;\n const blue = 0;\n ctx.strokeStyle = `rgb(${red},${green},${blue})`;\n\n ctx.beginPath();\n\n ctx.moveTo(lastPoint.x, lastPoint.y);\n ctx.lineTo(point.x, point.y);\n\n ctx.stroke();\n ctx.closePath();\n }\n }\n requestAnimationFrame(animatePoints);\n };\n\n animatePoints();\n }, [points]);\n\n useEffect(() => {\n addPoint(xPos, yPos);\n }, [xPos, yPos]);\n\n return (\n \n );\n};\n\nexport default PenCursor;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"SingleAnimation.jsx","path":"/src/components/SingleAnimation.jsx","content":"import styles from './SingleAnimation.module.css';\n\nexport default function SingleAnimation({\n x,\n y,\n timestamp,\n selectedCursorShape,\n}) {\n return (\n
\n \n
\n
\n \n
\n
\n
\n \n );\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"SingleAnimation.module.css","path":"/src/components/SingleAnimation.module.css","content":".goUp0 {\n opacity: 0;\n animation: goUpAnimation0 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation0 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -400px);\n }\n}\n\n.goUp1 {\n opacity: 0;\n animation: goUpAnimation1 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation1 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -300px);\n }\n}\n\n.goUp2 {\n opacity: 0;\n animation: goUpAnimation2 2s, fadeOut 2s;\n}\n\n@keyframes goUpAnimation2 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(0px, -200px);\n }\n}\n\n.leftRight0 {\n animation: leftRightAnimation0 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation0 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(50px, 0px);\n }\n}\n\n.leftRight1 {\n animation: leftRightAnimation1 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation1 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(100px, 0px);\n }\n}\n\n.leftRight2 {\n animation: leftRightAnimation2 0.3s alternate infinite ease-in-out;\n}\n\n@keyframes leftRightAnimation2 {\n from {\n transform: translate(0px, 0px);\n }\n\n to {\n transform: translate(-50px, 0px);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n }\n\n to {\n opacity: 0;\n }\n}\n"}]},{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"body {\n max-width: 100vw;\n min-height: 100vh;\n overflow-x: hidden;\n}\n\n.general-container {\n max-width: 100%;\n}\n\n.cursor-selector-container {\n user-select: none;\n\n height: 92px;\n width: 192px;\n\n position: fixed;\n bottom: 0;\n right: 0;\n\n padding: 20px;\n\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.cursor-selections-container {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.num-users-container {\n border-radius: 8px;\n background-color: rgba(27, 26, 26, 0.8);\n\n display: flex;\n justify-content: center;\n align-items: center;\n\n color: white;\n\n height: 40px;\n width: 192px;\n}\n\n.cursor-shape-selected {\n background-color: rgb(81, 76, 73);\n border-radius: 9px;\n}\n\n.cursor-shape-not-selected {\n background-color: rgb(81, 76, 73);\n opacity: 0.5;\n border-radius: 9px;\n}\n.cursor-shape-not-selected:hover {\n background-color: rgb(81, 76, 73);\n opacity: 0.7;\n border-radius: 9px;\n}\n\n.single-animation-container {\n user-select: none;\n pointer-events: none;\n position: absolute;\n left: -20px;\n top: -10px;\n}\n\n* {\n cursor: none;\n}\n\n.pen-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -10px;\n top: -30px;\n}\n.pen-cursor-canvas {\n position: fixed;\n top: 0;\n left: 0;\n}\n\n.heart-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -17px;\n top: -17px;\n}\n\n.thumbs-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -17px;\n top: -17px;\n}\n\n.cursor-cursor {\n user-select: none;\n pointer-events: none;\n position: fixed;\n left: -5px;\n top: -5px;\n}\n\n.cursor-name {\n position: fixed;\n left: 35px;\n top: -10px;\n}\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"App.jsx","path":"/src/App.jsx","content":"import { useEffect, useState } from 'react';\nimport yorkie from 'yorkie-js-sdk';\nimport Cursor from './components/Cursor';\nimport CursorSelections from './components/CursorSelections';\nimport './App.css';\n\nconst client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n});\n\nconst doc = new yorkie.Document('simultaneous-cursors', {\n enableDevtools: true,\n});\n\nconst App = () => {\n const [clients, setClients] = useState([]);\n\n const handleCursorShapeSelect = (cursorShape) => {\n doc.update((root, presence) => {\n presence.set({\n cursorShape,\n });\n });\n };\n\n useEffect(() => {\n const setup = async () => {\n await client.activate();\n\n doc.subscribe('presence', (event) => {\n setClients(doc.getPresences());\n });\n\n await client.attach(doc, {\n initialPresence: {\n cursorShape: 'cursor',\n cursor: {\n xPos: 0,\n yPos: 0,\n },\n pointerDown: false,\n },\n });\n\n window.addEventListener('beforeunload', () => {\n client.deactivate();\n });\n };\n\n setup();\n\n const handlePointerUp = () => {\n doc.update((root, presence) => {\n presence.set({\n pointerDown: false,\n });\n });\n };\n const handlePointerDown = () => {\n doc.update((root, presence) => {\n presence.set({\n pointerDown: true,\n });\n });\n };\n const handleMouseMove = (event) => {\n doc.update((root, presence) => {\n presence.set({\n cursor: {\n xPos: event.clientX,\n yPos: event.clientY,\n },\n });\n });\n };\n\n window.addEventListener('mousedown', handlePointerDown);\n window.addEventListener('mouseup', handlePointerUp);\n window.addEventListener('mousemove', handleMouseMove);\n\n return () => {\n window.removeEventListener('mousedown', handlePointerDown);\n window.removeEventListener('mouseup', handlePointerUp);\n window.removeEventListener('mousemove', handleMouseMove);\n };\n }, []);\n\n return (\n
\n {clients.map(\n ({ clientID, presence: { cursorShape, cursor, pointerDown } }) => {\n if (!cursor) return null;\n return (\n \n );\n },\n )}\n\n \n
\n );\n};\n\nexport default App;\n"},{"isFile":true,"isOpen":false,"language":"jsx","name":"main.jsx","path":"/src/main.jsx","content":"import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App.jsx';\n\nReactDOM.createRoot(document.getElementById('root')).render();\n"}]},{"isFile":false,"name":"public","path":"/public","children":[{"isFile":false,"name":"icons","path":"/public/icons","children":[{"isFile":true,"isOpen":false,"language":"svg","name":"icon_cursor.svg","path":"/public/icons/icon_cursor.svg","content":"\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_heart.svg","path":"/public/icons/icon_heart.svg","content":"\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_pen.svg","path":"/public/icons/icon_pen.svg","content":"\n\n\n\n\n\n\n\n\n\n\n\n\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"icon_thumbs.svg","path":"/public/icons/icon_thumbs.svg","content":"\n\n\n\n\n\n\n\n\n\n"}]},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/public/favicon.ico","content":""}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Simultaneous-Cursors Example\n\n

\n \n \"Live\n \n

\n\n\"simultaneous-cursors\"\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm simultaneous-cursors dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm simultaneous-cursors dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n \n Simultaneous Cursors - Yorkie Example\n \n \n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"simultaneous-cursors\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"vite\": \"^5.0.12\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/vanilla-codemirror6/fileInfo.ts b/examples/vanilla-codemirror6/fileInfo.ts index 2b3b784..48f2240 100644 --- a/examples/vanilla-codemirror6/fileInfo.ts +++ b/examples/vanilla-codemirror6/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vanilla-codemirror6","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"main.ts","path":"/src/main.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport yorkie, { DocEventType } from 'yorkie-js-sdk';\nimport type { EditOpInfo, OperationInfo } from 'yorkie-js-sdk';\nimport { basicSetup, EditorView } from 'codemirror';\nimport { keymap } from '@codemirror/view';\nimport {\n markdown,\n markdownKeymap,\n markdownLanguage,\n} from '@codemirror/lang-markdown';\nimport { Transaction, TransactionSpec } from '@codemirror/state';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc, YorkiePresence } from './type';\nimport './style.css';\n\nconst editorParentElem = document.getElementById('editor')!;\nconst peersElem = document.getElementById('peers')!;\nconst documentElem = document.getElementById('document')!;\nconst documentTextElem = document.getElementById('document-text')!;\nconst networkStatusElem = document.getElementById('network-status')!;\n\nasync function main() {\n // 01. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document(\n `codemirror6-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n {\n enableDevtools: true,\n },\n );\n doc.subscribe('connection', (event) => {\n network.statusListener(networkStatusElem)(event);\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeers(peersElem, doc.getPresences(), client.getID()!);\n }\n });\n await client.attach(doc);\n doc.update((root) => {\n if (!root.content) {\n root.content = new yorkie.Text();\n }\n }, 'create content if not exists');\n\n // 02-2. subscribe document event.\n const syncText = () => {\n const text = doc.getRoot().content;\n const selection = doc.getMyPresence().selection;\n const transactionSpec: TransactionSpec = {\n changes: { from: 0, to: view.state.doc.length, insert: text.toString() },\n annotations: [Transaction.remote.of(true)],\n };\n\n if (selection) {\n // Restore the cursor position when the text is replaced.\n const cursor = text.posRangeToIndexRange(selection);\n transactionSpec['selection'] = {\n anchor: cursor[0],\n head: cursor[1],\n };\n }\n view.dispatch(transactionSpec);\n };\n doc.subscribe((event) => {\n if (event.type === 'snapshot') {\n // The text is replaced to snapshot and must be re-synced.\n syncText();\n }\n displayLog(documentElem, documentTextElem, doc);\n });\n\n doc.subscribe('$.content', (event) => {\n if (event.type === 'remote-change') {\n const { operations } = event.value;\n handleOperations(operations);\n }\n });\n\n await client.sync();\n\n // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)\n const updateListener = EditorView.updateListener.of((viewUpdate) => {\n if (viewUpdate.docChanged) {\n for (const tr of viewUpdate.transactions) {\n const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];\n if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {\n continue;\n }\n if (tr.annotation(Transaction.remote)) {\n continue;\n }\n let adj = 0;\n tr.changes.iterChanges((fromA, toA, _, __, inserted) => {\n const insertText = inserted.toJSON().join('\\n');\n doc.update((root) => {\n root.content.edit(fromA + adj, toA + adj, insertText);\n }, `update content byA ${client.getID()}`);\n adj += insertText.length - (toA - fromA);\n });\n }\n }\n\n const hasFocus =\n viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();\n const sel = hasFocus ? viewUpdate.state.selection.main : null;\n\n doc.update((root, presence) => {\n if (sel && root.content) {\n const selection = root.content.indexRangeToPosRange([\n sel.anchor,\n sel.head,\n ]);\n\n if (\n JSON.stringify(selection) !==\n JSON.stringify(presence.get('selection'))\n ) {\n presence.set({\n selection,\n });\n }\n } else if (presence.get('selection')) {\n presence.set({\n selection: undefined,\n });\n }\n });\n });\n\n // 03-2. create codemirror instance\n const view = new EditorView({\n doc: '',\n extensions: [\n basicSetup,\n markdown({ base: markdownLanguage }),\n keymap.of(markdownKeymap),\n updateListener,\n ],\n parent: editorParentElem,\n });\n\n // 03-3. define event handler that apply remote changes to local\n function handleOperations(operations: Array) {\n for (const op of operations) {\n if (op.type === 'edit') {\n handleEditOp(op);\n }\n }\n }\n function handleEditOp(op: EditOpInfo) {\n const changes = [\n {\n from: Math.max(0, op.from),\n to: Math.max(0, op.to),\n insert: op.value!.content,\n },\n ];\n\n view.dispatch({\n changes,\n annotations: [Transaction.remote.of(true)],\n });\n }\n\n syncText();\n displayLog(documentElem, documentTextElem, doc);\n}\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"network.ts","path":"/src/network.ts","content":"import { DocEvent, StreamConnectionStatus } from 'yorkie-js-sdk';\nexport const network = {\n isOnline: false,\n showOffline: (elem: HTMLElement) => {\n network.isOnline = false;\n elem.innerHTML = ' ';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = ' ';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin-top: 1rem;\n margin-bottom: 1rem;\n\n font-family: monospace;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { TextPosStructRange, type Text } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID } of peers) {\n usernames.push(myClientID === clientID ? `${clientID}` : clientID);\n }\n elem.innerHTML = JSON.stringify(usernames);\n}\n\n// function to display document content\nexport function displayLog(\n elem: HTMLElement,\n textElem: HTMLElement,\n doc: Document,\n) {\n elem.innerText = doc.toJSON();\n textElem.innerText = doc.getRoot().content.toTestString();\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie CodeMirror6 Example\n\n

\n \n \"Live\n \n

\n\n\"CodeMirror6\"\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-codemirror6 dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n Yorkie + CodeMirror 6 Example\n \n \n
\n
\n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-codemirror6\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"@codemirror/commands\": \"6.1.2\",\n \"@codemirror/highlight\": \"^0.19.8\",\n \"@codemirror/lang-markdown\": \"^6.0.2\",\n \"@codemirror/language-data\": \"^6.1.0\",\n \"@codemirror/state\": \"^6.4.1\",\n \"@codemirror/view\": \"6.23.1\",\n \"codemirror\": \"^6.0.1\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vanilla-codemirror6","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"main.ts","path":"/src/main.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport yorkie, { DocEventType } from 'yorkie-js-sdk';\nimport type { EditOpInfo, OperationInfo } from 'yorkie-js-sdk';\nimport { basicSetup, EditorView } from 'codemirror';\nimport { keymap } from '@codemirror/view';\nimport {\n markdown,\n markdownKeymap,\n markdownLanguage,\n} from '@codemirror/lang-markdown';\nimport { Transaction, TransactionSpec } from '@codemirror/state';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc, YorkiePresence } from './type';\nimport './style.css';\n\nconst editorParentElem = document.getElementById('editor')!;\nconst peersElem = document.getElementById('peers')!;\nconst documentElem = document.getElementById('document')!;\nconst documentTextElem = document.getElementById('document-text')!;\nconst networkStatusElem = document.getElementById('network-status')!;\n\nasync function main() {\n // 01. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document(\n `codemirror6-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n {\n enableDevtools: true,\n },\n );\n doc.subscribe('connection', (event) => {\n network.statusListener(networkStatusElem)(event);\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeers(peersElem, doc.getPresences(), client.getID()!);\n }\n });\n await client.attach(doc);\n doc.update((root) => {\n if (!root.content) {\n root.content = new yorkie.Text();\n }\n }, 'create content if not exists');\n\n // 02-2. subscribe document event.\n const syncText = () => {\n const text = doc.getRoot().content;\n const selection = doc.getMyPresence().selection;\n const transactionSpec: TransactionSpec = {\n changes: { from: 0, to: view.state.doc.length, insert: text.toString() },\n annotations: [Transaction.remote.of(true)],\n };\n\n if (selection) {\n // Restore the cursor position when the text is replaced.\n const cursor = text.posRangeToIndexRange(selection);\n transactionSpec['selection'] = {\n anchor: cursor[0],\n head: cursor[1],\n };\n }\n view.dispatch(transactionSpec);\n };\n doc.subscribe((event) => {\n if (event.type === 'snapshot') {\n // The text is replaced to snapshot and must be re-synced.\n syncText();\n }\n displayLog(documentElem, documentTextElem, doc);\n });\n\n doc.subscribe('$.content', (event) => {\n if (event.type === 'remote-change') {\n const { operations } = event.value;\n handleOperations(operations);\n }\n });\n\n await client.sync();\n\n // 03-1. define function that bind the document with the codemirror(broadcast local changes to peers)\n const updateListener = EditorView.updateListener.of((viewUpdate) => {\n if (viewUpdate.docChanged) {\n for (const tr of viewUpdate.transactions) {\n const events = ['select', 'input', 'delete', 'move', 'undo', 'redo'];\n if (!events.map((event) => tr.isUserEvent(event)).some(Boolean)) {\n continue;\n }\n if (tr.annotation(Transaction.remote)) {\n continue;\n }\n let adj = 0;\n tr.changes.iterChanges((fromA, toA, _, __, inserted) => {\n const insertText = inserted.toJSON().join('\\n');\n doc.update((root) => {\n root.content.edit(fromA + adj, toA + adj, insertText);\n }, `update content byA ${client.getID()}`);\n adj += insertText.length - (toA - fromA);\n });\n }\n }\n\n const hasFocus =\n viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();\n const sel = hasFocus ? viewUpdate.state.selection.main : null;\n\n doc.update((root, presence) => {\n if (sel && root.content) {\n const selection = root.content.indexRangeToPosRange([\n sel.anchor,\n sel.head,\n ]);\n\n if (\n JSON.stringify(selection) !==\n JSON.stringify(presence.get('selection'))\n ) {\n presence.set({\n selection,\n });\n }\n } else if (presence.get('selection')) {\n presence.set({\n selection: undefined,\n });\n }\n });\n });\n\n // 03-2. create codemirror instance\n const view = new EditorView({\n doc: '',\n extensions: [\n basicSetup,\n markdown({ base: markdownLanguage }),\n keymap.of(markdownKeymap),\n updateListener,\n ],\n parent: editorParentElem,\n });\n\n // 03-3. define event handler that apply remote changes to local\n function handleOperations(operations: Array) {\n for (const op of operations) {\n if (op.type === 'edit') {\n handleEditOp(op);\n }\n }\n }\n function handleEditOp(op: EditOpInfo) {\n const changes = [\n {\n from: Math.max(0, op.from),\n to: Math.max(0, op.to),\n insert: op.value!.content,\n },\n ];\n\n view.dispatch({\n changes,\n annotations: [Transaction.remote.of(true)],\n });\n }\n\n syncText();\n displayLog(documentElem, documentTextElem, doc);\n}\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"network.ts","path":"/src/network.ts","content":"import { DocEvent, StreamConnectionStatus } from 'yorkie-js-sdk';\nexport const network = {\n isOnline: false,\n showOffline: (elem: HTMLElement) => {\n network.isOnline = false;\n elem.innerHTML = ' ';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = ' ';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin-top: 1rem;\n margin-bottom: 1rem;\n\n font-family: monospace;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { TextPosStructRange, type Text } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID } of peers) {\n usernames.push(myClientID === clientID ? `${clientID}` : clientID);\n }\n elem.innerHTML = JSON.stringify(usernames);\n}\n\n// function to display document content\nexport function displayLog(\n elem: HTMLElement,\n textElem: HTMLElement,\n doc: Document,\n) {\n elem.innerText = doc.toJSON();\n textElem.innerText = doc.getRoot().content.toTestString();\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie CodeMirror6 Example\n\n

\n \n \"Live\n \n

\n\n\"CodeMirror6\"\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-codemirror6 dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n Yorkie + CodeMirror 6 Example\n \n \n
\n
\n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-codemirror6\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"@codemirror/commands\": \"6.1.2\",\n \"@codemirror/highlight\": \"^0.19.8\",\n \"@codemirror/lang-markdown\": \"^6.0.2\",\n \"@codemirror/language-data\": \"^6.1.0\",\n \"@codemirror/state\": \"^6.4.1\",\n \"@codemirror/view\": \"6.23.1\",\n \"codemirror\": \"^6.0.1\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/vanilla-quill/fileInfo.ts b/examples/vanilla-quill/fileInfo.ts index c7a0089..802b9a9 100644 --- a/examples/vanilla-quill/fileInfo.ts +++ b/examples/vanilla-quill/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vanilla-quill","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"main.ts","path":"/src/main.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport yorkie, { DocEventType, Indexable, OperationInfo } from 'yorkie-js-sdk';\nimport Quill, { type DeltaOperation, type DeltaStatic } from 'quill';\nimport QuillCursors from 'quill-cursors';\nimport ColorHash from 'color-hash';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc, YorkiePresence } from './type';\nimport 'quill/dist/quill.snow.css';\nimport './style.css';\n\ntype TextValueType = {\n attributes?: Indexable;\n content?: string;\n};\n\nconst peersElem = document.getElementById('peers')!;\nconst documentElem = document.getElementById('document')!;\nconst documentTextElem = document.getElementById('document-text')!;\nconst networkStatusElem = document.getElementById('network-status')!;\nconst colorHash = new ColorHash();\nconst documentKey = `vanilla-quill-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`;\n\nfunction toDeltaOperation(\n textValue: T,\n): DeltaOperation {\n const { embed, ...restAttributes } = textValue.attributes ?? {};\n if (embed) {\n return { insert: embed, attributes: restAttributes };\n }\n\n return {\n insert: textValue.content || '',\n attributes: textValue.attributes,\n };\n}\n\nasync function main() {\n // 01-1. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document(documentKey, {\n enableDevtools: true,\n });\n doc.subscribe('connection', (event) => {\n network.statusListener(networkStatusElem)(event);\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeers(peersElem, doc.getPresences(), client.getID()!);\n }\n });\n\n await client.attach(doc, {\n initialPresence: {\n username: client.getID()!.slice(-2),\n color: colorHash.hex(client.getID()!.slice(-2)),\n selection: undefined,\n },\n });\n\n doc.update((root) => {\n if (!root.content) {\n root.content = new yorkie.Text();\n root.content.edit(0, 0, '\\n');\n }\n }, 'create content if not exists');\n\n // 02-2. subscribe document event.\n doc.subscribe((event) => {\n if (event.type === 'snapshot') {\n // The text is replaced to snapshot and must be re-synced.\n syncText();\n }\n displayLog(documentElem, documentTextElem, doc);\n });\n\n doc.subscribe('$.content', (event) => {\n if (event.type === 'remote-change') {\n handleOperations(event.value.operations);\n }\n updateAllCursors();\n });\n doc.subscribe('others', (event) => {\n if (event.type === DocEventType.Unwatched) {\n cursors.removeCursor(event.value.clientID);\n } else if (event.type === DocEventType.PresenceChanged) {\n updateCursor(event.value);\n }\n });\n\n function updateCursor(user: { clientID: string; presence: YorkiePresence }) {\n const { clientID, presence } = user;\n if (clientID === client.getID()) return;\n // TODO(chacha912): After resolving the presence initialization issue(#608),\n // remove the following check.\n if (!presence) return;\n\n const { username, color, selection } = presence;\n if (!selection) return;\n const range = doc.getRoot().content.posRangeToIndexRange(selection);\n cursors.createCursor(clientID, username, color);\n cursors.moveCursor(clientID, {\n index: range[0],\n length: range[1] - range[0],\n });\n }\n\n function updateAllCursors() {\n for (const user of doc.getPresences()) {\n updateCursor(user);\n }\n }\n\n await client.sync();\n\n // 03. create an instance of Quill\n Quill.register('modules/cursors', QuillCursors);\n const quill = new Quill('#editor', {\n modules: {\n toolbar: [\n ['bold', 'italic', 'underline', 'strike'],\n ['blockquote', 'code-block'],\n [{ header: 1 }, { header: 2 }],\n [{ list: 'ordered' }, { list: 'bullet' }],\n [{ script: 'sub' }, { script: 'super' }],\n [{ indent: '-1' }, { indent: '+1' }],\n [{ direction: 'rtl' }],\n [{ size: ['small', false, 'large', 'huge'] }],\n [{ header: [1, 2, 3, 4, 5, 6, false] }],\n [{ color: [] }, { background: [] }],\n [{ font: [] }],\n [{ align: [] }],\n ['image', 'video'],\n ['clean'],\n ],\n cursors: true,\n },\n theme: 'snow',\n });\n const cursors = quill.getModule('cursors');\n\n // 04. bind the document with the Quill.\n // 04-1. Quill to Document.\n quill\n .on('text-change', (delta, _, source) => {\n if (source === 'api' || !delta.ops) {\n return;\n }\n\n let from = 0,\n to = 0;\n console.log(`%c quill: ${JSON.stringify(delta.ops)}`, 'color: green');\n for (const op of delta.ops) {\n if (op.attributes !== undefined || op.insert !== undefined) {\n if (op.retain !== undefined) {\n to = from + op.retain;\n }\n console.log(\n `%c local: ${from}-${to}: ${op.insert} ${\n op.attributes ? JSON.stringify(op.attributes) : '{}'\n }`,\n 'color: green',\n );\n\n doc.update((root, presence) => {\n let range;\n if (op.attributes !== undefined && op.insert === undefined) {\n root.content.setStyle(from, to, op.attributes);\n } else if (op.insert !== undefined) {\n if (to < from) {\n to = from;\n }\n\n if (typeof op.insert === 'object') {\n range = root.content.edit(from, to, ' ', {\n embed: JSON.stringify(op.insert),\n ...op.attributes,\n });\n } else {\n range = root.content.edit(from, to, op.insert, op.attributes);\n }\n from = to + op.insert.length;\n }\n\n range &&\n presence.set({\n selection: root.content.indexRangeToPosRange(range),\n });\n }, `update style by ${client.getID()}`);\n } else if (op.delete !== undefined) {\n to = from + op.delete;\n console.log(`%c local: ${from}-${to}: ''`, 'color: green');\n\n doc.update((root, presence) => {\n const range = root.content.edit(from, to, '');\n range &&\n presence.set({\n selection: root.content.indexRangeToPosRange(range),\n });\n }, `update content by ${client.getID()}`);\n } else if (op.retain !== undefined) {\n from = to + op.retain;\n to = from;\n }\n }\n })\n .on('selection-change', (range, _, source) => {\n if (!range) {\n return;\n }\n\n // NOTE(chacha912): If the selection in the Quill editor does not match the range computed by yorkie,\n // additional updates are necessary. This condition addresses situations where Quill's selection behaves\n // differently, such as when inserting text before a range selection made by another user, causing\n // the second character onwards to be included in the selection.\n if (source === 'api') {\n const { selection } = doc.getMyPresence();\n if (selection) {\n const [from, to] = doc\n .getRoot()\n .content.posRangeToIndexRange(selection);\n const { index, length } = range;\n if (from === index && to === index + length) {\n return;\n }\n }\n }\n\n doc.update((root, presence) => {\n presence.set({\n selection: root.content.indexRangeToPosRange([\n range.index,\n range.index + range.length,\n ]),\n });\n }, `update selection by ${client.getID()}`);\n });\n\n // 04-2. document to Quill(remote).\n function handleOperations(ops: Array) {\n const deltaOperations = [];\n let prevTo = 0;\n for (const op of ops) {\n if (op.type === 'edit') {\n const from = op.from;\n const to = op.to;\n const retainFrom = from - prevTo;\n const retainTo = to - from;\n\n const { insert, attributes } = toDeltaOperation(op.value!);\n console.log(`%c remote: ${from}-${to}: ${insert}`, 'color: skyblue');\n\n if (retainFrom) {\n deltaOperations.push({ retain: retainFrom });\n }\n if (retainTo) {\n deltaOperations.push({ delete: retainTo });\n }\n if (insert) {\n const op: DeltaOperation = { insert };\n if (attributes) {\n op.attributes = attributes;\n }\n deltaOperations.push(op);\n }\n prevTo = to;\n } else if (op.type === 'style') {\n const from = op.from;\n const to = op.to;\n const retainFrom = from - prevTo;\n const retainTo = to - from;\n const { attributes } = toDeltaOperation(op.value!);\n console.log(\n `%c remote: ${from}-${to}: ${JSON.stringify(attributes)}`,\n 'color: skyblue',\n );\n\n if (retainFrom) {\n deltaOperations.push({ retain: retainFrom });\n }\n if (attributes) {\n const op: DeltaOperation = { attributes };\n if (retainTo) {\n op.retain = retainTo;\n }\n\n deltaOperations.push(op);\n }\n prevTo = to;\n }\n }\n\n if (deltaOperations.length) {\n console.log(\n `%c to quill: ${JSON.stringify(deltaOperations)}`,\n 'color: green',\n );\n const delta = {\n ops: deltaOperations,\n } as DeltaStatic;\n quill.updateContents(delta, 'api');\n }\n }\n\n // 05. synchronize text of document and Quill.\n function syncText() {\n const text = doc.getRoot().content;\n\n const delta = {\n ops: text.values().map((val) => toDeltaOperation(val)),\n } as DeltaStatic;\n quill.setContents(delta, 'api');\n }\n\n syncText();\n updateAllCursors();\n displayLog(documentElem, documentTextElem, doc);\n}\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"network.ts","path":"/src/network.ts","content":"import { DocEvent, StreamConnectionStatus } from 'yorkie-js-sdk';\nexport const network = {\n isOnline: false,\n showOffline: (elem: HTMLElement) => {\n network.isOnline = false;\n elem.innerHTML = ' ';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = ' ';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin: 1rem 0;\n font-family: monospace;\n}\n\n.ql-editor {\n min-height: 300px;\n overflow-y: auto;\n resize: vertical;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { type Text, TextPosStructRange } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n username: string;\n color: string;\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc, YorkiePresence } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID, presence } of peers) {\n usernames.push(\n myClientID === clientID\n ? `${presence.username}`\n : presence.username,\n );\n }\n elem.innerHTML = JSON.stringify(usernames);\n}\n\n// function to display document content\nexport function displayLog(\n elem: HTMLElement,\n textElem: HTMLElement,\n doc: Document,\n) {\n elem.innerText = doc.toJSON();\n textElem.innerText = doc.getRoot().content.toTestString();\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Quill Example\n\n

\n \n \"Live\n \n

\n\nThis demo shows the real-time collaborative version of the [Quill](https://quilljs.com/) editor with [Yorkie](https://yorkie.dev/) and [Vite](https://vitejs.dev/).\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n Yorkie + Quill Example\n \n \n
\n
\n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-quill\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@types/color-hash\": \"^1.0.2\",\n \"@types/quill\": \"^1.3.10\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"color-hash\": \"^2.0.2\",\n \"quill\": \"^1.3.7\",\n \"quill-cursors\": \"^4.0.0\",\n \"quill-delta\": \"^5.0.0\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vanilla-quill","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":true,"isOpen":false,"language":"typescript","name":"main.ts","path":"/src/main.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport yorkie, { DocEventType, Indexable, OperationInfo } from 'yorkie-js-sdk';\nimport Quill, { type DeltaOperation, type DeltaStatic } from 'quill';\nimport QuillCursors from 'quill-cursors';\nimport ColorHash from 'color-hash';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc, YorkiePresence } from './type';\nimport 'quill/dist/quill.snow.css';\nimport './style.css';\n\ntype TextValueType = {\n attributes?: Indexable;\n content?: string;\n};\n\nconst peersElem = document.getElementById('peers')!;\nconst documentElem = document.getElementById('document')!;\nconst documentTextElem = document.getElementById('document-text')!;\nconst networkStatusElem = document.getElementById('network-status')!;\nconst colorHash = new ColorHash();\nconst documentKey = `vanilla-quill-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`;\n\nfunction toDeltaOperation(\n textValue: T,\n): DeltaOperation {\n const { embed, ...restAttributes } = textValue.attributes ?? {};\n if (embed) {\n return { insert: embed, attributes: restAttributes };\n }\n\n return {\n insert: textValue.content || '',\n attributes: textValue.attributes,\n };\n}\n\nasync function main() {\n // 01-1. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document(documentKey, {\n enableDevtools: true,\n });\n doc.subscribe('connection', (event) => {\n network.statusListener(networkStatusElem)(event);\n });\n doc.subscribe('presence', (event) => {\n if (event.type !== DocEventType.PresenceChanged) {\n displayPeers(peersElem, doc.getPresences(), client.getID()!);\n }\n });\n\n await client.attach(doc, {\n initialPresence: {\n username: client.getID()!.slice(-2),\n color: colorHash.hex(client.getID()!.slice(-2)),\n selection: undefined,\n },\n });\n\n doc.update((root) => {\n if (!root.content) {\n root.content = new yorkie.Text();\n root.content.edit(0, 0, '\\n');\n }\n }, 'create content if not exists');\n\n // 02-2. subscribe document event.\n doc.subscribe((event) => {\n if (event.type === 'snapshot') {\n // The text is replaced to snapshot and must be re-synced.\n syncText();\n }\n displayLog(documentElem, documentTextElem, doc);\n });\n\n doc.subscribe('$.content', (event) => {\n if (event.type === 'remote-change') {\n handleOperations(event.value.operations);\n }\n updateAllCursors();\n });\n doc.subscribe('others', (event) => {\n if (event.type === DocEventType.Unwatched) {\n cursors.removeCursor(event.value.clientID);\n } else if (event.type === DocEventType.PresenceChanged) {\n updateCursor(event.value);\n }\n });\n\n function updateCursor(user: { clientID: string; presence: YorkiePresence }) {\n const { clientID, presence } = user;\n if (clientID === client.getID()) return;\n // TODO(chacha912): After resolving the presence initialization issue(#608),\n // remove the following check.\n if (!presence) return;\n\n const { username, color, selection } = presence;\n if (!selection) return;\n const range = doc.getRoot().content.posRangeToIndexRange(selection);\n cursors.createCursor(clientID, username, color);\n cursors.moveCursor(clientID, {\n index: range[0],\n length: range[1] - range[0],\n });\n }\n\n function updateAllCursors() {\n for (const user of doc.getPresences()) {\n updateCursor(user);\n }\n }\n\n await client.sync();\n\n // 03. create an instance of Quill\n Quill.register('modules/cursors', QuillCursors);\n const quill = new Quill('#editor', {\n modules: {\n toolbar: [\n ['bold', 'italic', 'underline', 'strike'],\n ['blockquote', 'code-block'],\n [{ header: 1 }, { header: 2 }],\n [{ list: 'ordered' }, { list: 'bullet' }],\n [{ script: 'sub' }, { script: 'super' }],\n [{ indent: '-1' }, { indent: '+1' }],\n [{ direction: 'rtl' }],\n [{ size: ['small', false, 'large', 'huge'] }],\n [{ header: [1, 2, 3, 4, 5, 6, false] }],\n [{ color: [] }, { background: [] }],\n [{ font: [] }],\n [{ align: [] }],\n ['image', 'video'],\n ['clean'],\n ],\n cursors: true,\n },\n theme: 'snow',\n });\n const cursors = quill.getModule('cursors');\n\n // 04. bind the document with the Quill.\n // 04-1. Quill to Document.\n quill\n .on('text-change', (delta, _, source) => {\n if (source === 'api' || !delta.ops) {\n return;\n }\n\n let from = 0,\n to = 0;\n console.log(`%c quill: ${JSON.stringify(delta.ops)}`, 'color: green');\n for (const op of delta.ops) {\n if (op.attributes !== undefined || op.insert !== undefined) {\n if (op.retain !== undefined) {\n to = from + op.retain;\n }\n console.log(\n `%c local: ${from}-${to}: ${op.insert} ${\n op.attributes ? JSON.stringify(op.attributes) : '{}'\n }`,\n 'color: green',\n );\n\n doc.update((root, presence) => {\n let range;\n if (op.attributes !== undefined && op.insert === undefined) {\n root.content.setStyle(from, to, op.attributes);\n } else if (op.insert !== undefined) {\n if (to < from) {\n to = from;\n }\n\n if (typeof op.insert === 'object') {\n range = root.content.edit(from, to, ' ', {\n embed: JSON.stringify(op.insert),\n ...op.attributes,\n });\n } else {\n range = root.content.edit(from, to, op.insert, op.attributes);\n }\n from = to + op.insert.length;\n }\n\n range &&\n presence.set({\n selection: root.content.indexRangeToPosRange(range),\n });\n }, `update style by ${client.getID()}`);\n } else if (op.delete !== undefined) {\n to = from + op.delete;\n console.log(`%c local: ${from}-${to}: ''`, 'color: green');\n\n doc.update((root, presence) => {\n const range = root.content.edit(from, to, '');\n range &&\n presence.set({\n selection: root.content.indexRangeToPosRange(range),\n });\n }, `update content by ${client.getID()}`);\n } else if (op.retain !== undefined) {\n from = to + op.retain;\n to = from;\n }\n }\n })\n .on('selection-change', (range, _, source) => {\n if (!range) {\n return;\n }\n\n // NOTE(chacha912): If the selection in the Quill editor does not match the range computed by yorkie,\n // additional updates are necessary. This condition addresses situations where Quill's selection behaves\n // differently, such as when inserting text before a range selection made by another user, causing\n // the second character onwards to be included in the selection.\n if (source === 'api') {\n const { selection } = doc.getMyPresence();\n if (selection) {\n const [from, to] = doc\n .getRoot()\n .content.posRangeToIndexRange(selection);\n const { index, length } = range;\n if (from === index && to === index + length) {\n return;\n }\n }\n }\n\n doc.update((root, presence) => {\n presence.set({\n selection: root.content.indexRangeToPosRange([\n range.index,\n range.index + range.length,\n ]),\n });\n }, `update selection by ${client.getID()}`);\n });\n\n // 04-2. document to Quill(remote).\n function handleOperations(ops: Array) {\n const deltaOperations = [];\n let prevTo = 0;\n for (const op of ops) {\n if (op.type === 'edit') {\n const from = op.from;\n const to = op.to;\n const retainFrom = from - prevTo;\n const retainTo = to - from;\n\n const { insert, attributes } = toDeltaOperation(op.value!);\n console.log(`%c remote: ${from}-${to}: ${insert}`, 'color: skyblue');\n\n if (retainFrom) {\n deltaOperations.push({ retain: retainFrom });\n }\n if (retainTo) {\n deltaOperations.push({ delete: retainTo });\n }\n if (insert) {\n const op: DeltaOperation = { insert };\n if (attributes) {\n op.attributes = attributes;\n }\n deltaOperations.push(op);\n }\n prevTo = to;\n } else if (op.type === 'style') {\n const from = op.from;\n const to = op.to;\n const retainFrom = from - prevTo;\n const retainTo = to - from;\n const { attributes } = toDeltaOperation(op.value!);\n console.log(\n `%c remote: ${from}-${to}: ${JSON.stringify(attributes)}`,\n 'color: skyblue',\n );\n\n if (retainFrom) {\n deltaOperations.push({ retain: retainFrom });\n }\n if (attributes) {\n const op: DeltaOperation = { attributes };\n if (retainTo) {\n op.retain = retainTo;\n }\n\n deltaOperations.push(op);\n }\n prevTo = to;\n }\n }\n\n if (deltaOperations.length) {\n console.log(\n `%c to quill: ${JSON.stringify(deltaOperations)}`,\n 'color: green',\n );\n const delta = {\n ops: deltaOperations,\n } as DeltaStatic;\n quill.updateContents(delta, 'api');\n }\n }\n\n // 05. synchronize text of document and Quill.\n function syncText() {\n const text = doc.getRoot().content;\n\n const delta = {\n ops: text.values().map((val) => toDeltaOperation(val)),\n } as DeltaStatic;\n quill.setContents(delta, 'api');\n }\n\n syncText();\n updateAllCursors();\n displayLog(documentElem, documentTextElem, doc);\n}\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"network.ts","path":"/src/network.ts","content":"import { DocEvent, StreamConnectionStatus } from 'yorkie-js-sdk';\nexport const network = {\n isOnline: false,\n showOffline: (elem: HTMLElement) => {\n network.isOnline = false;\n elem.innerHTML = ' ';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = ' ';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin: 1rem 0;\n font-family: monospace;\n}\n\n.ql-editor {\n min-height: 300px;\n overflow-y: auto;\n resize: vertical;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { type Text, TextPosStructRange } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n username: string;\n color: string;\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc, YorkiePresence } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID, presence } of peers) {\n usernames.push(\n myClientID === clientID\n ? `${presence.username}`\n : presence.username,\n );\n }\n elem.innerHTML = JSON.stringify(usernames);\n}\n\n// function to display document content\nexport function displayLog(\n elem: HTMLElement,\n textElem: HTMLElement,\n doc: Document,\n) {\n elem.innerText = doc.toJSON();\n textElem.innerText = doc.getRoot().content.toTestString();\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// \n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Quill Example\n\n

\n \n \"Live\n \n

\n\nThis demo shows the real-time collaborative version of the [Quill](https://quilljs.com/) editor with [Yorkie](https://yorkie.dev/) and [Vite](https://vitejs.dev/).\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n \n \n \n Yorkie + Quill Example\n \n \n
\n
\n
\n
\n
\n \n \n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-quill\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@types/color-hash\": \"^1.0.2\",\n \"@types/quill\": \"^1.3.10\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"color-hash\": \"^2.0.2\",\n \"quill\": \"^1.3.7\",\n \"quill-cursors\": \"^4.0.0\",\n \"quill-delta\": \"^5.0.0\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/examples/vuejs-kanban/fileInfo.ts b/examples/vuejs-kanban/fileInfo.ts index 0855d41..b8444b2 100644 --- a/examples/vuejs-kanban/fileInfo.ts +++ b/examples/vuejs-kanban/fileInfo.ts @@ -1,2 +1,2 @@ import { DirectoryInfo } from '@/utils/exampleFileUtils'; - export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vuejs-kanban","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"assets","path":"/src/assets","children":[{"isFile":true,"isOpen":false,"language":"css","name":"main.css","path":"/src/assets/main.css","content":"body {\n margin: 0;\n padding: 0;\n background: #807b77;\n}\n\n.kanban {\n margin: 20px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n align-items: flex-start;\n user-select: none;\n}\n\n.kanban .add-list {\n padding: 10px;\n color: #fff;\n cursor: pointer;\n background: #ffffff3d;\n margin-right: 10px;\n width: 260px;\n border-radius: 3px;\n flex-shrink: 0;\n}\n\n.kanban .add-list:hover {\n background: #ffffff52;\n}\n\n.kaban .add-list-opener::before {\n content: '+ ';\n}\n\n.delete {\n position: absolute;\n cursor: pointer;\n top: 2px;\n right: 2px;\n display: none;\n}\n\n.add-form {\n display: flex;\n flex-direction: column;\n}\n\n.add-form input[type='text'] {\n border: none;\n overflow: auto;\n outline: none;\n\n font-size: 1em;\n\n margin: 5px 0;\n padding: 5px;\n background: #fff;\n border-radius: 3px;\n box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);\n position: relative;\n word-break: break-word;\n}\n\n.add-form input[type='button'] {\n font-size: 1em;\n padding: 5px;\n}\n\n.add-form input[type='button'].pull-right {\n float: right;\n}\n\n.list {\n background: #ebecf0;\n margin-right: 10px;\n border-radius: 3px;\n padding: 10px;\n width: 260px;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n}\n\n.list:hover > .delete {\n display: inherit;\n}\n\n.list .title {\n font-weight: bold;\n padding: 3px;\n}\n\n.list .card {\n margin: 5px 0;\n padding: 5px;\n background: #fff;\n border-radius: 3px;\n box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);\n position: relative;\n word-break: break-word;\n font-size: 1em;\n}\n\n.list .card:hover {\n background: #091e4214;\n}\n\n.list .card:hover .delete {\n display: inherit;\n}\n\n.add-card-opener {\n margin: 5px 0;\n padding: 5px;\n color: #444;\n font-size: 0.9em;\n cursor: pointer;\n}\n\n.add-card-opener:hover {\n background: #091e4214;\n}\n\n.add-card-opener::before {\n content: '+ ';\n}\n"}]},{"isFile":true,"isOpen":false,"language":"javascript","name":"App.vue","path":"/src/App.vue","content":"\n\n\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"main.js","path":"/src/main.js","content":"import { createApp } from 'vue'\nimport App from './App.vue'\n\nimport './assets/main.css'\n\ncreateApp(App).mount('#app')\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\ncoverage\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Vue Kanban Example\n\n

\n \n \"Live\n \n

\n\n\"Vue\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vuejs-kanban dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n\n\n \n \n \n Vite App\n\n\n\n
\n \n\n\n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vuejs-kanban\",\n \"version\": \"0.0.0\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"vue\": \"^3.2.41\",\n \"yorkie-js-sdk\": \"^0.5.5\"\n },\n \"devDependencies\": {\n \"@vitejs/plugin-vue\": \"^5.0.3\",\n \"vite\": \"^5.0.12\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { fileURLToPath, URL } from 'node:url';\n\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs/plugin-vue';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [vue()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n {\n find: '@',\n replacement: fileURLToPath(new URL('./src', import.meta.url)),\n },\n ],\n },\n});\n"}]} \ No newline at end of file + export const FILE_INFO: DirectoryInfo = {"isFile":false,"name":"vuejs-kanban","path":"/","children":[{"isFile":false,"name":"src","path":"/src","children":[{"isFile":false,"name":"assets","path":"/src/assets","children":[{"isFile":true,"isOpen":false,"language":"css","name":"main.css","path":"/src/assets/main.css","content":"body {\n margin: 0;\n padding: 0;\n background: #807b77;\n}\n\n.kanban {\n margin: 20px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n align-items: flex-start;\n user-select: none;\n}\n\n.kanban .add-list {\n padding: 10px;\n color: #fff;\n cursor: pointer;\n background: #ffffff3d;\n margin-right: 10px;\n width: 260px;\n border-radius: 3px;\n flex-shrink: 0;\n}\n\n.kanban .add-list:hover {\n background: #ffffff52;\n}\n\n.kaban .add-list-opener::before {\n content: '+ ';\n}\n\n.delete {\n position: absolute;\n cursor: pointer;\n top: 2px;\n right: 2px;\n display: none;\n}\n\n.add-form {\n display: flex;\n flex-direction: column;\n}\n\n.add-form input[type='text'] {\n border: none;\n overflow: auto;\n outline: none;\n\n font-size: 1em;\n\n margin: 5px 0;\n padding: 5px;\n background: #fff;\n border-radius: 3px;\n box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);\n position: relative;\n word-break: break-word;\n}\n\n.add-form input[type='button'] {\n font-size: 1em;\n padding: 5px;\n}\n\n.add-form input[type='button'].pull-right {\n float: right;\n}\n\n.list {\n background: #ebecf0;\n margin-right: 10px;\n border-radius: 3px;\n padding: 10px;\n width: 260px;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n}\n\n.list:hover > .delete {\n display: inherit;\n}\n\n.list .title {\n font-weight: bold;\n padding: 3px;\n}\n\n.list .card {\n margin: 5px 0;\n padding: 5px;\n background: #fff;\n border-radius: 3px;\n box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);\n position: relative;\n word-break: break-word;\n font-size: 1em;\n}\n\n.list .card:hover {\n background: #091e4214;\n}\n\n.list .card:hover .delete {\n display: inherit;\n}\n\n.add-card-opener {\n margin: 5px 0;\n padding: 5px;\n color: #444;\n font-size: 0.9em;\n cursor: pointer;\n}\n\n.add-card-opener:hover {\n background: #091e4214;\n}\n\n.add-card-opener::before {\n content: '+ ';\n}\n"}]},{"isFile":true,"isOpen":false,"language":"javascript","name":"App.vue","path":"/src/App.vue","content":"\n\n\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"main.js","path":"/src/main.js","content":"import { createApp } from 'vue'\nimport App from './App.vue'\n\nimport './assets/main.css'\n\ncreateApp(App).mount('#app')\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Store\ndist\ndist-ssr\ncoverage\n*.local\n\n/cypress/videos/\n/cypress/screenshots/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Vue Kanban Example\n\n

\n \n \"Live\n \n

\n\n\"Vue\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vuejs-kanban dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"\n\n\n\n \n \n \n Vite App\n\n\n\n
\n \n\n\n\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vuejs-kanban\",\n \"version\": \"0.0.0\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"vue\": \"^3.2.41\",\n \"yorkie-js-sdk\": \"^0.5.6\"\n },\n \"devDependencies\": {\n \"@vitejs/plugin-vue\": \"^5.0.3\",\n \"vite\": \"^5.0.12\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { fileURLToPath, URL } from 'node:url';\n\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs/plugin-vue';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [vue()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n {\n find: '@',\n replacement: fileURLToPath(new URL('./src', import.meta.url)),\n },\n ],\n },\n});\n"}]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 917c4a7..a5bf846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "unist-util-visit": "^4.1.1", - "yorkie-js-sdk": "^0.5.5" + "yorkie-js-sdk": "^0.5.6" }, "devDependencies": { "@mdx-js/loader": "^2.1.5", @@ -9911,9 +9911,9 @@ } }, "node_modules/yorkie-js-sdk": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.5.5.tgz", - "integrity": "sha512-Y+3B4JGPMPSoGndZvQwf2N8w959kMcH6b2ej5H7HtzgJIUPx+qO/fMuXCeylvlh+OotIBqhawzQeueQ82+r8fw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.5.6.tgz", + "integrity": "sha512-McWQLt8/XCUUdPxNxHBkfg5qh2Y5bAbB3mAHx05+DAEbMrYwB9n7f3xtFMbBcN2AEdPUDuk08MvD7qwG3DFx7w==", "dependencies": { "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.4.0", @@ -16873,9 +16873,9 @@ "dev": true }, "yorkie-js-sdk": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.5.5.tgz", - "integrity": "sha512-Y+3B4JGPMPSoGndZvQwf2N8w959kMcH6b2ej5H7HtzgJIUPx+qO/fMuXCeylvlh+OotIBqhawzQeueQ82+r8fw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.5.6.tgz", + "integrity": "sha512-McWQLt8/XCUUdPxNxHBkfg5qh2Y5bAbB3mAHx05+DAEbMrYwB9n7f3xtFMbBcN2AEdPUDuk08MvD7qwG3DFx7w==", "requires": { "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.4.0", diff --git a/package.json b/package.json index c320313..da71aa2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "unist-util-visit": "^4.1.1", - "yorkie-js-sdk": "^0.5.5" + "yorkie-js-sdk": "^0.5.6" }, "devDependencies": { "@mdx-js/loader": "^2.1.5",