-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bump up Yorkie to v0.5.3 #172
Conversation
WalkthroughThe changes in this pull request involve updates to environment variables and file information constants across multiple files. Specifically, the Yorkie SDK version has been incremented in the Changes
Possibly related PRs
Suggested reviewers
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Outside diff range and nitpick comments (7)
examples/vuejs-kanban/fileInfo.ts (2)
2-2
: Consider documenting production deployment configurationWhile the local development setup is clear with the provided
.env
file, the.env.production
file is empty. Consider adding documentation or template values to guide production deployment configuration.Example documentation for
.env.production
:+# Production environment configuration +# VITE_YORKIE_API_ADDR='https://your-production-yorkie-server' +# VITE_YORKIE_API_KEY='your-production-api-key'
2-2
: Enhance documentation with system requirements and deployment guideThe README.md provides good instructions for local development. Consider enhancing it with:
- Node.js version requirements
- Browser compatibility information
- Production deployment guide
Example additions:
+## System Requirements + +- Node.js 16.x or higher +- Docker for running Yorkie server + +## Browser Support + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) + +## Production Deployment + +1. Configure `.env.production` with your production Yorkie server details +2. Build the application: `pnpm build` +3. Deploy the contents of the `dist` directory to your hosting serviceexamples/vanilla-codemirror6/fileInfo.ts (2)
2-2
: Consider adding performance monitoring.The implementation could benefit from basic performance monitoring to track synchronization latency and operation processing time.
Consider adding timing measurements for:
- Document attachment time
- Operation propagation delay
- Remote change application time
2-2
: Documentation improvement needed.The main.ts implementation would benefit from additional JSDoc comments explaining the key components and their interactions.
Consider adding documentation for:
- The document structure and its lifecycle
- The synchronization flow between peers
- The integration points with CodeMirror
examples/profile-stack/fileInfo.ts (1)
2-2
: Consider adding color theme configuration.The profile colors are hardcoded in the SVGs. Consider extracting these colors to a theme configuration for easier customization and maintenance.
This would allow for:
- Easier theme customization
- Dynamic color generation
- Better maintainability
examples/react-tldraw/fileInfo.ts (1)
2-2
: Consider adding JSDoc documentation.Adding JSDoc documentation for the
FILE_INFO
constant would help developers understand its purpose and usage in the codebase.+/** + * Represents the file structure of the react-tldraw example project. + * This constant is used for demonstration purposes to showcase the project organization. + * @type {DirectoryInfo} + */ export const FILE_INFO: DirectoryInfo = {examples/vanilla-quill/fileInfo.ts (1)
2-2
: Documentation: Add TypeScript type information to READMEThe example uses TypeScript with specific type definitions for Yorkie integration. Consider adding a section in the README about the TypeScript setup and available types.
Add the following section to README.md:
+## TypeScript Support + +This example includes TypeScript definitions for: +- Yorkie document structure (`YorkieDoc`) +- Presence information (`YorkiePresence`) +- Integration with Quill editor types
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (9)
- .env (1 hunks)
- examples/nextjs-scheduler/fileInfo.ts (1 hunks)
- examples/profile-stack/fileInfo.ts (1 hunks)
- examples/react-tldraw/fileInfo.ts (1 hunks)
- examples/react-todomvc/fileInfo.ts (1 hunks)
- examples/vanilla-codemirror6/fileInfo.ts (1 hunks)
- examples/vanilla-quill/fileInfo.ts (1 hunks)
- examples/vuejs-kanban/fileInfo.ts (1 hunks)
- package.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
- .env
🧰 Additional context used
🔇 Additional comments (15)
package.json (2)
37-37
: Ensure lock file is updated.Please make sure to commit the updated package-lock.json or yarn.lock file to ensure dependency resolution remains consistent across environments.
37-37
: LGTM, but please verify the changelog.The version bump from ^0.5.1 to ^0.5.3 looks good. Since this is a 0.x version, please ensure there are no breaking changes in the release notes.
examples/vuejs-kanban/fileInfo.ts (1)
2-2
: LGTM: Dependency updates look goodThe update to yorkie-js-sdk ^0.5.3 along with the complementary dev dependency updates maintains compatibility while bringing in the latest features and fixes.
examples/vanilla-codemirror6/fileInfo.ts (2)
2-2
: Version update confirmed in package.json content.The package.json content within FILE_INFO shows
yorkie-js-sdk
is correctly set to version^0.5.3
, which aligns with the PR objective.
2-2
: Verify compatibility with CodeMirror dependencies.The implementation uses several CodeMirror packages. Let's verify there are no known compatibility issues between Yorkie v0.5.3 and the specified CodeMirror versions.
✅ Verification successful
Let me gather more information about the CodeMirror dependencies and their usage.
Let me try again with the correct path to search for CodeMirror setup.
I notice from the file info in the review context that the dependencies and CodeMirror setup information is already available. Let me analyze the provided context:
From the
package.json
in the review context:"dependencies": { "@codemirror/commands": "6.1.2", "@codemirror/highlight": "^0.19.8", "@codemirror/lang-markdown": "^6.0.2", "@codemirror/language-data": "^6.1.0", "@codemirror/state": "^6.4.1", "@codemirror/view": "6.23.1", "codemirror": "^6.0.1", "yorkie-js-sdk": "^0.5.3" }And from
main.ts
, the CodeMirror setup is properly configured:const view = new EditorView({ doc: '', extensions: [ basicSetup, markdown({ base: markdownLanguage }), keymap.of(markdownKeymap), updateListener, ], parent: editorParentElem, });The CodeMirror dependencies are compatible and properly configured.
The implementation uses stable versions of CodeMirror 6 packages that are compatible with each other and with Yorkie v0.5.3. The setup follows the recommended patterns for CodeMirror 6 integration.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Search for any compatibility-related issues or discussions rg -i "compatibility|version|breaking.?change" README.mdLength of output: 174
Script:
#!/bin/bash # Check package.json for CodeMirror dependencies and their versions cat package.json | grep -A 10 "dependencies" # Search for any CodeMirror initialization or setup code ast-grep --pattern 'new EditorView({ $$$ })'Length of output: 411
Script:
#!/bin/bash # Check the specific package.json in the vanilla-codemirror6 example cat examples/vanilla-codemirror6/package.json | grep -A 10 "dependencies" # Search for CodeMirror initialization in main.ts cat examples/vanilla-codemirror6/src/main.ts | grep -A 15 "new EditorView"Length of output: 297
examples/profile-stack/fileInfo.ts (2)
2-2
: LGTM! Well-structured file information constant.The FILE_INFO constant is well-organized with a clear hierarchy of directories and files, making it easy to understand the project structure.
2-2
: Verify SVG gradient compatibility.The SVGs use linear gradients with specific color stops. While this works well in modern browsers, it's worth verifying browser compatibility and ensuring consistent rendering.
Run this script to check for potential gradient-related issues:
✅ Verification successful
After analyzing the results, I can now provide a final response since the script output shows all the gradient implementations across the SVG files.
SVG gradients are consistently implemented and browser-compatible
The verification shows that all SVG profile images follow the same robust gradient pattern:
- All use standard
linearGradient
withuserSpaceOnUse
units- Consistent coordinates (x1="26" y1="0" x2="26" y2="52") across all variants
- Two-color stops with proper offset values (0 and 1)
- Unique IDs to prevent conflicts
- Standard SVG namespace and viewBox attributes
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check for consistent gradient implementation across SVGs # Expected: All SVGs should follow the same gradient pattern # Test: Extract and compare gradient definitions rg -U 'linearGradient.*?</linearGradient>' --multilineLength of output: 16518
examples/react-todomvc/fileInfo.ts (2)
2-2
: Verify Yorkie SDK version consistency.Let's verify that the Yorkie SDK version in package.json matches the version mentioned in the PR title.
✅ Verification successful
Let me try a different approach to verify the Yorkie SDK version since the previous path was incorrect.
Looking at the provided code context in the review comment, I can see the package.json content directly in the FILE_INFO constant. Let me verify the version from that content.
Yorkie SDK version is correctly specified as ^0.5.3
Looking at the package.json content in the FILE_INFO constant, I can see:
"dependencies": { "yorkie-js-sdk": "^0.5.3" }This matches exactly with what we expect. The version is correctly specified as ^0.5.3.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify Yorkie SDK version consistency # Check package.json for Yorkie version echo "Checking Yorkie version in package.json..." rg '"yorkie-js-sdk":\s*"(\^?[0-9]+\.[0-9]+\.[0-9]+)"' examples/react-todomvc/package.json # The version should be "^0.5.3" according to the PR titleLength of output: 262
Script:
#!/bin/bash # Description: Verify Yorkie SDK version consistency using the file content from FILE_INFO # Extract and check package.json content for Yorkie version echo "Checking Yorkie version in package.json..." echo '{"isFile":false,"name":"react-todomvc","path":"/","children":[' | jq -r '.. | select(.name == "package.json")? | .content' | grep "yorkie-js-sdk"Length of output: 307
2-2
: Verify the consistency of file structure.Let's verify that the file structure in
FILE_INFO
matches the actual project structure.examples/react-tldraw/fileInfo.ts (2)
2-2
: LGTM! The file structure is well-organized.The
FILE_INFO
constant provides a comprehensive representation of the project structure, making it easier for developers to understand the codebase organization.
2-2
: Verify Yorkie v0.5.3 compatibility with the hooks implementation.The hooks implementation in the content string, particularly
useMultiplayerState.ts
, contains critical integration code with Yorkie. Let's verify its compatibility with v0.5.3.examples/vanilla-quill/fileInfo.ts (1)
2-2
: LGTM: Version update aligns with PR objectiveThe Yorkie SDK version has been updated to 0.5.3 in the package.json, which aligns with the PR objective to bump up Yorkie to v0.5.3.
examples/nextjs-scheduler/fileInfo.ts (3)
2-2
: Well-organized project structure following Next.js conventionsThe file structure follows Next.js app directory conventions with proper separation of concerns:
/app
for application code/app/utils
for utility functions/app/styles
for CSS modules- Clear component organization
2-2
: Clean and well-structured Scheduler implementationThe Scheduler component implementation:
- Uses proper TypeScript types
- Has clear separation of concerns
- Implements proper error handling
- Follows React best practices
2-2
: Successful Yorkie SDK version updateThe
yorkie-js-sdk
has been properly updated to version ^0.5.3 in package.json, which aligns with the PR objective.
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<YorkieDoc>(\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<TextOperationInfo>) {\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 = '<span class=\"red\"> </span>';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = '<span class=\"green\"> </span>';\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 ? `<b>${clientID}</b>` : 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<YorkieDoc>,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie CodeMirror6 Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/vanilla-codemirror6/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"CodeMirror6\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-codemirror6 dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Yorkie + CodeMirror 6 Example</title>\n </head>\n <body>\n <div id=\"network-status\"></div>\n <div id=\"editor\"></div>\n <div id=\"peers\"></div>\n <div id=\"document\"></div>\n <div id=\"document-text\"></div>\n <script type=\"module\" src=\"/src/main.ts\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-codemirror6\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"@codemirror/commands\": \"6.1.2\",\n \"@codemirror/highlight\": \"^0.19.8\",\n \"@codemirror/lang-markdown\": \"^6.0.2\",\n \"@codemirror/language-data\": \"^6.1.0\",\n \"@codemirror/state\": \"^6.4.1\",\n \"@codemirror/view\": \"6.23.1\",\n \"codemirror\": \"^6.0.1\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider enhancing error handling in main.ts.
The main.ts implementation within FILE_INFO could benefit from additional error handling:
- Network reconnection strategy
- Error handling for failed document attachments
- await client.attach(doc);
+ try {
+ await client.attach(doc);
+ } catch (err) {
+ console.error('Failed to attach document:', err);
+ // Implement retry logic or user notification
+ }
Committable suggestion was skipped due to low confidence.
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":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_624_277680)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_624_277680\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#8DECEC\"/>\n<stop offset=\"1\" stop-color=\"#3C9AF1\"/>\n</linearGradient>\n</defs>\n</svg>\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-green.svg","path":"/public/images/profile-green.svg","content":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_624_277695)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_624_277695\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#D7E38B\"/>\n<stop offset=\"1\" stop-color=\"#23C176\"/>\n</linearGradient>\n</defs>\n</svg>\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-orange.svg","path":"/public/images/profile-orange.svg","content":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_6879_210319)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_6879_210319\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#FF9754\"/>\n<stop offset=\"1\" stop-color=\"#F96767\"/>\n</linearGradient>\n</defs>\n</svg>\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-purple.svg","path":"/public/images/profile-purple.svg","content":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_624_277679)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_624_277679\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#84B5FF\"/>\n<stop offset=\"1\" stop-color=\"#855CF9\"/>\n</linearGradient>\n</defs>\n</svg>\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-red.svg","path":"/public/images/profile-red.svg","content":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_624_277678)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_624_277678\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#FC94D8\"/>\n<stop offset=\"1\" stop-color=\"#F44954\"/>\n</linearGradient>\n</defs>\n</svg>\n"},{"isFile":true,"isOpen":false,"language":"svg","name":"profile-yellow.svg","path":"/public/images/profile-yellow.svg","content":"<svg width=\"52\" height=\"52\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<circle cx=\"26\" cy=\"26\" r=\"26\" fill=\"url(#paint0_linear_6879_210318)\"/>\n<circle cx=\"18.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<circle cx=\"33.5\" cy=\"19.5\" r=\"3.5\" fill=\"#FEFDFB\"/>\n<path d=\"M17 28C17 28 19 33 26 33C33 33 35 28 35 28\" stroke=\"#FEFDFB\" stroke-width=\"5\" stroke-linecap=\"round\"/>\n<defs>\n<linearGradient id=\"paint0_linear_6879_210318\" x1=\"26\" y1=\"0\" x2=\"26\" y2=\"52\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#FDC433\"/>\n<stop offset=\"1\" stop-color=\"#FE924D\"/>\n</linearGradient>\n</defs>\n</svg>\n"}]},{"isFile":true,"isOpen":false,"language":"ico","name":"favicon.ico","path":"/public/favicon.ico","content":""}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Profile Stack Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/profile-stack/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"Profile Stack\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm profile-stack dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.ico\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Profile Stack - Yorkie Example</title>\n <link rel=\"stylesheet\" href=\"/style.css\" />\n </head>\n <body>\n <div id=\"app\">\n <div id=\"peerList\"></div>\n </div>\n <script type=\"module\" src=\"/main.js\"></script>\n </body>\n</html>\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 <div class=\"profile\">\n <img src=\"./images/profile-${color}.svg\" alt=\"profile\" class=\"profile-img\"/>\n </div>\n <div class=\"name speech-bubbles\">${name}</div>\n `;\n } else if (type === 'more') {\n $peer.innerHTML = `\n <img src=\"./images/profile-${color}.svg\" alt=\"profile\" class=\"profile-img\"/>\n <span class=\"name\">${name}</span>\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 <div class=\"profile\">\n +${peerCount - MAX_PEER_VIEW}\n </div>\n `;\n $peer.appendChild($peerMoreList);\n $peerList.appendChild($peer);\n }\n};\n\nmain();\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"profile-stack\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"yorkie-js-sdk\": \"^0.5.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/style.css","content":"* {\n margin: 0;\n padding: 0;\n}\n\nbody {\n --light-gray: #f5f3f1;\n --gray: #c2bdba;\n --black: #332e2b;\n --white: #fefdfb;\n\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n color: var(--black);\n}\n\nimg {\n vertical-align: top;\n}\n\n*::-webkit-scrollbar {\n width: 10px;\n height: 4px;\n}\n\n*::-webkit-scrollbar-thumb {\n background: var(--gray);\n border-radius: 10px;\n border: 3px solid var(--white);\n}\n\n*::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.speech-bubbles {\n padding: 16px;\n border: 1px solid var(--gray);\n border-radius: 16px;\n}\n\n.speech-bubbles:before {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -6px;\n margin-top: -8px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 6px solid transparent;\n border-right: 6px solid transparent;\n border-bottom: 8px solid var(--gray);\n}\n\n.speech-bubbles:after {\n position: absolute;\n top: 0;\n left: 50%;\n margin-left: -5px;\n margin-top: -6px;\n width: 0;\n height: 0;\n content: '';\n border-top: 0px solid transparent;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n border-bottom: 7px solid var(--white);\n}\n\n#peerList {\n display: inline-flex;\n border: 1px solid var(--gray);\n border-radius: 100px;\n white-space: nowrap;\n}\n\n.peer {\n position: relative;\n margin: 12px;\n}\n\n.profile-img {\n width: 52px;\n cursor: pointer;\n}\n\n.peer.me {\n order: -1;\n}\n\n.peer .name {\n font-weight: 900;\n white-space: nowrap;\n}\n\n.peer .speech-bubbles {\n display: none;\n position: absolute;\n top: 80px;\n left: 50%;\n transform: translate(-50%);\n background: var(--white);\n}\n\n.peer:hover .speech-bubbles {\n display: block;\n}\n\n.peer.more {\n display: flex;\n justify-content: center;\n align-items: center;\n width: 52px;\n height: 52px;\n background: var(--light-gray);\n border-radius: 100%;\n font-weight: 900;\n font-size: 24px;\n cursor: pointer;\n}\n\n.peer-more-list {\n font-size: 16px;\n}\n\n.peer-more-list .peer {\n display: flex;\n align-items: center;\n margin: 0 0 12px 0;\n}\n\n.peer-more-list .peer:last-child {\n margin-bottom: 0;\n}\n\n.peer-more-list .profile-img {\n margin-right: 8px;\n width: 26px;\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":"util.js","path":"/util.js","content":"const NAMES = [\n 'Ali',\n 'Beatriz',\n 'Charles',\n 'Diya',\n 'Eric',\n 'Fatima',\n 'Gabriel',\n 'Hanna',\n 'Johnson',\n 'Perry',\n 'Parker',\n 'Kelly',\n];\nexport const getRandomName = () => {\n const index = Math.floor(Math.random() * NAMES.length);\n return NAMES[index];\n};\n\nconst COLORS = ['red', 'yellow', 'orange', 'green', 'blue', 'purple'];\nexport const getRandomColor = () => {\n const index = Math.floor(Math.random() * COLORS.length);\n return COLORS[index];\n};\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider extracting SVG content to separate files.
The current implementation embeds SVG content directly in the FILE_INFO constant, which could make maintenance more challenging. Consider:
- Moving SVG content to separate files
- Loading them dynamically using file system operations
Here's a script to verify the feasibility of this refactoring:
#!/bin/bash
# Description: Check if SVGs already exist as separate files
# Expected: No matches if SVGs are only embedded in fileInfo.ts
# Test: Look for existing SVG files
fd -e svg -t f
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<Todo>;\n\n/**\n * `App` is the root component of the application.\n */\nexport default function App() {\n const [doc] = useState<Document<{ todos: JSONArray<Todo> }>>(\n () =>\n new yorkie.Document<{ todos: JSONArray<Todo> }>(\n `react-todomvc-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n ),\n );\n const [todos, setTodos] = useState<Array<Todo>>([]);\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<Todo> }>,\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 <div className=\"App\">\n <Header addTodo={actions.addTodo} />\n <MainSection todos={todos} actions={actions} />\n </div>\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<HTMLButtonElement, 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 <footer className=\"footer\">\n <span className=\"todo-count\">\n <strong>{activeCount || 'No'}</strong>\n {activeCount === 1 ? 'item' : 'items'} left\n </span>\n <ul className=\"filters\">\n {\n ['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map((filter) => (\n <li key={filter}>\n <button\n type=\"button\"\n className={classnames({ selected: filter === selectedFilter })}\n style={{ cursor: 'pointer' }}\n onClick={() => onShow(filter)}\n >\n {FILTER_TITLES[filter]}\n </button>\n </li>\n ))\n }\n </ul>\n {!!completedCount && (\n <button type=\"button\" className=\"clear-completed\" onClick={onClearCompleted}>\n Clear completed\n </button>\n )}\n </footer>\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 <header className=\"header\">\n <h1>todos</h1>\n <TodoTextInput\n newTodo\n onSave={(text: string) => {\n if (text.length !== 0) {\n props.addTodo(text);\n }\n }}\n placeholder=\"What needs to be done?\"\n />\n </header>\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<HTMLInputElement>) => void;\n\ninterface MainSectionProps {\n todos: Array<Todo>;\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 <section className=\"main\">\n <input\n className=\"toggle-all\"\n type=\"checkbox\"\n defaultChecked={completedCount === todos.length}\n onChange={actions.completeAll as ChangeEventHandler}\n />\n <ul className=\"todo-list\">\n {\n filteredTodos.map((todo) => (\n <TodoItem\n key={todo.id}\n todo={todo}\n editTodo={actions.editTodo}\n deleteTodo={actions.deleteTodo}\n completeTodo={actions.completeTodo}\n />\n ))\n }\n </ul>\n <Footer\n completedCount={completedCount}\n activeCount={activeCount}\n filter={filter}\n onClearCompleted={() => actions.clearCompleted()}\n onShow={setFilter}\n />\n </section>\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 <li\n className={classnames({\n completed: todo.completed,\n editing,\n })}\n >\n {editing ? (\n <TodoTextInput\n text={todo.text}\n editing={editing}\n onSave={(text: string) => {\n if (text.length === 0) {\n deleteTodo(todo.id);\n } else {\n editTodo(todo.id, text);\n }\n setEditing(false);\n }}\n />\n ) : (\n <div className=\"view\">\n <input\n id={`item-input-${todo.id}`}\n className=\"toggle\"\n type=\"checkbox\"\n checked={todo.completed}\n onChange={() => completeTodo(todo.id)}\n />\n <label htmlFor={`item-input-${todo.id}`} onDoubleClick={() => setEditing(true)}>{todo.text}</label>\n <button type=\"button\" aria-label=\"Delete\" className=\"destroy\" onClick={() => deleteTodo(todo.id)} />\n </div>\n )}\n </li>\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 <input\n className={classnames({\n edit: props.editing,\n 'new-todo': props.newTodo,\n })}\n type=\"text\"\n placeholder={props.placeholder}\n value={text}\n onBlur={(e: React.FocusEvent<HTMLInputElement>) => {\n if (!props.newTodo) {\n props.onSave(e.target.value);\n }\n }}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n setText(e.target.value);\n }}\n onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {\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 <App />,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React TodoMVC Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/react-todomvc/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"React TodoMVC\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-todomvc dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Vite + React + TS</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-todomvc\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"classnames\": \"^2.3.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"todomvc-app-css\": \"^2.4.2\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider splitting the file content into separate files.
The FILE_INFO
constant contains the actual content of multiple files, which makes this file large and harder to maintain. Consider extracting the file contents into separate files and importing them.
- Create a new directory
file-contents
with individual files for each content - Import and use them in the
FILE_INFO
constant:
import { appTsxContent } from './file-contents/app-tsx';
import { footerTsxContent } from './file-contents/footer-tsx';
// ... more imports
export const FILE_INFO: DirectoryInfo = {
// ... structure remains same
children: [
{
// ... other properties
content: appTsxContent
}
// ... other files
]
};
Remove sensitive information from environment file content.
The .env
file content is exposed in the FILE_INFO
constant. Even though these are example environment variables, it's a security best practice to not include actual environment file contents in source code.
Apply this diff to replace the actual content with placeholder text:
-"content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"
+"content":"# Environment variables for the example\n# Replace with actual values when running the example"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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<Todo>;\n\n/**\n * `App` is the root component of the application.\n */\nexport default function App() {\n const [doc] = useState<Document<{ todos: JSONArray<Todo> }>>(\n () =>\n new yorkie.Document<{ todos: JSONArray<Todo> }>(\n `react-todomvc-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n ),\n );\n const [todos, setTodos] = useState<Array<Todo>>([]);\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<Todo> }>,\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 <div className=\"App\">\n <Header addTodo={actions.addTodo} />\n <MainSection todos={todos} actions={actions} />\n </div>\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<HTMLButtonElement, 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 <footer className=\"footer\">\n <span className=\"todo-count\">\n <strong>{activeCount || 'No'}</strong>\n {activeCount === 1 ? 'item' : 'items'} left\n </span>\n <ul className=\"filters\">\n {\n ['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map((filter) => (\n <li key={filter}>\n <button\n type=\"button\"\n className={classnames({ selected: filter === selectedFilter })}\n style={{ cursor: 'pointer' }}\n onClick={() => onShow(filter)}\n >\n {FILTER_TITLES[filter]}\n </button>\n </li>\n ))\n }\n </ul>\n {!!completedCount && (\n <button type=\"button\" className=\"clear-completed\" onClick={onClearCompleted}>\n Clear completed\n </button>\n )}\n </footer>\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 <header className=\"header\">\n <h1>todos</h1>\n <TodoTextInput\n newTodo\n onSave={(text: string) => {\n if (text.length !== 0) {\n props.addTodo(text);\n }\n }}\n placeholder=\"What needs to be done?\"\n />\n </header>\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<HTMLInputElement>) => void;\n\ninterface MainSectionProps {\n todos: Array<Todo>;\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 <section className=\"main\">\n <input\n className=\"toggle-all\"\n type=\"checkbox\"\n defaultChecked={completedCount === todos.length}\n onChange={actions.completeAll as ChangeEventHandler}\n />\n <ul className=\"todo-list\">\n {\n filteredTodos.map((todo) => (\n <TodoItem\n key={todo.id}\n todo={todo}\n editTodo={actions.editTodo}\n deleteTodo={actions.deleteTodo}\n completeTodo={actions.completeTodo}\n />\n ))\n }\n </ul>\n <Footer\n completedCount={completedCount}\n activeCount={activeCount}\n filter={filter}\n onClearCompleted={() => actions.clearCompleted()}\n onShow={setFilter}\n />\n </section>\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 <li\n className={classnames({\n completed: todo.completed,\n editing,\n })}\n >\n {editing ? (\n <TodoTextInput\n text={todo.text}\n editing={editing}\n onSave={(text: string) => {\n if (text.length === 0) {\n deleteTodo(todo.id);\n } else {\n editTodo(todo.id, text);\n }\n setEditing(false);\n }}\n />\n ) : (\n <div className=\"view\">\n <input\n id={`item-input-${todo.id}`}\n className=\"toggle\"\n type=\"checkbox\"\n checked={todo.completed}\n onChange={() => completeTodo(todo.id)}\n />\n <label htmlFor={`item-input-${todo.id}`} onDoubleClick={() => setEditing(true)}>{todo.text}</label>\n <button type=\"button\" aria-label=\"Delete\" className=\"destroy\" onClick={() => deleteTodo(todo.id)} />\n </div>\n )}\n </li>\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 <input\n className={classnames({\n edit: props.editing,\n 'new-todo': props.newTodo,\n })}\n type=\"text\"\n placeholder={props.placeholder}\n value={text}\n onBlur={(e: React.FocusEvent<HTMLInputElement>) => {\n if (!props.newTodo) {\n props.onSave(e.target.value);\n }\n }}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n setText(e.target.value);\n }}\n onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {\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 <App />,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React TodoMVC Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/react-todomvc/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"React TodoMVC\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-todomvc dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Vite + React + TS</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-todomvc\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"classnames\": \"^2.3.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"todomvc-app-css\": \"^2.4.2\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} | |
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<Todo>;\n\n/**\n * `App` is the root component of the application.\n */\nexport default function App() {\n const [doc] = useState<Document<{ todos: JSONArray<Todo> }>>(\n () =>\n new yorkie.Document<{ todos: JSONArray<Todo> }>(\n `react-todomvc-${new Date()\n .toISOString()\n .substring(0, 10)\n .replace(/-/g, '')}`,\n ),\n );\n const [todos, setTodos] = useState<Array<Todo>>([]);\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<Todo> }>,\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 <div className=\"App\">\n <Header addTodo={actions.addTodo} />\n <MainSection todos={todos} actions={actions} />\n </div>\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<HTMLButtonElement, 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 <footer className=\"footer\">\n <span className=\"todo-count\">\n <strong>{activeCount || 'No'}</strong>\n {activeCount === 1 ? 'item' : 'items'} left\n </span>\n <ul className=\"filters\">\n {\n ['SHOW_ALL', 'SHOW_ACTIVE', 'SHOW_COMPLETED'].map((filter) => (\n <li key={filter}>\n <button\n type=\"button\"\n className={classnames({ selected: filter === selectedFilter })}\n style={{ cursor: 'pointer' }}\n onClick={() => onShow(filter)}\n >\n {FILTER_TITLES[filter]}\n </button>\n </li>\n ))\n }\n </ul>\n {!!completedCount && (\n <button type=\"button\" className=\"clear-completed\" onClick={onClearCompleted}>\n Clear completed\n </button>\n )}\n </footer>\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 <header className=\"header\">\n <h1>todos</h1>\n <TodoTextInput\n newTodo\n onSave={(text: string) => {\n if (text.length !== 0) {\n props.addTodo(text);\n }\n }}\n placeholder=\"What needs to be done?\"\n />\n </header>\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<HTMLInputElement>) => void;\n\ninterface MainSectionProps {\n todos: Array<Todo>;\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 <section className=\"main\">\n <input\n className=\"toggle-all\"\n type=\"checkbox\"\n defaultChecked={completedCount === todos.length}\n onChange={actions.completeAll as ChangeEventHandler}\n />\n <ul className=\"todo-list\">\n {\n filteredTodos.map((todo) => (\n <TodoItem\n key={todo.id}\n todo={todo}\n editTodo={actions.editTodo}\n deleteTodo={actions.deleteTodo}\n completeTodo={actions.completeTodo}\n />\n ))\n }\n </ul>\n <Footer\n completedCount={completedCount}\n activeCount={activeCount}\n filter={filter}\n onClearCompleted={() => actions.clearCompleted()}\n onShow={setFilter}\n />\n </section>\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 <li\n className={classnames({\n completed: todo.completed,\n editing,\n })}\n >\n {editing ? (\n <TodoTextInput\n text={todo.text}\n editing={editing}\n onSave={(text: string) => {\n if (text.length === 0) {\n deleteTodo(todo.id);\n } else {\n editTodo(todo.id, text);\n }\n setEditing(false);\n }}\n />\n ) : (\n <div className=\"view\">\n <input\n id={`item-input-${todo.id}`}\n className=\"toggle\"\n type=\"checkbox\"\n checked={todo.completed}\n onChange={() => completeTodo(todo.id)}\n />\n <label htmlFor={`item-input-${todo.id}`} onDoubleClick={() => setEditing(true)}>{todo.text}</label>\n <button type=\"button\" aria-label=\"Delete\" className=\"destroy\" onClick={() => deleteTodo(todo.id)} />\n </div>\n )}\n </li>\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 <input\n className={classnames({\n edit: props.editing,\n 'new-todo': props.newTodo,\n })}\n type=\"text\"\n placeholder={props.placeholder}\n value={text}\n onBlur={(e: React.FocusEvent<HTMLInputElement>) => {\n if (!props.newTodo) {\n props.onSave(e.target.value);\n }\n }}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n setText(e.target.value);\n }}\n onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {\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 <App />,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"# Environment variables for the example\n# Replace with actual values when running the example"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React TodoMVC Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/react-todomvc/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"React TodoMVC\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-todomvc dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Vite + React + TS</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-todomvc\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"classnames\": \"^2.3.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"todomvc-app-css\": \"^2.4.2\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
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<Record<string, JSONObject<TDShape>>>;\n bindings: JSONObject<Record<string, JSONObject<TDBinding>>>;\n assets: JSONObject<Record<string, JSONObject<TDAsset>>>;\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<YorkieDocType, YorkiePresenceType>;\n\nexport function useMultiplayerState(roomId: string) {\n const [app, setApp] = useState<TldrawApp>();\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<string, TDShape | undefined>,\n bindings: Record<string, TDBinding | undefined>,\n ) => {\n if (!app || client === undefined || doc === undefined) return;\n\n const getUpdatedPropertyList = <T extends object>(\n source: T,\n target: T,\n ) => {\n return (Object.keys(source) as Array<keyof T>).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<string, TDShape> = JSON.parse(\n root.shapes.toJSON!(),\n );\n const bindingRecord: Record<string, TDBinding> = JSON.parse(\n root.bindings.toJSON!(),\n );\n const assetRecord: Record<string, TDAsset> = 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<YorkieDocType, YorkiePresenceType>(roomId, {\n enableDevtools: true,\n });\n\n // 02-1. Subscribe peers-changed event and update tldraw users state\n doc.subscribe('my-presence', (event) => {\n if (event.type === yorkie.DocEventType.Initialized) {\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n }\n });\n doc.subscribe('others', (event) => {\n // remove leaved users\n if (event.type === yorkie.DocEventType.Unwatched) {\n app?.removeUser(event.value.presence.tdUser.id);\n }\n\n // update users\n const allPeers = doc\n .getPresences()\n .map((peer) => peer.presence.tdUser);\n app?.updateUsers(allPeers);\n });\n\n // 02-2. Attach document with initialPresence.\n const option = app?.currentUser && {\n initialPresence: { tdUser: app.currentUser },\n };\n await client.attach(doc, option);\n\n // 03. Initialize document if document not exists.\n doc.update((root) => {\n if (!root.shapes) {\n root.shapes = {};\n }\n if (!root.bindings) {\n root.bindings = {};\n }\n if (!root.assets) {\n root.assets = {};\n }\n }, 'create shapes/bindings/assets object if not exists');\n\n // 04. Subscribe document event and handle changes.\n doc.subscribe((event) => {\n if (event.type === 'remote-change') {\n handleChanges();\n }\n });\n\n // 05. Sync client to sync document with other peers.\n await client.sync();\n\n if (stillAlive) {\n // Update the document with initial content\n handleChanges();\n\n // Zoom to fit the content & finish loading\n if (app) {\n app.zoomToFit();\n if (app.zoom > 1) {\n app.resetZoom();\n }\n app.setIsLoading(false);\n }\n\n setLoading(false);\n }\n } catch (e) {\n console.error(e);\n }\n }\n\n setupDocument();\n\n return () => {\n window.removeEventListener('beforeunload', handleDisconnect);\n stillAlive = false;\n };\n }, [app]);\n\n return {\n onMount,\n onChangePage,\n loading,\n onChangePresence,\n };\n}\n"}]},{"isFile":true,"isOpen":false,"language":"css","name":"App.css","path":"/src/App.css","content":"html,\n* {\n box-sizing: border-box;\n}\n\nbody {\n overscroll-behavior: none;\n margin: 0px;\n padding: 0px;\n font-size: 1em;\n font-family: Arial, Helvetica, sans-serif;\n}\n\n.tldraw {\n position: fixed;\n top: 0px;\n left: 0px;\n right: 0px;\n bottom: 0px;\n width: 100%;\n height: 100%;\n}"},{"isFile":true,"isOpen":false,"language":"tsx","name":"App.tsx","path":"/src/App.tsx","content":"import { Tldraw, useFileSystem } from '@tldraw/tldraw';\nimport { useMultiplayerState } from './hooks/useMultiplayerState';\nimport CustomCursor from './CustomCursor';\nimport './App.css';\n\n/*\nThis demo shows how to integrate TLDraw with a multiplayer room\nvia Yorkie.\n\nWarning: Keeping images enabled for multiplayer applications\nwithout providing a storage bucket based solution will cause\nmassive base64 string to be written to the multiplayer storage.\nIt's recommended to use a storage bucket based solution, such as\nAmazon AWS S3.\n*/\n\nexport default function App() {\n const fileSystemEvents = useFileSystem();\n const { ...events } = useMultiplayerState(\n `tldraw-${(new Date()).toISOString().substring(0, 10).replace(/-/g, '')}`\n );\n const component = { Cursor: CustomCursor };\n\n return (\n <div className=\"tldraw\">\n <Tldraw\n components={component}\n autofocus\n disableAssets={true}\n showPages={false}\n {...fileSystemEvents}\n {...events}\n />\n </div>\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 <div\n style={{\n display: 'flex',\n width: 'fit-content',\n alignItems: 'center',\n gap: 8,\n }}\n >\n <div\n style={{\n width: 12,\n height: 12,\n background: color,\n borderRadius: '100%',\n }}\n />\n <div\n style={{\n background: 'white',\n padding: '4px 8px',\n borderRadius: 4,\n whiteSpace: 'nowrap',\n }}\n >\n {metadata!.name}\n </div>\n </div>\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 <React.StrictMode>\n <App />\n </React.StrictMode>,\n);\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"tldraw.d.ts","path":"/src/tldraw.d.ts","content":"import { Indexable, Json } from '@yorkie-js-sdk/src/document/document';\nimport { TDUser } from '@tldraw/tldraw';\n\ndeclare module '@tldraw/tldraw' {\n interface TDUser extends Indexable {}\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite-env.d.ts","path":"/src/vite-env.d.ts","content":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie React tldraw Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/react-tldraw/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"React tldraw\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm react-tldraw dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>react-tldraw</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"react-tldraw\",\n \"private\": true,\n \"version\": \"0.1.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"dependencies\": {\n \"@react-hook/throttle\": \"^2.2.0\",\n \"@tldraw/core\": \"^1.23.2\",\n \"@tldraw/tldraw\": \"1.26.3\",\n \"lodash\": \"^4.17.21\",\n \"randomcolor\": \"^0.6.2\",\n \"react\": \"^18.2.0\",\n \"react-dom\": \"^18.2.0\",\n \"unique-names-generator\": \"^4.7.1\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/lodash\": \"^4.14.198\",\n \"@types/randomcolor\": \"^0.5.5\",\n \"@types/react\": \"^18.2.0\",\n \"@types/react-dom\": \"^18.2.0\",\n \"@vitejs/plugin-react\": \"^4.2.1\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\",\n \"vite-tsconfig-paths\": \"^4.3.1\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"esModuleInterop\": false,\n \"allowSyntheticDefaultImports\": true,\n \"strict\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.node.json","path":"/tsconfig.node.json","content":"{\n \"compilerOptions\": {\n \"composite\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"allowSyntheticDefaultImports\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"vite.config.ts","path":"/vite.config.ts","content":"import path from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n plugins: [react(), tsconfigPaths()],\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider extracting type definitions to a separate file.
The types defined in types.ts
content are crucial for the Yorkie integration. Consider moving them to a dedicated types directory for better organization and reusability.
Create a new directory structure:
+ /src/types/
+ /src/types/yorkie.ts # For Yorkie-related types
+ /src/types/tldraw.ts # For TLDraw-related types
Committable suggestion was skipped due to low confidence.
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<T extends TextValueType>(\n textValue: T,\n): DeltaOperation {\n const { embed, ...restAttributes } = textValue.attributes ?? {};\n if (embed) {\n return { insert: embed, attributes: restAttributes };\n }\n\n return {\n insert: textValue.content || '',\n attributes: textValue.attributes,\n };\n}\n\nasync function main() {\n // 01-1. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(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<OperationInfo>) {\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 = '<span class=\"red\"> </span>';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = '<span class=\"green\"> </span>';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin: 1rem 0;\n font-family: monospace;\n}\n\n.ql-editor {\n min-height: 300px;\n overflow-y: auto;\n resize: vertical;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { type Text, TextPosStructRange } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n username: string;\n color: string;\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc, YorkiePresence } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID, presence } of peers) {\n usernames.push(\n myClientID === clientID\n ? `<b>${presence.username}</b>`\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<YorkieDoc, YorkiePresence>,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Quill Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/vanilla-quill/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\nThis demo shows the real-time collaborative version of the [Quill](https://quilljs.com/) editor with [Yorkie](https://yorkie.dev/) and [Vite](https://vitejs.dev/).\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Yorkie + Quill Example</title>\n </head>\n <body>\n <div id=\"network-status\"></div>\n <div id=\"editor\"></div>\n <div id=\"peers\"></div>\n <div id=\"document\"></div>\n <div id=\"document-text\"></div>\n <script type=\"module\" src=\"/src/main.ts\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-quill\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@types/color-hash\": \"^1.0.2\",\n \"@types/quill\": \"^1.3.10\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"color-hash\": \"^2.0.2\",\n \"quill\": \"^1.3.7\",\n \"quill-cursors\": \"^4.0.0\",\n \"quill-delta\": \"^5.0.0\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Consider removing sensitive configuration from FILE_INFO
The .env
file content is exposed in the FILE_INFO
constant. Even though it's empty by default, exposing environment file structure in client-side code is not recommended.
Consider removing the content of .env
and .env.production
files from FILE_INFO
:
{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env",
-"content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"
+"content":""
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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<T extends TextValueType>(\n textValue: T,\n): DeltaOperation {\n const { embed, ...restAttributes } = textValue.attributes ?? {};\n if (embed) {\n return { insert: embed, attributes: restAttributes };\n }\n\n return {\n insert: textValue.content || '',\n attributes: textValue.attributes,\n };\n}\n\nasync function main() {\n // 01-1. create client with RPCAddr then activate it.\n const client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {\n apiKey: import.meta.env.VITE_YORKIE_API_KEY,\n });\n await client.activate();\n\n // 02-1. create a document then attach it into the client.\n const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(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<OperationInfo>) {\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 = '<span class=\"red\"> </span>';\n },\n showOnline: (elem: HTMLElement) => {\n network.isOnline = true;\n elem.innerHTML = '<span class=\"green\"> </span>';\n },\n statusListener: (elem: HTMLElement) => {\n return (event: DocEvent) => {\n if (\n network.isOnline &&\n event.value == StreamConnectionStatus.Disconnected\n ) {\n network.showOffline(elem);\n } else if (\n !network.isOnline &&\n event.value == StreamConnectionStatus.Connected\n ) {\n network.showOnline(elem);\n }\n };\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"css","name":"style.css","path":"/src/style.css","content":"body {\n background: white;\n}\n\n.green {\n background-color: green;\n}\n.red {\n background-color: red;\n}\n\n#network-status span {\n display: inline-block;\n height: 0.8rem;\n width: 0.8rem;\n border-radius: 0.4rem;\n}\n\n#network-status:before {\n content: 'network: ';\n font-size: 1rem;\n}\n\n#peers:before {\n display: block;\n content: 'peers: ';\n font-size: 1rem;\n}\n\n#document:before {\n display: block;\n content: 'document: ';\n font-size: 1rem;\n}\n\n#document-text:before {\n display: block;\n content: 'text: ';\n font-size: 1rem;\n}\n\n#network-status,\n#peers,\n#document,\n#document-text {\n margin: 1rem 0;\n font-family: monospace;\n}\n\n.ql-editor {\n min-height: 300px;\n overflow-y: auto;\n resize: vertical;\n}\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"type.ts","path":"/src/type.ts","content":"import { type Text, TextPosStructRange } from 'yorkie-js-sdk';\n\nexport type YorkieDoc = {\n content: Text;\n};\n\nexport type YorkiePresence = {\n username: string;\n color: string;\n selection?: TextPosStructRange;\n};\n"},{"isFile":true,"isOpen":false,"language":"typescript","name":"utils.ts","path":"/src/utils.ts","content":"/* eslint-disable jsdoc/require-jsdoc */\nimport { Document, Indexable } from 'yorkie-js-sdk';\nimport { YorkieDoc, YorkiePresence } from './type';\n\n// function to display peers\nexport function displayPeers(\n elem: HTMLElement,\n peers: Array<{ clientID: string; presence: Indexable }>,\n myClientID: string,\n) {\n const usernames = [];\n for (const { clientID, presence } of peers) {\n usernames.push(\n myClientID === clientID\n ? `<b>${presence.username}</b>`\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<YorkieDoc, YorkiePresence>,\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":"/// <reference types=\"vite/client\" />\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"VITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Quill Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/vanilla-quill/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\nThis demo shows the real-time collaborative version of the [Quill](https://quilljs.com/) editor with [Yorkie](https://yorkie.dev/) and [Vite](https://vitejs.dev/).\n\n## How to run demo\n\n### With Yorkie Dashboard\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nCreate an account on [Yorkie Dashboard](https://yorkie.dev/dashboard)\nCreate a new project and copy your public key from the dashboard\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='https://api.yorkie.dev'\nVITE_YORKIE_API_KEY='your_key_xxxx'\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n\n### With local Yorkie server\n\nInstall dependencies\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nAt project root, run below command to start Yorkie.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nUpdate the `.env` file like so:\n\n```\nVITE_YORKIE_API_ADDR='http://localhost:8080'\nVITE_YORKIE_API_KEY=''\n```\n\nStart demo project\n\n```bash\n# In the root directory of the repository.\n$ pnpm vanilla-quill dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"markup","name":"index.html","path":"/index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Yorkie + Quill Example</title>\n </head>\n <body>\n <div id=\"network-status\"></div>\n <div id=\"editor\"></div>\n <div id=\"peers\"></div>\n <div id=\"document\"></div>\n <div id=\"document-text\"></div>\n <script type=\"module\" src=\"/src/main.ts\"></script>\n </body>\n</html>\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"vanilla-quill\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc && vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@types/color-hash\": \"^1.0.2\",\n \"@types/quill\": \"^1.3.10\",\n \"typescript\": \"^5.3.3\",\n \"vite\": \"^5.0.12\"\n },\n \"dependencies\": {\n \"color-hash\": \"^2.0.2\",\n \"quill\": \"^1.3.7\",\n \"quill-cursors\": \"^4.0.0\",\n \"quill-delta\": \"^5.0.0\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"useDefineForClassFields\": true,\n \"module\": \"ESNext\",\n \"lib\": [\"ESNext\", \"DOM\"],\n \"moduleResolution\": \"Node\",\n \"strict\": true,\n \"sourceMap\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"]\n }\n },\n \"include\": [\"src\"]\n}\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"vite.config.js","path":"/vite.config.js","content":"import { defineConfig } from 'vite';\nimport path from 'path';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n base: '',\n resolve: {\n alias: [\n {\n find: '@yorkie-js-sdk/src',\n replacement: path.resolve(__dirname, '../../packages/sdk/src'),\n },\n ],\n },\n});\n"}]} |
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<ContentTypes>;\n actions: { [name: string]: any };\n}\n\nexport type ChangeEventHandler = (\n event: React.ChangeEvent<HTMLInputElement>,\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<CalendarValue>(new Date());\n const [text, setText] = useState<string>('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 <article>\n <div>\n <Calendar\n onChange={onChange}\n value={date}\n locale=\"en-EN\"\n showNeighboringMonth={false}\n formatDay={(locale, date) =>\n date.toLocaleString('en', { day: 'numeric' })\n }\n tileClassName={({ date }) =>\n content.find((item) => item.date === parseDate(date))\n ? 'highlight'\n : ''\n }\n />\n <p>selected day : {currentDate}</p>\n <div className={styles.memo}>\n {content.map((item, i: number) => {\n if (item.date === currentDate) {\n return <p key={i}>{item.text}</p>;\n }\n })}\n </div>\n <div className={styles.inputForm_editor}>\n <h3>input form</h3>\n <textarea\n className={styles.textArea}\n value={text}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>\n setText(e.target.value)\n }\n />\n </div>\n <button className=\"button\" onClick={() => eventHandler('PUSH')}>\n push\n </button>\n <button className=\"button\" onClick={() => eventHandler('DELETE')}>\n pop\n </button>\n </div>\n </article>\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 <html lang=\"en\">\n <body>{children}</body>\n </html>\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 <h1>404 not found</h1>;\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<ContentTypes> = [\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<Array<string>>([]);\n const [content, setContent] = useState<Array<ContentTypes>>(defaultContent);\n\n // create Yorkie Document with useState value\n const [doc] = useState<Document<{ content: JSONArray<ContentTypes> }>>(\n () =>\n new yorkie.Document<{ content: JSONArray<ContentTypes> }>(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<ContentTypes> }>,\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 <main className={styles.main}>\n <p>\n peers : [\n {peers.map((man: string, i: number) => {\n return <span key={i}> {man}, </span>;\n })}{' '}\n ]\n </p>\n <Scheduler content={content} actions={actions} />\n </main>\n );\n}\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"NEXT_PUBLIC_YORKIE_API_ADDR='http://localhost:8080'\nNEXT_PUBLIC_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n extends: ['next', 'plugin:prettier/recommended'],\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\n '@next/next/no-html-link-for-pages': 'off',\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Next.js scheduler Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/nextjs-scheduler/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"Next.js scheduler\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm nextjs-scheduler dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"next.config.js","path":"/next.config.js","content":"/** @type {import('next').NextConfig} */\nconst nextConfig = {\n output: 'export',\n distDir: 'dist',\n basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',\n assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '',\n reactStrictMode: false,\n};\n\nmodule.exports = nextConfig;\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"nextjs-scheduler\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev -p 5174\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"14.1.3\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.2.0\",\n \"@types/react-dom\": \"18.2.0\",\n \"eslint-config-next\": \"^14.2.5\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"prettier\": \"^3.3.3\",\n \"typescript\": \"5.3.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": false,\n \"forceConsistentCasingInFileNames\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"plugins\": [\n {\n \"name\": \"next\"\n }\n ],\n \"paths\": {\n \"@/*\": [\"./*\"],\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"],\n \"react\": [\"./node_modules/@types/react\"]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\"node_modules\"]\n}\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Sensitive information exposed in FILE_INFO
The .env
file content is exposed in the FILE_INFO
constant. Even though the API key is empty, exposing environment configuration in source code is not recommended.
Consider:
- Removing the actual content of the
.env
file - Using placeholder values instead
- Adding
.env
to.gitignore
if not already included
-"content":"NEXT_PUBLIC_YORKIE_API_ADDR='http://localhost:8080'\nNEXT_PUBLIC_YORKIE_API_KEY=''\n"
+"content":"# Environment variables placeholder"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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<ContentTypes>;\n actions: { [name: string]: any };\n}\n\nexport type ChangeEventHandler = (\n event: React.ChangeEvent<HTMLInputElement>,\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<CalendarValue>(new Date());\n const [text, setText] = useState<string>('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 <article>\n <div>\n <Calendar\n onChange={onChange}\n value={date}\n locale=\"en-EN\"\n showNeighboringMonth={false}\n formatDay={(locale, date) =>\n date.toLocaleString('en', { day: 'numeric' })\n }\n tileClassName={({ date }) =>\n content.find((item) => item.date === parseDate(date))\n ? 'highlight'\n : ''\n }\n />\n <p>selected day : {currentDate}</p>\n <div className={styles.memo}>\n {content.map((item, i: number) => {\n if (item.date === currentDate) {\n return <p key={i}>{item.text}</p>;\n }\n })}\n </div>\n <div className={styles.inputForm_editor}>\n <h3>input form</h3>\n <textarea\n className={styles.textArea}\n value={text}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>\n setText(e.target.value)\n }\n />\n </div>\n <button className=\"button\" onClick={() => eventHandler('PUSH')}>\n push\n </button>\n <button className=\"button\" onClick={() => eventHandler('DELETE')}>\n pop\n </button>\n </div>\n </article>\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 <html lang=\"en\">\n <body>{children}</body>\n </html>\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 <h1>404 not found</h1>;\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<ContentTypes> = [\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<Array<string>>([]);\n const [content, setContent] = useState<Array<ContentTypes>>(defaultContent);\n\n // create Yorkie Document with useState value\n const [doc] = useState<Document<{ content: JSONArray<ContentTypes> }>>(\n () =>\n new yorkie.Document<{ content: JSONArray<ContentTypes> }>(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<ContentTypes> }>,\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 <main className={styles.main}>\n <p>\n peers : [\n {peers.map((man: string, i: number) => {\n return <span key={i}> {man}, </span>;\n })}{' '}\n ]\n </p>\n <Scheduler content={content} actions={actions} />\n </main>\n );\n}\n"}]},{"isFile":true,"isOpen":false,"language":"","name":".env","path":"/.env","content":"NEXT_PUBLIC_YORKIE_API_ADDR='http://localhost:8080'\nNEXT_PUBLIC_YORKIE_API_KEY=''\n"},{"isFile":true,"isOpen":false,"language":"production","name":".env.production","path":"/.env.production","content":""},{"isFile":true,"isOpen":false,"language":"javascript","name":".eslintrc.js","path":"/.eslintrc.js","content":"module.exports = {\n extends: ['next', 'plugin:prettier/recommended'],\n rules: {\n 'prettier/prettier': [\n 'error',\n {\n endOfLine: 'auto',\n },\n ],\n '@next/next/no-html-link-for-pages': 'off',\n },\n};\n"},{"isFile":true,"isOpen":false,"language":"","name":".gitignore","path":"/.gitignore","content":"# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"},{"isFile":true,"isOpen":false,"language":"markdown","name":"README.md","path":"/README.md","content":"# Yorkie Next.js scheduler Example\n\n<p>\n <a href=\"https://yorkie.dev/yorkie-js-sdk/examples/nextjs-scheduler/\" target=\"_blank\">\n <img src=\"https://img.shields.io/badge/preview-message?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAyNCAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuODU3MTcgMi43ODE5OUwxMS4yNzUxIDkuMTI2NzhDMTEuNTU0NCA5LjUyODAxIDEyLjEwNjIgOS42MjY3NiAxMi41MDc0IDkuMzQ3NDRDMTIuNTkzNCA5LjI4NzUgMTIuNjY4MSA5LjIxMjggMTIuNzI4MSA5LjEyNjc4TDE3LjE0NiAyLjc4MTk5QzE3LjcwNDggMS45Nzk1NCAxNy41MDcyIDAuODc2MTMxIDE2LjcwNDggMC4zMTc0OTRDMTYuNDA4IDAuMTEwODM3IDE2LjA1NSAwIDE1LjY5MzIgMEg4LjMxMDAxQzcuMzMyMiAwIDYuNTM5NTUgMC43OTI2NTQgNi41Mzk1NSAxLjc3MDQ2QzYuNTM5NjggMi4xMzIxMSA2LjY1MDUxIDIuNDg1MTEgNi44NTcxNyAyLjc4MTk5WiIgZmlsbD0iIzUxNEM0OSIvPgo8cGF0aCBkPSJNMTMuODA4OSAxNC4yMzg4QzE0LjEyMzEgMTQuNDE4IDE0LjQ4NDcgMTQuNDk2NiAxNC44NDUgMTQuNDY0MkwyMi45MjYgMTMuNzM1QzIzLjU3NTMgMTMuNjc2NSAyNC4wNTQgMTMuMTAyNyAyMy45OTU1IDEyLjQ1MzVDMjMuOTkyNCAxMi40MTkyIDIzLjk4NzggMTIuMzg1MSAyMy45ODE3IDEyLjM1MTNDMjMuNzM4OSAxMC45OTY4IDIzLjI2MTEgOS42OTUyNyAyMi41Njk5IDguNTA1NDZDMjEuODc4NiA3LjMxNTY1IDIwLjk4NDggNi4yNTU3NyAxOS45Mjg2IDUuMzczOTFDMTkuNDI4MiA0Ljk1NjE0IDE4LjY4MzkgNS4wMjMwNyAxOC4yNjYyIDUuNTIzNTZDMTguMjQ0MiA1LjU0OTkgMTguMjIzMyA1LjU3NzI2IDE4LjIwMzYgNS42MDU1MUwxMy41NjcgMTIuMjY0MUMxMy4zNjAzIDEyLjU2MSAxMy4yNDk1IDEyLjkxNCAxMy4yNDk1IDEzLjI3NThWMTMuMjUzN0MxMy4yNDk1IDEzLjQ1NjIgMTMuMzAxNiAxMy42NTU0IDEzLjQwMDggMTMuODMxOUMxMy41MDUgMTQuMDA1NCAxMy42NTIxIDE0LjE0OTMgMTMuODI4MSAxNC4yNDk2IiBmaWxsPSIjRkRDNDMzIi8+CjxwYXRoIGQ9Ik0xMC42NDE2IDEzLjc0MzRDMTAuNTM3NSAxMy45NTU5IDEwLjM3MiAxNC4xMzIyIDEwLjE2NjUgMTQuMjQ5NEwxMC4xOTE1IDE0LjIzNTFDOS44NzczNCAxNC40MTQzIDkuNTE1NjkgMTQuNDkyOSA5LjE1NTQ0IDE0LjQ2MDVMMS4wNzQ0MSAxMy43MzEzQzEuMDQwMTggMTMuNzI4MyAxLjAwNjA3IDEzLjcyMzcgMC45NzIyMjUgMTMuNzE3NkMwLjMzMDYyIDEzLjYwMjUgLTAuMDk2MzExOSAxMi45ODkyIDAuMDE4NzI0MiAxMi4zNDc2QzAuMjYxNTIyIDEwLjk5MyAwLjczOTM1NCA5LjY5MTU2IDEuNDMwNDYgOC41MDE2M0MyLjEyMTU3IDcuMzExNjkgMy4wMTU1MSA2LjI1MjA2IDQuMDcxODQgNS4zNzAwOEM0LjA5ODE4IDUuMzQ4MDYgNC4xMjU1NCA1LjMyNzE5IDQuMTUzNzkgNS4zMDc0N0M0LjY4ODc2IDQuOTM1IDUuNDI0MjcgNS4wNjY3MSA1Ljc5Njg3IDUuNjAxNjhMMTAuNDMzNCAxMi4yNjA0QzEwLjY0MDEgMTIuNTU3MyAxMC43NTA5IDEyLjkxMDMgMTAuNzUwOSAxMy4yNzIxVjEzLjI0MzJDMTAuNzUwOSAxMy40Nzk3IDEwLjY3OTggMTMuNzExIDEwLjU0NjggMTMuOTA2NyIgZmlsbD0iI0ZEQzQzMyIvPgo8L3N2Zz4K&color=FEF3D7\" alt=\"Live Preview\" />\n </a>\n</p>\n\n<img width=\"500\" alt=\"Next.js scheduler\" src=\"thumbnail.jpg\"/>\n\n## How to run demo\n\nAt project root, run below command to start Yorkie server.\n\n```bash\n$ docker compose -f docker/docker-compose.yml up --build -d\n```\n\nThen install dependencies and run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm install\n```\n\nNow you can run the demo.\n\n```bash\n# In the root directory of the repository.\n$ pnpm nextjs-scheduler dev\n\n# Or in the directory of the example.\n$ pnpm dev\n```\n"},{"isFile":true,"isOpen":false,"language":"javascript","name":"next.config.js","path":"/next.config.js","content":"/** @type {import('next').NextConfig} */\nconst nextConfig = {\n output: 'export',\n distDir: 'dist',\n basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',\n assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '',\n reactStrictMode: false,\n};\n\nmodule.exports = nextConfig;\n"},{"isFile":true,"isOpen":false,"language":"json","name":"package.json","path":"/package.json","content":"{\n \"name\": \"nextjs-scheduler\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev -p 5174\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"next lint\"\n },\n \"dependencies\": {\n \"next\": \"14.1.3\",\n \"react\": \"18.2.0\",\n \"react-calendar\": \"^4.6.0\",\n \"react-dom\": \"18.2.0\",\n \"yorkie-js-sdk\": \"^0.5.3\"\n },\n \"devDependencies\": {\n \"@types/node\": \"20.4.2\",\n \"@types/react\": \"18.2.0\",\n \"@types/react-dom\": \"18.2.0\",\n \"eslint-config-next\": \"^14.2.5\",\n \"eslint-config-prettier\": \"^9.1.0\",\n \"prettier\": \"^3.3.3\",\n \"typescript\": \"5.3.3\"\n }\n}\n"},{"isFile":true,"isOpen":false,"language":"jpg","name":"thumbnail.jpg","path":"/thumbnail.jpg","content":""},{"isFile":true,"isOpen":false,"language":"json","name":"tsconfig.json","path":"/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"allowJs\": false,\n \"skipLibCheck\": true,\n \"strict\": false,\n \"forceConsistentCasingInFileNames\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Node\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"plugins\": [\n {\n \"name\": \"next\"\n }\n ],\n \"paths\": {\n \"@/*\": [\"./*\"],\n \"@yorkie-js-sdk/src/*\": [\"../../packages/sdk/src/*\"],\n \"react\": [\"./node_modules/@types/react\"]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \"dist/types/**/*.ts\"\n ],\n \"exclude\": [\"node_modules\"]\n}\n"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution.
What this PR does / why we need it?
Bump up Yorkie to v0.5.3
Any background context you want to provide?
What are the relevant tickets?
Fixes #
Checklist
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Chores
package.json
to reflect the new Yorkie SDK version in dependencies.