From 3e81130278c50366a9c53e50539d46b436fc4792 Mon Sep 17 00:00:00 2001 From: Yourim Cha <81357083+chacha912@users.noreply.github.com> Date: Fri, 24 May 2024 18:35:51 +0900 Subject: [PATCH] Bump up Yorkie to v0.4.20 (#135) --- .env | 6 +++--- examples/nextjs-scheduler/fileInfo.ts | 2 +- examples/profile-stack/fileInfo.ts | 2 +- examples/react-tldraw/fileInfo.ts | 2 +- examples/react-todomvc/fileInfo.ts | 2 +- examples/simultaneous-cursors/fileInfo.ts | 2 +- examples/vanilla-codemirror6/fileInfo.ts | 2 +- examples/vanilla-quill/fileInfo.ts | 2 +- examples/vuejs-kanban/fileInfo.ts | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.env b/.env index 53dbf0a..4ffc713 100644 --- a/.env +++ b/.env @@ -1,10 +1,10 @@ # Common Environment Variables -NEXT_PUBLIC_YORKIE_VERSION='0.4.19' -NEXT_PUBLIC_YORKIE_JS_VERSION='0.4.19' +NEXT_PUBLIC_YORKIE_VERSION='0.4.20' +NEXT_PUBLIC_YORKIE_JS_VERSION='0.4.20' NEXT_PUBLIC_YORKIE_IOS_VERSION='0.4.17' NEXT_PUBLIC_YORKIE_ANDROID_VERSION='0.4.16' NEXT_PUBLIC_DASHBOARD_PATH='/dashboard' -NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.4.19/yorkie-js-sdk.js' +NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.4.20/yorkie-js-sdk.js' # Development Environment Variables NEXT_PUBLIC_SITE_URL='http://localhost:3000' diff --git a/examples/nextjs-scheduler/fileInfo.ts b/examples/nextjs-scheduler/fileInfo.ts index f206f7f..c2289d0 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":"NEXT_PUBLIC_YORKIE_API_ADDR='https://api.yorkie.dev'\nNEXT_PUBLIC_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\nNEXT_PUBLIC_BASE_PATH='/yorkie-js-sdk/examples/nextjs-scheduler'\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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-lock.json","path":"/package-lock.json","content":"{\n \"name\": \"nextjs-example\",\n \"version\": \"0.0.0\",\n \"lockfileVersion\": 2,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"nextjs-example\",\n \"version\": \"0.0.0\",\n \"dependencies\": {\n \"next\": \"13.5.4\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.4.6\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.0.24\",\n \"@types/react-dom\": \"18.0.8\",\n \"typescript\": \"4.6.4\"\n }\n },\n \"node_modules/@next/env\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz\",\n \"integrity\": \"sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==\"\n },\n \"node_modules/@next/swc-darwin-arm64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz\",\n \"integrity\": \"sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-darwin-x64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz\",\n \"integrity\": \"sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-arm64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-ia32-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==\",\n \"cpu\": [\n \"ia32\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-x64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@swc/helpers\": {\n \"version\": \"0.5.2\",\n \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz\",\n \"integrity\": \"sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==\",\n \"dependencies\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@types/google-protobuf\": {\n \"version\": \"3.15.6\",\n \"resolved\": \"https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz\",\n \"integrity\": \"sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==\"\n },\n \"node_modules/@types/lodash\": {\n \"version\": \"4.14.196\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz\",\n \"integrity\": \"sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==\"\n },\n \"node_modules/@types/lodash.memoize\": {\n \"version\": \"4.1.7\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz\",\n \"integrity\": \"sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==\",\n \"dependencies\": {\n \"@types/lodash\": \"*\"\n }\n },\n \"node_modules/@types/long\": {\n \"version\": \"4.0.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz\",\n \"integrity\": \"sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\"\n },\n \"node_modules/@types/node\": {\n \"version\": \"20.4.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz\",\n \"integrity\": \"sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==\",\n \"dev\": true\n },\n \"node_modules/@types/prop-types\": {\n \"version\": \"15.7.5\",\n \"resolved\": \"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz\",\n \"integrity\": \"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==\",\n \"devOptional\": true\n },\n \"node_modules/@types/react\": {\n \"version\": \"18.0.24\",\n \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz\",\n \"integrity\": \"sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==\",\n \"devOptional\": true,\n \"dependencies\": {\n \"@types/prop-types\": \"*\",\n \"@types/scheduler\": \"*\",\n \"csstype\": \"^3.0.2\"\n }\n },\n \"node_modules/@types/react-dom\": {\n \"version\": \"18.0.8\",\n \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz\",\n \"integrity\": \"sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==\",\n \"dev\": true,\n \"dependencies\": {\n \"@types/react\": \"*\"\n }\n },\n \"node_modules/@types/scheduler\": {\n \"version\": \"0.16.3\",\n \"resolved\": \"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz\",\n \"integrity\": \"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==\",\n \"devOptional\": true\n },\n \"node_modules/@wojtekmaj/date-utils\": {\n \"version\": \"1.5.0\",\n \"resolved\": \"https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz\",\n \"integrity\": \"sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==\",\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/date-utils?sponsor=1\"\n }\n },\n \"node_modules/busboy\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz\",\n \"integrity\": \"sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==\",\n \"dependencies\": {\n \"streamsearch\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">=10.16.0\"\n }\n },\n \"node_modules/caniuse-lite\": {\n \"version\": \"1.0.30001516\",\n \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz\",\n \"integrity\": \"sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/browserslist\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/caniuse-lite\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ]\n },\n \"node_modules/client-only\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\"\n },\n \"node_modules/clsx\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz\",\n \"integrity\": \"sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==\",\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/csstype\": {\n \"version\": \"3.1.2\",\n \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz\",\n \"integrity\": \"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==\",\n \"devOptional\": true\n },\n \"node_modules/get-user-locale\": {\n \"version\": \"2.3.0\",\n \"resolved\": \"https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.0.tgz\",\n \"integrity\": \"sha512-I3rQvAUwu2nauRD9YyQBSXVFJZixNouwA+eZld51Sn4Pn0N1qFbgcgOi/nPigJPQlNY519mT95fiSPRgflQiTA==\",\n \"dependencies\": {\n \"@types/lodash.memoize\": \"^4.1.7\",\n \"lodash.memoize\": \"^4.1.1\"\n },\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/get-user-locale?sponsor=1\"\n }\n },\n \"node_modules/glob-to-regexp\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz\",\n \"integrity\": \"sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==\"\n },\n \"node_modules/google-protobuf\": {\n \"version\": \"3.21.2\",\n \"resolved\": \"https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz\",\n \"integrity\": \"sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==\"\n },\n \"node_modules/graceful-fs\": {\n \"version\": \"4.2.11\",\n \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\"\n },\n \"node_modules/grpc-web\": {\n \"version\": \"1.4.2\",\n \"resolved\": \"https://registry.npmjs.org/grpc-web/-/grpc-web-1.4.2.tgz\",\n \"integrity\": \"sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw==\"\n },\n \"node_modules/js-tokens\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\"\n },\n \"node_modules/lodash.memoize\": {\n \"version\": \"4.1.2\",\n \"resolved\": \"https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz\",\n \"integrity\": \"sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==\"\n },\n \"node_modules/long\": {\n \"version\": \"5.2.3\",\n \"resolved\": \"https://registry.npmjs.org/long/-/long-5.2.3.tgz\",\n \"integrity\": \"sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==\"\n },\n \"node_modules/loose-envify\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n \"dependencies\": {\n \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n },\n \"bin\": {\n \"loose-envify\": \"cli.js\"\n }\n },\n \"node_modules/nanoid\": {\n \"version\": \"3.3.6\",\n \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz\",\n \"integrity\": \"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==\",\n \"funding\": [\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"bin\": {\n \"nanoid\": \"bin/nanoid.cjs\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n }\n },\n \"node_modules/next\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/next/-/next-13.5.4.tgz\",\n \"integrity\": \"sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==\",\n \"dependencies\": {\n \"@next/env\": \"13.5.4\",\n \"@swc/helpers\": \"0.5.2\",\n \"busboy\": \"1.6.0\",\n \"caniuse-lite\": \"^1.0.30001406\",\n \"postcss\": \"8.4.31\",\n \"styled-jsx\": \"5.1.1\",\n \"watchpack\": \"2.4.0\"\n },\n \"bin\": {\n \"next\": \"dist/bin/next\"\n },\n \"engines\": {\n \"node\": \">=16.14.0\"\n },\n \"optionalDependencies\": {\n \"@next/swc-darwin-arm64\": \"13.5.4\",\n \"@next/swc-darwin-x64\": \"13.5.4\",\n \"@next/swc-linux-arm64-gnu\": \"13.5.4\",\n \"@next/swc-linux-arm64-musl\": \"13.5.4\",\n \"@next/swc-linux-x64-gnu\": \"13.5.4\",\n \"@next/swc-linux-x64-musl\": \"13.5.4\",\n \"@next/swc-win32-arm64-msvc\": \"13.5.4\",\n \"@next/swc-win32-ia32-msvc\": \"13.5.4\",\n \"@next/swc-win32-x64-msvc\": \"13.5.4\"\n },\n \"peerDependencies\": {\n \"@opentelemetry/api\": \"^1.1.0\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"sass\": \"^1.3.0\"\n },\n \"peerDependenciesMeta\": {\n \"@opentelemetry/api\": {\n \"optional\": true\n },\n \"sass\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/object-assign\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/picocolors\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\",\n \"integrity\": \"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\"\n },\n \"node_modules/postcss\": {\n \"version\": \"8.4.31\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/postcss/\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"dependencies\": {\n \"nanoid\": \"^3.3.6\",\n \"picocolors\": \"^1.0.0\",\n \"source-map-js\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || >=14\"\n }\n },\n \"node_modules/prop-types\": {\n \"version\": \"15.8.1\",\n \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.4.0\",\n \"object-assign\": \"^4.1.1\",\n \"react-is\": \"^16.13.1\"\n }\n },\n \"node_modules/react\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react/-/react-18.2.0.tgz\",\n \"integrity\": \"sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/react-calendar\": {\n \"version\": \"4.6.0\",\n \"resolved\": \"https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz\",\n \"integrity\": \"sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==\",\n \"dependencies\": {\n \"@wojtekmaj/date-utils\": \"^1.1.3\",\n \"clsx\": \"^2.0.0\",\n \"get-user-locale\": \"^2.2.1\",\n \"prop-types\": \"^15.6.0\",\n \"tiny-warning\": \"^1.0.0\"\n },\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/react-calendar?sponsor=1\"\n },\n \"peerDependencies\": {\n \"@types/react\": \"^16.8.0 || ^17.0.0 || ^18.0.0\",\n \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0\",\n \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@types/react\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/react-dom\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz\",\n \"integrity\": \"sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\",\n \"scheduler\": \"^0.23.0\"\n },\n \"peerDependencies\": {\n \"react\": \"^18.2.0\"\n }\n },\n \"node_modules/react-is\": {\n \"version\": \"16.13.1\",\n \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\"\n },\n \"node_modules/scheduler\": {\n \"version\": \"0.23.0\",\n \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz\",\n \"integrity\": \"sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"node_modules/source-map-js\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\",\n \"integrity\": \"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/streamsearch\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz\",\n \"integrity\": \"sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==\",\n \"engines\": {\n \"node\": \">=10.0.0\"\n }\n },\n \"node_modules/styled-jsx\": {\n \"version\": \"5.1.1\",\n \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz\",\n \"integrity\": \"sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==\",\n \"dependencies\": {\n \"client-only\": \"0.0.1\"\n },\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"peerDependencies\": {\n \"react\": \">= 16.8.0 || 17.x.x || ^18.0.0-0\"\n },\n \"peerDependenciesMeta\": {\n \"@babel/core\": {\n \"optional\": true\n },\n \"babel-plugin-macros\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/tiny-warning\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz\",\n \"integrity\": \"sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==\"\n },\n \"node_modules/tslib\": {\n \"version\": \"2.6.2\",\n \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz\",\n \"integrity\": \"sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==\"\n },\n \"node_modules/typescript\": {\n \"version\": \"4.6.4\",\n \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz\",\n \"integrity\": \"sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==\",\n \"dev\": true,\n \"bin\": {\n \"tsc\": \"bin/tsc\",\n \"tsserver\": \"bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n }\n },\n \"node_modules/watchpack\": {\n \"version\": \"2.4.0\",\n \"resolved\": \"https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz\",\n \"integrity\": \"sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==\",\n \"dependencies\": {\n \"glob-to-regexp\": \"^0.4.1\",\n \"graceful-fs\": \"^4.1.2\"\n },\n \"engines\": {\n \"node\": \">=10.13.0\"\n }\n },\n \"node_modules/yorkie-js-sdk\": {\n \"version\": \"0.4.6\",\n \"resolved\": \"https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.6.tgz\",\n \"integrity\": \"sha512-wy5bWi397Ud/7e0zcE/5le/yg8wyz5FgsmBEVSeB8CXAu7sJhPQsQF/jdxbFZf+tym8PxfzFGkyIn+Lpsaf7og==\",\n \"dependencies\": {\n \"@types/google-protobuf\": \"^3.15.5\",\n \"@types/long\": \"^4.0.1\",\n \"google-protobuf\": \"^3.19.4\",\n \"grpc-web\": \"^1.3.1\",\n \"long\": \"^5.2.0\"\n }\n }\n },\n \"dependencies\": {\n \"@next/env\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz\",\n \"integrity\": \"sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==\"\n },\n \"@next/swc-darwin-arm64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz\",\n \"integrity\": \"sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==\",\n \"optional\": true\n },\n \"@next/swc-darwin-x64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz\",\n \"integrity\": \"sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==\",\n \"optional\": true\n },\n \"@next/swc-linux-arm64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==\",\n \"optional\": true\n },\n \"@next/swc-linux-arm64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==\",\n \"optional\": true\n },\n \"@next/swc-linux-x64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==\",\n \"optional\": true\n },\n \"@next/swc-linux-x64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==\",\n \"optional\": true\n },\n \"@next/swc-win32-arm64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==\",\n \"optional\": true\n },\n \"@next/swc-win32-ia32-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==\",\n \"optional\": true\n },\n \"@next/swc-win32-x64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==\",\n \"optional\": true\n },\n \"@swc/helpers\": {\n \"version\": \"0.5.2\",\n \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz\",\n \"integrity\": \"sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==\",\n \"requires\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"@types/google-protobuf\": {\n \"version\": \"3.15.6\",\n \"resolved\": \"https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz\",\n \"integrity\": \"sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==\"\n },\n \"@types/lodash\": {\n \"version\": \"4.14.196\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz\",\n \"integrity\": \"sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==\"\n },\n \"@types/lodash.memoize\": {\n \"version\": \"4.1.7\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz\",\n \"integrity\": \"sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==\",\n \"requires\": {\n \"@types/lodash\": \"*\"\n }\n },\n \"@types/long\": {\n \"version\": \"4.0.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz\",\n \"integrity\": \"sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\"\n },\n \"@types/node\": {\n \"version\": \"20.4.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz\",\n \"integrity\": \"sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==\",\n \"dev\": true\n },\n \"@types/prop-types\": {\n \"version\": \"15.7.5\",\n \"resolved\": \"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz\",\n \"integrity\": \"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==\",\n \"devOptional\": true\n },\n \"@types/react\": {\n \"version\": \"18.0.24\",\n \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz\",\n \"integrity\": \"sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==\",\n \"devOptional\": true,\n \"requires\": {\n \"@types/prop-types\": \"*\",\n \"@types/scheduler\": \"*\",\n \"csstype\": \"^3.0.2\"\n }\n },\n \"@types/react-dom\": {\n \"version\": \"18.0.8\",\n \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz\",\n \"integrity\": \"sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==\",\n \"dev\": true,\n \"requires\": {\n \"@types/react\": \"*\"\n }\n },\n \"@types/scheduler\": {\n \"version\": \"0.16.3\",\n \"resolved\": \"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz\",\n \"integrity\": \"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==\",\n \"devOptional\": true\n },\n \"@wojtekmaj/date-utils\": {\n \"version\": \"1.5.0\",\n \"resolved\": \"https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz\",\n \"integrity\": \"sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==\"\n },\n \"busboy\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz\",\n \"integrity\": \"sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==\",\n \"requires\": {\n \"streamsearch\": \"^1.1.0\"\n }\n },\n \"caniuse-lite\": {\n \"version\": \"1.0.30001516\",\n \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz\",\n \"integrity\": \"sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==\"\n },\n \"client-only\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\"\n },\n \"clsx\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz\",\n \"integrity\": \"sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==\"\n },\n \"csstype\": {\n \"version\": \"3.1.2\",\n \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz\",\n \"integrity\": \"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==\",\n \"devOptional\": true\n },\n \"get-user-locale\": {\n \"version\": \"2.3.0\",\n \"resolved\": \"https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.0.tgz\",\n \"integrity\": \"sha512-I3rQvAUwu2nauRD9YyQBSXVFJZixNouwA+eZld51Sn4Pn0N1qFbgcgOi/nPigJPQlNY519mT95fiSPRgflQiTA==\",\n \"requires\": {\n \"@types/lodash.memoize\": \"^4.1.7\",\n \"lodash.memoize\": \"^4.1.1\"\n }\n },\n \"glob-to-regexp\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz\",\n \"integrity\": \"sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==\"\n },\n \"google-protobuf\": {\n \"version\": \"3.21.2\",\n \"resolved\": \"https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz\",\n \"integrity\": \"sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==\"\n },\n \"graceful-fs\": {\n \"version\": \"4.2.11\",\n \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\"\n },\n \"grpc-web\": {\n \"version\": \"1.4.2\",\n \"resolved\": \"https://registry.npmjs.org/grpc-web/-/grpc-web-1.4.2.tgz\",\n \"integrity\": \"sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw==\"\n },\n \"js-tokens\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\"\n },\n \"lodash.memoize\": {\n \"version\": \"4.1.2\",\n \"resolved\": \"https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz\",\n \"integrity\": \"sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==\"\n },\n \"long\": {\n \"version\": \"5.2.3\",\n \"resolved\": \"https://registry.npmjs.org/long/-/long-5.2.3.tgz\",\n \"integrity\": \"sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==\"\n },\n \"loose-envify\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n \"requires\": {\n \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n }\n },\n \"nanoid\": {\n \"version\": \"3.3.6\",\n \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz\",\n \"integrity\": \"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==\"\n },\n \"next\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/next/-/next-13.5.4.tgz\",\n \"integrity\": \"sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==\",\n \"requires\": {\n \"@next/env\": \"13.5.4\",\n \"@next/swc-darwin-arm64\": \"13.5.4\",\n \"@next/swc-darwin-x64\": \"13.5.4\",\n \"@next/swc-linux-arm64-gnu\": \"13.5.4\",\n \"@next/swc-linux-arm64-musl\": \"13.5.4\",\n \"@next/swc-linux-x64-gnu\": \"13.5.4\",\n \"@next/swc-linux-x64-musl\": \"13.5.4\",\n \"@next/swc-win32-arm64-msvc\": \"13.5.4\",\n \"@next/swc-win32-ia32-msvc\": \"13.5.4\",\n \"@next/swc-win32-x64-msvc\": \"13.5.4\",\n \"@swc/helpers\": \"0.5.2\",\n \"busboy\": \"1.6.0\",\n \"caniuse-lite\": \"^1.0.30001406\",\n \"postcss\": \"8.4.31\",\n \"styled-jsx\": \"5.1.1\",\n \"watchpack\": \"2.4.0\"\n }\n },\n \"object-assign\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\"\n },\n \"picocolors\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\",\n \"integrity\": \"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\"\n },\n \"postcss\": {\n \"version\": \"8.4.31\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n \"requires\": {\n \"nanoid\": \"^3.3.6\",\n \"picocolors\": \"^1.0.0\",\n \"source-map-js\": \"^1.0.2\"\n }\n },\n \"prop-types\": {\n \"version\": \"15.8.1\",\n \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n \"requires\": {\n \"loose-envify\": \"^1.4.0\",\n \"object-assign\": \"^4.1.1\",\n \"react-is\": \"^16.13.1\"\n }\n },\n \"react\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react/-/react-18.2.0.tgz\",\n \"integrity\": \"sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"react-calendar\": {\n \"version\": \"4.6.0\",\n \"resolved\": \"https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz\",\n \"integrity\": \"sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==\",\n \"requires\": {\n \"@wojtekmaj/date-utils\": \"^1.1.3\",\n \"clsx\": \"^2.0.0\",\n \"get-user-locale\": \"^2.2.1\",\n \"prop-types\": \"^15.6.0\",\n \"tiny-warning\": \"^1.0.0\"\n }\n },\n \"react-dom\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz\",\n \"integrity\": \"sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\",\n \"scheduler\": \"^0.23.0\"\n }\n },\n \"react-is\": {\n \"version\": \"16.13.1\",\n \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\"\n },\n \"scheduler\": {\n \"version\": \"0.23.0\",\n \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz\",\n \"integrity\": \"sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"source-map-js\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\",\n \"integrity\": \"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\"\n },\n \"streamsearch\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz\",\n \"integrity\": \"sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==\"\n },\n \"styled-jsx\": {\n \"version\": \"5.1.1\",\n \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz\",\n \"integrity\": \"sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==\",\n \"requires\": {\n \"client-only\": \"0.0.1\"\n }\n },\n \"tiny-warning\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz\",\n \"integrity\": \"sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==\"\n },\n \"tslib\": {\n \"version\": \"2.6.2\",\n \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz\",\n \"integrity\": \"sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==\"\n },\n \"typescript\": {\n \"version\": \"4.6.4\",\n \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz\",\n \"integrity\": \"sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==\",\n \"dev\": true\n },\n \"watchpack\": {\n \"version\": \"2.4.0\",\n \"resolved\": \"https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz\",\n \"integrity\": \"sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==\",\n \"requires\": {\n \"glob-to-regexp\": \"^0.4.1\",\n \"graceful-fs\": \"^4.1.2\"\n }\n },\n \"yorkie-js-sdk\": {\n \"version\": \"0.4.6\",\n \"resolved\": \"https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.6.tgz\",\n \"integrity\": \"sha512-wy5bWi397Ud/7e0zcE/5le/yg8wyz5FgsmBEVSeB8CXAu7sJhPQsQF/jdxbFZf+tym8PxfzFGkyIn+Lpsaf7og==\",\n \"requires\": {\n \"@types/google-protobuf\": \"^3.15.5\",\n \"@types/long\": \"^4.0.1\",\n \"google-protobuf\": \"^3.19.4\",\n \"grpc-web\": \"^1.3.1\",\n \"long\": \"^5.2.0\"\n }\n }\n }\n}\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 5173\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"13.5.4\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.4.19\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.0.24\",\n \"@types/react-dom\": \"18.0.8\",\n \"typescript\": \"4.6.4\"\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\": [\n \"DOM\",\n \"DOM.Iterable\",\n \"ESNext\"\n ],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": true,\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 \"./*\"\n ]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\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":"NEXT_PUBLIC_YORKIE_API_ADDR='https://api.yorkie.dev'\nNEXT_PUBLIC_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\nNEXT_PUBLIC_BASE_PATH='/yorkie-js-sdk/examples/nextjs-scheduler'\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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-lock.json","path":"/package-lock.json","content":"{\n \"name\": \"nextjs-example\",\n \"version\": \"0.0.0\",\n \"lockfileVersion\": 2,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"nextjs-example\",\n \"version\": \"0.0.0\",\n \"dependencies\": {\n \"next\": \"13.5.4\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.4.6\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.0.24\",\n \"@types/react-dom\": \"18.0.8\",\n \"typescript\": \"4.6.4\"\n }\n },\n \"node_modules/@next/env\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz\",\n \"integrity\": \"sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==\"\n },\n \"node_modules/@next/swc-darwin-arm64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz\",\n \"integrity\": \"sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-darwin-x64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz\",\n \"integrity\": \"sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-arm64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-ia32-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==\",\n \"cpu\": [\n \"ia32\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-x64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@swc/helpers\": {\n \"version\": \"0.5.2\",\n \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz\",\n \"integrity\": \"sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==\",\n \"dependencies\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@types/google-protobuf\": {\n \"version\": \"3.15.6\",\n \"resolved\": \"https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz\",\n \"integrity\": \"sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==\"\n },\n \"node_modules/@types/lodash\": {\n \"version\": \"4.14.196\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz\",\n \"integrity\": \"sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==\"\n },\n \"node_modules/@types/lodash.memoize\": {\n \"version\": \"4.1.7\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz\",\n \"integrity\": \"sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==\",\n \"dependencies\": {\n \"@types/lodash\": \"*\"\n }\n },\n \"node_modules/@types/long\": {\n \"version\": \"4.0.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz\",\n \"integrity\": \"sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\"\n },\n \"node_modules/@types/node\": {\n \"version\": \"20.4.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz\",\n \"integrity\": \"sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==\",\n \"dev\": true\n },\n \"node_modules/@types/prop-types\": {\n \"version\": \"15.7.5\",\n \"resolved\": \"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz\",\n \"integrity\": \"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==\",\n \"devOptional\": true\n },\n \"node_modules/@types/react\": {\n \"version\": \"18.0.24\",\n \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz\",\n \"integrity\": \"sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==\",\n \"devOptional\": true,\n \"dependencies\": {\n \"@types/prop-types\": \"*\",\n \"@types/scheduler\": \"*\",\n \"csstype\": \"^3.0.2\"\n }\n },\n \"node_modules/@types/react-dom\": {\n \"version\": \"18.0.8\",\n \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz\",\n \"integrity\": \"sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==\",\n \"dev\": true,\n \"dependencies\": {\n \"@types/react\": \"*\"\n }\n },\n \"node_modules/@types/scheduler\": {\n \"version\": \"0.16.3\",\n \"resolved\": \"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz\",\n \"integrity\": \"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==\",\n \"devOptional\": true\n },\n \"node_modules/@wojtekmaj/date-utils\": {\n \"version\": \"1.5.0\",\n \"resolved\": \"https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz\",\n \"integrity\": \"sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==\",\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/date-utils?sponsor=1\"\n }\n },\n \"node_modules/busboy\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz\",\n \"integrity\": \"sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==\",\n \"dependencies\": {\n \"streamsearch\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">=10.16.0\"\n }\n },\n \"node_modules/caniuse-lite\": {\n \"version\": \"1.0.30001516\",\n \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz\",\n \"integrity\": \"sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/browserslist\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/caniuse-lite\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ]\n },\n \"node_modules/client-only\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\"\n },\n \"node_modules/clsx\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz\",\n \"integrity\": \"sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==\",\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/csstype\": {\n \"version\": \"3.1.2\",\n \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz\",\n \"integrity\": \"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==\",\n \"devOptional\": true\n },\n \"node_modules/get-user-locale\": {\n \"version\": \"2.3.0\",\n \"resolved\": \"https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.0.tgz\",\n \"integrity\": \"sha512-I3rQvAUwu2nauRD9YyQBSXVFJZixNouwA+eZld51Sn4Pn0N1qFbgcgOi/nPigJPQlNY519mT95fiSPRgflQiTA==\",\n \"dependencies\": {\n \"@types/lodash.memoize\": \"^4.1.7\",\n \"lodash.memoize\": \"^4.1.1\"\n },\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/get-user-locale?sponsor=1\"\n }\n },\n \"node_modules/glob-to-regexp\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz\",\n \"integrity\": \"sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==\"\n },\n \"node_modules/google-protobuf\": {\n \"version\": \"3.21.2\",\n \"resolved\": \"https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz\",\n \"integrity\": \"sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==\"\n },\n \"node_modules/graceful-fs\": {\n \"version\": \"4.2.11\",\n \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\"\n },\n \"node_modules/grpc-web\": {\n \"version\": \"1.4.2\",\n \"resolved\": \"https://registry.npmjs.org/grpc-web/-/grpc-web-1.4.2.tgz\",\n \"integrity\": \"sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw==\"\n },\n \"node_modules/js-tokens\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\"\n },\n \"node_modules/lodash.memoize\": {\n \"version\": \"4.1.2\",\n \"resolved\": \"https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz\",\n \"integrity\": \"sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==\"\n },\n \"node_modules/long\": {\n \"version\": \"5.2.3\",\n \"resolved\": \"https://registry.npmjs.org/long/-/long-5.2.3.tgz\",\n \"integrity\": \"sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==\"\n },\n \"node_modules/loose-envify\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n \"dependencies\": {\n \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n },\n \"bin\": {\n \"loose-envify\": \"cli.js\"\n }\n },\n \"node_modules/nanoid\": {\n \"version\": \"3.3.6\",\n \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz\",\n \"integrity\": \"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==\",\n \"funding\": [\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"bin\": {\n \"nanoid\": \"bin/nanoid.cjs\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n }\n },\n \"node_modules/next\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/next/-/next-13.5.4.tgz\",\n \"integrity\": \"sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==\",\n \"dependencies\": {\n \"@next/env\": \"13.5.4\",\n \"@swc/helpers\": \"0.5.2\",\n \"busboy\": \"1.6.0\",\n \"caniuse-lite\": \"^1.0.30001406\",\n \"postcss\": \"8.4.31\",\n \"styled-jsx\": \"5.1.1\",\n \"watchpack\": \"2.4.0\"\n },\n \"bin\": {\n \"next\": \"dist/bin/next\"\n },\n \"engines\": {\n \"node\": \">=16.14.0\"\n },\n \"optionalDependencies\": {\n \"@next/swc-darwin-arm64\": \"13.5.4\",\n \"@next/swc-darwin-x64\": \"13.5.4\",\n \"@next/swc-linux-arm64-gnu\": \"13.5.4\",\n \"@next/swc-linux-arm64-musl\": \"13.5.4\",\n \"@next/swc-linux-x64-gnu\": \"13.5.4\",\n \"@next/swc-linux-x64-musl\": \"13.5.4\",\n \"@next/swc-win32-arm64-msvc\": \"13.5.4\",\n \"@next/swc-win32-ia32-msvc\": \"13.5.4\",\n \"@next/swc-win32-x64-msvc\": \"13.5.4\"\n },\n \"peerDependencies\": {\n \"@opentelemetry/api\": \"^1.1.0\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"sass\": \"^1.3.0\"\n },\n \"peerDependenciesMeta\": {\n \"@opentelemetry/api\": {\n \"optional\": true\n },\n \"sass\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/object-assign\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/picocolors\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\",\n \"integrity\": \"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\"\n },\n \"node_modules/postcss\": {\n \"version\": \"8.4.31\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/postcss/\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"dependencies\": {\n \"nanoid\": \"^3.3.6\",\n \"picocolors\": \"^1.0.0\",\n \"source-map-js\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || >=14\"\n }\n },\n \"node_modules/prop-types\": {\n \"version\": \"15.8.1\",\n \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.4.0\",\n \"object-assign\": \"^4.1.1\",\n \"react-is\": \"^16.13.1\"\n }\n },\n \"node_modules/react\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react/-/react-18.2.0.tgz\",\n \"integrity\": \"sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/react-calendar\": {\n \"version\": \"4.6.0\",\n \"resolved\": \"https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz\",\n \"integrity\": \"sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==\",\n \"dependencies\": {\n \"@wojtekmaj/date-utils\": \"^1.1.3\",\n \"clsx\": \"^2.0.0\",\n \"get-user-locale\": \"^2.2.1\",\n \"prop-types\": \"^15.6.0\",\n \"tiny-warning\": \"^1.0.0\"\n },\n \"funding\": {\n \"url\": \"https://github.com/wojtekmaj/react-calendar?sponsor=1\"\n },\n \"peerDependencies\": {\n \"@types/react\": \"^16.8.0 || ^17.0.0 || ^18.0.0\",\n \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0\",\n \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@types/react\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/react-dom\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz\",\n \"integrity\": \"sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\",\n \"scheduler\": \"^0.23.0\"\n },\n \"peerDependencies\": {\n \"react\": \"^18.2.0\"\n }\n },\n \"node_modules/react-is\": {\n \"version\": \"16.13.1\",\n \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\"\n },\n \"node_modules/scheduler\": {\n \"version\": \"0.23.0\",\n \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz\",\n \"integrity\": \"sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==\",\n \"dependencies\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"node_modules/source-map-js\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\",\n \"integrity\": \"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/streamsearch\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz\",\n \"integrity\": \"sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==\",\n \"engines\": {\n \"node\": \">=10.0.0\"\n }\n },\n \"node_modules/styled-jsx\": {\n \"version\": \"5.1.1\",\n \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz\",\n \"integrity\": \"sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==\",\n \"dependencies\": {\n \"client-only\": \"0.0.1\"\n },\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"peerDependencies\": {\n \"react\": \">= 16.8.0 || 17.x.x || ^18.0.0-0\"\n },\n \"peerDependenciesMeta\": {\n \"@babel/core\": {\n \"optional\": true\n },\n \"babel-plugin-macros\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/tiny-warning\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz\",\n \"integrity\": \"sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==\"\n },\n \"node_modules/tslib\": {\n \"version\": \"2.6.2\",\n \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz\",\n \"integrity\": \"sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==\"\n },\n \"node_modules/typescript\": {\n \"version\": \"4.6.4\",\n \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz\",\n \"integrity\": \"sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==\",\n \"dev\": true,\n \"bin\": {\n \"tsc\": \"bin/tsc\",\n \"tsserver\": \"bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n }\n },\n \"node_modules/watchpack\": {\n \"version\": \"2.4.0\",\n \"resolved\": \"https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz\",\n \"integrity\": \"sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==\",\n \"dependencies\": {\n \"glob-to-regexp\": \"^0.4.1\",\n \"graceful-fs\": \"^4.1.2\"\n },\n \"engines\": {\n \"node\": \">=10.13.0\"\n }\n },\n \"node_modules/yorkie-js-sdk\": {\n \"version\": \"0.4.6\",\n \"resolved\": \"https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.6.tgz\",\n \"integrity\": \"sha512-wy5bWi397Ud/7e0zcE/5le/yg8wyz5FgsmBEVSeB8CXAu7sJhPQsQF/jdxbFZf+tym8PxfzFGkyIn+Lpsaf7og==\",\n \"dependencies\": {\n \"@types/google-protobuf\": \"^3.15.5\",\n \"@types/long\": \"^4.0.1\",\n \"google-protobuf\": \"^3.19.4\",\n \"grpc-web\": \"^1.3.1\",\n \"long\": \"^5.2.0\"\n }\n }\n },\n \"dependencies\": {\n \"@next/env\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz\",\n \"integrity\": \"sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==\"\n },\n \"@next/swc-darwin-arm64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz\",\n \"integrity\": \"sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==\",\n \"optional\": true\n },\n \"@next/swc-darwin-x64\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz\",\n \"integrity\": \"sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==\",\n \"optional\": true\n },\n \"@next/swc-linux-arm64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==\",\n \"optional\": true\n },\n \"@next/swc-linux-arm64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==\",\n \"optional\": true\n },\n \"@next/swc-linux-x64-gnu\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz\",\n \"integrity\": \"sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==\",\n \"optional\": true\n },\n \"@next/swc-linux-x64-musl\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz\",\n \"integrity\": \"sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==\",\n \"optional\": true\n },\n \"@next/swc-win32-arm64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==\",\n \"optional\": true\n },\n \"@next/swc-win32-ia32-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==\",\n \"optional\": true\n },\n \"@next/swc-win32-x64-msvc\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz\",\n \"integrity\": \"sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==\",\n \"optional\": true\n },\n \"@swc/helpers\": {\n \"version\": \"0.5.2\",\n \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz\",\n \"integrity\": \"sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==\",\n \"requires\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"@types/google-protobuf\": {\n \"version\": \"3.15.6\",\n \"resolved\": \"https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz\",\n \"integrity\": \"sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==\"\n },\n \"@types/lodash\": {\n \"version\": \"4.14.196\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz\",\n \"integrity\": \"sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==\"\n },\n \"@types/lodash.memoize\": {\n \"version\": \"4.1.7\",\n \"resolved\": \"https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz\",\n \"integrity\": \"sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==\",\n \"requires\": {\n \"@types/lodash\": \"*\"\n }\n },\n \"@types/long\": {\n \"version\": \"4.0.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz\",\n \"integrity\": \"sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\"\n },\n \"@types/node\": {\n \"version\": \"20.4.2\",\n \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz\",\n \"integrity\": \"sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==\",\n \"dev\": true\n },\n \"@types/prop-types\": {\n \"version\": \"15.7.5\",\n \"resolved\": \"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz\",\n \"integrity\": \"sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==\",\n \"devOptional\": true\n },\n \"@types/react\": {\n \"version\": \"18.0.24\",\n \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-18.0.24.tgz\",\n \"integrity\": \"sha512-wRJWT6ouziGUy+9uX0aW4YOJxAY0bG6/AOk5AW5QSvZqI7dk6VBIbXvcVgIw/W5Jrl24f77df98GEKTJGOLx7Q==\",\n \"devOptional\": true,\n \"requires\": {\n \"@types/prop-types\": \"*\",\n \"@types/scheduler\": \"*\",\n \"csstype\": \"^3.0.2\"\n }\n },\n \"@types/react-dom\": {\n \"version\": \"18.0.8\",\n \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.8.tgz\",\n \"integrity\": \"sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==\",\n \"dev\": true,\n \"requires\": {\n \"@types/react\": \"*\"\n }\n },\n \"@types/scheduler\": {\n \"version\": \"0.16.3\",\n \"resolved\": \"https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz\",\n \"integrity\": \"sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==\",\n \"devOptional\": true\n },\n \"@wojtekmaj/date-utils\": {\n \"version\": \"1.5.0\",\n \"resolved\": \"https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz\",\n \"integrity\": \"sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==\"\n },\n \"busboy\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz\",\n \"integrity\": \"sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==\",\n \"requires\": {\n \"streamsearch\": \"^1.1.0\"\n }\n },\n \"caniuse-lite\": {\n \"version\": \"1.0.30001516\",\n \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz\",\n \"integrity\": \"sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==\"\n },\n \"client-only\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\"\n },\n \"clsx\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz\",\n \"integrity\": \"sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==\"\n },\n \"csstype\": {\n \"version\": \"3.1.2\",\n \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz\",\n \"integrity\": \"sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==\",\n \"devOptional\": true\n },\n \"get-user-locale\": {\n \"version\": \"2.3.0\",\n \"resolved\": \"https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.0.tgz\",\n \"integrity\": \"sha512-I3rQvAUwu2nauRD9YyQBSXVFJZixNouwA+eZld51Sn4Pn0N1qFbgcgOi/nPigJPQlNY519mT95fiSPRgflQiTA==\",\n \"requires\": {\n \"@types/lodash.memoize\": \"^4.1.7\",\n \"lodash.memoize\": \"^4.1.1\"\n }\n },\n \"glob-to-regexp\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz\",\n \"integrity\": \"sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==\"\n },\n \"google-protobuf\": {\n \"version\": \"3.21.2\",\n \"resolved\": \"https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz\",\n \"integrity\": \"sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==\"\n },\n \"graceful-fs\": {\n \"version\": \"4.2.11\",\n \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\"\n },\n \"grpc-web\": {\n \"version\": \"1.4.2\",\n \"resolved\": \"https://registry.npmjs.org/grpc-web/-/grpc-web-1.4.2.tgz\",\n \"integrity\": \"sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw==\"\n },\n \"js-tokens\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\"\n },\n \"lodash.memoize\": {\n \"version\": \"4.1.2\",\n \"resolved\": \"https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz\",\n \"integrity\": \"sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==\"\n },\n \"long\": {\n \"version\": \"5.2.3\",\n \"resolved\": \"https://registry.npmjs.org/long/-/long-5.2.3.tgz\",\n \"integrity\": \"sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==\"\n },\n \"loose-envify\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n \"requires\": {\n \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n }\n },\n \"nanoid\": {\n \"version\": \"3.3.6\",\n \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz\",\n \"integrity\": \"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==\"\n },\n \"next\": {\n \"version\": \"13.5.4\",\n \"resolved\": \"https://registry.npmjs.org/next/-/next-13.5.4.tgz\",\n \"integrity\": \"sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==\",\n \"requires\": {\n \"@next/env\": \"13.5.4\",\n \"@next/swc-darwin-arm64\": \"13.5.4\",\n \"@next/swc-darwin-x64\": \"13.5.4\",\n \"@next/swc-linux-arm64-gnu\": \"13.5.4\",\n \"@next/swc-linux-arm64-musl\": \"13.5.4\",\n \"@next/swc-linux-x64-gnu\": \"13.5.4\",\n \"@next/swc-linux-x64-musl\": \"13.5.4\",\n \"@next/swc-win32-arm64-msvc\": \"13.5.4\",\n \"@next/swc-win32-ia32-msvc\": \"13.5.4\",\n \"@next/swc-win32-x64-msvc\": \"13.5.4\",\n \"@swc/helpers\": \"0.5.2\",\n \"busboy\": \"1.6.0\",\n \"caniuse-lite\": \"^1.0.30001406\",\n \"postcss\": \"8.4.31\",\n \"styled-jsx\": \"5.1.1\",\n \"watchpack\": \"2.4.0\"\n }\n },\n \"object-assign\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\"\n },\n \"picocolors\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\",\n \"integrity\": \"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\"\n },\n \"postcss\": {\n \"version\": \"8.4.31\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n \"requires\": {\n \"nanoid\": \"^3.3.6\",\n \"picocolors\": \"^1.0.0\",\n \"source-map-js\": \"^1.0.2\"\n }\n },\n \"prop-types\": {\n \"version\": \"15.8.1\",\n \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n \"requires\": {\n \"loose-envify\": \"^1.4.0\",\n \"object-assign\": \"^4.1.1\",\n \"react-is\": \"^16.13.1\"\n }\n },\n \"react\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react/-/react-18.2.0.tgz\",\n \"integrity\": \"sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"react-calendar\": {\n \"version\": \"4.6.0\",\n \"resolved\": \"https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz\",\n \"integrity\": \"sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==\",\n \"requires\": {\n \"@wojtekmaj/date-utils\": \"^1.1.3\",\n \"clsx\": \"^2.0.0\",\n \"get-user-locale\": \"^2.2.1\",\n \"prop-types\": \"^15.6.0\",\n \"tiny-warning\": \"^1.0.0\"\n }\n },\n \"react-dom\": {\n \"version\": \"18.2.0\",\n \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz\",\n \"integrity\": \"sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\",\n \"scheduler\": \"^0.23.0\"\n }\n },\n \"react-is\": {\n \"version\": \"16.13.1\",\n \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\"\n },\n \"scheduler\": {\n \"version\": \"0.23.0\",\n \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz\",\n \"integrity\": \"sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==\",\n \"requires\": {\n \"loose-envify\": \"^1.1.0\"\n }\n },\n \"source-map-js\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\",\n \"integrity\": \"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\"\n },\n \"streamsearch\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz\",\n \"integrity\": \"sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==\"\n },\n \"styled-jsx\": {\n \"version\": \"5.1.1\",\n \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz\",\n \"integrity\": \"sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==\",\n \"requires\": {\n \"client-only\": \"0.0.1\"\n }\n },\n \"tiny-warning\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz\",\n \"integrity\": \"sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==\"\n },\n \"tslib\": {\n \"version\": \"2.6.2\",\n \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz\",\n \"integrity\": \"sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==\"\n },\n \"typescript\": {\n \"version\": \"4.6.4\",\n \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz\",\n \"integrity\": \"sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==\",\n \"dev\": true\n },\n \"watchpack\": {\n \"version\": \"2.4.0\",\n \"resolved\": \"https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz\",\n \"integrity\": \"sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==\",\n \"requires\": {\n \"glob-to-regexp\": \"^0.4.1\",\n \"graceful-fs\": \"^4.1.2\"\n }\n },\n \"yorkie-js-sdk\": {\n \"version\": \"0.4.6\",\n \"resolved\": \"https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.6.tgz\",\n \"integrity\": \"sha512-wy5bWi397Ud/7e0zcE/5le/yg8wyz5FgsmBEVSeB8CXAu7sJhPQsQF/jdxbFZf+tym8PxfzFGkyIn+Lpsaf7og==\",\n \"requires\": {\n \"@types/google-protobuf\": \"^3.15.5\",\n \"@types/long\": \"^4.0.1\",\n \"google-protobuf\": \"^3.19.4\",\n \"grpc-web\": \"^1.3.1\",\n \"long\": \"^5.2.0\"\n }\n }\n }\n}\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 5173\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"13.5.4\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.4.20\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.0.24\",\n \"@types/react-dom\": \"18.0.8\",\n \"typescript\": \"4.6.4\"\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\": [\n \"DOM\",\n \"DOM.Iterable\",\n \"ESNext\"\n ],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": true,\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 \"./*\"\n ]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n"}]} \ No newline at end of file diff --git a/examples/profile-stack/fileInfo.ts b/examples/profile-stack/fileInfo.ts index cec9653..817f7ef 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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\": \"^3.2.7\"\n },\n \"dependencies\": {\n \"yorkie-js-sdk\": \"^0.4.19\"\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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\": \"^3.2.7\"\n },\n \"dependencies\": {\n \"yorkie-js-sdk\": \"^0.4.20\"\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n});\n"}]} \ No newline at end of file diff --git a/examples/react-tldraw/fileInfo.ts b/examples/react-tldraw/fileInfo.ts index f6ba619..0b199ff 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 await client.attach(doc, {\n initialPresence: {\n tdUser: app?.currentUser,\n },\n });\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":"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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'"},{"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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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/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.4.19\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.14.198\",\n \"@types/randomcolor\": \"^0.5.5\",\n \"@types/react\": \"^18.0.24\",\n \"@types/react-dom\": \"^18.0.8\",\n \"@vitejs/plugin-react\": \"^2.2.0\",\n \"typescript\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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 },\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 { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\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 await client.attach(doc, {\n initialPresence: {\n tdUser: app?.currentUser,\n },\n });\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":"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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'"},{"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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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/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.4.20\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.14.198\",\n \"@types/randomcolor\": \"^0.5.5\",\n \"@types/react\": \"^18.0.24\",\n \"@types/react-dom\": \"^18.0.8\",\n \"@vitejs/plugin-react\": \"^2.2.0\",\n \"typescript\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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 },\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 { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\n});\n"}]} \ No newline at end of file diff --git a/examples/react-todomvc/fileInfo.ts b/examples/react-todomvc/fileInfo.ts index a64ea6e..e24bb54 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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.4.19\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.0.24\",\n \"@types/react-dom\": \"^18.0.8\",\n \"@vitejs/plugin-react\": \"^2.2.0\",\n \"typescript\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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 },\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 { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nNow you can run the demo.\n\n```bash\n$ npm run 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.4.20\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.0.24\",\n \"@types/react-dom\": \"^18.0.8\",\n \"@vitejs/plugin-react\": \"^2.2.0\",\n \"typescript\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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 },\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 { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\n});\n"}]} \ No newline at end of file diff --git a/examples/simultaneous-cursors/fileInfo.ts b/examples/simultaneous-cursors/fileInfo.ts index c5f9a7a..a2d756b 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm 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$ npm run dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n$ npm 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$ npm run 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.4.19\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.0.37\",\n \"@types/react-dom\": \"^18.0.11\",\n \"@vitejs/plugin-react\": \"^4.0.0\",\n \"vite\": \"^4.3.9\"\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'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm 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$ npm run dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n$ npm 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$ npm run 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.4.20\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.0.37\",\n \"@types/react-dom\": \"^18.0.11\",\n \"@vitejs/plugin-react\": \"^4.0.0\",\n \"vite\": \"^4.3.9\"\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'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react()],\n})\n"}]} \ No newline at end of file diff --git a/examples/vanilla-codemirror6/fileInfo.ts b/examples/vanilla-codemirror6/fileInfo.ts index 0aa37a1..a99b863 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, 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 } from '@codemirror/state';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc } 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 view.dispatch({\n changes: { from: 0, to: view.state.doc.length, insert: text.toString() },\n annotations: [Transaction.remote.of(true)],\n });\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\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 operations.forEach((op) => {\n if (op.type === 'edit') {\n handleEditOp(op);\n }\n });\n }\n function handleEditOp(op: any) {\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 { type Text } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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.1.2\",\n \"@codemirror/view\": \"^6.3.1\",\n \"codemirror\": \"^6.0.1\",\n \"yorkie-js-sdk\": \"^0.4.19\"\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 \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noImplicitReturns\": true,\n \"skipLibCheck\": true\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\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 { TextOperationInfo, EditOpInfo } 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 } from '@codemirror/state';\nimport { network } from './network';\nimport { displayLog, displayPeers } from './utils';\nimport { YorkieDoc } 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 view.dispatch({\n changes: { from: 0, to: view.state.doc.length, insert: text.toString() },\n annotations: [Transaction.remote.of(true)],\n });\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\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 { type Text } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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.1.2\",\n \"@codemirror/view\": \"^6.3.1\",\n \"codemirror\": \"^6.0.1\",\n \"yorkie-js-sdk\": \"^0.4.20\"\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 \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noImplicitReturns\": true,\n \"skipLibCheck\": true\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n})\n"}]} \ No newline at end of file diff --git a/examples/vanilla-quill/fileInfo.ts b/examples/vanilla-quill/fileInfo.ts index b2dca24..5762a71 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: JSON.parse(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 | undefined;\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, 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm 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$ npm run dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n$ npm 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$ npm run 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\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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.4.19\"\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 \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noImplicitReturns\": true,\n \"skipLibCheck\": true\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\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: JSON.parse(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 | undefined;\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, 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm 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$ npm run dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n$ npm 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$ npm run 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\": \"^4.6.4\",\n \"vite\": \"^3.2.7\"\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.4.20\"\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 \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"noImplicitReturns\": true,\n \"skipLibCheck\": true\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';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n})\n"}]} \ No newline at end of file diff --git a/examples/vuejs-kanban/fileInfo.ts b/examples/vuejs-kanban/fileInfo.ts index 3388e3e..65402d4 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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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.4.19\"\n },\n \"devDependencies\": {\n \"@vitejs/plugin-vue\": \"^3.1.2\",\n \"vite\": \"^3.2.7\"\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'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [vue()],\n resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url))\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":"VITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='cedaovjuioqlk4pjqn6g'\n"},{"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$ npm install\n```\n\nStart demo project\n\n```bash\n$ npm run 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.4.20\"\n },\n \"devDependencies\": {\n \"@vitejs/plugin-vue\": \"^3.1.2\",\n \"vite\": \"^3.2.7\"\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'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [vue()],\n resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url))\n }\n }\n})\n"}]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e853bf3..845d14b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "unist-util-visit": "^4.1.1", - "yorkie-js-sdk": "^0.4.19" + "yorkie-js-sdk": "^0.4.20" }, "devDependencies": { "@mdx-js/loader": "^2.1.5", @@ -9844,9 +9844,9 @@ } }, "node_modules/yorkie-js-sdk": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.19.tgz", - "integrity": "sha512-jYyWTlnyiyacrlQHJ8BEx3IXb+T5V+3FrppfT/faSs9b2bHVP9wSvI9Pbt384T2YkflgYMpshdV88IESxxtmpg==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.20.tgz", + "integrity": "sha512-e9uXXYfm7vVL/kGJ5tyyL8Erod9P1Ggiq5p6wP8FRh2G4BrJk9kAzHXUjvWqhw5XywiQsVXvWtIX1rlD6WsYqg==", "dependencies": { "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.2.0", @@ -16771,9 +16771,9 @@ "dev": true }, "yorkie-js-sdk": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.19.tgz", - "integrity": "sha512-jYyWTlnyiyacrlQHJ8BEx3IXb+T5V+3FrppfT/faSs9b2bHVP9wSvI9Pbt384T2YkflgYMpshdV88IESxxtmpg==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/yorkie-js-sdk/-/yorkie-js-sdk-0.4.20.tgz", + "integrity": "sha512-e9uXXYfm7vVL/kGJ5tyyL8Erod9P1Ggiq5p6wP8FRh2G4BrJk9kAzHXUjvWqhw5XywiQsVXvWtIX1rlD6WsYqg==", "requires": { "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.2.0", diff --git a/package.json b/package.json index bacccf0..9864932 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "unist-util-visit": "^4.1.1", - "yorkie-js-sdk": "^0.4.19" + "yorkie-js-sdk": "^0.4.20" }, "devDependencies": { "@mdx-js/loader": "^2.1.5",