├── favicon.ico ├── .firebaserc ├── src ├── vite-env.d.ts ├── types │ └── ServerDoc.tsx ├── utils │ ├── time.ts │ ├── caret.ts │ └── writing.ts ├── components │ ├── index.ts │ ├── Home │ │ ├── styles.ts │ │ └── index.tsx │ ├── Tree │ │ ├── styles.ts │ │ └── index.tsx │ ├── Pad │ │ ├── styles.ts │ │ └── index.tsx │ └── MarkdownRenderer │ │ └── index.tsx ├── main.tsx ├── index.css ├── App.tsx └── services │ └── firebase.ts ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── firebase.json ├── tsconfig.json ├── .github └── workflows │ ├── firebase-hosting-merge.yml │ └── firebase-hosting-pull-request.yml ├── package.json ├── README.md ├── index.html └── CONFIG.md /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enzoferraribf/missopad/HEAD/favicon.ico -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "missopad-e13a9" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module "react-alert-template-basic"; 3 | -------------------------------------------------------------------------------- /src/types/ServerDoc.tsx: -------------------------------------------------------------------------------- 1 | export interface ServerDoc { 2 | content: string; 3 | author: string; 4 | updatedAt: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | export function lessThan(date: number, seconds: number) { 2 | const anHourAgo = Date.now() - seconds; 3 | 4 | return date > anHourAgo; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Home from "./Home"; 2 | import Pad from "./Pad"; 3 | import Tree from "./Tree"; 4 | import MarkdownRenderer from "./MarkdownRenderer"; 5 | 6 | export { Home, Pad, MarkdownRenderer, Tree }; 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "App"; 5 | import "index.css"; 6 | 7 | const root = document.getElementById("root")!; 8 | 9 | ReactDOM.createRoot(root).render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /dist 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | .firebase 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | *.log 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /src/utils/caret.ts: -------------------------------------------------------------------------------- 1 | export function calculateCaretPosition( 2 | currentPosition: number, 3 | localContent: string, 4 | serverContent: any 5 | ) { 6 | const beforeCaret = localContent.substring(0, currentPosition); 7 | const newBeforeCaret = serverContent.substring(0, currentPosition); 8 | 9 | if (beforeCaret !== newBeforeCaret) { 10 | currentPosition += serverContent.length - localContent.length; 11 | } 12 | 13 | return currentPosition; 14 | } 15 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | }, 16 | "emulators": { 17 | "auth": { 18 | "port": 9099 19 | }, 20 | "database": { 21 | "port": 9000 22 | }, 23 | "ui": { 24 | "enabled": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/writing.ts: -------------------------------------------------------------------------------- 1 | import { ref, runTransaction, serverTimestamp } from "firebase/database"; 2 | import { db } from "services/firebase"; 3 | 4 | interface Writing { 5 | content: string; 6 | author: string; 7 | } 8 | 9 | export async function handleWriting(pathname: string, writing: Writing) { 10 | const dbRef = ref(db, pathname); 11 | 12 | await runTransaction(dbRef, (snapshot) => { 13 | snapshot = { 14 | ...snapshot, 15 | ...writing, 16 | updatedAt: serverTimestamp(), 17 | }; 18 | 19 | return snapshot; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: 'JetBrains Mono', monospace !important; 4 | background-color: #0d1117 !important; 5 | } 6 | 7 | /* width */ 8 | ::-webkit-scrollbar { 9 | width: 7px; 10 | } 11 | 12 | /* Track */ 13 | ::-webkit-scrollbar-track { 14 | box-shadow: inset 0 0 5px black; 15 | border-radius: 10px; 16 | } 17 | 18 | /* Handle */ 19 | ::-webkit-scrollbar-thumb { 20 | background: #191f29; 21 | border-radius: 10px; 22 | } 23 | 24 | body { 25 | background-color: #0d1117; 26 | color: #ffffff; 27 | } 28 | 29 | .cursor { 30 | background-color: #ffffff !important; 31 | } 32 | 33 | 34 | .markdown-body { 35 | color: #ffffff; 36 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import { positions, Provider } from "react-alert"; 3 | import AlertTemplate from "react-alert-template-basic"; 4 | 5 | import { Home, Pad } from "components"; 6 | 7 | const options = { 8 | timeout: 0, 9 | position: positions.BOTTOM_CENTER, 10 | }; 11 | 12 | function App() { 13 | return ( 14 | 15 | 16 | 17 | } /> 18 | } /> 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | "references": [ 28 | { 29 | "path": "./tsconfig.node.json" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | "on": 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "16" 17 | cache: "yarn" 18 | - run: yarn 19 | - run: CI='' yarn build 20 | 21 | - uses: FirebaseExtended/action-hosting-deploy@v0 22 | with: 23 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 24 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_MISSOPAD_E13A9 }}" 25 | channelId: live 26 | projectId: missopad-e13a9 27 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | "on": pull_request 6 | jobs: 7 | build_and_preview: 8 | if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: "16" 15 | cache: "yarn" 16 | - run: yarn 17 | - run: CI='' yarn build 18 | 19 | - uses: FirebaseExtended/action-hosting-deploy@v0 20 | with: 21 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 22 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_MISSOPAD_E13A9 }}" 23 | projectId: missopad-e13a9 24 | -------------------------------------------------------------------------------- /src/components/Home/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HomeContainer = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | height: 80vh; 9 | width: 100vw; 10 | `; 11 | 12 | export const Title = styled.h1` 13 | font-size: 8vw; 14 | `; 15 | 16 | export const ExplorerForm = styled.form` 17 | margin-top: 20px; 18 | 19 | display: flex; 20 | flex-direction: row; 21 | align-items: baseline; 22 | 23 | label { 24 | font-size: 3vw; 25 | } 26 | 27 | input { 28 | font-size: 3vw; 29 | width: 20vw; 30 | 31 | background-color: transparent; 32 | border: none; 33 | color: white; 34 | 35 | outline: none; 36 | } 37 | 38 | a { 39 | margin-left: 10px; 40 | 41 | font-size: 3vw; 42 | text-decoration: none; 43 | transition: all 0.3s ease; 44 | 45 | :hover { 46 | opacity: 0.7; 47 | } 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /src/services/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { connectDatabaseEmulator, getDatabase } from "firebase/database"; 3 | import { signInAnonymously, getAuth, connectAuthEmulator } from "firebase/auth"; 4 | 5 | const firebaseConfig = { 6 | apiKey: "AIzaSyDblWst7F3bynCSVn9IX_t_TmLhdjJ7xWU", 7 | authDomain: "missopad-e13a9.firebaseapp.com", 8 | databaseURL: "https://missopad-e13a9-default-rtdb.firebaseio.com", 9 | projectId: "missopad-e13a9", 10 | storageBucket: "missopad-e13a9.appspot.com", 11 | messagingSenderId: "502498677789", 12 | appId: "1:502498677789:web:0f4a46c9290bc4c8d222f4", 13 | }; 14 | 15 | const app = initializeApp(firebaseConfig); 16 | 17 | const db = getDatabase(); 18 | 19 | const auth = getAuth(); 20 | 21 | if (window.location.hostname === "localhost") { 22 | connectDatabaseEmulator(db, "localhost", 9000); 23 | connectAuthEmulator(auth, "http://localhost:9099"); 24 | } 25 | 26 | export { app, db, signInAnonymously, auth }; -------------------------------------------------------------------------------- /src/components/Tree/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DrawerContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | padding: 3em 1em; 7 | position: absolute; 8 | height: 100%; 9 | min-width: 160px; 10 | top: 0; 11 | overflow-y: scroll; 12 | overflow-x: hidden; 13 | background-color: #0d0f17; 14 | z-index: 999; 15 | `; 16 | 17 | export const NonWrappableColumn = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | white-space: nowrap; 21 | `; 22 | 23 | interface RouteLinkProps { 24 | level: number; 25 | } 26 | 27 | export const RouteLink = styled.a` 28 | color: white; 29 | margin-bottom: 0.2em; 30 | overflow-wrap: break-word; 31 | text-decoration: none; 32 | margin-left: ${(props) => props.level - 1}em; 33 | `; 34 | 35 | 36 | export const MissoGatesLogo = styled.h3` 37 | position: absolute; 38 | top: -10px; 39 | left: 10px; 40 | z-index: 9999; 41 | white-space: nowrap; 42 | transition: all 0.3s ease; 43 | 44 | :hover { 45 | cursor: pointer; 46 | opacity: 0.7; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { ChangeEvent, FormEvent, useState } from "react"; 3 | 4 | import { ExplorerForm, HomeContainer, Title } from "./styles"; 5 | 6 | function Home() { 7 | const [document, setDocument] = useState(""); 8 | 9 | const navigate = useNavigate(); 10 | 11 | function handleDocumentChange(e: ChangeEvent) { 12 | const document = e.target.value; 13 | 14 | setDocument(document); 15 | } 16 | 17 | function handleFormSubmit(e: FormEvent) { 18 | e.preventDefault(); 19 | navigate(`/${document}`); 20 | } 21 | 22 | return ( 23 | 24 | MISSOPAD 25 | 26 | 27 | 28 | 29 | 35 | 36 | 🚀 37 | 38 | 39 | ); 40 | } 41 | 42 | export default Home; 43 | -------------------------------------------------------------------------------- /src/components/Pad/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const PadHeader = styled.header` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | width: 99vw; 9 | height: 3vh; 10 | `; 11 | 12 | export const HeaderTitle = styled.h1` 13 | color: #c9d1d9; 14 | font-size: large; 15 | 16 | transition: all 0.3s ease; 17 | 18 | :hover { 19 | cursor: pointer; 20 | opacity: 0.7; 21 | } 22 | `; 23 | 24 | 25 | export const PadContainer = styled.div` 26 | display: flex; 27 | flex-direction: row; 28 | `; 29 | 30 | export const Editor = styled.textarea` 31 | border: 3px; 32 | outline: none; 33 | resize: none; 34 | width: 50vw; 35 | height: 96vh; 36 | overflow-y: scroll; 37 | white-space: pre-wrap; 38 | background-color: #0d1117; 39 | color: #c9d1d9; 40 | padding: 30px; 41 | `; 42 | 43 | export interface PreviewerProps { 44 | onlyView?: boolean; 45 | } 46 | 47 | export const Previewer = styled.div` 48 | padding: 30px; 49 | width: ${(props) => (props.onlyView ? "100vw" : "50vw")}; 50 | height: 96vh; 51 | overflow-y: scroll; 52 | `; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "missopad", 3 | "private": true, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@monaco-editor/react": "^4.4.6", 12 | "firebase": "^9.6.1", 13 | "github-markdown-css": "^5.1.0", 14 | "react": "^18.0.0", 15 | "react-alert": "^7.0.3", 16 | "react-alert-template-basic": "^1.0.2", 17 | "react-dom": "^18.0.0", 18 | "react-markdown": "^8.0.2", 19 | "react-router-dom": "^6.2.1", 20 | "react-scripts": "^5.0.1", 21 | "react-syntax-highlighter": "^15.5.0", 22 | "remark-breaks": "^3.0.2", 23 | "remark-gfm": "^3.0.1", 24 | "styled-components": "^5.3.5" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^17.0.26", 34 | "@types/react": "^18.0.0", 35 | "@types/react-alert": "^7.0.2", 36 | "@types/react-dom": "^18.0.0", 37 | "@types/react-router-dom": "^5.3.2", 38 | "@types/react-syntax-highlighter": "^13.5.2", 39 | "@types/styled-components": "^5.1.25", 40 | "@vitejs/plugin-react": "^1.3.0", 41 | "typescript": "^4.6.3", 42 | "vite": "^2.9.5", 43 | "vite-tsconfig-paths": "^3.4.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Share and update documents in real-time 5 |

6 |

7 | 8 |
9 | 10 |

11 | 12 |

13 | 14 | Missopad is a real-time collaborative tool that renders Markdown. You can access any page without the need to authenticate in any way. Simply access a url (e.g https://missopad.com/) and start editing! 15 | 16 | # How to contribute 17 | 18 | The whole project is hosted on firebase. The main credentials, located at `src/services/firebase.ts` **will not work** on your local machine: 19 | 20 | ```ts 21 | const firebaseConfig = { 22 | apiKey: "", 23 | authDomain: "", 24 | databaseURL: "", 25 | projectId: "", 26 | storageBucket: "", 27 | messagingSenderId: "", 28 | appId: "", 29 | }; 30 | ``` 31 | 32 | Use the [Firebase Emulator](https://firebase.google.com/docs/emulator-suite/connect_and_prototype) or your own project credentials to run the app. 33 | 34 | For more information about how to install and configure Firebase, we've made a special md to help you achieve that, see [Firebase Config](https://github.com/oenzoferrari/missopad/blob/main/CONFIG.md) 35 | 36 | Please open a PR describing the changes and your new features. Any help will be much apreciated 😀 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MissoPad 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/MarkdownRenderer/index.tsx: -------------------------------------------------------------------------------- 1 | import RemarkGfm from "remark-gfm"; 2 | import RemarkBreaks from "remark-breaks"; 3 | import ReactMarkdown from "react-markdown"; 4 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 5 | 6 | import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism"; 7 | 8 | import "github-markdown-css"; 9 | 10 | interface MarkdownRendererProps { 11 | content: string; 12 | } 13 | 14 | function MarkdownRenderer({ content }: MarkdownRendererProps) { 15 | return ( 16 | 33 | ); 34 | } 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | /> 44 | ); 45 | } 46 | 47 | export default MarkdownRenderer; 48 | -------------------------------------------------------------------------------- /CONFIG.md: -------------------------------------------------------------------------------- 1 | # How to Configure Firebase Emulator 2 | 3 | ## Requeriments 4 | 5 | - Install latest [Java](https://www.java.com/it/download/) 6 | - Install latest [Node.js](https://nodejs.org/en/) 7 | - [Firebase](https://firebase.google.com/?hl=pt) Account 8 | - Firebase project (create one at Firebase Console) 9 | - Create a Firebase app under project configurations 10 | - Basic knowledge of CLI 11 | 12 | ## Setup Firebase 13 | 14 | 1. Run this command to install Firebase globally with yarn. 15 | 16 | ``` 17 | npm install -g yarn 18 | 19 | yarn global add firebase-tools 20 | ``` 21 | 22 | 2. Login with your Firebase account 23 | 24 | ``` 25 | firebase login 26 | ``` 27 | 28 | ## Create Firebase Project 29 | 30 | Now you should have everything installed and configured to start using Firebase. 31 | 32 | Make sure you're on the root project folder 33 | 34 | **IMPORTANT: remove default firebase project from /.firebaserc** 35 | 36 | 1. Init Firebase project 37 | 38 | ``` 39 | firebase init 40 | ``` 41 | 42 | It will prompt some questions: 43 | 44 | 1) You choose yes or no 45 | 2) Select only Emulators, since we want to run it only locally 46 | 3) Select Authentication and Database Emulators 47 | 4) Leave everything default and install emulator 48 | 49 | 2. Get Credentials, copy and paste it on src/services/Firebase.ts 50 | 51 | Note: don't commit this file changes into main 52 | 53 | ``` 54 | firebase apps:sdkconfig 55 | ``` 56 | 57 | 3. Start the Firebase Emulator 58 | 59 | ``` 60 | firebase emulators:start 61 | ``` 62 | 63 | 4. Open Firebase UI, go to Database Realtime and copy the database URL then paste it in firebaseConfig 64 | 65 | Your firebase config should look like this: 66 | 67 | ```ts 68 | const firebaseConfig = { 69 | projectId: "cafapad-f91a5", 70 | appId: "1:642534084468:web:79511934ecd3f597d5a621", 71 | storageBucket: "cafapad-f91a5.appspot.com", 72 | apiKey: "AIzaSyCd1HXPPGgtlfxuJyFW0s6s4OWdp-DcWyM", 73 | authDomain: "cafapad-f91a5.firebaseapp.com", 74 | messagingSenderId: "642534084468", 75 | measurementId: "G-JM36P6BECX", 76 | databaseURL: "http://localhost:9000/?ns=cafapad-f91a5", 77 | }; 78 | ``` 79 | 80 | 5. Restart Firebase Emulator 81 | 82 | ## Testing Firebase Configuration 83 | 84 | Start missopad application with `npm run start` and try creating a page, it should appear the data you inserted on Firebase UI Emulator. 85 | 86 | If you get there, congratulations! You've successfully installed and configured Firebase, now it's time to start coding and make missopad better! 87 | -------------------------------------------------------------------------------- /src/components/Tree/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import { DataSnapshot, get, ref } from "firebase/database"; 4 | 5 | import { db } from "services/firebase"; 6 | 7 | import { DrawerContainer, MissoGatesLogo, NonWrappableColumn, RouteLink } from "./styles"; 8 | 9 | const ROOT_SYMBOL = ".\\"; 10 | const CHILD_SYMBOL = "↳ "; 11 | 12 | function Tree() { 13 | const { pathname } = useLocation(); 14 | const [parentRoute, setParentRoute] = useState(null); 15 | const [myKey,setMyKey] = useState(null); 16 | const [routes, setRoutes] = useState([]); 17 | const [showMissogates, setShowMissogates] = useState(false); 18 | 19 | useEffect(() => { 20 | const dbRef = ref(db, pathname); 21 | 22 | async function handleRoutes() { 23 | const routes = await get(dbRef); 24 | 25 | if (dbRef.parent) { 26 | const parent = await get(dbRef.parent); 27 | setParentRoute(parent) 28 | } 29 | setMyKey(routes.key) 30 | setRoutes(getDataSnapshotArray(routes)); 31 | } 32 | if (showMissogates) 33 | handleRoutes(); 34 | }, [pathname, showMissogates]); 35 | 36 | function getDataSnapshotArray(node: DataSnapshot): DataSnapshot[] { 37 | const children: DataSnapshot[] = []; 38 | 39 | node.forEach((child) => { 40 | if (child.size > 0) { 41 | children.push(child); 42 | } 43 | }); 44 | 45 | return children; 46 | } 47 | 48 | function getTree() { 49 | return <> 50 | {parentRoute?.key && 51 | 52 | 53 | {"↰ " + parentRoute.key} 54 | 55 | 56 | } 57 | { 58 | routes 59 | .filter(({ size }) => size > 0) 60 | .map((route) => ( 61 | {mountTree(route,"", parentRoute?.key? 2:1,true )} 62 | )) 63 | } 64 | 65 | } 66 | 67 | function getRouteLink( 68 | route: DataSnapshot, 69 | subpath: string = "", 70 | level: number = 1, 71 | root: boolean = false 72 | ) { 73 | return ( 74 | 75 | {(root? ROOT_SYMBOL : CHILD_SYMBOL) + route.key} 76 | 77 | ); 78 | } 79 | 80 | function mountTree( 81 | node: DataSnapshot, 82 | subpath: string = "", 83 | recursion: number = 1, 84 | root: boolean = false 85 | ): JSX.Element { 86 | if (recursion > 4) return <>; 87 | 88 | const children: DataSnapshot[] = getDataSnapshotArray(node); 89 | 90 | return ( 91 | <> 92 | {getRouteLink(node, subpath, recursion++,root)} 93 | 94 | {children.map((route) => 95 | mountTree(route, subpath + "/" + node.key, recursion,false) 96 | )} 97 | 98 | ); 99 | } 100 | 101 | 102 | return ( 103 | <> 104 | setShowMissogates(!showMissogates)}> 105 | ✈️ {showMissogates && " MissoGates"} 106 | 107 | 108 | { 109 | showMissogates && 110 | {routes.length === 0 ?

No gates here :(

: getTree()} 111 |
112 | } 113 | 114 | 115 | 116 | 117 | ); 118 | } 119 | 120 | export default Tree; 121 | -------------------------------------------------------------------------------- /src/components/Pad/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAlert } from "react-alert"; 2 | import { useLocation } from "react-router-dom"; 3 | import { get, onValue, ref } from "firebase/database"; 4 | import { useEffect, ChangeEvent, useState, useCallback } from "react"; 5 | 6 | import { auth, db, signInAnonymously } from "services/firebase"; 7 | 8 | import { handleWriting } from "utils/writing"; 9 | import { lessThan } from "utils/time"; 10 | 11 | import { Tree, MarkdownRenderer } from "components"; 12 | import { 13 | //Editor, 14 | HeaderTitle, 15 | PadContainer, 16 | PadHeader, 17 | Previewer, 18 | } from "./styles"; 19 | 20 | import Editor from '@monaco-editor/react' 21 | 22 | import { ServerDoc } from "types/ServerDoc"; 23 | 24 | const POLL_TIME = 3000; 25 | 26 | function Pad() { 27 | const alert = useAlert(); 28 | 29 | const { pathname } = useLocation(); 30 | 31 | const [saved, setSaved] = useState(false); 32 | const [content, setContent] = useState(""); 33 | const [userId, setUserId] = useState(""); 34 | const [loaded, setLoaded] = useState(false); 35 | const [disabled, setDisabled] = useState(false); 36 | const [onlyView, setOnlyView] = useState(false); 37 | 38 | 39 | useEffect(() => { 40 | async function signIn() { 41 | const { user } = await signInAnonymously(auth); 42 | setUserId(user.uid); 43 | } 44 | 45 | signIn(); 46 | }, []); 47 | 48 | const handleDisable = useCallback( 49 | (serverDoc: ServerDoc) => { 50 | if (!userId) setDisabled(true); 51 | 52 | const isDifferentAuthor = serverDoc.author !== userId; 53 | 54 | const newDisabled = 55 | isDifferentAuthor && lessThan(serverDoc.updatedAt, POLL_TIME); 56 | 57 | setDisabled(newDisabled); 58 | }, 59 | [userId] 60 | ); 61 | 62 | useEffect(() => { 63 | if (!userId) return; 64 | 65 | const dbRef = ref(db, pathname); 66 | 67 | const unsubscribe = onValue(dbRef, (snapshot) => { 68 | setLoaded(true); 69 | 70 | if (!snapshot) return; 71 | 72 | const serverDoc: ServerDoc = snapshot.val(); 73 | 74 | setContent(serverDoc.content); 75 | handleDisable(serverDoc); 76 | }); 77 | 78 | const interval = setInterval(async () => { 79 | const snapshot = await get(dbRef); 80 | 81 | const serverDoc: ServerDoc = snapshot.val(); 82 | 83 | handleDisable(serverDoc); 84 | }, POLL_TIME); 85 | 86 | return () => { 87 | unsubscribe(); 88 | clearInterval(interval); 89 | }; 90 | }, [pathname, userId, handleDisable]); 91 | 92 | useEffect(() => { 93 | if (!disabled) { 94 | (alert as any).removeAll(); 95 | } 96 | 97 | const alertsActive = (alert as any).alerts.length; 98 | 99 | if (disabled && !alertsActive) { 100 | alert.show("someone is typing"); 101 | } 102 | }, [alert, disabled]); 103 | 104 | async function handleTextChange(value: string | undefined, e: ChangeEvent) { 105 | setSaved(false); 106 | 107 | const text = value ?? e.target.value; 108 | 109 | if (!loaded) return; 110 | 111 | await handleWriting(pathname, { content: text, author: userId }); 112 | 113 | setSaved(true); 114 | } 115 | 116 | return ( 117 |
118 | 119 | 120 | 121 | 122 | setOnlyView(!onlyView)}> 123 | MISSOPAD 124 | 125 | 126 | {saved ? "👌" : "⌛"} 127 | 128 | {!loaded && loading...} 129 | 130 | 131 | 132 | {!onlyView && ( 133 | } 141 | options={{ 142 | minimap: { 143 | enabled: false, 144 | }, 145 | padding: { 146 | top: 10, 147 | bottom: 10 148 | }, 149 | readOnly: !loaded || disabled 150 | }} 151 | /> 152 | )} 153 | 154 | 155 | 156 | 157 | 158 |
159 | ); 160 | } 161 | 162 | export default Pad; 163 | --------------------------------------------------------------------------------