├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── app.test.js └── index.test.js ├── app.js ├── config └── config.json ├── controllers ├── home.js ├── rooms.js └── socketio.js ├── demos ├── abstriangulation.js └── index.js ├── friendlyRandom.js ├── lib ├── socket.io.js └── srcdoc-polyfill.min.js ├── migrations └── 20180606232013-create-room.js ├── models ├── index.js └── room.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── bootstrap.min.css │ ├── main.css │ └── room.css ├── favicon.ico ├── img │ └── preview.png └── js │ ├── bootstrap.bundle.min.js │ ├── jquery.min.js │ └── room.js ├── server.js ├── shrinkwrap.yaml ├── src └── room.js ├── views ├── main.html └── room.html └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 healeycodes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## PairCode 3 | 4 | A CodePen clone (but _faster_). Mostly because I love __pair programming__. 5 | 6 | [![Build Status](https://travis-ci.org/healeycodes/PairCode.svg?branch=master)](https://travis-ci.org/healeycodes/PairCode) 7 | 8 |
9 | 10 | - Join a code room with your team and view changes as they happen. 11 | - Fork rooms, or delete them if you don't like your creation. 12 | 13 |
14 | 15 | ![preview image](https://raw.githubusercontent.com/healeycodes/paircode/master/public/img/preview.png "Image of a room on Deux Codes") 16 | 17 |
18 | 19 | ### Tech Stack 20 | 21 | Back end: Node.js, Express with EJS templates, socket.io, SQLite via Sequelize. 22 | 23 | Front end: HTML5/CSS3, JavaScript(ES6+), Webpack w/ Babel. 24 | 25 | Tested with: Jest. 26 | 27 |
28 | 29 | ### Setup 30 | 31 | Webpack builds the client code. 32 | 33 | `npm install` 34 | 35 | `npm run-script build` 36 | 37 |
38 | 39 | ### Tests 40 | 41 | API tested by Jest. 42 | 43 | `npm test` 44 | 45 |
46 | 47 | ### Run 48 | 49 | Pages and socket.io run by Express. 50 | 51 | Set the enviroment variable `PORT` to host on a different port. Default is `3000`. 52 | 53 | `npm start` 54 | 55 | ### License 56 | 57 | MIT. 58 | -------------------------------------------------------------------------------- /__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const { app } = require("../app.js"); 3 | const { models } = require("../models"); 4 | 5 | describe("app", () => { 6 | beforeAll(() => { 7 | return expect(require("../models").sequelize.sync()).not.toBeUndefined(); 8 | }); 9 | 10 | describe("Testing root path", () => { 11 | test("It should respond to the GET method with code 200", () => { 12 | return request(app) 13 | .get("/") 14 | .expect(200); 15 | }); 16 | }); 17 | 18 | describe("Testing new-room path", () => { 19 | test("It should forward with code 302", () => { 20 | return request(app) 21 | .get("/new-room") 22 | .expect(302); 23 | }); 24 | }); 25 | 26 | describe("Testing new-room forward", () => { 27 | test("The URL should contain 'room'", () => { 28 | request(app) 29 | .get("/new-room") 30 | .then(res => { 31 | return expect(res.header.location).toMatch(/room/); 32 | }) 33 | .catch(err => new Error(err.message)); 34 | }); 35 | }); 36 | 37 | describe("Testing room server-side render", () => { 38 | test("The page should contain the room's ID", () => { 39 | request(app) 40 | .get("/new-room") 41 | .then(res => request(app).get(res.header.location)) 42 | .then(roomRes => { 43 | return expect(roomRes.text).toMatch( 44 | new RegExp(res.header.location.split("/")[2]) 45 | ); 46 | }) 47 | .catch(err => new Error(err.message)); 48 | }); 49 | }); 50 | 51 | describe("Testing database integration", () => { 52 | beforeAll(() => { 53 | return require("../models").sequelize.sync(); 54 | }); 55 | beforeEach(() => { 56 | this.Room = require("../models").Room; 57 | }); 58 | describe("A room is created during this route", () => { 59 | it("Creates a Room", () => { 60 | let initialRes = null; 61 | request(app) 62 | .get("/new-room") 63 | .then(res => { 64 | initialRes = res; 65 | request(app).get(res.header.location); 66 | }) 67 | .then(() => 68 | this.Room.findOne({ 69 | where: { 70 | roomid: initialRes.header.location.split("/")[2] 71 | } 72 | }) 73 | ) 74 | .then(room => { 75 | return expect(room).not.toBeNull(); 76 | }) 77 | .catch(err => new Error(err.message)); 78 | }); 79 | }); 80 | }); 81 | 82 | afterAll(() => { 83 | // Wait for verbose SQL logging to complete 84 | function delay(t, v) { 85 | return new Promise(function(resolve) { 86 | setTimeout(resolve.bind(null, v), t); 87 | }); 88 | } 89 | return delay(5000).then(() => true); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | describe("models/index", () => { 2 | it("Returns the Room model", () => { 3 | const models = require("../models"); 4 | expect(models.Room).not.toBeUndefined(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Express 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const friendlyWords = require("friendly-words"); 5 | const ejs = require("ejs").renderFile; 6 | const app = express(); 7 | const http = require("http").createServer(app); 8 | app.use(express.static(__dirname + "/public/")); 9 | app.use("/public", express.static(__dirname + "public")); 10 | app.use("/favicon.ico", express.static(__dirname + "public/favicon.ico")); 11 | app.set("views", __dirname + "/views"); 12 | app.engine("html", ejs); 13 | app.set("view engine", "html"); 14 | app.use(bodyParser.json()); 15 | 16 | // Controllers 17 | const models = require("./models"); 18 | require("./controllers/rooms")(app, models); 19 | require("./controllers/home")(app, models); 20 | require("./controllers/socketio")(http); 21 | 22 | module.exports = { 23 | app: app, 24 | http: http, 25 | models: models 26 | }; 27 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "database": "development", 4 | "host": "localhost", 5 | "dialect": "sqlite", 6 | "operatorsAliases": false, 7 | "location": ".data/dev.sqlite" 8 | }, 9 | "test": { 10 | "database": "test", 11 | "host": "localhost", 12 | "dialect": "sqlite", 13 | "operatorsAliases": false, 14 | "location": ".data/test.sqlite" 15 | }, 16 | "production": { 17 | "database": "production", 18 | "host": "localhost", 19 | "dialect": "sqlite", 20 | "operatorsAliases": false, 21 | "location": ".data/prod.sqlite" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, models) { 2 | // Home 3 | app.get("/", (req, res) => 4 | res.render("main.html", { 5 | popup: "" 6 | }) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /controllers/rooms.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, models) { 2 | const demos = require("../demos"); 3 | const rndID = require("../friendlyRandom").get; 4 | const errorPage = (res, error = "Unspecified error.") => 5 | res.render("main.html", { 6 | popup: error 7 | }); 8 | 9 | // Create room 10 | app.get("/new-room", async (req, res) => { 11 | const newRoomId = rndID(); 12 | await models.Room.create({ 13 | roomid: newRoomId, 14 | html: "", 15 | css: "", 16 | js: "" 17 | }); 18 | res.redirect("/room/" + newRoomId); 19 | }); 20 | 21 | // Create demo room 22 | app.get("/demo/:demoId", async (req, res) => { 23 | const newRoomId = rndID(); 24 | const demo = demos[req.params.demoId]; 25 | await models.Room.create({ 26 | roomid: newRoomId, 27 | html: demo.html, 28 | css: demo.css, 29 | js: demo.js 30 | }); 31 | res.redirect("/room/" + newRoomId); 32 | }); 33 | 34 | // Join room 35 | app.get("/room/:roomId", async (req, res) => { 36 | const room = await models.Room.findOne({ 37 | where: { 38 | roomid: req.params.roomId 39 | } 40 | }); 41 | if (room) { 42 | res.render("room.html", { 43 | title: "PairCode", 44 | roomId: req.params.roomId, 45 | roomRoute: `/room/${req.params.roomId}`, 46 | html: room.html, 47 | css: room.css, 48 | js: room.js, 49 | srcdoc: `${room.html}` 50 | }); 51 | } else { 52 | errorPage(res, "No room by that ID."); 53 | } 54 | }); 55 | 56 | // Save room 57 | app.post("/room/:roomId/save", async (req, res) => { 58 | const json = req.body; 59 | await models.Room.update( 60 | { 61 | html: json.html, 62 | css: json.css, 63 | js: json.js 64 | }, 65 | { 66 | where: { 67 | roomid: req.params.roomId 68 | } 69 | } 70 | ); 71 | // Send timestamp 72 | res.send(`${new Date()}`.substr(0, 21)); 73 | }); 74 | 75 | // Delete room 76 | app.get("/room/:roomId/delete", async (req, res) => { 77 | await models.Room.destroy({ 78 | where: { 79 | roomid: req.params.roomId 80 | }, 81 | force: true 82 | }); 83 | res.redirect("/"); 84 | }); 85 | 86 | // Fork room 87 | app.get("/room/:roomId/fork", async (req, res) => { 88 | const newRoomId = rndID(); 89 | const room = await models.Room.findOne({ 90 | where: { 91 | roomid: req.params.roomId 92 | } 93 | }); 94 | if (room) { 95 | await models.Room.create({ 96 | roomid: newRoomId, 97 | html: room.html, 98 | css: room.css, 99 | js: room.js 100 | }); 101 | res.redirect("/room/" + newRoomId); 102 | } else { 103 | errorPage(res, "Trying to fork an unknown room."); 104 | } 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /controllers/socketio.js: -------------------------------------------------------------------------------- 1 | module.exports = function(http) { 2 | const io = require("socket.io")(http); 3 | io.on("connection", socket => { 4 | socket.on("join-room", msg => { 5 | if (msg.roomId) { 6 | socket.join(msg.roomId); 7 | } 8 | }); 9 | 10 | // Share user updates 11 | socket.on("update", msg => { 12 | if (msg.roomId && msg.data) { 13 | socket.broadcast.to(msg.roomId).emit("update", msg.data); 14 | } 15 | }); 16 | 17 | // Report room latency 18 | socket.on("_ping", msg => { 19 | const room = msg.roomId 20 | ? io.sockets.adapter.rooms[msg.roomId] 21 | : undefined; 22 | const roomCount = room ? room.length : 0; 23 | socket.emit("_pong", { 24 | time: msg.time, 25 | roomCount: roomCount 26 | }); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /demos/abstriangulation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | html: ` 6 | 7 | `, 8 | css: `body { 9 | font-family: sans-serif; 10 | padding: 0; 11 | margin: 0; 12 | background-color: #eff1ea; 13 | overflow: hidden; 14 | }`, 15 | js: `/** 16 | * requestAnimationFrame 17 | */ 18 | var requestAnimationFrame = (function(){ 19 | return window.requestAnimationFrame || 20 | window.webkitRequestAnimationFrame || 21 | window.mozRequestAnimationFrame || 22 | window.oRequestAnimationFrame || 23 | window.msRequestAnimationFrame || 24 | function (callback) { 25 | window.setTimeout(callback, 1000 / 60); 26 | }; 27 | })(); 28 | 29 | /** 30 | * Delaunay 31 | */ 32 | var Delaunay = (function() { 33 | 34 | /** 35 | * Node 36 | * @public 37 | */ 38 | function Node(x, y, id) { 39 | this.x = x; 40 | this.y = y; 41 | this.id = !isNaN(id) && isFinite(id) ? id : null; 42 | } 43 | 44 | Node.prototype = { 45 | eq: function(p) { 46 | var dx = this.x - p.x, 47 | dy = this.y - p.y; 48 | return (dx < 0 ? -dx : dx) < 0.0001 && (dy < 0 ? -dy : dy) < 0.0001; 49 | }, 50 | 51 | toString: function() { 52 | return '(x: ' + this.x + ', y: ' + this.y + ')'; 53 | } 54 | }; 55 | 56 | /** 57 | * Edge 58 | */ 59 | function Edge(p0, p1) { 60 | this.nodes = [p0, p1]; 61 | } 62 | 63 | Edge.prototype = { 64 | eq: function(edge) { 65 | var na = this.nodes, nb = edge.nodes; 66 | var na0 = na[0], na1 = na[1], nb0 = nb[0], nb1 = nb[1]; 67 | return (na0.eq(nb0) && na1.eq(nb1)) || (na0.eq(nb1) && na1.eq(nb0)); 68 | } 69 | }; 70 | 71 | /** 72 | * Triangle 73 | */ 74 | function Triangle(p0, p1, p2) { 75 | this.nodes = [p0, p1, p2]; 76 | this.edges = [new Edge(p0, p1), new Edge(p1, p2), new Edge(p2, p0)]; 77 | this._createId(); 78 | this._createCircumscribedCircle(); 79 | } 80 | 81 | Triangle.prototype = { 82 | id: null, 83 | _circle: null, 84 | 85 | _createId: function() { 86 | var nodes, id0, id1, id2; 87 | 88 | nodes = this.nodes; 89 | id0 = nodes[0].id; 90 | id1 = nodes[1].id; 91 | id2 = nodes[2].id; 92 | 93 | if (id0 !== null && id1 !== null && id2 !== null) { 94 | this.id = [id0, id1, id2].sort().join('_'); 95 | } 96 | }, 97 | 98 | _createCircumscribedCircle: function() { 99 | var nodes, p0, p1, p2, 100 | ax, bx, c, t, u, 101 | circle, dx, dy; 102 | 103 | nodes = this.nodes; 104 | p0 = nodes[0]; 105 | p1 = nodes[1]; 106 | p2 = nodes[2]; 107 | 108 | ax = p1.x - p0.x, ay = p1.y - p0.y; 109 | bx = p2.x - p0.x, by = p2.y - p0.y; 110 | c = 2 * (ax * by - ay * bx); 111 | 112 | t = (p1.x * p1.x - p0.x * p0.x + p1.y * p1.y - p0.y * p0.y); 113 | u = (p2.x * p2.x - p0.x * p0.x + p2.y * p2.y - p0.y * p0.y); 114 | 115 | if (!this._circle) this._circle = {}; 116 | 117 | circle = this._circle; 118 | circle.x = ((p2.y - p0.y) * t + (p0.y - p1.y) * u) / c; 119 | circle.y = ((p0.x - p2.x) * t + (p1.x - p0.x) * u) / c; 120 | 121 | dx = p0.x - circle.x; 122 | dy = p0.y - circle.y; 123 | circle.radiusSq = dx * dx + dy * dy; 124 | }, 125 | 126 | circleContains: function(p) { 127 | var circle, dx, dy, distSq; 128 | 129 | circle = this._circle; 130 | dx = circle.x - p.x, 131 | dy = circle.y - p.y; 132 | distSq = dx * dx + dy * dy; 133 | 134 | return distSq < circle.radiusSq; 135 | } 136 | }; 137 | 138 | 139 | /** 140 | * @constructor 141 | * @public 142 | */ 143 | function Delaunay(width, height) { 144 | this.width = width; 145 | this.height = height; 146 | 147 | this._triangles = null; 148 | 149 | this.clear(); 150 | } 151 | 152 | Delaunay.prototype = { 153 | 154 | clear: function() { 155 | var p0 = new Node(0, 0), 156 | p1 = new Node(this.width, 0), 157 | p2 = new Node(this.width, this.height), 158 | p3 = new Node(0, this.height); 159 | 160 | this._triangles = [ 161 | new Triangle(p0, p1, p2), 162 | new Triangle(p0, p2, p3) 163 | ]; 164 | 165 | return this; 166 | }, 167 | 168 | multipleInsert: function(m) { 169 | for (var i = 0, len = m.length; i < len; i++) { 170 | this.insert(m[i]); 171 | } 172 | 173 | return this; 174 | }, 175 | 176 | insert: function(p) { 177 | var triangles = this._triangles, 178 | t, 179 | temps = [], 180 | edges = [], 181 | edge, 182 | polygon = [], 183 | isDuplicate, 184 | i, ilen, j, jlen; 185 | 186 | for (ilen = triangles.length, i = 0; i < ilen; i++) { 187 | t = triangles[i]; 188 | 189 | if (t.circleContains(p)) { 190 | edges.push(t.edges[0], t.edges[1], t.edges[2]); 191 | } else { 192 | temps.push(t); 193 | } 194 | } 195 | 196 | edgesLoop: for (ilen = edges.length, i = 0; i < ilen; i++) { 197 | edge = edges[i]; 198 | 199 | // 辺を比較して重複していれば削除 200 | for (jlen = polygon.length, j = 0; j < jlen; j++) { 201 | if (edge.eq(polygon[j])) { 202 | polygon.splice(j, 1); 203 | continue edgesLoop; 204 | } 205 | } 206 | 207 | polygon.push(edge); 208 | } 209 | 210 | for (ilen = polygon.length, i = 0; i < ilen; i++) { 211 | edge = polygon[i]; 212 | temps.push(new Triangle(edge.nodes[0], edge.nodes[1], p)); 213 | } 214 | 215 | this._triangles = temps; 216 | 217 | return this; 218 | }, 219 | 220 | getTriangles: function() { 221 | return this._triangles.slice(); 222 | } 223 | }; 224 | 225 | Delaunay.Node = Node; 226 | 227 | return Delaunay; 228 | 229 | })(); 230 | 231 | 232 | /** 233 | * Particle 234 | * @super Delaunay.Node 235 | */ 236 | var Particle = (function(Node) { 237 | 238 | var currentId = 0, 239 | getId = function() { return currentId++; }; 240 | 241 | function Particle(x, y) { 242 | Node.call(this, x, y, getId()); 243 | this.vx = 0; 244 | this.vy = 0; 245 | } 246 | 247 | Particle.prototype = new Node(); 248 | 249 | return Particle; 250 | 251 | })(Delaunay.Node); 252 | 253 | 254 | // Initialize 255 | 256 | (function() { 257 | 258 | // Configs 259 | 260 | var BACKGROUND_COLOR = '#eff1ea', // 背景色 261 | LINE_COLOR = '#303030', // 線の色 262 | FILL_COLORS = [ // 塗りに使用する色, 三角形の生成順に選択される 263 | '#00cbd6', '#83d302', '#e80051', '#2087db', '#f4d002', 264 | '#eda3d4', '#2e8720', '#ea2ebb', '#213877', '#fc771e', 265 | '#a6dbd9', '#c8e067', '#ed5131', '#e2d9d9', '#f4eea8' 266 | ], 267 | PATTERNS_URL = [ // パターンの画像 URL, 三角形の生成順に選択される 268 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAChJREFUeNpiVFBQ2M8ABPfv33dkQAJMDDgA4////7FK4NRBugRAgAEAXhEHBXvZgh4AAAAASUVORK5CYII%3D', 269 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAB1JREFUeNpiUFBQ2P///38GEGYEETDAxIAEAAIMACllChoZz6oRAAAAAElFTkSuQmCC', 270 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1JREFUeNpiVFBQ2M/AwODIgAAgPgMTNkGYBIYgSDETNkGYDgxBdKNQ7AIIMABhpgcrohF6AgAAAABJRU5ErkJggg%3D%3D', 271 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADJJREFUeNpi+P//PwMyVlBQ2M/EgAQUFRX3AylHJnQBEJsJXQAEGEFmIAvcv3+fASDAANwmFUHSvnUvAAAAAElFTkSuQmCC', 272 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAGCAYAAADkOT91AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpiUFBQ2P///38GGGZiQAOEBRhB+vCqAAgwAAmADR3HFFILAAAAAElFTkSuQmCC', 273 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAKCAYAAACJxx+AAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEdJREFUeNpiUlBQ2A/EDP///wdjdD4jiAME+4HYERvNxAAByIIofEaQMYqKigy4ANiE+/fvwwVAbGQ+A50ciQ8wMRAAAAEGAKNCOWlhLo6PAAAAAElFTkSuQmCC' 274 | ]; 275 | 276 | 277 | // Vars 278 | 279 | var canvas, context, 280 | screenWidth, screenHeight, screenMinSize, 281 | centerX, centerY, 282 | delaunay, 283 | particles = [], 284 | colorIndex = 0, 285 | colorTable = {}, 286 | patterns = [], 287 | patternIndex = 0, 288 | patternTable = {}, 289 | backgroundPattern, 290 | mouse = { x: 0, y: 0 }, 291 | time, 292 | gui, control, maxSpeedCtl, minSpeedCtl, 293 | img, count, onLoad, 294 | i, len; 295 | 296 | 297 | // Event Listeners 298 | 299 | function resize(e) { 300 | screenWidth = canvas.width = window.innerWidth; 301 | screenHeight = canvas.height = window.innerHeight; 302 | screenMinSize = Math.min(screenWidth, screenHeight); 303 | centerX = screenWidth * 0.5; 304 | centerY = screenHeight * 0.5; 305 | 306 | context = canvas.getContext('2d'); 307 | context.lineWidth = 3.5; 308 | context.strokeStyle = LINE_COLOR; 309 | context.lineCap = context.lineJoin = 'round'; 310 | 311 | if (delaunay) { 312 | delaunay.width = screenWidth; 313 | delaunay.height = screenHeight; 314 | } 315 | } 316 | 317 | function mouseMove(e) { 318 | mouse.x = e.clientX; 319 | mouse.y = e.clientY; 320 | } 321 | 322 | 323 | // Functions 324 | 325 | function addParticle(x, y) { 326 | if (particles.length >= control.maxNum) { 327 | particles.shift(); 328 | addParticle(x, y); 329 | return; 330 | } 331 | var p = new Particle(x, y), 332 | l = Math.random() * (control.maxSpeed - control.minSpeed) + control.minSpeed, 333 | a = Math.random() * Math.PI * 2; 334 | p.vx = l * Math.cos(a); 335 | p.vy = l * Math.sin(a); 336 | particles.push(p); 337 | } 338 | 339 | 340 | // GUI Control 341 | 342 | control = { 343 | spawnTime: 500, 344 | maxNum: 25, 345 | maxSpeed: 1, 346 | minSpeed: 0.5 347 | }; 348 | 349 | 350 | // Init 351 | 352 | canvas = document.getElementById('c'); 353 | 354 | window.addEventListener('resize', resize, false); 355 | resize(null); 356 | 357 | mouse.x = screenWidth * 0.5; 358 | mouse.y = screenHeight * 0.5; 359 | 360 | delaunay = new Delaunay(screenWidth, screenHeight); 361 | 362 | for (i = 0, len = control.maxNum; i < len; i++) { 363 | addParticle(Math.random() * screenMinSize + centerX - screenMinSize * 0.5, Math.random() * screenMinSize + centerY - screenMinSize * 0.5); 364 | } 365 | 366 | 367 | // Loop 368 | 369 | var loop = function() { 370 | var TWO_PI = Math.PI * 2, 371 | w = screenWidth, 372 | h = screenHeight, 373 | ctx = context, 374 | now = new Date().getTime(), 375 | dx, dy, distSq, ax, ay, 376 | triangles, t, id, p0, p1, p2, 377 | ct, pt, cl, pl, 378 | i, len, p; 379 | 380 | if (now - time > control.spawnTime) { 381 | addParticle(mouse.x, mouse.y); 382 | time = now; 383 | } 384 | 385 | ctx.save(); 386 | ctx.fillStyle = BACKGROUND_COLOR; 387 | ctx.fillRect(0, 0, screenWidth, screenHeight); 388 | ctx.globalAlpha = 0.15; 389 | ctx.fillStyle = backgroundPattern; 390 | ctx.fillRect(0, 0, screenWidth, screenHeight); 391 | ctx.restore(); 392 | 393 | delaunay.clear(); 394 | 395 | for (len = particles.length, i = 0; i < len; i++) { 396 | p = particles[i]; 397 | 398 | p.x += p.vx; 399 | p.y += p.vy; 400 | 401 | // 反射 402 | if (p.x < 0) { 403 | p.x = 0; 404 | if (p.vx < 0) p.vx *= -1; 405 | } 406 | if (p.x > w) { 407 | p.x = w; 408 | if (p.vx > 0) p.vx *= -1; 409 | } 410 | if (p.y < 0) { 411 | p.y = 0; 412 | if (p.vy < 0) p.vy *= -1; 413 | } 414 | if (p.y > h) { 415 | p.y = h; 416 | if (p.vy > 0) p.vy *= -1; 417 | } 418 | } 419 | 420 | triangles = delaunay.multipleInsert(particles).getTriangles(); 421 | 422 | ct = colorTable; 423 | pt = patternTable; 424 | cl = FILL_COLORS.length; 425 | pl = patterns.length; 426 | 427 | for (len = triangles.length, i = 0; i < len; i++) { 428 | t = triangles[i]; 429 | id = t.id; 430 | p0 = t.nodes[0]; 431 | p1 = t.nodes[1]; 432 | p2 = t.nodes[2]; 433 | 434 | if (id === null) continue; 435 | 436 | if (!ct[id]) { 437 | ct[id] = FILL_COLORS[colorIndex]; 438 | colorIndex = (colorIndex + 1) % cl; 439 | } 440 | if (!pt[id]) { 441 | pt[id] = patterns[patternIndex]; 442 | patternIndex = (patternIndex + 1) % pl; 443 | } 444 | 445 | ctx.save(); 446 | ctx.beginPath(); 447 | ctx.moveTo(p0.x, p0.y); 448 | ctx.lineTo(p1.x, p1.y); 449 | ctx.lineTo(p2.x, p2.y); 450 | ctx.closePath(); 451 | ctx.fillStyle = ct[id]; 452 | ctx.fill(); 453 | ctx.translate(p0.x, p0.y); 454 | ctx.rotate(Math.atan2(p0.y - p1.y, p0.x - p1.x)); 455 | ctx.fillStyle = pt[id]; 456 | ctx.fill(); 457 | ctx.stroke(); 458 | ctx.restore(); 459 | } 460 | 461 | requestAnimationFrame(loop); 462 | }; 463 | 464 | // Load Images 465 | 466 | count = PATTERNS_URL.length; 467 | onLoad = function(e) { 468 | patterns.push(context.createPattern(e.target, 'repeat')); 469 | 470 | if (--count === 0) { 471 | backgroundPattern = patterns[Math.floor(patterns.length * Math.random())]; 472 | patterns.push('rgba(0, 0, 0, 0)'); 473 | 474 | canvas.addEventListener('mousemove', mouseMove, false); 475 | 476 | time = new Date().getTime(); 477 | 478 | // Start update 479 | loop(); 480 | } 481 | }; 482 | 483 | for (i = 0, len = PATTERNS_URL.length; i < len; i++) { 484 | img = new Image(); 485 | img.addEventListener('load', onLoad, false); 486 | img.src = PATTERNS_URL[i]; 487 | } 488 | 489 | })(); 490 | ` 491 | }; 492 | -------------------------------------------------------------------------------- /demos/index.js: -------------------------------------------------------------------------------- 1 | const abstriangulation = require('./abstriangulation') 2 | module.exports = { abstriangulation } -------------------------------------------------------------------------------- /friendlyRandom.js: -------------------------------------------------------------------------------- 1 | const friendlyWords = require("friendly-words"); 2 | 3 | // e.g. 'thread-pasta-resolution' 4 | const get = () => { 5 | const pick = arr => arr[Math.floor(Math.random() * arr.length)]; 6 | return `${pick(friendlyWords.predicates)}-${pick( 7 | friendlyWords.objects 8 | )}-${pick(friendlyWords.objects)}`; 9 | }; 10 | 11 | module.exports = { 12 | get 13 | }; -------------------------------------------------------------------------------- /lib/socket.io.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Socket.IO v2.1.1 3 | * (c) 2014-2018 Guillermo Rauch 4 | * Released under the MIT License. 5 | */ 6 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,p=r.id,h=r.path,f=u[p]&&h in u[p].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(u[p]||(c("new io instance for %s",s),u[p]=a(s,e)),n=u[p]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(12),c=n(3)("socket.io-client");t.exports=e=r;var u=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(12),e.Socket=n(37)},function(t,e,n){(function(e){"use strict";function r(t,n){var r=t;n=n||e.location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof n?n.protocol+"//"+t:"https://"+t),i("parse %s",t),r=o(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var s=r.host.indexOf(":")!==-1,a=s?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+a+":"+r.port,r.href=r.protocol+"://"+a+(n&&n.port===r.port?"":":"+r.port),r}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r}).call(e,function(){return this}())},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(3)("socket.io-parser"),l=n(8),d=n(9),y=n(10),m=n(11);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){(function(e){function r(t,n){if(!(this instanceof r))return new r(t,n);n=n||{},t&&"object"==typeof t&&(n=t,t=null),t?(t=p(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=p(n.host).host),this.secure=null!=n.secure?n.secure:e.location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.agent=n.agent||!1,this.hostname=n.hostname||(e.location?location.hostname:"localhost"),this.port=n.port||(e.location&&location.port?location.port:this.secure?443:80),this.query=n.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==n.upgrade,this.path=(n.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!n.forceJSONP,this.jsonp=!1!==n.jsonp,this.forceBase64=!!n.forceBase64,this.enablesXDR=!!n.enablesXDR,this.timestampParam=n.timestampParam||"t",this.timestampRequests=n.timestampRequests,this.transports=n.transports||["polling","websocket"],this.transportOptions=n.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=n.policyPort||843,this.rememberUpgrade=n.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=n.onlyBinaryUpgrades,this.perMessageDeflate=!1!==n.perMessageDeflate&&(n.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=n.pfx||null,this.key=n.key||null,this.passphrase=n.passphrase||null,this.cert=n.cert||null,this.ca=n.ca||null,this.ciphers=n.ciphers||null,this.rejectUnauthorized=void 0===n.rejectUnauthorized||n.rejectUnauthorized,this.forceNode=!!n.forceNode;var o="object"==typeof e&&e;o.global===o&&(n.extraHeaders&&Object.keys(n.extraHeaders).length>0&&(this.extraHeaders=n.extraHeaders),n.localAddress&&(this.localAddress=n.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,this.open()}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(36),u=n(21),p=n(2),h=n(30);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(w,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=w(e>>>10&1023|55296),e=56320|1023&e),o+=w(e);return o}function c(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function u(t,e){return w(t>>e&63|128)}function p(t,e){if(0==(4294967168&t))return w(t);var n="";return 0==(4294965248&t)?n=w(t>>6&31|192):0==(4294901760&t)?(c(t,e)||(t=65533),n=w(t>>12&15|224),n+=u(t,6)):0==(4292870144&t)&&(n=w(t>>18&7|240),n+=u(t,12),n+=u(t,6)),n+=w(63&t|128)}function h(t,e){e=e||{};for(var n,r=!1!==e.strict,o=s(t),i=o.length,a=-1,c="";++a=v)throw Error("Invalid byte index");var t=255&g[b];if(b++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function l(t){var e,n,r,o,i;if(b>v)throw Error("Invalid byte index");if(b==v)return!1;if(e=255&g[b],b++,0==(128&e))return e;if(192==(224&e)){if(n=f(),i=(31&e)<<6|n,i>=128)return i;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=f(),r=f(),i=(15&e)<<12|n<<6|r,i>=2048)return c(i,t)?i:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=f(),r=f(),o=f(),i=(7&e)<<18|n<<12|r<<6|o,i>=65536&&i<=1114111))return i;throw Error("Invalid UTF-8 detected")}function d(t,e){e=e||{};var n=!1!==e.strict;g=s(t),v=g.length,b=0;for(var r,o=[];(r=l(n))!==!1;)o.push(r);return a(o)}var y="object"==typeof e&&e,m=("object"==typeof t&&t&&t.exports==y&&t,"object"==typeof o&&o);m.global!==m&&m.window!==m||(i=m);var g,v,b,w=String.fromCharCode,k={version:"2.1.2",encode:h,decode:d};r=function(){return k}.call(e,n,e,t),!(void 0!==r&&(t.exports=r))}(this)}).call(e,n(27)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){(function(e){function n(t){for(var e=0;e0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),p=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=p,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(c,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){(function(e){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=h&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=o),i.call(this,t)}var o,i=n(20),s=n(21),a=n(30),c=n(31),u=n(32),p=n(3)("engine.io-client:websocket"),h=e.WebSocket||e.MozWebSocket;if("undefined"==typeof window)try{o=n(35)}catch(f){}var l=h;l||"undefined"!=typeof window||(l=o),t.exports=r,c(r,i),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function n(){r.emit("flush"),setTimeout(function(){r.writable=!0,r.emit("drain")},0)}var r=this;this.writable=!1;for(var o=t.length,i=0,a=o;i0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); 8 | //# sourceMappingURL=socket.io.js.map -------------------------------------------------------------------------------- /lib/srcdoc-polyfill.min.js: -------------------------------------------------------------------------------- 1 | /*! srcdoc-polyfill - v1.0.0 - 2017-01-29 2 | * http://github.com/jugglinmike/srcdoc-polyfill/ 3 | * Copyright (c) 2017 Mike Pennisi; Licensed MIT */ 4 | !function(a,b){var c=window.srcDoc;"function"==typeof define&&define.amd?define(["exports"],function(d){b(d,c),a.srcDoc=d}):"object"==typeof exports?b(exports,c):(a.srcDoc={},b(a.srcDoc,c))}(this,function(a,b){var c,d,e,f=!!("srcdoc"in document.createElement("iframe")),g="Polyfill may not function in the presence of the `sandbox` attribute. Consider using the `force` option.",h=/\ballow-same-origin\b/,i=function(a,b){var c=a.getAttribute("sandbox");"string"!=typeof c||h.test(c)||(b&&b.force?a.removeAttribute("sandbox"):b&&b.force===!1||(e(g),a.setAttribute("data-srcdoc-polyfill",g)))},j={compliant:function(a,b,c){b&&(i(a,c),a.setAttribute("srcdoc",b))},legacy:function(a,b,c){var d;a&&a.getAttribute&&(b?a.setAttribute("srcdoc",b):b=a.getAttribute("srcdoc"),b&&(i(a,c),d="javascript: window.frameElement.getAttribute('srcdoc');",a.contentWindow&&(a.contentWindow.location=d),a.setAttribute("src",d)))}},k=a;if(e=window.console&&window.console.error?function(a){window.console.error("[srcdoc-polyfill] "+a)}:function(){},k.set=j.compliant,k.noConflict=function(){return window.srcDoc=b,k},!f)for(k.set=j.legacy,d=document.getElementsByTagName("iframe"),c=d.length;c--;)k.set(d[c])}); -------------------------------------------------------------------------------- /migrations/20180606232013-create-room.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Rooms', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER 9 | }, 10 | roomid: { 11 | type: Sequelize.STRING 12 | }, 13 | html: { 14 | type: Sequelize.TEXT 15 | }, 16 | css: { 17 | type: Sequelize.TEXT 18 | }, 19 | js: { 20 | type: Sequelize.TEXT 21 | }, 22 | createdAt: { 23 | allowNull: false, 24 | type: Sequelize.DATE 25 | }, 26 | updatedAt: { 27 | allowNull: false, 28 | type: Sequelize.DATE 29 | } 30 | }); 31 | }, 32 | down: (queryInterface, Sequelize) => { 33 | return queryInterface.dropTable('Rooms'); 34 | } 35 | }; -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | const setup = () => { 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const Sequelize = require("sequelize"); 5 | const basename = path.basename(__filename); 6 | const env = process.env.NODE_ENV || "development"; 7 | const config = require("../config/config.json")[env]; 8 | const db = {}; 9 | 10 | const sequelize = config.use_env_variable 11 | ? new Sequelize(process.env[config.use_env_variable], config) 12 | : new Sequelize(config.database, config.username, config.password, config); 13 | 14 | fs.readdirSync(__dirname) 15 | .filter(file => { 16 | return ( 17 | file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js" 18 | ); 19 | }) 20 | .forEach(file => { 21 | const model = sequelize["import"](path.join(__dirname, file)); 22 | db[model.name] = model; 23 | }); 24 | 25 | Object.keys(db).forEach(modelName => { 26 | if (db[modelName].associate) { 27 | db[modelName].associate(db); 28 | } 29 | }); 30 | 31 | db.sequelize = sequelize; 32 | db.Sequelize = Sequelize; 33 | 34 | return db; 35 | }; 36 | 37 | module.exports = setup(); 38 | -------------------------------------------------------------------------------- /models/room.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | return sequelize.define( 3 | "Room", 4 | { 5 | roomid: DataTypes.STRING, 6 | html: DataTypes.TEXT, 7 | css: DataTypes.TEXT, 8 | js: DataTypes.TEXT 9 | }, 10 | {} 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PairCode", 3 | "version": "0.1.0", 4 | "description": "A HTML/CSS/JS sandbox with real time pair coding. Powered by socket.io and Node.js.", 5 | "dependencies": { 6 | "body-parser": "^1.19.0", 7 | "ejs": "^3.0.1", 8 | "express": "^4.17.1", 9 | "friendly-words": "^1.1.10", 10 | "sequelize": "^5.21.0", 11 | "sequelize-cli": "^5.5.1", 12 | "sqlite3": "^4.1.1", 13 | "socket.io": "^2.3.0" 14 | }, 15 | "scripts": { 16 | "build": "npx webpack --config webpack.config.js", 17 | "start": "node server.js", 18 | "test": "jest --forceExit" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.4.4", 22 | "@babel/preset-env": "^7.4.4", 23 | "babel-loader": "^8.0.5", 24 | "jest": "^24.7.1", 25 | "supertest": "^3.1.0", 26 | "webpack": "^4.30.0", 27 | "webpack-cli": "^3.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | background-color: #2f4f4f; 6 | color: #f0f8ff; 7 | } 8 | 9 | p { 10 | font-size: 1.3em; 11 | } 12 | 13 | .main { 14 | width: 100vw; 15 | margin: auto; 16 | margin-top: 20px; 17 | } 18 | 19 | .link, 20 | .link:hover, 21 | .link:link { 22 | color: #f0f8ff; 23 | text-decoration: underline; 24 | } 25 | 26 | .link:visited, 27 | .link:active { 28 | color: #f0f8ff; 29 | } 30 | 31 | .centered { 32 | text-align: center; 33 | } 34 | 35 | .heading { 36 | font-size: 6em; 37 | font-family: "Permanent Marker", cursive; 38 | text-shadow: 2px 2px 0 #bcbcbc, 4px 4px 0 #9c9c9c; 39 | } 40 | 41 | .heading { 42 | margin-bottom: 20px; 43 | } 44 | 45 | .create-room-con { 46 | margin-left: 195px; 47 | } 48 | 49 | .create-room-btn { 50 | margin: 10px 0 25px 0; 51 | } 52 | 53 | .alert { 54 | margin-top: 5%; 55 | width: 50%; 56 | left: 25%; 57 | } 58 | 59 | .splash-img { 60 | border-radius: 5px; 61 | max-width: 90%; 62 | opacity: 0.75; 63 | } 64 | 65 | .splash-img:hover { 66 | opacity: 0.9; 67 | } 68 | 69 | .create-room-btn { 70 | font-size: 1.5em; 71 | margin-top: 10px; 72 | animation: float 6s ease-in-out infinite; 73 | } 74 | 75 | .github-button-con { 76 | position: absolute; 77 | left: 10px; 78 | top: 10px; 79 | } 80 | 81 | .section-top { 82 | animation: fadein 1s; 83 | } 84 | 85 | .section-bottom { 86 | animation: fadein 2s; 87 | } 88 | 89 | @keyframes fadein { 90 | from { 91 | opacity: 0; 92 | } 93 | to { 94 | opacity: 1; 95 | } 96 | } 97 | 98 | @keyframes float { 99 | 0% { 100 | box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); 101 | transform: translatey(0px); 102 | } 103 | 50% { 104 | box-shadow: 0 25px 15px 0px rgba(0, 0, 0, 0.2); 105 | transform: translatey(-7px); 106 | } 107 | 100% { 108 | box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); 109 | transform: translatey(0px); 110 | } 111 | } 112 | 113 | @media only screen and (max-width: 600px) { 114 | body { 115 | padding-top: 35px; 116 | font-size: 0.5em; 117 | } 118 | .create-room-btn { 119 | font-size: 1.75em; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /public/css/room.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body, 7 | html { 8 | height: 100%; 9 | width: 100%; 10 | position: relative; 11 | font-family: "Source Code Pro", monospace; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .share-link { 17 | float: left; 18 | width: 25%; 19 | } 20 | 21 | .top-bar { 22 | display: flex; 23 | } 24 | 25 | .top-item { 26 | flex-grow: 1; 27 | margin: auto; 28 | text-align: center; 29 | } 30 | 31 | #user-code { 32 | height: calc(100% - 38px); 33 | width: 25%; 34 | float: left; 35 | display: flex; 36 | flex-direction: column; 37 | } 38 | 39 | #live-frame { 40 | height: calc(100% - 38px); 41 | width: 75%; 42 | float: right; 43 | border: none; 44 | } 45 | 46 | section { 47 | height: 100%; 48 | width: 100%; 49 | } 50 | 51 | textarea { 52 | height: 100%; 53 | width: 100%; 54 | resize: none; 55 | background-color: #2f4f4f; 56 | color: #f0f8ff; 57 | padding: 2px; 58 | } 59 | 60 | #data { 61 | width: 74.5%; 62 | float: right; 63 | } 64 | 65 | #last-saved-con { 66 | text-align: center; 67 | } 68 | 69 | ::-webkit-input-placeholder { 70 | /* Chrome/Opera/Safari */ 71 | color: #f0f8ff; 72 | font-size: 2em; 73 | padding-left: 10px; 74 | } 75 | 76 | ::-moz-placeholder { 77 | /* Firefox 19+ */ 78 | color: #f0f8ff; 79 | font-size: 2em; 80 | padding-left: 10px; 81 | } 82 | 83 | :-ms-input-placeholder { 84 | /* IE 10+ */ 85 | color: #f0f8ff; 86 | font-size: 2em; 87 | padding-left: 10px; 88 | } 89 | 90 | :-moz-placeholder { 91 | /* Firefox 18- */ 92 | color: #f0f8ff; 93 | font-size: 2em; 94 | padding-left: 10px; 95 | } 96 | 97 | ::placeholder { 98 | /* Generic */ 99 | color: #f0f8ff; 100 | font-size: 2em; 101 | padding-left: 10px; 102 | } 103 | 104 | div.col-sm { 105 | padding: 0%; 106 | } 107 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/PairCode/26c21295a34b1d191c7e04deb54d8246fbd738b7/public/favicon.ico -------------------------------------------------------------------------------- /public/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/healeycodes/PairCode/26c21295a34b1d191c7e04deb54d8246fbd738b7/public/img/preview.png -------------------------------------------------------------------------------- /public/js/room.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){var r=n(1),o=n(3),i=document.querySelector("#data").getAttribute("data-roomid"),s="",a={html:null,css:null,js:null},c=function(){var t=document.getElementById("live-frame"),e="".concat(a.html,"`; 16 | srcDoc.set(liveFrame, newContent); 17 | }; 18 | 19 | const updateUserCode = () => { 20 | document.querySelector("#html textarea").value = page.html; 21 | document.querySelector("#css textarea").value = page.css; 22 | document.querySelector("#js textarea").value = page.js; 23 | }; 24 | 25 | const socket = io(); 26 | socket.emit("join-room", { 27 | roomId: roomId 28 | }); 29 | socket.on("connect", () => { 30 | userId = socket.id; 31 | }); 32 | socket.on("update", msg => { 33 | page.html = msg.html; 34 | page.css = msg.css; 35 | page.js = msg.js; 36 | updateFrame(); 37 | updateUserCode(); 38 | }); 39 | socket.on("_pong", msg => { 40 | document.querySelector("#ping").innerText = `${ 41 | msg.roomCount 42 | } in room / ping: ${Date.now() - msg.time}ms`; 43 | }); 44 | 45 | const saveCode = roomId => { 46 | fetch(`/room/${roomId}/save`, { 47 | method: "POST", 48 | headers: { 49 | "Content-Type": "application/json" 50 | }, 51 | body: JSON.stringify({ 52 | html: page.html, 53 | css: page.css, 54 | js: page.js 55 | }) 56 | }) 57 | .then(res => res.text()) 58 | .then( 59 | text => 60 | (document.querySelector("#last-saved").innerText = `Autosaved @ ${text}`) 61 | ); 62 | }; 63 | 64 | const handleInput = () => { 65 | const html = document.querySelector("#html textarea").value; 66 | const css = document.querySelector("#css textarea").value; 67 | const js = document.querySelector("#js textarea").value; 68 | 69 | if (html == page.html && css == page.css && js == page.js) { 70 | return; 71 | } 72 | 73 | page.html = html; 74 | page.css = css; 75 | page.js = js; 76 | 77 | updateFrame(); 78 | 79 | // Share our data to the room 80 | socket.emit("update", { 81 | data: { 82 | id: userId, 83 | html: html, 84 | css: css, 85 | js: js 86 | }, 87 | roomId: roomId 88 | }); 89 | 90 | saveCode(roomId); 91 | }; 92 | 93 | window.addEventListener("change", handleInput, false); 94 | window.addEventListener("keypress", handleInput, false); 95 | window.addEventListener("input", handleInput, false); 96 | window.addEventListener("textInput", handleInput, false); 97 | window.addEventListener("paste", handleInput, false); 98 | 99 | const ping = () => { 100 | socket.emit("_ping", { 101 | time: Date.now(), 102 | roomId: roomId, 103 | roomCount: null 104 | }); 105 | setTimeout(ping, 2000); 106 | }; 107 | ping(); 108 | 109 | // Home button 110 | document.querySelector("#home-btn").onclick = () => window.location.assign("/"); 111 | 112 | // Fork button 113 | document.querySelector("#fork-btn").onclick = () => 114 | window.location.assign(`/room/${roomId}/fork`); 115 | 116 | // Delete button 117 | document.querySelector("#delete-btn").onclick = () => 118 | window.location.assign(`/room/${roomId}/delete`); 119 | 120 | // Share link 121 | const shareLink = document.querySelector(".share-link input"); 122 | shareLink.value = `${window.location.protocol}//${window.location.hostname}${shareLink.value}`; 123 | 124 | handleInput(); 125 | -------------------------------------------------------------------------------- /views/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PairCode 5 | 6 | 10 | 16 | 17 | 18 | 19 |
20 | Follow @healeycodes 27 | Star 35 |
36 |
37 |
38 |
39 | <%if (popup) {%> 40 | 50 | <%}%> 51 |

PairCode

52 |

53 | Pair programming sandbox for HTML/CSS/JS. Create and 54 | Fork Code Rooms. 55 |

56 |

57 | Open source project. Raise an issue on the 58 | GitHub repo. 61 |

62 | 69 |
70 |
71 | 72 | 77 | 78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /views/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%-title-%> 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 |
32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 | 48 | 59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 |
71 |
72 | 73 |
74 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: { 5 | room: path.resolve(__dirname, "src/room.js") 6 | }, 7 | output: { 8 | filename: "[name].js", 9 | path: path.resolve(__dirname, "public/js") 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | use: { 15 | loader: "babel-loader", 16 | options: { 17 | presets: ["@babel/preset-env"] 18 | } 19 | } 20 | } 21 | ] 22 | }, 23 | mode: "production" 24 | }; 25 | --------------------------------------------------------------------------------