├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── index.js ├── kubernetes.yaml ├── package.json ├── public ├── buzzer-logo.svg ├── host.js ├── join.js ├── lets-play.png └── style.css ├── screenshots ├── host-v3.png ├── player-buzzer-v3.png └── player-join-v3.png └── views ├── host.pug └── index.pug /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | ENV NODE_ENV production 7 | 8 | COPY package.json /usr/src/app/ 9 | RUN npm install 10 | COPY . /usr/src/app 11 | 12 | EXPOSE 8090 13 | 14 | CMD ["node", "./index.js"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Buffer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Buzzer 3 |

4 | 5 | A little buzzer app for running your own quizzes or game shows! Uses websockets to sent messages. 6 | 7 | ## Running the app 8 | 9 | You'll need [Node.js](https://nodejs.org) or [Docker](https://www.docker.com/) to run this 10 | application. For Node: 11 | 12 | ``` 13 | npm install 14 | node index.js 15 | ``` 16 | 17 | For Docker: 18 | 19 | ``` 20 | docker build -t buzzer . 21 | docker run -p 8090:8090 buzzer 22 | ``` 23 | 24 | Open http://localhost:8090 in your browser to start! 25 | 26 | ## How to use 27 | 28 | The players goto the homepage (`http://localhost:8090/`) and they can enter their name and team 29 | number. Joining will give them a giant buzzer button! 30 | 31 | The host heads over to `/host` and will be able to see everyone that buzzes in and clear the list 32 | in between questions. 33 | 34 | Join a team | Buzz in | Host view | 35 | :-------------------------:|:-------------------------:|:-------------------------:| 36 | Join a team | Buzz in | Host view 37 | 38 | ## License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const express = require('express') 3 | const socketio = require('socket.io') 4 | 5 | const app = express(); 6 | const server = http.Server(app); 7 | const io = socketio(server); 8 | 9 | const title = 'Buffer Buzzer' 10 | 11 | let data = { 12 | users: new Set(), 13 | buzzes: new Set(), 14 | } 15 | 16 | const getData = () => ({ 17 | users: [...data.users], 18 | buzzes: [...data.buzzes].map(b => { 19 | const [ name, team ] = b.split('-') 20 | return { name, team } 21 | }) 22 | }) 23 | 24 | app.use(express.static('public')) 25 | app.set('view engine', 'pug') 26 | 27 | app.get('/', (req, res) => res.render('index', { title })) 28 | app.get('/host', (req, res) => res.render('host', Object.assign({ title }, getData()))) 29 | 30 | io.on('connection', (socket) => { 31 | socket.on('join', (user) => { 32 | data.users.add(user.id) 33 | io.emit('active', [...data.users].length) 34 | console.log(`${user.name} joined!`) 35 | }) 36 | 37 | socket.on('buzz', (user) => { 38 | data.buzzes.add(`${user.name}-${user.team}`) 39 | io.emit('buzzes', [...data.buzzes]) 40 | console.log(`${user.name} buzzed in!`) 41 | }) 42 | 43 | socket.on('clear', () => { 44 | data.buzzes = new Set() 45 | io.emit('buzzes', [...data.buzzes]) 46 | console.log(`Clear buzzes`) 47 | }) 48 | }) 49 | 50 | server.listen(8090, () => console.log('Listening on 8090')) 51 | -------------------------------------------------------------------------------- /kubernetes.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: buzzer 5 | labels: 6 | app: buzzer 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | labels: 12 | app: buzzer 13 | spec: 14 | containers: 15 | - name: buzzer 16 | image: bufferapp/buzzer:1.3.0 17 | ports: 18 | - containerPort: 8090 19 | resources: 20 | limits: 21 | cpu: 100m 22 | memory: 200Mi 23 | restartPolicy: Always 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: buzzer 29 | labels: 30 | app: buzzer 31 | spec: 32 | ports: 33 | - port: 80 34 | targetPort: 8090 35 | protocol: TCP 36 | name: http 37 | selector: 38 | app: buzzer 39 | type: LoadBalancer 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buzzer", 3 | "version": "1.2.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Dan Farrelly (http://danfarrelly.nyc)", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "4.14.1", 13 | "pug": "2.0.0-beta11", 14 | "socketio": "1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /public/buzzer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/host.js: -------------------------------------------------------------------------------- 1 | const socket = io() 2 | const active = document.querySelector('.js-active') 3 | const buzzList = document.querySelector('.js-buzzes') 4 | const clear = document.querySelector('.js-clear') 5 | 6 | socket.on('active', (numberActive) => { 7 | active.innerText = `${numberActive} joined` 8 | }) 9 | 10 | socket.on('buzzes', (buzzes) => { 11 | buzzList.innerHTML = buzzes 12 | .map(buzz => { 13 | const p = buzz.split('-') 14 | return { name: p[0], team: p[1] } 15 | }) 16 | .map(user => `
  • ${user.name} on Team ${user.team}
  • `) 17 | .join('') 18 | }) 19 | 20 | clear.addEventListener('click', () => { 21 | socket.emit('clear') 22 | }) 23 | 24 | -------------------------------------------------------------------------------- /public/join.js: -------------------------------------------------------------------------------- 1 | const socket = io() 2 | const body = document.querySelector('.js-body') 3 | const form = document.querySelector('.js-join') 4 | const joined = document.querySelector('.js-joined') 5 | const buzzer = document.querySelector('.js-buzzer') 6 | const joinedInfo = document.querySelector('.js-joined-info') 7 | const editInfo = document.querySelector('.js-edit') 8 | 9 | let user = {} 10 | 11 | const getUserInfo = () => { 12 | user = JSON.parse(localStorage.getItem('user')) || {} 13 | if (user.name) { 14 | form.querySelector('[name=name]').value = user.name 15 | form.querySelector('[name=team]').value = user.team 16 | } 17 | } 18 | const saveUserInfo = () => { 19 | localStorage.setItem('user', JSON.stringify(user)) 20 | } 21 | 22 | form.addEventListener('submit', (e) => { 23 | e.preventDefault() 24 | user.name = form.querySelector('[name=name]').value 25 | user.team = form.querySelector('[name=team]').value 26 | if (!user.id) { 27 | user.id = Math.floor(Math.random() * new Date()) 28 | } 29 | socket.emit('join', user) 30 | saveUserInfo() 31 | joinedInfo.innerText = `${user.name} on Team ${user.team}` 32 | form.classList.add('hidden') 33 | joined.classList.remove('hidden') 34 | body.classList.add('buzzer-mode') 35 | }) 36 | 37 | buzzer.addEventListener('click', (e) => { 38 | socket.emit('buzz', user) 39 | }) 40 | 41 | editInfo.addEventListener('click', () => { 42 | joined.classList.add('hidden') 43 | form.classList.remove('hidden') 44 | body.classList.remove('buzzer-mode') 45 | }) 46 | 47 | getUserInfo() 48 | -------------------------------------------------------------------------------- /public/lets-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/buzzer/d839b4e0c165eefa035e0e9518b77eac3bee80ca/public/lets-play.png -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | height: 100%; 7 | } 8 | 9 | body { 10 | padding: 0; 11 | margin: 0; 12 | font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, 13 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; 14 | /* background: linear-gradient(-45deg, #a6f4e5 0%, #23e6bf 47%); */ 15 | /* background-color: rgb(44, 75, 255); */ 16 | background-color: #fff; 17 | color: #1A2634; 18 | text-align: center; 19 | } 20 | 21 | body, 22 | input, 23 | button { 24 | font-size: 1em; 25 | } 26 | 27 | p { 28 | margin: 2em 0; 29 | } 30 | 31 | ol { 32 | text-align: left; 33 | } 34 | 35 | li { 36 | margin: 0.5em 0; 37 | } 38 | 39 | .hidden { 40 | display: none; 41 | } 42 | 43 | header { 44 | background-color: #fff; 45 | } 46 | 47 | .logo { 48 | width: 100%; 49 | max-width: 310px; 50 | max-height: 120px; 51 | padding: 1em 0; 52 | } 53 | 54 | .container { 55 | width: 100%; 56 | max-width: 400px; 57 | padding: 1em; 58 | margin: 0 auto; 59 | } 60 | .view button:last-child { 61 | margin-bottom: 0; 62 | } 63 | 64 | .buzzer-mode { 65 | background-color: rgb(16, 21, 78); 66 | color: rgb(243, 175, 185); 67 | } 68 | 69 | input[type="text"], 70 | input[type="number"], 71 | button { 72 | display: block; 73 | padding: 0.8em 1.4em; 74 | margin: 1em 0; 75 | width: 100%; 76 | border-radius: 5px; 77 | font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, 78 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; 79 | font-weight: 500; 80 | } 81 | 82 | input[type="text"], 83 | input[type="number"] { 84 | border: 1px solid #B8B8B8; 85 | } 86 | 87 | input[type="text"]:focus, 88 | input[type="number"]:focus, 89 | input[type="text"]:active, 90 | input[type="number"]:active { 91 | border-color: #2c4bff; 92 | box-shadow: 0 0 0 3px #ABB7FF; 93 | outline: none; 94 | } 95 | 96 | button { 97 | padding: 1.4em 1.4em; 98 | font-size: 1.2em; 99 | background-color: #161D99; 100 | color: #fff; 101 | cursor: pointer; 102 | border: 0; 103 | } 104 | 105 | button:hover { 106 | background-color: rgb(22, 29, 153); 107 | } 108 | 109 | button:active { 110 | box-shadow: rgb(171, 183, 255) 0px 0px 0px 3px; 111 | outline: 0px; 112 | transform: translateY( 1px); 113 | } 114 | button:focus { 115 | box-shadow: 0 0 0 3px #ABB7FF; 116 | } 117 | 118 | .main-image { 119 | width: 90%; 120 | } 121 | 122 | .buzzer { 123 | height: 10em; 124 | font-size: 2em; 125 | background-color: rgb(243, 175, 185); 126 | color: rgb(18, 30, 102); 127 | } 128 | .buzzer:focus { 129 | box-shadow: 0 0 0 3px #ABB7FF; 130 | } 131 | .buzzer:hover, 132 | .buzzer:active { 133 | background-color: #e97284; 134 | } 135 | 136 | .secondary { 137 | background-color: #fff; 138 | color: rgb(16, 21, 78); 139 | } 140 | 141 | .secondary:hover { 142 | background-color: rgb(44, 75, 255); 143 | } 144 | -------------------------------------------------------------------------------- /screenshots/host-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/buzzer/d839b4e0c165eefa035e0e9518b77eac3bee80ca/screenshots/host-v3.png -------------------------------------------------------------------------------- /screenshots/player-buzzer-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/buzzer/d839b4e0c165eefa035e0e9518b77eac3bee80ca/screenshots/player-buzzer-v3.png -------------------------------------------------------------------------------- /screenshots/player-join-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/buzzer/d839b4e0c165eefa035e0e9518b77eac3bee80ca/screenshots/player-join-v3.png -------------------------------------------------------------------------------- /views/host.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title=title 5 | meta(name='viewport' content='width=device-width, initial-scale=1') 6 | link(rel="stylesheet" href='/style.css') 7 | body 8 | header 9 | img.logo(src='/buzzer-logo.svg') 10 | 11 | div.container 12 | h2.js-active=`${users.length} joined` 13 | ol.js-buzzes 14 | each buzz in buzzes 15 | li=`${buzz.name} on Team ${buzz.team}` 16 | button.js-clear='Clear buzzes' 17 | 18 | script(src='/socket.io/socket.io.js') 19 | script(src='/host.js') 20 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title=title 5 | meta(name='viewport' content='width=device-width, initial-scale=1') 6 | link(rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:500,700|Roboto:400,500,700,900") 7 | link(rel="stylesheet" href='/style.css') 8 | body.js-body 9 | header 10 | img.logo(src='/buzzer-logo.svg') 11 | 12 | div.container 13 | form.view.js-join 14 | img(src="/lets-play.png" class="main-image") 15 | h2="Let's play!" 16 | input(type='text' name='name' placeholder='Your name' required='required') 17 | input(type='number' name='team' placeholder='Your team number' required='required') 18 | button(type='submit')='Join!' 19 | 20 | div.view.js-joined.hidden 21 | button.js-buzzer.buzzer()='Buzz!!!' 22 | p.js-joined-info="" 23 | button.js-edit.secondary='Change teams' 24 | 25 | script(src='/socket.io/socket.io.js') 26 | script(src='/join.js') 27 | --------------------------------------------------------------------------------