Skip to content
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

Merged
merged 1 commit into from
Oct 23, 2024
Merged

Bump up Yorkie to v0.5.3 #172

merged 1 commit into from
Oct 23, 2024

Conversation

chacha912
Copy link
Contributor

@chacha912 chacha912 commented Oct 23, 2024

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

  • Added relevant tests or not required
  • Didn't break anything

Summary by CodeRabbit

Release Notes

  • New Features

    • Updated Yorkie SDK versions to 0.5.3, enhancing performance and compatibility.
    • Improved project structure representation in various examples, ensuring clearer organization and accessibility of files.
  • Bug Fixes

    • Resolved discrepancies in file content representation across multiple examples.
  • Documentation

    • Updated README.md in the Vue.js Kanban example to include new instructions for running the demo.
  • Chores

    • Updated the package.json to reflect the new Yorkie SDK version in dependencies.

Copy link

coderabbitai bot commented Oct 23, 2024

Walkthrough

The 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 .env file and package.json. Additionally, the FILE_INFO constants in various example files have been modified to reflect a more detailed hierarchy of files and directories, enhancing the representation of the project's structure without altering the underlying logic.

Changes

File Path Change Summary
.env Updated NEXT_PUBLIC_YORKIE_VERSION and NEXT_PUBLIC_YORKIE_JS_VERSION to 0.5.3. Updated NEXT_PUBLIC_JS_SDK_URL to point to version 0.5.3.
package.json Updated yorkie-js-sdk dependency version from ^0.5.1 to ^0.5.3.
examples/nextjs-scheduler/fileInfo.ts Updated FILE_INFO constant with a more detailed hierarchy of files in the nextjs-scheduler directory.
examples/profile-stack/fileInfo.ts Expanded FILE_INFO constant to include detailed SVG definitions for profile images.
examples/react-tldraw/fileInfo.ts Updated FILE_INFO constant to include a detailed representation of the src/hooks directory.
examples/react-todomvc/fileInfo.ts Enhanced FILE_INFO constant with a more organized representation of the react-todomvc project structure.
examples/vanilla-codemirror6/fileInfo.ts Updated FILE_INFO constant to include detailed file contents for the CodeMirror 6 editor project.
examples/vanilla-quill/fileInfo.ts Enhanced FILE_INFO constant with a comprehensive representation of the src directory files.
examples/vuejs-kanban/fileInfo.ts Updated FILE_INFO constant with modified content for App.vue and updated package.json version for yorkie-js-sdk.

Possibly related PRs

  • Bump up Yorkie to v0.5.1 #171: This PR updates the Yorkie SDK version in the .env file and package.json, which is directly related to the changes made in the main PR that also involve updating the Yorkie SDK version in the same files.

Suggested reviewers

  • hackerwins

Poem

In the garden of code where the rabbits play,
The Yorkie SDK hops to a brighter day.
With files all in order, and versions anew,
We dance through the changes, oh what a view!
So let’s celebrate progress, with joy we proclaim,
In the world of our project, nothing's the same! 🐇✨


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?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 configuration

While 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 guide

The 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 service
examples/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:

  1. Document attachment time
  2. Operation propagation delay
  3. 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:

  1. The document structure and its lifecycle
  2. The synchronization flow between peers
  3. 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 README

The 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

📥 Commits

Files that changed from the base of the PR and between eacd7f8 and a0788e4.

⛔ 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 good

The 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.md

Length 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 with userSpaceOnUse 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>' --multiline

Length 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 title

Length 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 objective

The 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 conventions

The 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 implementation

The 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 update

The 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"}]}
Copy link

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:

  1. Network reconnection strategy
  2. 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"}]}
Copy link

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:

  1. Moving SVG content to separate files
  2. 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 &nbsp;{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"}]}
Copy link

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.

  1. Create a new directory file-contents with individual files for each content
  2. 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
  ]
};

⚠️ Potential issue

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.

Suggested change
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 &nbsp;{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 &nbsp;{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"}]}
Copy link

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"}]}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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"}]}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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:

  1. Removing the actual content of the .env file
  2. Using placeholder values instead
  3. 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.

Suggested change
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"}]}

Copy link
Member

@hackerwins hackerwins left a 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.

@hackerwins hackerwins merged commit b500581 into main Oct 23, 2024
2 checks passed
@hackerwins hackerwins deleted the v0.5.3 branch October 23, 2024 08:03
@coderabbitai coderabbitai bot mentioned this pull request Oct 28, 2024
2 tasks
@coderabbitai coderabbitai bot mentioned this pull request Nov 11, 2024
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants