├── .gitignore ├── .vscode └── launch.json ├── README.md ├── __Screenshots__ ├── sc1.png ├── sc2.png └── sc3.png ├── package-lock.json ├── package.json ├── public ├── esbuild.wasm ├── favicon.ico ├── iframes.html ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── Assets │ └── resize.png ├── Bundler │ └── index.ts ├── Redux │ ├── action-creators │ │ └── index.ts │ ├── action-types │ │ └── index.ts │ ├── actions │ │ └── index.ts │ ├── index.ts │ ├── middlewares │ │ └── bundlerMiddleware.ts │ ├── reducers │ │ ├── bundleReducer.ts │ │ ├── cellReducer.ts │ │ └── index.ts │ └── store.ts ├── components │ ├── ActionBar │ │ ├── ActionBar.tsx │ │ └── styles.ts │ ├── AddCell │ │ ├── AddCell.tsx │ │ └── styles.ts │ ├── CellList │ │ ├── CellList.tsx │ │ ├── CellListItem │ │ │ ├── CellItem.tsx │ │ │ └── styles.tsx │ │ └── styles.ts │ ├── CodeCell │ │ ├── Cell.tsx │ │ └── styles.ts │ ├── CodeEditor │ │ ├── Editor.tsx │ │ ├── styles.ts │ │ └── types.d.ts │ ├── MarkdownEditor │ │ ├── MarkdownEditor.tsx │ │ ├── resizerReset.css │ │ └── styles.ts │ ├── PreviewWindow │ │ └── PreviewWindow.tsx │ └── ResizableCell │ │ ├── ResizableCell.tsx │ │ └── styles.css ├── hooks │ ├── useActions.ts │ └── useTypedSelector.ts ├── index.tsx ├── interfaces │ ├── ActionBar │ │ └── ActionBarInterface.ts │ ├── Bundle │ │ ├── BundleInterface.ts │ │ └── BundleMiddlewareInterface.ts │ ├── Cell │ │ ├── AddCellInterface.ts │ │ └── CellInterface.ts │ ├── CodeEditor │ │ └── EditorPropsInterface.ts │ ├── Plugins │ │ └── unpkgPluginInterface.ts │ ├── PreviewWindow │ │ └── PreviewWindowInterface.ts │ ├── ResizableCell │ │ └── ResizablePropsInterface.ts │ └── index.ts ├── plugins │ ├── index.ts │ ├── unpkgFetchPackagePlugin.ts │ └── unpkgPathPlugin.ts ├── react-app-env.d.ts ├── styles.ts └── theme.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![David](https://img.shields.io/david/dev/SoumavaBanerjee/JS-Note?style=plastic) 3 | ![GitHub language count](https://img.shields.io/github/languages/count/SoumavaBanerjee/JS-Note) 4 | ![Website](https://img.shields.io/website?down_color=lightgrey&down_message=down&up_color=green&up_message=running&url=https%3A%2F%2Fjs-note.netlify.app%2F) 5 | 6 | # JS NOTE: Notebook for javascript 7 | 8 | Execute code, document ideas in full markdown syntax in a fast paced cell interface (like jupiter notebook). 9 | Skip setting up a dev environment and code directly. 10 | 11 | ## Features: 12 | 13 | 🔷 All the stuff happens client side, no servers involved.
14 | 🔷 Dynamic import of NPM modules on the fly!
15 | 🔷 No configurations required!
16 | 17 | ## Built with: 18 | 19 | - React.js with Typescript! 20 | - ES-Build as transpiler and bundler! 21 | 22 | ## Screenshots: 23 | ### Cell view 24 | ![Cell Types](https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/main/__Screenshots__/sc1.png) 25 | ### Markdown cell 26 | ![markdown editor](https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/main/__Screenshots__/sc2.png) 27 | ### Code cell 28 | ![code cell editor](https://github.com/SoumavaBanerjee/JS-Note/blob/main/__Screenshots__/sc3.png) 29 | 30 | ## Setting up Local Dev Environment: 31 | ### Requirements: 32 | - Node >= 10.x.x 33 | 34 | clone the project in your workspace and run npm i
35 | After installation of dependencies, run npm start
36 | 37 | that's it! 38 | 39 | ## Future plans 40 | - Publishing to npm as a package to facilitate direct invoking of app through terminal. 41 | - Developing a social coding experience like codepen. 42 | 43 | -------------------------------------------------------------------------------- /__Screenshots__/sc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/__Screenshots__/sc1.png -------------------------------------------------------------------------------- /__Screenshots__/sc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/__Screenshots__/sc2.png -------------------------------------------------------------------------------- /__Screenshots__/sc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/__Screenshots__/sc3.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-transpiler-bundler", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@monaco-editor/react": "^4.1.1", 9 | "@testing-library/jest-dom": "^5.11.9", 10 | "@testing-library/react": "^11.2.5", 11 | "@testing-library/user-event": "^12.8.3", 12 | "@types/jest": "^26.0.21", 13 | "@types/node": "^12.20.6", 14 | "@types/prettier": "^2.2.3", 15 | "@types/react": "^17.0.3", 16 | "@types/react-dom": "^17.0.2", 17 | "@types/react-redux": "^7.1.16", 18 | "@types/react-resizable": "^1.7.2", 19 | "@uiw/react-md-editor": "2.1.1", 20 | "axios": "^0.21.1", 21 | "esbuild": "^0.9.6", 22 | "esbuild-wasm": "^0.9.6", 23 | "immer": "^9.0.1", 24 | "localforage": "^1.9.0", 25 | "monaco-editor": "^0.23.0", 26 | "nanoid": "^3.1.22", 27 | "prettier": "^2.2.1", 28 | "react": "^17.0.1", 29 | "react-dom": "^17.0.1", 30 | "react-redux": "^7.2.3", 31 | "react-resizable": "^1.11.1", 32 | "react-scripts": "4.0.3", 33 | "redux": "^4.0.5", 34 | "redux-thunk": "^2.3.0", 35 | "typescript": "^4.2.3", 36 | "web-vitals": "^1.1.1" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "gh-pages": "^3.1.0", 64 | "redux-devtools-extension": "^2.13.9" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/esbuild.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/public/esbuild.wasm -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/public/favicon.ico -------------------------------------------------------------------------------- /public/iframes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Iframe 9 | 10 | 11 | 12 |

Hi there

13 | 14 | 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import * as esbuild from "esbuild-wasm"; 3 | import CellList from "./components/CellList/CellList"; 4 | import makeStyles from "./styles"; 5 | import { CssBaseline, ThemeProvider } from "@material-ui/core"; 6 | import theme from "./theme"; 7 | 8 | const App: React.FC = () => { 9 | const classes = makeStyles(); 10 | 11 | /** 12 | *In the old version, .startService() returned a promise that resolved to a service object. 13 | But in the new version, .initialize() returns a promise that resolves to undefined. 14 | This is an intentional design change. There is no more service object. 15 | Instead, you just call transform and build on esbuild's API object itself. 16 | * 17 | */ 18 | 19 | // Start es-build service once 20 | 21 | /* I'm stopping this to isolate it during markdown. Turn it back on again */ 22 | const startService = async () => { 23 | await esbuild.initialize({ 24 | wasmURL: "/esbuild.wasm", 25 | worker: true, 26 | }); 27 | }; 28 | useEffect(() => { 29 | startService(); 30 | }, []); 31 | 32 | return ( 33 | 34 |
35 | 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/Assets/resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoumavaBanerjee/JS-Note/b4250c7f833a0b8d0c727f51a1373d3e2b264192/src/Assets/resize.png -------------------------------------------------------------------------------- /src/Bundler/index.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild-wasm"; 2 | import * as plugins from "../plugins/index"; 3 | 4 | // eslint-disable-next-line import/no-anonymous-default-export 5 | const bundle = async (rawCode: string) => { 6 | try { 7 | /** 8 | * @EntryPoint entry file to start bundling, 9 | * @bundle bundling should occur or not 10 | * @write return the file as an in-memory buffer 11 | * @color colored warnings 12 | * @define Define environment variable value 13 | * @plugin Define all custom written plugins 14 | */ 15 | 16 | const bunduledCode = await esbuild.build({ 17 | entryPoints: ["index.js"], 18 | bundle: true, 19 | write: false, 20 | color: true, 21 | define: { 22 | "process.env.NODE_ENV": '"development"', // set development to a string not a variable. 23 | global: "window", 24 | }, 25 | plugins: [ 26 | plugins.unpkgPathPlugin(), 27 | plugins.unpkgFetchPackagePlugin(rawCode), 28 | ], 29 | }); 30 | console.log(bunduledCode); 31 | return { code: bunduledCode.outputFiles[0].text, error: "" }; 32 | } catch (err) { 33 | return { code: "", error: err.message }; 34 | } 35 | }; 36 | 37 | export default bundle; 38 | -------------------------------------------------------------------------------- /src/Redux/action-creators/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeleteCellAction, 3 | InsertCellAction, 4 | MoveCellAction, 5 | UpdateCellAction, 6 | } from "../actions"; 7 | import { ActionType } from "../action-types"; 8 | import { CellDirection, CellType } from "../../interfaces"; 9 | 10 | export const moveCell = ( 11 | id: string, 12 | direction: CellDirection 13 | ): MoveCellAction => { 14 | return { 15 | type: ActionType.MOVE_CELL, 16 | payload: { 17 | id, 18 | direction, 19 | }, 20 | }; 21 | }; 22 | 23 | export const deleteCell = (id: string): DeleteCellAction => { 24 | return { 25 | type: ActionType.DELETE_CELL, 26 | payload: id, 27 | }; 28 | }; 29 | 30 | export const insertCellAfter = ( 31 | id: string | null, 32 | type: CellType 33 | ): InsertCellAction => { 34 | return { 35 | type: ActionType.INSERT_CELL_AFTER, 36 | payload: { 37 | id, 38 | type, 39 | }, 40 | }; 41 | }; 42 | 43 | export const updateCell = (id: string, content: string): UpdateCellAction => { 44 | return { 45 | type: ActionType.UPDATE_CELL, 46 | payload: { 47 | id, 48 | content, 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/Redux/action-types/index.ts: -------------------------------------------------------------------------------- 1 | export enum ActionType { 2 | MOVE_CELL = "move_cell", 3 | DELETE_CELL = "delete_Cell", 4 | INSERT_CELL_AFTER = "insert_cell_after", 5 | UPDATE_CELL = "update_cell", 6 | BUNDLE_CREATED = "bundle_created", 7 | } 8 | -------------------------------------------------------------------------------- /src/Redux/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from "../action-types/index"; 2 | import { CellDirection, CellType } from "../../interfaces/index"; 3 | 4 | export interface MoveCellAction { 5 | type: ActionType.MOVE_CELL; 6 | payload: { 7 | id: string; 8 | direction: CellDirection; 9 | }; 10 | } 11 | 12 | export interface DeleteCellAction { 13 | type: ActionType.DELETE_CELL; 14 | payload: string; 15 | } 16 | 17 | export interface InsertCellAction { 18 | type: ActionType.INSERT_CELL_AFTER; 19 | payload: { 20 | id: string | null; 21 | type: CellType; 22 | }; 23 | } 24 | 25 | export interface UpdateCellAction { 26 | type: ActionType.UPDATE_CELL; 27 | payload: { 28 | id: string; 29 | content: string; 30 | }; 31 | } 32 | 33 | export interface BundleCreatedAction { 34 | type: ActionType.BUNDLE_CREATED; 35 | payload: { 36 | cellId: string; 37 | bundle: { 38 | code: string; 39 | error: string; 40 | }; 41 | }; 42 | } 43 | 44 | export type Action = 45 | | MoveCellAction 46 | | DeleteCellAction 47 | | InsertCellAction 48 | | UpdateCellAction 49 | | BundleCreatedAction; 50 | -------------------------------------------------------------------------------- /src/Redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./reducers"; 3 | export * as actionCreators from "./action-creators"; 4 | -------------------------------------------------------------------------------- /src/Redux/middlewares/bundlerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "../../interfaces"; 2 | import { ActionType } from "../action-types"; 3 | import bundle from "../../Bundler"; 4 | 5 | let timer: any; 6 | export const bundleMiddleware: Middleware = ({ getState, dispatch }) => ( 7 | next 8 | ) => (action) => { 9 | next(action); 10 | 11 | if (action.type !== ActionType.UPDATE_CELL) return; 12 | 13 | // check for text cell-type 14 | const { 15 | cell: { data: cellData }, 16 | } = getState(); 17 | const cell = cellData[action.payload.id]; 18 | 19 | if (cell.type === "text") return; 20 | 21 | clearTimeout(timer); 22 | timer = setTimeout(async () => { 23 | console.log("initiating bundling"); 24 | const result = await bundle(action.payload.content); 25 | 26 | dispatch({ 27 | type: ActionType.BUNDLE_CREATED, 28 | payload: { 29 | cellId: action.payload.id, 30 | bundle: result, 31 | }, 32 | }); 33 | 34 | console.log("bundling finished", "action dispatched"); 35 | }, 1000); 36 | }; 37 | -------------------------------------------------------------------------------- /src/Redux/reducers/bundleReducer.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer"; 2 | import { ActionType } from "../action-types"; 3 | import { Action } from "../actions"; 4 | import { BundleState } from "../../interfaces"; 5 | 6 | const initialState: BundleState = {}; 7 | 8 | // sample bundle state will look like this: 9 | // const dummystate = { 10 | // cdHHD_VY: { 11 | // code: 12 | // "console.log(`Hey! If you're reading this, why don't you give me a interview oppurtunity?`);", 13 | // error: "", 14 | // }, 15 | // }; 16 | 17 | const reducer = produce( 18 | (state: BundleState = initialState, action: Action): BundleState => { 19 | switch (action.type) { 20 | case ActionType.BUNDLE_CREATED: 21 | state[action.payload.cellId] = action.payload.bundle; 22 | return state; 23 | default: 24 | return state; 25 | } 26 | }, 27 | initialState 28 | ); 29 | 30 | export default reducer; 31 | -------------------------------------------------------------------------------- /src/Redux/reducers/cellReducer.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { nanoid } from "nanoid"; 3 | 4 | import { Action } from "../actions/index"; 5 | import { CellInterface } from "../../interfaces/index"; 6 | import { ActionType } from "../action-types"; 7 | 8 | interface cellState { 9 | loading: boolean; 10 | error: string; 11 | order: string[]; 12 | data: { 13 | [keys: string]: CellInterface; 14 | }; 15 | } 16 | 17 | const initialState: cellState = { 18 | loading: false, 19 | error: "", 20 | order: [], 21 | data: {}, 22 | }; 23 | 24 | // PS: data has been modeled like that (instead of an array) to prevent .find()/.filter() calls. 25 | // This simplefies accesing stuff. 26 | 27 | // state will look something like this: 28 | // const stateDummy = [ 29 | // { 30 | // loading: false, 31 | // error: "", 32 | // order: ["id1", "id2", "id3", "id4"], 33 | // data: { 34 | // id1: { 35 | // id: "Hs2UHJ_Vw", 36 | // type: "code", 37 | // content: "const xyz = 231283", 38 | // }, 39 | // id2: { 40 | // id: "DswZH_B_S", 41 | // type: "text", 42 | // content: "# Header '\n' -rest of the stuff ", 43 | // }, 44 | // }, 45 | // }, 46 | // ]; 47 | 48 | const reducer = produce((state: cellState = initialState, action: Action) => { 49 | switch (action.type) { 50 | case ActionType.UPDATE_CELL: 51 | const { content, id } = action.payload; 52 | state.data[id].content = content; 53 | return state; 54 | 55 | case ActionType.MOVE_CELL: 56 | const { direction } = action.payload; 57 | const idIndex = state.order.findIndex((id) => id === action.payload.id); 58 | const swapTargetIndex = direction === "up" ? idIndex - 1 : idIndex + 1; 59 | 60 | // check for swapTargetIndex out of bounds 61 | if (swapTargetIndex < 0 || swapTargetIndex > state.order.length - 1) { 62 | console.error("swapTargetIndex is out of bounds"); 63 | return state; 64 | } 65 | 66 | // Swap id orders 67 | state.order[idIndex] = state.order[swapTargetIndex]; 68 | state.order[swapTargetIndex] = action.payload.id; 69 | 70 | return state; 71 | 72 | case ActionType.DELETE_CELL: 73 | // from both order and data 74 | delete state.data[action.payload]; 75 | const index = state.order.findIndex((id) => id === action.payload); 76 | state.order.splice(index, 1); 77 | return state; 78 | 79 | case ActionType.INSERT_CELL_AFTER: 80 | const { type } = action.payload; 81 | const newCell: CellInterface = { 82 | content: "", 83 | type, 84 | id: nanoid(), 85 | }; 86 | 87 | // insert new cell in data 88 | state.data[newCell.id] = newCell; 89 | 90 | // get index of previous cell and insert newCell after it 91 | const prevCellIndex = state.order.findIndex( 92 | (id) => id === action.payload.id 93 | ); 94 | 95 | if (prevCellIndex === -1) { 96 | state.order.unshift(newCell.id); 97 | } else { 98 | state.order.splice(prevCellIndex + 1, 0, newCell.id); 99 | } 100 | 101 | return state; 102 | default: 103 | return state; 104 | } 105 | }, initialState); 106 | 107 | export default reducer; 108 | -------------------------------------------------------------------------------- /src/Redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import cellReducer from "./cellReducer"; 3 | import bundleReducer from "./bundleReducer"; 4 | 5 | const rootReducer = combineReducers({ 6 | cell: cellReducer, 7 | bundle: bundleReducer, 8 | }); 9 | 10 | export default rootReducer; 11 | export type RootState = ReturnType; 12 | -------------------------------------------------------------------------------- /src/Redux/store.ts: -------------------------------------------------------------------------------- 1 | //eslint-disable-next-line 2 | import { createStore, applyMiddleware, compose } from "redux"; 3 | import thunk from "redux-thunk"; 4 | import rootReducer from "./reducers"; 5 | import { bundleMiddleware } from "./middlewares/bundlerMiddleware"; 6 | import { composeWithDevTools } from "redux-devtools-extension"; 7 | 8 | // Testing 9 | import { ActionType } from "./action-types"; 10 | 11 | export const Store = createStore( 12 | rootReducer, 13 | {}, 14 | composeWithDevTools(applyMiddleware(thunk, bundleMiddleware)) 15 | ); 16 | 17 | Store.dispatch({ 18 | type: ActionType.INSERT_CELL_AFTER, 19 | payload: { 20 | id: null, 21 | type: "text", 22 | }, 23 | }); 24 | 25 | Store.dispatch({ 26 | type: ActionType.INSERT_CELL_AFTER, 27 | payload: { 28 | id: null, 29 | type: "code", 30 | }, 31 | }); 32 | 33 | Store.dispatch({ 34 | type: ActionType.INSERT_CELL_AFTER, 35 | payload: { 36 | id: null, 37 | type: "text", 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/ActionBar/ActionBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ActionBarinterface } from "../../interfaces"; 3 | import { Button } from "@material-ui/core"; 4 | import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward"; 5 | import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward"; 6 | import CloseIcon from "@material-ui/icons/Close"; 7 | 8 | import { useAction } from "../../hooks/useActions"; 9 | 10 | import makeStyles from "./styles"; 11 | 12 | const ActionBar: React.FC = ({ id }) => { 13 | const classes = makeStyles(); 14 | 15 | const { moveCell, deleteCell } = useAction(); 16 | 17 | return ( 18 |
19 | 30 | 41 | 52 |
53 | ); 54 | }; 55 | 56 | export default ActionBar; 57 | -------------------------------------------------------------------------------- /src/components/ActionBar/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | actionBarWrapper: { 5 | display: "flex", 6 | justifyContent: "flex-end", 7 | alignItems: "center", 8 | backgroundColor: theme.palette.background.paper, 9 | marginBottom: "-0.5rem", 10 | }, 11 | actionButtons: { 12 | marginRight: theme.spacing(2), 13 | opacity: 0.5, 14 | transition: "opacity 0.3s", 15 | "&:hover": { 16 | opacity: 1, 17 | }, 18 | }, 19 | })); 20 | -------------------------------------------------------------------------------- /src/components/AddCell/AddCell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { AddCellInterface } from "../../interfaces"; 4 | import { useAction } from "../../hooks/useActions"; 5 | 6 | import makeStyles from "./styles"; 7 | 8 | import { Button } from "@material-ui/core"; 9 | import AddIcon from "@material-ui/icons/Add"; 10 | 11 | const AddCell: React.FC = ({ previousCellId }) => { 12 | const { insertCellAfter } = useAction(); 13 | const classes = makeStyles(); 14 | 15 | return ( 16 |
17 |
18 |
19 | 31 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default AddCell; 49 | -------------------------------------------------------------------------------- /src/components/AddCell/styles.ts: -------------------------------------------------------------------------------- 1 | import { grey } from "@material-ui/core/colors"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | export default makeStyles((theme) => ({ 4 | addCellWrapper: { 5 | display: "flex", 6 | justifyContent: "center", 7 | alignItems: "center", 8 | marginTop: theme.spacing(3), 9 | marginBottom: theme.spacing(3), 10 | opacity: 0, 11 | transition: "opacity 0.2s ease-in 0.1s", 12 | "&:hover": { 13 | opacity: 1, 14 | }, 15 | }, 16 | 17 | button: { 18 | marginLeft: theme.spacing(2), 19 | marginRight: theme.spacing(2), 20 | }, 21 | 22 | linethrough: { 23 | position: "absolute", 24 | height: "1px", 25 | width: "50%", 26 | backgroundColor: grey[400], 27 | zIndex: -1, 28 | }, 29 | 30 | buttonWrapper: { 31 | display: "flex", 32 | justifyContent: "center", 33 | width: "20%", 34 | backgroundColor: theme.palette.background.default, 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /src/components/CellList/CellList.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { useTypedSelector } from "../../hooks/useTypedSelector"; 3 | import CellItem from "./CellListItem/CellItem"; 4 | import AddCell from "../AddCell/AddCell"; 5 | 6 | import makeStyles from "./styles"; 7 | 8 | const CellList: React.FC = () => { 9 | const classes = makeStyles(); 10 | 11 | // sort data according to order from state 12 | const cells = useTypedSelector(({ cell: { data, order } }) => 13 | order.map((id) => data[id]) 14 | ); 15 | 16 | const renderedCells = cells.map((cell) => ( 17 | 18 | 19 | 20 | 21 | )); 22 | 23 | return ( 24 |
25 | {/* when no cells present, set opacity to 1 */} 26 |
27 | 28 |
29 | {renderedCells} 30 |
31 | ); 32 | }; 33 | 34 | export default CellList; 35 | -------------------------------------------------------------------------------- /src/components/CellList/CellListItem/CellItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CellListItemInterface } from "../../../interfaces/index"; 3 | 4 | import Cell from "../../CodeCell/Cell"; 5 | import Editor from "../../MarkdownEditor/MarkdownEditor"; 6 | import ActionBar from "../../ActionBar/ActionBar"; 7 | 8 | import makeStyles from "./styles"; 9 | 10 | const CellItem: React.FC = ({ cell }) => { 11 | let child: JSX.Element; 12 | const classes = makeStyles(); 13 | 14 | if (cell.type === "code") { 15 | child = ; 16 | } else { 17 | child = ; 18 | } 19 | 20 | return ( 21 |
22 | 23 | {child} 24 |
25 | ); 26 | }; 27 | 28 | export default CellItem; 29 | -------------------------------------------------------------------------------- /src/components/CellList/CellListItem/styles.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | CellListItemWrapper: { 5 | marginTop: theme.spacing(2), 6 | marginBottom: theme.spacing(2), 7 | }, 8 | })); 9 | -------------------------------------------------------------------------------- /src/components/CellList/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | maxOpacity: { 5 | "& div": { 6 | opacity: 1, 7 | }, 8 | }, 9 | })); 10 | -------------------------------------------------------------------------------- /src/components/CodeCell/Cell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTypedSelector } from "../../hooks/useTypedSelector"; 3 | 4 | import CodeEditor from "../CodeEditor/Editor"; 5 | import PreviewWindow from "../PreviewWindow/PreviewWindow"; 6 | import ResizableCell from "../ResizableCell/ResizableCell"; 7 | 8 | import { Paper } from "@material-ui/core"; 9 | import makeStyles from "./styles"; 10 | 11 | import { CellListItemInterface } from "../../interfaces"; 12 | 13 | import { useAction } from "../../hooks/useActions"; 14 | 15 | const Cell: React.FC = ({ cell }) => { 16 | const classes = makeStyles(); 17 | const bundle = useTypedSelector((state) => state.bundle[cell.id]); 18 | 19 | const { updateCell } = useAction(); 20 | 21 | return ( 22 | 23 | 24 | updateCell(cell.id, value)} 27 | /> 28 | {!bundle ? ( 29 | 30 | ) : ( 31 | 32 | )} 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Cell; 39 | -------------------------------------------------------------------------------- /src/components/CodeCell/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | wrapper: { 5 | display: "flex", 6 | flexDirection: "row", 7 | height: "100%", 8 | padding: theme.spacing(2), 9 | }, 10 | [theme.breakpoints.down("sm")]: { 11 | wrapper: { 12 | flexDirection: "column", 13 | }, 14 | }, 15 | gridContainer: { 16 | height: "100%", 17 | marginTop: theme.spacing(2), 18 | marginBottom: theme.spacing(2), 19 | padding: theme.spacing(2), 20 | }, 21 | })); 22 | -------------------------------------------------------------------------------- /src/components/CodeEditor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import MonacoEditor, { Monaco } from "@monaco-editor/react"; 2 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 3 | import { EditorPropsInterface } from "../../interfaces/index"; 4 | import React, { useRef } from "react"; 5 | import prettier from "prettier"; 6 | import parser from "prettier/parser-babel"; 7 | 8 | import { Button } from "@material-ui/core"; 9 | import makeStyles from "./styles"; 10 | 11 | const Editor: React.FC = ({ onChange, initialValue }) => { 12 | const monacoEditorRef = useRef(); 13 | const classes = makeStyles(); 14 | 15 | /** 16 | * On change in model, get the value of it's content and 17 | * pass it as an argument to onChange to set the input state 18 | */ 19 | const onEditorDidMount = ( 20 | editor: monaco.editor.IStandaloneCodeEditor, 21 | monacoInstance: Monaco 22 | ) => { 23 | monacoEditorRef.current = editor; 24 | editor.onDidChangeModelContent(() => { 25 | onChange(editor.getValue()); 26 | }); 27 | }; 28 | 29 | /** 30 | * Get unformatted code, format it and set it to model 31 | */ 32 | const onFormatClick = () => { 33 | const rawCode = monacoEditorRef!.current!.getModel()!.getValue(); 34 | const formattedCode = prettier 35 | .format(rawCode, { 36 | parser: "babel", 37 | plugins: [parser], 38 | semi: true, 39 | singleQuote: true, 40 | }) 41 | .replace(/\n$/, ""); 42 | 43 | monacoEditorRef.current?.setValue(formattedCode); 44 | }; 45 | 46 | // breaking changes. Revert to Grid conatiner if necessary 47 | return ( 48 |
49 | 58 | 76 |
77 | ); 78 | }; 79 | 80 | export default Editor; 81 | -------------------------------------------------------------------------------- /src/components/CodeEditor/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | editorFormatBtn: { 5 | position: "absolute", 6 | top: "10px", 7 | right: "10px", 8 | zIndex: 2, 9 | }, 10 | editorWrapper: { 11 | position: "relative", 12 | overflow: "hidden", 13 | height: "100%", 14 | width: "100%", 15 | }, 16 | })); 17 | -------------------------------------------------------------------------------- /src/components/CodeEditor/types.d.ts: -------------------------------------------------------------------------------- 1 | // Only to prevent Typescript errors 2 | declare module "monaco-jsx-highlighter"; 3 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/MarkdownEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import MDEditor from "@uiw/react-md-editor"; 3 | import { Paper } from "@material-ui/core"; 4 | 5 | import { CellListItemInterface } from "../../interfaces"; 6 | import { useAction } from "../../hooks/useActions"; 7 | 8 | import makeStyles from "./styles"; 9 | 10 | // re-style the drag handle; 11 | import "./resizerReset.css"; 12 | 13 | const MarkdownEditor: React.FC = ({ cell }) => { 14 | const classes = makeStyles(); 15 | const [editing, setEditing] = useState(false); 16 | const { updateCell } = useAction(); 17 | const markdownRef = useRef(null); 18 | 19 | // Toogle between view mode and editor mode 20 | useEffect(() => { 21 | const listner = (event: MouseEvent) => { 22 | // detect if clicked within editor div 23 | if ( 24 | markdownRef.current && 25 | event.target && 26 | markdownRef.current.contains(event.target as Node) 27 | ) { 28 | console.log("element clicked within editor"); 29 | return; 30 | } 31 | 32 | console.log("element clicked is not within editor"); 33 | 34 | setEditing(false); 35 | console.log(event.target); 36 | }; 37 | 38 | document.addEventListener("click", listner, { capture: true }); 39 | 40 | return () => { 41 | document.removeEventListener("click", listner, { capture: true }); 42 | }; 43 | }, []); 44 | 45 | if (editing) { 46 | return ( 47 | 52 | 55 | updateCell(cell.id, updatedTextValue || "") 56 | } 57 | /> 58 | 59 | ); 60 | } 61 | 62 | return ( 63 | setEditing(true)} 65 | className={`${classes.PaperContainer} text-editor`} 66 | elevation={2} 67 | > 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default MarkdownEditor; 74 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/resizerReset.css: -------------------------------------------------------------------------------- 1 | .w-md-editor-bar { 2 | display: flex; 3 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=") 4 | no-repeat; 5 | background-color: #424242; 6 | background-position: 50%; 7 | position: relative; 8 | margin-top: -2px; 9 | width: 100%; 10 | } 11 | 12 | .w-md-editor-bar svg { 13 | display: none; 14 | } 15 | 16 | .w-md-editor .title { 17 | line-height: unset; 18 | font-size: unset; 19 | font-weight: unset; 20 | color: #d4d4d4 !important; 21 | } 22 | 23 | .text-editor em { 24 | font-style: italic; 25 | } 26 | 27 | .text-editor .wmde-markdown hr { 28 | border-top: 1px solid #dee5ed; 29 | } 30 | 31 | .text-editor .wmde-markdown ol { 32 | list-style: decimal; 33 | } 34 | 35 | .text-editor .w-md-editor-show-live { 36 | /* Hide menu bar buttons to prevent accidental delete */ 37 | z-index: 20; 38 | } 39 | 40 | .text-editor .w-md-editor-toolbar { 41 | border-bottom: 1px solid gray; 42 | background-color: hsl(0, 0%, 22%); 43 | } 44 | 45 | .text-editor .w-md-editor-toolbar li button { 46 | color: #8db8d1; 47 | } 48 | 49 | .text-editor .w-md-editor-content { 50 | background-color: #202123; 51 | } 52 | 53 | .text-editor .w-md-editor, 54 | .text-editor .w-md-editor .w-md-editor-text-pre { 55 | color: #d4d4d4; 56 | } 57 | 58 | .text-editor .w-md-editor-text-pre .bold { 59 | color: unset; 60 | } 61 | 62 | .text-editor .token.list.punctuation { 63 | background-color: unset; 64 | } 65 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | 3 | export default makeStyles((theme) => ({ 4 | PaperContainer: { 5 | padding: theme.spacing(2), 6 | // Solved Line height problem of code editor 7 | lineHeight: 1, 8 | }, 9 | })); 10 | -------------------------------------------------------------------------------- /src/components/PreviewWindow/PreviewWindow.tsx: -------------------------------------------------------------------------------- 1 | import { PreviewWindowInterface } from "../../interfaces/index"; 2 | import { useRef, useEffect } from "react"; 3 | 4 | const executableScript = ` 5 | 6 | 7 |
8 | 27 | 28 | `; 29 | 30 | const PreviewWindow: React.FC = ({ 31 | code, 32 | errorMessage, 33 | }) => { 34 | const iframeRef = useRef(); 35 | 36 | // Upon change in code, refresh window and execute the script 37 | // 50ms delay for iframe to load up 38 | 39 | useEffect(() => { 40 | iframeRef.current.srcdoc = executableScript; 41 | setTimeout(() => { 42 | iframeRef.current.contentWindow.postMessage(code, "*"); 43 | }, 25); 44 | }, [code]); 45 | 46 | console.error(errorMessage); 47 | 48 | return ( 49 | <> 50 |