├── 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 |
--------------------------------------------------------------------------------