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.1 #171

Merged
merged 1 commit into from
Oct 15, 2024
Merged

Bump up Yorkie to v0.5.1 #171

merged 1 commit into from
Oct 15, 2024

Conversation

chacha912
Copy link
Contributor

@chacha912 chacha912 commented Oct 15, 2024

What this PR does / why we need it?

Bump up Yorkie to v0.5.1

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

  • New Features

    • Updated to Yorkie JavaScript SDK version 0.5.1, enhancing functionality and performance.
    • Improved directory structure representations in multiple example applications for better clarity and organization.
  • Bug Fixes

    • Corrected export statements in example files to ensure proper accessibility of FILE_INFO.
  • Chores

    • Updated environment variable values and dependency versions for consistency and to leverage new features.

Copy link

coderabbitai bot commented Oct 15, 2024

Walkthrough

The changes in this pull request involve updates to environment variables in the .env file, specifically incrementing version numbers and modifying a URL for the Yorkie JavaScript SDK. Additionally, the package.json file reflects an updated dependency version for the same SDK. Multiple files in the examples directory have modifications to the FILE_INFO constant, including structural changes and export status updates. A type definition in src/type.ts has also been adjusted.

Changes

File Path Change Summary
.env Updated NEXT_PUBLIC_YORKIE_VERSION and NEXT_PUBLIC_YORKIE_JS_VERSION to 0.5.1; modified NEXT_PUBLIC_JS_SDK_URL to point to version 0.5.1.
package.json Updated yorkie-js-sdk dependency version from ^0.5.0 to ^0.5.1.
examples/nextjs-scheduler/fileInfo.ts Updated FILE_INFO to describe directory structure for Next.js application.
examples/profile-stack/fileInfo.ts Changed export status of FILE_INFO from non-exported to exported.
examples/react-tldraw/fileInfo.ts Simplified FILE_INFO structure with fewer entries in children.
examples/react-todomvc/fileInfo.ts Enhanced children array with detailed information about files in the src directory.
examples/vanilla-codemirror6/fileInfo.ts Updated content of children array without structural changes.
examples/vanilla-quill/fileInfo.ts Altered content of main.ts, updated handling of embed attribute and YorkiePresence type.
examples/vuejs-kanban/fileInfo.ts Reformatted FILE_INFO object while preserving structure.
src/type.ts Updated YorkiePresence type to have selection?: TextPosStructRange; instead of `selection: TextPosStructRange

Poem

🐇 In the garden where code does bloom,
New versions rise, dispelling gloom.
Yorkie's here, with updates bright,
Files restructured, all feels right.
Hopping through changes, we celebrate,
A world of code, oh, isn't it great? 🌼✨


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: 8

🧹 Outside diff range and nitpick comments (4)
examples/vuejs-kanban/fileInfo.ts (1)

2-2: LGTM! Consider splitting large object for improved maintainability.

The updated FILE_INFO constant correctly represents the directory structure and file contents for the Vue.js Kanban board application. The structure and content appear appropriate and consistent with a typical Vue.js project.

For improved maintainability, consider splitting this large object into smaller, more manageable pieces. For example:

const SRC_FILES = {
  // ... content of src directory
};

const CONFIG_FILES = {
  // ... content of configuration files
};

export const FILE_INFO: DirectoryInfo = {
  isFile: false,
  name: "vuejs-kanban",
  path: "/",
  children: [
    SRC_FILES,
    CONFIG_FILES,
    // ... other top-level files
  ]
};

This approach would make it easier to update and maintain specific parts of the file structure in the future.

examples/profile-stack/fileInfo.ts (1)

2-2: Consider adding documentation and automation for FILE_INFO.

While the content of FILE_INFO remains unchanged, its new export status increases its importance. Consider the following suggestions:

  1. Add documentation explaining the purpose and structure of FILE_INFO.
  2. If not already in place, consider implementing an automated process to update FILE_INFO when the project structure changes.

These steps will enhance maintainability and ensure FILE_INFO stays up-to-date with the actual project structure.

examples/react-todomvc/fileInfo.ts (1)

1-2: Consider the implications of exposing file contents

The FILE_INFO constant contains the entire file structure and content of the React TodoMVC example. While this can be useful for demonstration purposes, it's important to consider the following:

  1. Security: Exposing the full content of all files, including potential sensitive information, could pose a security risk.
  2. Performance: This large constant might impact the initial load time of the application.
  3. Maintenance: Any changes to the files in the project would require manual updates to this constant.

Consider alternatives such as:

  • Dynamically loading file contents when needed.
  • Excluding sensitive information or configuration files.
  • Implementing a build step to automatically generate this constant from the actual file structure.
examples/nextjs-scheduler/fileInfo.ts (1)

2-2: Consider adding JSDoc comments to utility functions.

The "utils" directory is well-organized with focused TypeScript files. To further improve code maintainability and developer experience, consider adding JSDoc comments to the exported functions in handlePeers.ts and parseDate.ts. This will provide better IntelliSense support and documentation.

Example improvement for parseDate.ts:

/**
 * Transforms a Date object into a DD-MM-YYYY string format.
 * @param {Date} date - The date to be transformed.
 * @returns {string} The formatted date string.
 */
export function parseDate(date: Date): string {
  // ... existing implementation
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 92b8193 and 346979a.

⛔ 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)
  • package.json
🧰 Additional context used
🔇 Additional comments (10)
.env (3)

2-2: LGTM: Version update for NEXT_PUBLIC_YORKIE_VERSION

The update from '0.5.0' to '0.5.1' aligns with the PR objective to bump up Yorkie to v0.5.1.


3-3: LGTM: Version update for NEXT_PUBLIC_YORKIE_JS_VERSION

The update from '0.5.0' to '0.5.1' is consistent with the change to NEXT_PUBLIC_YORKIE_VERSION and aligns with the PR objective.


4-5: Clarify: iOS and Android versions unchanged

While the main Yorkie version and JS SDK have been updated to 0.5.1, the iOS and Android versions remain at 0.4.24. Is this intentional? If not, should these versions also be updated to maintain consistency across platforms?

examples/vanilla-codemirror6/fileInfo.ts (1)

2-2: LGTM: Yorkie SDK version updated as expected.

The yorkie-js-sdk dependency has been correctly updated from ^0.5.0 to ^0.5.1 in the package.json file representation. This change aligns with the PR objective of bumping up Yorkie to v0.5.1.

Let's verify if this change is consistent across the project:

✅ Verification successful

Verified: yorkie-js-sdk has been successfully updated to ^0.5.1 with no remaining references to 0.5.0.

The package.json file correctly reflects the updated dependency:

"yorkie-js-sdk": "^0.5.1"

Additionally, no instances of version 0.5.0 were found in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if yorkie-js-sdk version is consistently updated across the project

# Test 1: Check package.json files
echo "Checking package.json files:"
rg --type json '"yorkie-js-sdk":\s*"\^0\.5\.1"' -g 'package.json'

# Test 2: Check for any remaining references to the old version
echo "Checking for any remaining references to version 0.5.0:"
rg "yorkie-js-sdk.*0\.5\.0" --type ts --type js --type json

Length of output: 358

examples/profile-stack/fileInfo.ts (1)

2-2: LGTM. Verify usage of exported constant.

The change to export the FILE_INFO constant looks good. This modification allows the constant to be used in other parts of the application.

To ensure this change is intentional and properly utilized, please verify the usage of FILE_INFO in other files:

examples/react-tldraw/fileInfo.ts (1)

2-2: Structure simplification: Potential impact on application

The FILE_INFO constant has been updated to simplify its structure while maintaining the overall hierarchy. This change could potentially impact other parts of the application that rely on this file structure.

To ensure this change doesn't break existing functionality, let's verify its usage:

Consider adding a comment explaining the purpose and structure of FILE_INFO to improve maintainability. For example:

/**
 * FILE_INFO: Represents the directory structure and file contents of the react-tldraw example.
 * This constant is used to [explain its purpose in the application].
 */
export const FILE_INFO: DirectoryInfo = { ... };
✅ Verification successful

FILE_INFO constant is widely used across multiple example projects

The FILE_INFO constant in examples/react-tldraw/fileInfo.ts is part of a broader pattern used across various example projects in the repository. This constant is utilized to define the file structure and contents for each demo project, including calendar, kanban, tldraw, simultaneous-cursors, todomvc, profile-stack, codemirror, and quill examples.

Given its widespread use, any changes to the structure or content of FILE_INFO should be carefully considered, as they could potentially impact multiple example projects. When modifying this constant, it's important to ensure that the changes are compatible with all the examples that rely on it.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usages of FILE_INFO in the codebase
rg "FILE_INFO" --type typescript --type javascript

Length of output: 85


Script:

#!/bin/bash
# Search for usages of FILE_INFO in TypeScript and JavaScript files using correct type identifiers
rg "FILE_INFO" --type ts --type js

Length of output: 222863

examples/nextjs-scheduler/fileInfo.ts (3)

2-2: LGTM: FILE_INFO constant declaration.

The FILE_INFO constant is correctly exported and typed as DirectoryInfo. This allows for type checking and easier consumption of the file structure information throughout the application.


2-2: LGTM: App directory structure follows Next.js conventions.

The "app" directory structure adheres to Next.js 13+ conventions, with appropriate separation of concerns. The "utils" and "styles" subdirectories promote code organization and maintainability.


2-2: Overall, well-structured Next.js application with room for optimization.

The FILE_INFO constant provides a comprehensive view of a well-organized Next.js application. The structure follows best practices and conventions. Consider implementing the suggested optimizations for improved performance, maintainability, and developer experience. These include using CSS Modules, adding JSDoc comments, and optimizing React components.

examples/vanilla-quill/fileInfo.ts (1)

2-2: ⚠️ Potential issue

Handle optional selection property in YorkiePresence

In type.ts, the selection property in YorkiePresence has been made optional:

export type YorkiePresence = {
  username: string;
  color: string;
  selection?: TextPosStructRange;
};

Ensure that all usages of presence.selection in the codebase properly check for undefined before accessing it to prevent runtime errors.

Run the following script to identify usages of presence.selection without proper undefined checks:

This script searches for lines where presence.selection is used without preceding null or undefined checks. Review these instances to ensure that they handle the case where selection is undefined.

✅ Verification successful

All usages of presence.selection are properly checked for undefined.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all usages of presence.selection without null or undefined checks.

# Search for occurrences where presence.selection is accessed
rg --type typescript 'presence\.selection' \
  | rg -v 'if\s*\(.*presence\.selection' \
  | rg -v 'presence\.selection\s*!==' \
  | rg -v 'presence\.selection\s*===' \
  | rg -v 'presence\.selection\s*!=' \
  | rg -v 'presence\.selection\s*=='

Length of output: 260


Script:

#!/bin/bash
# Description: Find all usages of presence.selection without null or undefined checks.

# Search for occurrences where presence.selection is accessed in .ts and .tsx files
rg 'presence\.selection' --glob '*.ts' --glob '*.tsx' \
  | rg -v 'if\s*\(.*presence\.selection' \
  | rg -v 'presence\.selection\s*!==' \
  | rg -v 'presence\.selection\s*===' \
  | rg -v 'presence\.selection\s*!=' \
  | rg -v 'presence\.selection\s*=='

Length of output: 236

NEXT_PUBLIC_YORKIE_IOS_VERSION='0.4.24'
NEXT_PUBLIC_YORKIE_ANDROID_VERSION='0.4.24'
NEXT_PUBLIC_DASHBOARD_PATH='/dashboard'
NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.0/yorkie-js-sdk.js'
NEXT_PUBLIC_JS_SDK_URL='https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.1/yorkie-js-sdk.js'
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Issue Found: New SDK URL is not accessible

The updated URL https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.1/yorkie-js-sdk.js returned a 404 error, indicating that the resource is not available at this location. Please verify if version 0.5.1 has been correctly published on the CDN or consider reverting to the previous stable version 0.5.0 to ensure functionality.

🔗 Analysis chain

LGTM: Updated SDK URL, but verify accessibility

The URL update from version 0.5.0 to 0.5.1 is consistent with the other version changes. However, it's crucial to ensure that this new URL is accessible and the SDK is available at this location.

Please run the following script to verify the accessibility of the new SDK URL:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the accessibility of the new Yorkie JS SDK URL

# Test: Check if the new SDK URL is accessible
curl -I https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.5.1/yorkie-js-sdk.js

# Expected output: HTTP/2 200 or similar success status
# If not, the URL might not be valid or the resource might not be available yet

Length of output: 1213

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.1\"\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

Improve consistency and clarity in the FILE_INFO structure

While the overall structure of FILE_INFO is well-organized, consider the following improvements:

  1. Clarify the purpose of the isOpen property, as it's consistently set to false for all files. If it's not being used, consider removing it to simplify the structure.

  2. For binary files (like images), represent the content more appropriately. For example:

    content: null, // or "binary_content"
    isBinary: true,
  3. Standardize the language property. For example:

    language: file.endsWith('.ts') ? 'typescript' : 
              file.endsWith('.tsx') ? 'typescript' :
              file.endsWith('.js') ? 'javascript' :
              file.endsWith('.json') ? 'json' :
              file.endsWith('.html') ? 'html' :
              file.endsWith('.css') ? 'css' :
              file.endsWith('.md') ? 'markdown' :
              '',
  4. Consider adding a size property for each file, which could be useful for performance optimizations or user information.

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.1\"\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

Reconsider including full file contents in FILE_INFO

While the current structure effectively represents the directory tree, including full file contents within the FILE_INFO constant may lead to several issues:

  1. Increased bundle size, potentially impacting load times.
  2. Maintenance challenges when updating file contents.
  3. Possible security concerns if sensitive information is included.

Consider refactoring this approach:

  1. Store only metadata (file names, paths, and types) in FILE_INFO.
  2. Implement a separate mechanism to load file contents on-demand.

Example refactored structure:

export const FILE_INFO: DirectoryInfo = {
  isFile: false,
  name: "react-tldraw",
  path: "/",
  children: [
    {
      isFile: false,
      name: "src",
      path: "/src",
      children: [
        {
          isFile: true,
          name: "hooks/types.ts",
          path: "/src/hooks/types.ts",
          language: "typescript"
        },
        // ... other files
      ]
    },
    // ... other directories
  ]
};

This approach would significantly reduce the size of FILE_INFO and make it easier to maintain. File contents could then be loaded asynchronously when needed.

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.1\"\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.

🛠️ Refactor suggestion

Consider using CSS Modules for better style scoping.

While the current CSS organization is good, consider fully embracing CSS Modules for all styles, not just page.module.css. This would provide better encapsulation and reduce the risk of style conflicts, especially in a growing application.

Example of converting calendar.css to a CSS Module:

  1. Rename calendar.css to Calendar.module.css
  2. Update the import in the corresponding component:
import styles from './styles/Calendar.module.css';

// Usage
<div className={styles.reactCalendar}>
  {/* ... */}
</div>

🛠️ Refactor suggestion

Consider performance optimizations for main components.

The Scheduler.tsx and page.tsx components are well-structured, but consider the following optimizations:

  1. Use useMemo or useCallback for expensive computations or callback functions to prevent unnecessary re-renders.
  2. Implement dynamic imports for large components to improve initial load time.
  3. Consider using React.memo for child components that don't need frequent updates.

Example optimization for Scheduler.tsx:

import dynamic from 'next/dynamic';

const DynamicCalendar = dynamic(() => import('react-calendar'), {
  ssr: false,
  loading: () => <p>Loading calendar...</p>
});

// In your component
const memoizedEventHandler = useCallback((event: string) => {
  // ... event handler logic
}, [content, actions]);

💡 Codebase verification

Based on the analysis of the files, there are a few concerns and actions we need to take. Let's generate the final response:

The investigation has revealed some potential security issues, particularly with the handling of API keys. Here are the findings and recommendations:

  1. API Key Exposure: The .env and .env.production files contain a public API key (NEXT_PUBLIC_EXAMPLES_API_KEY='cedaovjuioqlk4pjqn6g'). Even though it's prefixed with NEXT_PUBLIC_, exposing API keys in version control is not a secure practice.

    Recommendation: Remove this API key from the .env files and manage it securely outside of version control. Use a secret management system or environment variables set at runtime for production environments.

  2. Empty Configuration Files: The nextjs-scheduler/.env and docker/docker-compose.yml files appear to be empty or inaccessible. This could be intentional for the nextjs-scheduler/.env, but the empty docker-compose file is unexpected.

    Recommendation: Verify the docker/docker-compose.yml file's existence and permissions. If it's supposed to contain configuration, ensure it's properly populated and secured.

  3. Proper API Key Handling in Code: The BasicExampleView.tsx file demonstrates good practice by accepting the API key as a prop rather than hardcoding it.

    Recommendation: Continue this practice of passing sensitive information as props or environment variables rather than hardcoding them in the source code.

  4. Further Investigation Needed: There might be other files containing sensitive information that we haven't checked.

    Recommendation: Conduct a thorough review of all configuration files, especially those not tracked in version control, to ensure no other sensitive information is exposed.

To address these issues:

  1. Remove the NEXT_PUBLIC_EXAMPLES_API_KEY from .env and .env.production files.
  2. Implement a secure method for managing and distributing API keys, especially for production environments.
  3. Review and correct the docker/docker-compose.yml file if it's supposed to contain configuration.
  4. Perform a comprehensive audit of all project files to identify and secure any other potential exposures of sensitive information.
🔗 Analysis chain

Review configuration files for sensitive information.

The FILE_INFO structure accurately represents a Next.js application. However, be cautious about including sensitive information in configuration files like .env or package.json. Ensure that no API keys, secrets, or other sensitive data are exposed in these files.

To check for potential sensitive information, you can run the following script:


Execute a comprehensive search for sensitive information across the entire codebase.

The initial check did not reveal any sensitive information in the specified files. To ensure no sensitive data is present elsewhere in the repository, please run the following script:

[/run_scripts]


To further investigate the potential exposure of sensitive information, we need to examine the content of some specific files. Let's run the following scripts to get more context:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential sensitive information in configuration files
grep -E '(api[_-]?key|secret|password|token)' .env .env.* package.json next.config.js

Length of output: 96


Script:

#!/bin/bash
# Comprehensive search for potential sensitive information in the entire codebase
rg -i --no-ignore --hidden '(api[_-]?key|secret|password|token)' .

Length of output: 232461


Script:

#!/bin/bash
# Check content of .env and .env.production files
echo "Contents of .env files:"
cat .env .env.production 2>/dev/null

echo -e "\nContents of nextjs-scheduler/.env:"
cat ./examples/nextjs-scheduler/.env 2>/dev/null

echo -e "\nContents of .env.production in the root directory:"
cat ./.env.production 2>/dev/null

# Check content of configuration files that might contain real credentials
echo -e "\nContents of docker/docker-compose.yml:"
cat ./docker/docker-compose.yml 2>/dev/null

# Check files that appear to have actual API keys or tokens
echo -e "\nContents of components/exampleView/BasicView/BasicExampleView.tsx:"
cat ./components/exampleView/BasicView/BasicExampleView.tsx 2>/dev/null

Length of output: 6397

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.1\"\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

Ensure safe handling of the embed attribute in toDeltaOperation function

In the toDeltaOperation function within main.ts, the embed value is returned directly:

if (embed) {
  return { insert: embed, attributes: restAttributes };
}

Without proper sanitization, this could introduce security vulnerabilities such as XSS attacks if embed contains malicious content. Consider sanitizing or validating the embed content before inserting it to ensure that only safe content is processed.

Apply this diff to sanitize the embed content:

 function toDeltaOperation<T extends TextValueType>(
   textValue: T,
 ): DeltaOperation {
   const { embed, ...restAttributes } = textValue.attributes ?? {};
   if (embed) {
+    const sanitizedEmbed = sanitizeEmbed(embed);
     return { insert: sanitizedEmbed, attributes: restAttributes };
   }

   return {
     insert: textValue.content || '',
     attributes: textValue.attributes,
   };
 }

+function sanitizeEmbed(embed: any): any {
+  // Implement sanitization logic here to prevent XSS or other vulnerabilities
+  // For example, you can use a library like DOMPurify if running in the browser
+  return embed;
+}

🛠️ Refactor suggestion

Consider externalizing large code strings for maintainability

The FILE_INFO constant includes large code strings embedded directly within the TypeScript file, which can make the code difficult to read and maintain. Consider storing these code contents in separate files and importing them or reading them dynamically. This approach will enhance readability and simplify code maintenance.

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 eacd7f8 into main Oct 15, 2024
2 checks passed
@hackerwins hackerwins deleted the v0.5.1 branch October 15, 2024 06:49
This was referenced Oct 23, 2024
@coderabbitai coderabbitai bot mentioned this pull request Nov 11, 2024
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Nov 25, 2024
2 tasks
@coderabbitai coderabbitai bot mentioned this pull request Dec 12, 2024
2 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