├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── atom
│ └── index.js
├── setupTests.js
├── App.test.js
├── views
│ ├── Main.js
│ └── Home.js
├── index.css
├── reportWebVitals.js
├── firebaseConfig.js
├── index.js
├── App.js
├── components
│ ├── userForm.js
│ ├── CreateNewPlayGround.js
│ ├── JoinExistingPlayground.js
│ └── MultiUserSandpack.js
└── logo.svg
├── .env.local.example
├── .gitignore
├── README.md
├── LICENSE
└── package.json
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hussamkhatib/Real-time-collaborative-sandpack/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hussamkhatib/Real-time-collaborative-sandpack/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hussamkhatib/Real-time-collaborative-sandpack/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/atom/index.js:
--------------------------------------------------------------------------------
1 | import { atomWithStorage } from "jotai/utils";
2 |
3 | export const userAtom = atomWithStorage("darkMode", null);
4 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | REACT_APP_FIREBASE_API_KEY=""
2 | REACT_APP_FIREBASE_PROJECT_ID=""
3 | REACT_APP_FIREBASE_DATABASE_URL=""
4 | REACT_APP_FIREBASE_APP_ID=""
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/views/Main.js:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai";
2 | import { userAtom } from "../atom";
3 | import UserForm from "../components/userForm";
4 | import MultiUserSandpack from "../components/MultiUserSandpack";
5 |
6 | const Main = () => {
7 | const [user] = useAtom(userAtom);
8 | return user ? : ;
9 | };
10 |
11 | export default Main;
12 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .env
4 |
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/firebaseConfig.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/database";
3 |
4 | const app = {
5 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
6 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
7 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
8 | appId: process.env.REACT_APP_FIREBASE_APP_ID,
9 | };
10 |
11 | function initialize() {
12 | if (!firebase.apps.length) firebase.initializeApp(app);
13 | else firebase.app();
14 | }
15 |
16 | export default initialize;
17 |
--------------------------------------------------------------------------------
/src/views/Home.js:
--------------------------------------------------------------------------------
1 | import CreateNewPlayGround from "../components/CreateNewPlayGround";
2 | import JoinExistingPlayground from "../components/JoinExistingPlayground";
3 | import { Box, Heading } from "@chakra-ui/react";
4 |
5 | const Home = () => {
6 | return (
7 |
8 |
9 | Example of Collabrative Code Editor using Sandpack and Firepad-x
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default Home;
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 | import { BrowserRouter } from "react-router-dom";
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import initialize from "./firebaseConfig";
3 | import { Routes, Route } from "react-router-dom";
4 | import Home from "./views/Home";
5 | import { Provider } from "jotai";
6 | import Main from "./views/Main";
7 | import { ChakraProvider, extendTheme } from "@chakra-ui/react";
8 |
9 | export default function App() {
10 | useEffect(() => {
11 | initialize();
12 | }, []);
13 |
14 | const theme = extendTheme({
15 | colors: {
16 | main: {
17 | 500: "#6FEC5B",
18 | },
19 | },
20 | });
21 |
22 | return (
23 |
24 |
25 |
26 | } />
27 | } />
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/userForm.js:
--------------------------------------------------------------------------------
1 | import { userAtom } from "../atom";
2 | import { useRef } from "react";
3 | import { Button } from "@chakra-ui/react";
4 | import { Box, Input, FormLabel } from "@chakra-ui/react";
5 | import { useAtom } from "jotai";
6 |
7 | const UserForm = () => {
8 | const [, setUser] = useAtom(userAtom);
9 | const _input = useRef();
10 |
11 | const handleSubmit = (e) => {
12 | e.preventDefault();
13 | setUser(_input.current.value);
14 | };
15 |
16 | return (
17 |
18 |
19 | Enter your Name
20 |
21 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default UserForm;
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Real-time-collaborative-sandpack
2 |
3 | This example shows how you can use Sandpack and firepad-x to build a collbrative text editor.
4 |
5 | https://user-images.githubusercontent.com/52914487/181750695-f7d5bb29-7d40-46ff-a316-5fdf31c1cc4b.mp4
6 |
7 |
8 | ## Running locally
9 | ### Step 1. Clone the repo
10 | ```bash
11 | git clone https://github.com/hussamkhatib/Real-time-collaborative-sandpack.git`
12 | ```
13 | ### Step 2. Set up environment variables
14 |
15 | Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
16 |
17 | ```bash
18 | cp .env.local.example .env.local
19 | ```
20 |
21 | Then set each variable on `.env.local`:
22 |
23 | ### Step 3. Run in development mode
24 | ```bash
25 | yarn install
26 | yarn dev
27 | ```
28 |
29 | Credits:
30 | - [Sandpack](https://sandpack.codesandbox.io/)
31 | - [firepad-x](https://github.com/interviewstreet/firepad-x)
32 | - [Collaborative Coding in Monaco Editor - Shubham Shekhar](https://dev.to/shubham567/collaborative-coding-in-monaco-editor-4foa)
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 mohammed hussam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/CreateNewPlayGround.js:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 | import { useNavigate } from "react-router-dom";
3 | import { Box, Button, Heading, Select } from "@chakra-ui/react";
4 | import { useRef } from "react";
5 |
6 | const templates = [
7 | "angular",
8 | "react",
9 | "react-ts",
10 | "vanilla",
11 | "vanilla-ts",
12 | "vue",
13 | "vue3",
14 | "svelte",
15 | "solid",
16 | ];
17 |
18 | const CreateNewPlayGround = () => {
19 | let navigate = useNavigate();
20 | const _template = useRef();
21 | const createNewPlayGround = (e) => {
22 | e.preventDefault();
23 | const id = uuidv4();
24 | navigate(`/${id}/${_template.current.value}`);
25 | };
26 | return (
27 |
28 |
29 | Create New Playground
30 |
31 |
32 |
33 |
40 |
43 |
44 |
45 | );
46 | };
47 | export default CreateNewPlayGround;
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sandpack",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@chakra-ui/react": "^2.2.4",
7 | "@codesandbox/sandpack-react": "0.16.0",
8 | "@emotion/react": "^11",
9 | "@emotion/styled": "^11",
10 | "@hackerrank/firepad": "0.3.1",
11 | "@monaco-editor/react": "4.4.1",
12 | "@testing-library/jest-dom": "^5.16.4",
13 | "@testing-library/react": "^13.3.0",
14 | "@testing-library/user-event": "^13.5.0",
15 | "firebase": "8.8.1",
16 | "framer-motion": "^6",
17 | "jotai": "^1.7.6",
18 | "monaco-editor": "0.33.0",
19 | "react": "^18.1.0",
20 | "react-dom": "^18.1.0",
21 | "react-router-dom": "6",
22 | "react-scripts": "5.0.1",
23 | "uuid": "^8.3.2",
24 | "web-vitals": "^2.1.4"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/JoinExistingPlayground.js:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import firebase from "firebase/app";
3 | import { useRef } from "react";
4 | import {
5 | Box,
6 | Button,
7 | Input,
8 | FormControl,
9 | FormLabel,
10 | FormErrorMessage,
11 | FormHelperText,
12 | Heading,
13 | } from "@chakra-ui/react";
14 | import { atom, useAtom } from "jotai";
15 |
16 | const errorAtom = atom(null);
17 |
18 | const JoinExistingPlayground = () => {
19 | const _input = useRef();
20 | const [error, setError] = useAtom(errorAtom);
21 | let navigate = useNavigate();
22 | const joinExistinPlayground = (e) => {
23 | e.preventDefault();
24 | const id = _input.current.value;
25 | const dbRef = firebase.database().ref();
26 | dbRef
27 | .child(id)
28 | .get()
29 | .then((snapshot) => {
30 | if (snapshot.exists()) navigate(`/${id}`);
31 | else setError(`playground "${id}" does not exist`);
32 | })
33 | .catch((error) => {
34 | console.error(error);
35 | });
36 | };
37 | return (
38 |
39 |
40 | Or Join Existing Playground
41 |
42 |
43 | Playground Id
44 |
45 | {error ? (
46 | {error}
47 | ) : (
48 |
49 | Eg: "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed/react"
50 |
51 | )}
52 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default JoinExistingPlayground;
61 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/MultiUserSandpack.js:
--------------------------------------------------------------------------------
1 | import Editor from "@monaco-editor/react";
2 | import React, { useRef, useEffect, useState } from "react";
3 | import { userAtom } from "../atom";
4 | import firebase from "firebase/app";
5 | import { fromMonaco } from "@hackerrank/firepad";
6 | import { useAtom } from "jotai";
7 | import {
8 | useActiveCode,
9 | SandpackStack,
10 | FileTabs,
11 | useSandpack,
12 | SandpackProvider,
13 | SandpackLayout,
14 | SandpackPreview,
15 | } from "@codesandbox/sandpack-react";
16 | import "@codesandbox/sandpack-react/dist/index.css";
17 | import { useParams, useSearchParams } from "react-router-dom";
18 |
19 | const MultiUserSandpack = () => {
20 | const params = useParams();
21 | const template = params.template;
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default MultiUserSandpack;
33 |
34 | const SandpackEditor = () => {
35 | const params = useParams();
36 | let [, setSearchParams] = useSearchParams();
37 | const [name] = useAtom(userAtom);
38 | const editorRef = useRef(null);
39 | const [editorLoaded, setEditorLoaded] = useState(false);
40 | const { code, updateCode } = useActiveCode();
41 | const { sandpack } = useSandpack();
42 | const { activePath } = sandpack;
43 |
44 | function handleEditorDidMount(editor, monaco) {
45 | editorRef.current = editor;
46 | setEditorLoaded(true);
47 | }
48 |
49 | useEffect(() => {
50 | if (!editorLoaded) {
51 | return;
52 | }
53 |
54 | const dbRef = firebase
55 | .database()
56 | .ref()
57 | .child(
58 | `${params.id}/${params.template}/${replaceInvalidCharacters(
59 | activePath,
60 | "-"
61 | )}`
62 | );
63 | const firepad = fromMonaco(dbRef, editorRef.current);
64 | firepad.setUserName(name);
65 | setSearchParams({
66 | file: activePath,
67 | });
68 | return () => {
69 | firepad.dispose();
70 | setEditorLoaded(false);
71 | };
72 | }, [
73 | editorLoaded,
74 | activePath,
75 | name,
76 | params.id,
77 | params.template,
78 | setSearchParams,
79 | ]);
80 |
81 | return (
82 |
83 |
84 |
85 | {
95 | updateCode(value || "");
96 | }}
97 | />
98 |
99 |
100 | );
101 | };
102 |
103 | /**
104 | * This function replaces special characters like \,#,. etc
105 | * which are not allowed in firebase database ref
106 | * with the string provided in paramter
107 | * @param {the path that will be provided to firebase database reference} str
108 | * @param {the string that gets replaced by invalid characters } replaceStr
109 | */
110 |
111 | function replaceInvalidCharacters(str, replaceStr) {
112 | return str.replace(/[ ./#$[\] ]/g, replaceStr);
113 | }
114 |
--------------------------------------------------------------------------------