diff --git a/frontend/app/(app)/code/[id]/page.tsx b/frontend/app/(app)/code/[id]/page.tsx index 765c4b8..79948fb 100644 --- a/frontend/app/(app)/code/[id]/page.tsx +++ b/frontend/app/(app)/code/[id]/page.tsx @@ -1,4 +1,5 @@ import Navbar from "@/components/editor/navbar" +import { Room } from "@/components/editor/room" import { Sandbox, User, UsersToSandboxes } from "@/lib/types" import { currentUser } from "@clerk/nextjs" import dynamic from "next/dynamic" @@ -48,10 +49,12 @@ export default async function CodePage({ params }: { params: { id: string } }) { return (
+ {/* */}
+ {/*
*/}
) } diff --git a/frontend/app/(app)/layout.tsx b/frontend/app/(app)/layout.tsx index a191f40..44e4a39 100644 --- a/frontend/app/(app)/layout.tsx +++ b/frontend/app/(app)/layout.tsx @@ -1,3 +1,4 @@ +import { User } from "@/lib/types" import { currentUser } from "@clerk/nextjs" import { redirect } from "next/navigation" @@ -13,9 +14,9 @@ export default async function AppAuthLayout({ } const dbUser = await fetch(`http://localhost:8787/api/user?id=${user.id}`) - const dbUserJSON = await dbUser.json() + const dbUserJSON = (await dbUser.json()) as User - if (!dbUserJSON?.id) { + if (!dbUserJSON.id) { const res = await fetch("http://localhost:8787/api/user", { method: "POST", headers: { diff --git a/frontend/app/api/lb-auth/route.ts b/frontend/app/api/lb-auth/route.ts new file mode 100644 index 0000000..05bdb6a --- /dev/null +++ b/frontend/app/api/lb-auth/route.ts @@ -0,0 +1,43 @@ +import { User } from "@/lib/types" +import { currentUser } from "@clerk/nextjs" +import { Liveblocks } from "@liveblocks/node" +import { NextRequest } from "next/server" + +const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY! + +const liveblocks = new Liveblocks({ + secret: API_KEY!, +}) + +export async function POST(request: NextRequest) { + const clerkUser = await currentUser() + + if (!clerkUser) { + return new Response("Unauthorized", { status: 401 }) + } + + const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`) + const user = (await res.json()) as User + + // Create a session for the current user + // userInfo is made available in Liveblocks presence hooks, e.g. useOthers + const session = liveblocks.prepareSession(user.id, { + userInfo: { + id: user.id, + name: user.name, + email: user.email, + }, + }) + + // Give the user access to the room + user.sandbox.forEach((sandbox) => { + session.allow(`${sandbox.id}`, session.FULL_ACCESS) + }) + user.usersToSandboxes.forEach((userToSandbox) => { + session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS) + }) + + // Authorize the user and return the result + const { body, status } = await session.authorize() + return new Response(body, { status }) +} diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 5e089e3..85d0eb0 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -1,9 +1,17 @@ "use client" -import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" -import monaco from "monaco-editor" import { useEffect, useRef, useState } from "react" -// import theme from "./theme.json" +import monaco from "monaco-editor" +import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" +import { io } from "socket.io-client" +import { toast } from "sonner" +import { useClerk } from "@clerk/nextjs" + +import * as Y from "yjs" +import LiveblocksProvider from "@liveblocks/yjs" +import { MonacoBinding } from "y-monaco" +import { Awareness } from "y-protocols/awareness" +import { useRoom } from "@/liveblocks.config" import { ResizableHandle, @@ -22,17 +30,12 @@ import { } from "lucide-react" import Tab from "../ui/tab" import Sidebar from "./sidebar" -import { useClerk } from "@clerk/nextjs" -import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" - -import { io } from "socket.io-client" -import { processFileType, validateName } from "@/lib/utils" -import { toast } from "sonner" import EditorTerminal from "./terminal" import { Button } from "../ui/button" -import { User } from "@/lib/types" -import { Input } from "../ui/input" import GenerateInput from "./generate" +import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" +import { User } from "@/lib/types" +import { processFileType, validateName } from "@/lib/utils" export default function CodeEditor({ userData, @@ -62,6 +65,7 @@ export default function CodeEditor({ const [terminals, setTerminals] = useState([]) const clerk = useClerk() + const room = useRoom() const editorRef = useRef(null) const editorContainerRef = useRef(null) @@ -230,7 +234,7 @@ export default function CodeEditor({ if (e.key === "s" && (e.metaKey || e.ctrlKey)) { e.preventDefault() - const activeTab = tabs.find((t) => t.id === activeId) + // const activeTab = tabs.find((t) => t.id === activeId) // console.log("saving:", activeTab?.name, editorRef.current?.getValue()) setTabs((prev) => @@ -258,6 +262,27 @@ export default function CodeEditor({ } }) + useEffect(() => { + if (!editorRef.current) return + + const yDoc = new Y.Doc() + const yText = yDoc.getText("monaco") + const yProvider: any = new LiveblocksProvider(room, yDoc) + + const binding = new MonacoBinding( + yText, + editorRef.current.getModel() as monaco.editor.ITextModel, + new Set([editorRef.current]), + yProvider.awareness as Awareness + ) + + return () => { + yDoc.destroy() + yProvider.destroy() + binding.destroy() + } + }, [editorRef, room]) + // connection/disconnection effect + resizeobserver useEffect(() => { socket.connect() diff --git a/frontend/components/editor/room.tsx b/frontend/components/editor/room.tsx new file mode 100644 index 0000000..13ae1bd --- /dev/null +++ b/frontend/components/editor/room.tsx @@ -0,0 +1,22 @@ +"use client" + +import { RoomProvider } from "@/liveblocks.config" +import { useSearchParams } from "next/navigation" +import { ClientSideSuspense } from "@liveblocks/react" + +export function Room({ children }: { children: React.ReactNode }) { + // const roomId = useExampleRoomId("liveblocks:examples:nextjs-yjs-monaco"); + + return ( + + Loading!!!!}> + {() => children} + + + ) +} diff --git a/frontend/liveblocks.config.ts b/frontend/liveblocks.config.ts new file mode 100644 index 0000000..c5115b8 --- /dev/null +++ b/frontend/liveblocks.config.ts @@ -0,0 +1,161 @@ +import { createClient } from "@liveblocks/client" +import { createRoomContext, createLiveblocksContext } from "@liveblocks/react" +import YLiveblocksProvider from "@liveblocks/yjs" + +const client = createClient({ + // publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!, + authEndpoint: "/api/lb-auth", + // throttle: 100, + async resolveUsers({ userIds }) { + // Used only for Comments and Notifications. Return a list of user information + // retrieved from `userIds`. This info is used in comments, mentions etc. + + // const usersData = await __fetchUsersFromDB__(userIds); + // + // return usersData.map((userData) => ({ + // name: userData.name, + // avatar: userData.avatar.src, + // })); + + return [] + }, + async resolveMentionSuggestions({ text }) { + // Used only for Comments. Return a list of userIds that match `text`. + // These userIds are used to create a mention list when typing in the + // composer. + // + // For example when you type "@jo", `text` will be `"jo"`, and + // you should to return an array with John and Joanna's userIds: + // ["john@example.com", "joanna@example.com"] + + // const users = await getUsers({ search: text }); + // return users.map((user) => user.id); + + return [] + }, + async resolveRoomsInfo({ roomIds }) { + // Used only for Comments and Notifications. Return a list of room information + // retrieved from `roomIds`. + + // const roomsData = await __fetchRoomsFromDB__(roomIds); + // + // return roomsData.map((roomData) => ({ + // name: roomData.name, + // url: roomData.url, + // })); + + return [] + }, +}) + +// Presence represents the properties that exist on every user in the Room +// and that will automatically be kept in sync. Accessible through the +// `user.presence` property. Must be JSON-serializable. +type Presence = { + // cursor: { x: number, y: number } | null, + // ... +} + +// Optionally, Storage represents the shared document that persists in the +// Room, even after all users leave. Fields under Storage typically are +// LiveList, LiveMap, LiveObject instances, for which updates are +// automatically persisted and synced to all connected clients. +type Storage = { + // author: LiveObject<{ firstName: string, lastName: string }>, + // ... +} + +// Optionally, UserMeta represents static/readonly metadata on each user, as +// provided by your own custom auth back end (if used). Useful for data that +// will not change during a session, like a user's name or avatar. +type UserMeta = { + // id?: string, // Accessible through `user.id` + // info?: Json, // Accessible through `user.info` +} + +// Optionally, the type of custom events broadcast and listened to in this +// room. Use a union for multiple events. Must be JSON-serializable. +type RoomEvent = { + // type: "NOTIFICATION", + // ... +} + +// Optionally, when using Comments, ThreadMetadata represents metadata on +// each thread. Can only contain booleans, strings, and numbers. +export type ThreadMetadata = { + // resolved: boolean; + // quote: string; + // time: number; +} + +// Room-level hooks, use inside `RoomProvider` +export const { + suspense: { + RoomProvider, + useRoom, + useMyPresence, + useUpdateMyPresence, + useSelf, + useOthers, + useOthersMapped, + useOthersListener, + useOthersConnectionIds, + useOther, + useBroadcastEvent, + useEventListener, + useErrorListener, + useStorage, + useObject, + useMap, + useList, + useBatch, + useHistory, + useUndo, + useRedo, + useCanUndo, + useCanRedo, + useMutation, + useStatus, + useLostConnectionListener, + useThreads, + useCreateThread, + useEditThreadMetadata, + useCreateComment, + useEditComment, + useDeleteComment, + useAddReaction, + useRemoveReaction, + useThreadSubscription, + useMarkThreadAsRead, + useRoomNotificationSettings, + useUpdateRoomNotificationSettings, + + // These hooks can be exported from either context + // useUser, + // useRoomInfo + }, +} = createRoomContext( + client +) + +// Project-level hooks, use inside `LiveblocksProvider` +export const { + suspense: { + LiveblocksProvider, + useMarkInboxNotificationAsRead, + useMarkAllInboxNotificationsAsRead, + useInboxNotifications, + useUnreadInboxNotificationsCount, + + // These hooks can be exported from either context + useUser, + useRoomInfo, + }, +} = createLiveblocksContext(client) + +export type TypedLiveblocksProvider = YLiveblocksProvider< + Presence, + Storage, + UserMeta, + RoomEvent +> diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5403b1f..ac33b95 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,10 @@ "@clerk/nextjs": "^4.29.12", "@clerk/themes": "^1.7.12", "@hookform/resolvers": "^3.3.4", + "@liveblocks/client": "^1.12.0", + "@liveblocks/node": "^1.12.0", + "@liveblocks/react": "^1.12.0", + "@liveblocks/yjs": "^1.12.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-context-menu": "^2.1.5", @@ -38,6 +42,9 @@ "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "vscode-icons-js": "^11.6.1", + "y-monaco": "^0.1.5", + "y-protocols": "^1.0.6", + "yjs": "^13.6.15", "zod": "^3.23.3" }, "devDependencies": { @@ -340,6 +347,57 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@liveblocks/client": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.12.0.tgz", + "integrity": "sha512-TL4sPbWBlrGF7UXLNx2RYuZi/Z51jXALMFAdaWkYE0Qz7mMwxTVSyjPVR7ZXVuqdSo0CTQ1rpPyZeqcoUDtEoQ==", + "dependencies": { + "@liveblocks/core": "1.12.0" + } + }, + "node_modules/@liveblocks/core": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.12.0.tgz", + "integrity": "sha512-cPuVZwSh+EBnJL8DA999h4QDNSlOMFyxHPPHm9qVBf9Cl+NY7/Bg3cyKFiHOki6g7y8dQj8No2tpnAHWdqqalA==" + }, + "node_modules/@liveblocks/node": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@liveblocks/node/-/node-1.12.0.tgz", + "integrity": "sha512-xq9EusU2ntZ2vkIQT6TrqPWv4N+txMkzn2FgNllJxt+VdJhhvbt1UkAIokywIZSElFC6gt1VUNMRFM+a7nSJUw==", + "dependencies": { + "@liveblocks/core": "1.12.0", + "@stablelib/base64": "^1.0.1", + "fast-sha256": "^1.3.0", + "node-fetch": "^2.6.1" + } + }, + "node_modules/@liveblocks/react": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.12.0.tgz", + "integrity": "sha512-bxI420FRqUqEAHIyC6jTT40JiMDaum6+7Ag6osme2N0YsshOKZx4/z3uCD3LPdOQAzhaHiv9ZmVYwRFp3z9RfA==", + "dependencies": { + "@liveblocks/client": "1.12.0", + "@liveblocks/core": "1.12.0", + "nanoid": "^3", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17 || ^18" + } + }, + "node_modules/@liveblocks/yjs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@liveblocks/yjs/-/yjs-1.12.0.tgz", + "integrity": "sha512-jCGUlfh8hW2Cr5jRrx/c/dBaY3u6psIIVPETDO91AvKgeVld50I7tcYGjFARWwaOYMhSD3c57iAsOjEkGIvpHA==", + "dependencies": { + "@liveblocks/client": "1.12.0", + "@liveblocks/core": "1.12.0", + "js-base64": "^3.7.5" + }, + "peerDependencies": { + "yjs": "^13.6.1" + } + }, "node_modules/@monaco-editor/loader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", @@ -1329,6 +1387,11 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -2008,6 +2071,11 @@ "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.3.tgz", "integrity": "sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A==" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2228,6 +2296,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -2253,6 +2330,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + }, "node_modules/js-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", @@ -2266,6 +2348,26 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/lib0": { + "version": "0.2.93", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.93.tgz", + "integrity": "sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2522,6 +2624,25 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz", @@ -3376,6 +3497,11 @@ "to-no-case": "^1.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3519,6 +3645,20 @@ "tslib": "^2.6.2" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3645,6 +3785,41 @@ "node": ">=0.4.0" } }, + "node_modules/y-monaco": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.5.tgz", + "integrity": "sha512-zyTCMXtewIG+jP9JikJ5NN3/jLCqpkDY3titPIGBrmVeZsJr6qVeFVP3hPfmWOPHGlGZ39KCIzXrC7mFZRNFIw==", + "dependencies": { + "lib0": "^0.2.43" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "monaco-editor": ">=0.20.0", + "yjs": "^13.3.1" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/yaml": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", @@ -3656,6 +3831,22 @@ "node": ">= 14" } }, + "node_modules/yjs": { + "version": "13.6.15", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.15.tgz", + "integrity": "sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==", + "dependencies": { + "lib0": "^0.2.86" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zod": { "version": "3.23.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6f50c1e..e201b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,10 @@ "@clerk/nextjs": "^4.29.12", "@clerk/themes": "^1.7.12", "@hookform/resolvers": "^3.3.4", + "@liveblocks/client": "^1.12.0", + "@liveblocks/node": "^1.12.0", + "@liveblocks/react": "^1.12.0", + "@liveblocks/yjs": "^1.12.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-context-menu": "^2.1.5", @@ -39,6 +43,9 @@ "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "vscode-icons-js": "^11.6.1", + "y-monaco": "^0.1.5", + "y-protocols": "^1.0.6", + "yjs": "^13.6.15", "zod": "^3.23.3" }, "devDependencies": {