├── server ├── app │ └── server.js ├── index.js └── utils.js ├── client ├── src │ ├── js │ │ ├── constants.ts │ │ ├── types.ts │ │ ├── Preview.tsx │ │ ├── index.tsx │ │ ├── Footer.tsx │ │ ├── Editor.tsx │ │ ├── useCodeshow.tsx │ │ └── CodeMirrorEditor.ts │ └── css │ │ └── index.css ├── src_app │ ├── js │ │ ├── App.tsx │ │ ├── App.tsx │ │ └── index.tsx │ └── css │ │ ├── index.css │ │ └── index.css └── public │ ├── app_0.0.5.css │ ├── imgs │ ├── moon.svg │ ├── folder.svg │ ├── minus-circle.svg │ ├── chevrons-down.svg │ ├── x-circle.svg │ ├── plus-circle.svg │ ├── arrow-left-circle.svg │ ├── arrow-right-circle.svg │ ├── file-text.svg │ └── sun.svg │ ├── codeshow_0.0.5.css │ └── scripts │ └── what-the-form.txt ├── sdk ├── build.js ├── bumpVersion.js ├── dev.js ├── config.js └── helpers │ └── utils.js ├── config └── index.js ├── Dockerfile ├── README.md ├── cloudbuild.yaml ├── package.json ├── LICENSE ├── .gitignore └── yarn.lock /server/app/server.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | 3 | } -------------------------------------------------------------------------------- /client/src/js/constants.ts: -------------------------------------------------------------------------------- 1 | export enum THEME { 2 | LIGHT = 'light', 3 | DARK = 'dark' 4 | } -------------------------------------------------------------------------------- /client/src_app/js/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function App() { 4 | return ( 5 |
6 | Hello world 7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /client/src_app/js/App.tsx : -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function App() { 4 | return ( 5 |
6 | Hello world 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /sdk/build.js: -------------------------------------------------------------------------------- 1 | const { compileCSS, compileJS } = require('./helpers/utils'); 2 | 3 | const apps = require('./config'); 4 | 5 | apps.forEach(({ css, js }) => { 6 | compileCSS(css.inputCSS, css.outputCSS) 7 | compileJS(js.inputJS, js.outputJS); 8 | }); -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | dirs: [ 5 | { path: path.normalize(__dirname + '/../client/src_app'), name: 'client' }, 6 | { path: path.normalize(__dirname + '/../server/app'), name: 'server' }, 7 | ], 8 | } -------------------------------------------------------------------------------- /client/public/app_0.0.5.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box}body,html{width:100%;height:100%;margin:0;padding:1em;font-family:Helvetica,Arial,sans-serif;font-size:24px;line-height:32px}li,p,ul{margin:0;padding:0}input{font-size:1em;padding:.2em .4em;border:solid 1px #000;border-radius:6px} -------------------------------------------------------------------------------- /client/src/js/types.ts: -------------------------------------------------------------------------------- 1 | export type Item = { 2 | path: string, 3 | name: string, 4 | type: 'folder' | 'file', 5 | children: Item[] 6 | } 7 | export type Command = { 8 | name: string; 9 | args: string; 10 | } 11 | export type Script = { 12 | slides: Command[]; 13 | } -------------------------------------------------------------------------------- /client/public/imgs/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/minus-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/chevrons-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/x-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/plus-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sdk/bumpVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const PKG_FILE = __dirname + '/../package.json'; 4 | 5 | const pkg = require(PKG_FILE); 6 | const version = pkg.version.split('.').map(n => Number(n)); 7 | version[2] += 1; 8 | pkg.version = version.join('.'); 9 | 10 | fs.writeFileSync(PKG_FILE, JSON.stringify(pkg, null, 2)); 11 | console.log('Version bumped to ' + pkg.version); -------------------------------------------------------------------------------- /client/public/imgs/arrow-left-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/imgs/arrow-right-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src_app/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body, html { 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 1em; 9 | font-family: Helvetica, Arial, sans-serif; 10 | font-size: 24px; 11 | line-height: 32px; 12 | } 13 | p, ul, li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | input { 18 | font-size: 1em; 19 | padding: 0.2em 0.4em; 20 | border: solid 1px #000; 21 | border-radius: 6px; 22 | } -------------------------------------------------------------------------------- /client/src_app/css/index.css : -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body, html { 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 1em; 9 | font-family: Helvetica, Arial, sans-serif; 10 | font-size: 24px; 11 | line-height: 32px; 12 | } 13 | p, ul, li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | input { 18 | font-size: 1em; 19 | padding: 0.2em 0.4em; 20 | border: solid 1px #000; 21 | border-radius: 6px; 22 | } 23 | -------------------------------------------------------------------------------- /client/public/imgs/file-text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.18 2 | 3 | WORKDIR /usr 4 | COPY package.json ./ 5 | COPY yarn.lock ./ 6 | COPY client ./client 7 | COPY config ./config 8 | COPY sdk ./sdk 9 | COPY server ./server 10 | 11 | RUN apk add --update --no-cache libc6-compat 12 | RUN apk add --update --no-cache \ 13 | make \ 14 | g++ \ 15 | automake \ 16 | autoconf \ 17 | libtool \ 18 | nasm \ 19 | libjpeg-turbo-dev 20 | RUN npm install 21 | RUN npm run build 22 | 23 | CMD [ "node", "./server/index.js" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codeshow 2 | 3 | An interactive React playground for live-coding sessions. 4 | 5 | --- 6 | 7 | 0. Run `yarn install` and after that `yarn dev`. 8 | 1. Open http://localhost:9090/?script=/scripts/what-the-form.txt 9 | 2. Move accross the "slides" using the navigation in the footer or by pressing F9 (next slide) or F8 (previous slide) 10 | 3. Some slides have their own steps that you can go over with F12. (in the footer there is a hint if you can do that) 11 | 12 | Check out http://localhost:9090/scripts/what-the-form.txt to see how to define your slides and steps. 13 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { codeshow, app: appUI, FileExplorer } = require('./utils'); 3 | 4 | const app = express(); 5 | const port = 9090; 6 | 7 | app.use(express.static(__dirname + '/../client/public')); 8 | app.get('/api/files', FileExplorer.getFiles); 9 | app.get('/api/file', FileExplorer.getFileContent); 10 | app.post('/api/file', express.json(), FileExplorer.saveFileContent); 11 | app.get('/app', appUI()); 12 | app.get('/', codeshow()); 13 | 14 | // Start the server 15 | app.listen(port, () => { 16 | console.log(`Server is listening on port ${port}`); 17 | }); -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Build the container image 3 | - name: "gcr.io/cloud-builders/gcloud" 4 | args: 5 | ["builds", "submit", "--tag", "gcr.io/wisepot/codeshow"] 6 | # Deploy container image to Cloud Run 7 | - name: "gcr.io/cloud-builders/gcloud" 8 | args: 9 | [ 10 | "run", 11 | "deploy", 12 | "codeshow", 13 | "--image", 14 | "gcr.io/wisepot/codeshow", 15 | "--region", 16 | "europe-west1", 17 | "--platform", 18 | "managed", 19 | "--min-instances", 20 | "0", 21 | "--allow-unauthenticated" 22 | ] -------------------------------------------------------------------------------- /sdk/dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chokidar = require("chokidar"); 3 | 4 | const { runServer, compileCSS, compileJS } = require('./helpers/utils'); 5 | const serverPath = path.normalize(`${__dirname}/../server/index.js`); 6 | 7 | runServer(`${__dirname}/../server/**/*.js`, `node ${serverPath}`); 8 | 9 | const apps = require('./config'); 10 | 11 | apps.forEach(({ css, js }) => { 12 | chokidar.watch(css.watch).on("all", () => compileCSS(css.inputCSS, css.outputCSS)); 13 | chokidar.watch(js.watch, { ignoreInitial: true }).on("all", () => compileJS(js.inputJS, js.outputJS)); 14 | compileJS(js.inputJS, js.outputJS); 15 | }); -------------------------------------------------------------------------------- /client/public/imgs/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src_app/js/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | window.addEventListener('load', () => { 7 | const root = createRoot(document.querySelector('#root')); 8 | root.render(); 9 | 10 | // reading the zoom level 11 | const iframes = window.parent.document.getElementsByTagName('iframe'); 12 | for (var i = 0; i < iframes.length; i++) { 13 | if (iframes[i].contentWindow === window) { 14 | const zoomLevel = Number(iframes[i].getAttribute('data-zoom-level') || 1); 15 | setZoomLevel(zoomLevel); 16 | break; 17 | } 18 | } 19 | }); 20 | 21 | function setZoomLevel(zoomLevel) { 22 | document.querySelector('body').style.fontSize = `${zoomLevel}em`; 23 | } -------------------------------------------------------------------------------- /client/src/js/Preview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import CodeMirrorEditor from './CodeMirrorEditor'; 3 | 4 | type PreviewProps = { 5 | zoomLevel: number 6 | } 7 | 8 | export default function Preview({ zoomLevel }: PreviewProps) { 9 | const style = { 10 | width: '100%', 11 | height: '100%', 12 | border: 'none', 13 | overflow: 'hidden', 14 | } 15 | useEffect(() => { 16 | const removeCallback = CodeMirrorEditor.addEventListener('save', () => { 17 | const el = document.querySelector('#preview-iframe'); 18 | if (el) { 19 | el.src = el.src + ''; 20 | } 21 | }); 22 | return () => { 23 | removeCallback(); 24 | } 25 | }, []); 26 | 27 | return ( 28 |
29 | 30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /sdk/config.js: -------------------------------------------------------------------------------- 1 | const version = require(`${__dirname}/../package.json`).version; 2 | 3 | module.exports = [ 4 | { 5 | css: { 6 | watch: `${__dirname}/../client/src/css/*.css`, 7 | inputCSS: `${__dirname}/../client/src/css/index.css`, 8 | outputCSS: `${__dirname}/../client/public/codeshow_${version}.css` 9 | }, 10 | js: { 11 | watch: `${__dirname}/../client/src/js/**/*.*`, 12 | inputJS: `${__dirname}/../client/src/js/index.tsx`, 13 | outputJS: `${__dirname}/../client/public/codeshow_${version}.js` 14 | } 15 | }, 16 | { 17 | css: { 18 | watch: `${__dirname}/../client/src_app/css/*.css`, 19 | inputCSS: `${__dirname}/../client/src_app/css/index.css`, 20 | outputCSS: `${__dirname}/../client/public/app_${version}.css` 21 | }, 22 | js: { 23 | watch: `${__dirname}/../client/src_app/js/**/*.*`, 24 | inputJS: `${__dirname}/../client/src_app/js/index.tsx`, 25 | outputJS: `${__dirname}/../client/public/app_${version}.js` 26 | } 27 | } 28 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@krasimir/codeshow", 3 | "description": "...", 4 | "version": "0.0.5", 5 | "scripts": { 6 | "clean": "rm -f ./client/public/*.js && rm -f ./client/public/*.js.map && rm -f ./client/public/*.css && rm -f ./client/public/*.css.map", 7 | "dev": "yarn clean && node ./sdk/dev.js", 8 | "build": "yarn clean && node ./sdk/build.js", 9 | "version-bump": "node ./sdk/bumpVersion.js" 10 | }, 11 | "author": "Krasimir Tsonev", 12 | "dependencies": { 13 | "@codemirror/commands": "^6.6.0", 14 | "@codemirror/lang-javascript": "^6.2.2", 15 | "@codemirror/state": "^6.4.1", 16 | "@codemirror/view": "^6.29.0", 17 | "chalk": "4.1.2", 18 | "chokidar": "3.5.2", 19 | "clean-css": "5.2.2", 20 | "codemirror": "^6.0.1", 21 | "cssbun": "1.2.0", 22 | "esbuild": "0.14.28", 23 | "express": "4.18.2", 24 | "lodash": "4.17.21", 25 | "node-fetch": "2", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "thememirror": "^2.0.1", 29 | "typescript": "^5.2.2", 30 | "uglify-js": "3.17.4" 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Krasimir Tsonev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/js/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import Editor from './Editor'; 5 | import Footer from './Footer'; 6 | import Preview from './Preview'; 7 | import { THEME } from './constants'; 8 | import useCodeshow from './useCodeshow'; 9 | import CodeMirrorEditor from './CodeMirrorEditor'; 10 | 11 | const DEFAULT_THEME = localStorage.getItem('codeshow_theme') || THEME.DARK; 12 | const DEFAULT_ZOOM_LEVEL = Number(localStorage.getItem('codeshow_zoom') || 1); 13 | 14 | function App() { 15 | const [ theme, setTheme ] = useState(DEFAULT_THEME); 16 | const [ zoomLevel, setZoomLevel ] = useState(DEFAULT_ZOOM_LEVEL); 17 | const { 18 | getCurrentSlide, 19 | currentSlideIndex, 20 | maxSlides, 21 | nextSlide, 22 | previousSlide, 23 | waitingFor 24 | } = useCodeshow(); 25 | 26 | function onZoomIn() { 27 | setZoomLevel(zoomLevel + 0.1); 28 | localStorage.setItem('codeshow_zoom', (zoomLevel + 0.1).toString()); 29 | } 30 | function onZoomOut() { 31 | setZoomLevel(zoomLevel - 0.1); 32 | localStorage.setItem('codeshow_zoom', (zoomLevel - 0.1).toString()); 33 | } 34 | 35 | CodeMirrorEditor.onZoomIn = onZoomIn; 36 | CodeMirrorEditor.onZoomOut = onZoomOut; 37 | 38 | return ( 39 | <> 40 |
41 | 45 | 46 |
47 |