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