Initial commit
This commit is contained in:
		
						commit
						628428dd38
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | node_modules | ||||||
							
								
								
									
										44
									
								
								choices.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								choices.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  |     <head> | ||||||
|  |         <style> | ||||||
|  |             #rows input { | ||||||
|  |                 display: block; | ||||||
|  |                 margin-bottom: 1em; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             #question { | ||||||
|  |                 margin-bottom: 2em; | ||||||
|  |             } | ||||||
|  |         </style> | ||||||
|  |     </head> | ||||||
|  |     <body> | ||||||
|  |         <form method="post" action="/choices"> | ||||||
|  |             <div id="question"> | ||||||
|  |                 Question: | ||||||
|  |                 <input type="text" name="question"> | ||||||
|  |             </div> | ||||||
|  |             <div id="rows"> | ||||||
|  |                 <input type="text" name="choice" /> | ||||||
|  |                 <input type="text" name="choice" /> | ||||||
|  |                 <input type="text" name="choice" /> | ||||||
|  |             </div> | ||||||
|  |             <input type="submit" value="OK"> | ||||||
|  |             <button type="button" onclick="more()">+</button> | ||||||
|  |             <button type="button" onclick="less()">-</button> | ||||||
|  |         </form> | ||||||
|  | 
 | ||||||
|  |     </body> | ||||||
|  |     <script> | ||||||
|  |         const rows = document.getElementById("rows"); | ||||||
|  |         function more() { | ||||||
|  |             const row = document.createElement('input'); | ||||||
|  |             row.setAttribute("type", "text"); | ||||||
|  |             row.setAttribute("name", "choice"); | ||||||
|  |             rows.appendChild(row); | ||||||
|  |         } | ||||||
|  |         function less() { | ||||||
|  |             rows.removeChild(rows.lastChild); | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </html> | ||||||
							
								
								
									
										136
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  |     <head> | ||||||
|  |         <meta name="viewport" content="width=device-width,initial-scale=1.0"> | ||||||
|  |         <title>Quiz</title> | ||||||
|  |         <style> | ||||||
|  |             body { | ||||||
|  |                 border: solid 1px; | ||||||
|  |                 border-radius: 2rem; | ||||||
|  |                 margin: 1rem; | ||||||
|  |                 padding: 2rem; | ||||||
|  |             } | ||||||
|  |             ul { | ||||||
|  |                 list-style-type: none; | ||||||
|  |                 padding-left: 0px; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             li { | ||||||
|  |                 white-space: pre; | ||||||
|  |                 padding: 0.5em; | ||||||
|  |                 margin-bottom: 0.5em; | ||||||
|  |                 background-color: lightblue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             ul.ready li { | ||||||
|  |                 border: 1px solid; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             ul.voted li { | ||||||
|  |                 border: 1px dotted; | ||||||
|  |             }  | ||||||
|  | 
 | ||||||
|  |             li.choice { | ||||||
|  |                 font-weight: bold; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             li.winner { | ||||||
|  |                 background-color: lightgreen; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             #nonetwork { | ||||||
|  |                 position: fixed; | ||||||
|  |                 width: 100%; | ||||||
|  |                 height: 100%; | ||||||
|  |                 top: 0; | ||||||
|  |                 left: 0; | ||||||
|  |                 right: 0; | ||||||
|  |                 bottom: 0; | ||||||
|  |             } | ||||||
|  |         </style> | ||||||
|  |     </head> | ||||||
|  |     <body> | ||||||
|  |         <svg id="nonetwork" fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" version="1.1"  preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||||||
|  |             <title>no-wifi-line</title> | ||||||
|  |             <path class="clr-i-outline clr-i-outline-path-1" d="M18,24.42a4,4,0,1,0,4,4A4,4,0,0,0,18,24.42Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,18,30.42Z"></path><path class="clr-i-outline clr-i-outline-path-2" d="M26.21,21.85a1,1,0,0,0-.23-1.4,13.56,13.56,0,0,0-5-2.23l3.87,3.87A1,1,0,0,0,26.21,21.85Z"></path><path class="clr-i-outline clr-i-outline-path-3" d="M18.05,10.72a20.88,20.88,0,0,0-4.16.43l1.74,1.74a19,19,0,0,1,2.42-.17A18.76,18.76,0,0,1,28.64,16a1,1,0,0,0,1.12-1.65A20.75,20.75,0,0,0,18.05,10.72Z"></path><path class="clr-i-outline clr-i-outline-path-4" d="M33.55,8.2A28.11,28.11,0,0,0,8.11,5.36L9.69,6.93A26,26,0,0,1,32.45,9.87a1,1,0,0,0,1.1-1.67Z"></path><path class="clr-i-outline clr-i-outline-path-5" d="M1.84,4.75,4.27,7.18c-.62.34-1.23.7-1.83,1.1A1,1,0,1,0,3.56,9.94C4.26,9.47,5,9,5.74,8.65l3.87,3.87A20.59,20.59,0,0,0,6.23,14.4,1,1,0,0,0,7.36,16a18.82,18.82,0,0,1,3.77-2l4.16,4.16A13.51,13.51,0,0,0,10,20.55a1,1,0,0,0,1.18,1.61A11.52,11.52,0,0,1,17,20l10.8,10.8,1.41-1.41-26-26Z"></path> | ||||||
|  |             <rect x="0" y="0" width="36" height="36" fill-opacity="0"/> | ||||||
|  |         </svg> | ||||||
|  |         <div id="question">Waiting...</div> | ||||||
|  |         <ul id="choices"> | ||||||
|  |         </ul> | ||||||
|  |     </body> | ||||||
|  |     <script src="/socket.io/socket.io.js"></script> | ||||||
|  |     <script> | ||||||
|  |         const socket = io(); | ||||||
|  | 
 | ||||||
|  |         const nonetwork = document.getElementById("nonetwork"); | ||||||
|  |         const choice_buttons = document.getElementById("choices"); | ||||||
|  |         const question = document.getElementById("question"); | ||||||
|  |         let choices = []; | ||||||
|  | 
 | ||||||
|  |         let voted = false; | ||||||
|  | 
 | ||||||
|  |         function vote(idx) { | ||||||
|  |             voted = true; | ||||||
|  |             console.log(`voting ${idx}`); | ||||||
|  |             socket.emit('vote', idx); | ||||||
|  |             choice_buttons.className = 'voted'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         socket.on('disconnect', () => { | ||||||
|  |             nonetwork.style.display = 'block'; | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         socket.on('choices', (new_choices, new_question) => { | ||||||
|  |             console.log(`new question ${new_question}: ${new_choices}`); | ||||||
|  | 
 | ||||||
|  |             choices = new_choices; | ||||||
|  |             voted = false; | ||||||
|  |             choice_buttons.className = 'ready'; | ||||||
|  |             nonetwork.style.display = 'none'; | ||||||
|  |             question.textContent = new_question; | ||||||
|  | 
 | ||||||
|  |             choice_buttons.replaceChildren(...choices.map((txt,idx) => { | ||||||
|  |                 const li = document.createElement('li'); | ||||||
|  |                 li.textContent = txt | ||||||
|  |                 li.addEventListener('click', () => { | ||||||
|  |                     if (voted) { return; } | ||||||
|  |                     vote(idx+1) | ||||||
|  |                     li.className = 'choice'; | ||||||
|  |                 }); | ||||||
|  |                 return li; | ||||||
|  |             })) | ||||||
|  | 
 | ||||||
|  |             if (window.location.hash === '#abstain') { | ||||||
|  |                 vote(0); | ||||||
|  |             } | ||||||
|  |         });  | ||||||
|  |          | ||||||
|  |         socket.on('results', (results) => { | ||||||
|  | 
 | ||||||
|  |             console.log(`results: ${results}`); | ||||||
|  | 
 | ||||||
|  |             const sum = results.reduce((a,b) => a+b); | ||||||
|  |             console.log(`sum is ${sum}`); | ||||||
|  |             const max = Math.max(...results); | ||||||
|  |             results.map((score, idx) => { | ||||||
|  |              | ||||||
|  |                 const li = choice_buttons.children[idx]; | ||||||
|  |                 const width = Math.round(100 * score / sum); | ||||||
|  |                 console.log(`set ${idx} to ${width}`); | ||||||
|  |                 li.style.width = `${width}%`; | ||||||
|  |                 li.textContent = `${choices[idx]} : ${score}`; | ||||||
|  |                 if (score === max) { | ||||||
|  |                     li.classList.add('winner'); | ||||||
|  |                 } else { | ||||||
|  |                     li.classList.remove('winner'); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 //bar.setAttribute("max", sum); | ||||||
|  |                 //bar.setAttribute("value", score); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  |          | ||||||
|  |     </script> | ||||||
|  | </html> | ||||||
							
								
								
									
										116
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | |||||||
|  | import express from 'express'; | ||||||
|  | import { createServer } from 'node:http'; | ||||||
|  | import { fileURLToPath } from 'node:url'; | ||||||
|  | import { dirname, join } from 'node:path'; | ||||||
|  | import { Server } from 'socket.io'; | ||||||
|  | import { randomUUID } from 'node:crypto'; | ||||||
|  | import session from 'express-session'; | ||||||
|  | import formidable from 'express-formidable'; | ||||||
|  | 
 | ||||||
|  | const app = express(); | ||||||
|  | const server = createServer(app); | ||||||
|  | const io = new Server(server); | ||||||
|  | 
 | ||||||
|  | const rootdir = dirname(fileURLToPath(import.meta.url)); | ||||||
|  | function page(name) { return join(rootdir, name) }; | ||||||
|  | 
 | ||||||
|  | let admin_password = process.env.PASSWORD; | ||||||
|  | if (!admin_password) { | ||||||
|  |     console.log("Please define the PASSWORD env variable."); | ||||||
|  |     process.exit(1); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let question = "Rien pour l'instant !"; | ||||||
|  | let choices = []; | ||||||
|  | let results = []; | ||||||
|  | const voted = new Set(); | ||||||
|  | 
 | ||||||
|  | function admin_only(req,res,next) { | ||||||
|  |     if (!req.session.admin) { | ||||||
|  |         return res.sendStatus(403) | ||||||
|  |     } else { | ||||||
|  |         return next() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function set_choices(new_choices, new_question) { | ||||||
|  |     choices = new_choices; | ||||||
|  |     question = new_question; | ||||||
|  |     results = new_choices.map(() => 0); | ||||||
|  |     voted.clear(); | ||||||
|  | 
 | ||||||
|  |     io.emit('choices', choices, question); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | app.use(session({ | ||||||
|  |     resave: false, | ||||||
|  |     saveUninitialized: false,  | ||||||
|  |     secret: randomUUID(), | ||||||
|  | })) | ||||||
|  | app.use(formidable()); | ||||||
|  | 
 | ||||||
|  | app.get('/', (req, res) => { | ||||||
|  |     res.sendFile(page("index.html")); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | app.get('/login', (req,res) => { | ||||||
|  |     res.sendFile(page("login.html")); | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | app.post('/login', (req, res) => { | ||||||
|  |     if (req.fields.password !== admin_password) { | ||||||
|  |         return res.sendStatus(403); | ||||||
|  |     } | ||||||
|  |     req.session.admin = true; | ||||||
|  |     res.redirect('/choices'); | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | app.use('/choices', admin_only); | ||||||
|  | app.get('/choices', (req, res) => { | ||||||
|  |     res.sendFile(page("choices.html")); | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | app.post('/choices', (req, res) => { | ||||||
|  |     console.log(JSON.stringify(req.fields)); | ||||||
|  |     set_choices(req.fields.choice, req.fields.question); | ||||||
|  |     res.redirect("/#abstain") | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | io.on('connection', (socket) => { | ||||||
|  |     console.log('user connected'); | ||||||
|  | 
 | ||||||
|  |     socket.on('disconnect', (reason, desc) => { | ||||||
|  |         console.log('user disconnected') | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     socket.on('vote', (idx) => { | ||||||
|  | 
 | ||||||
|  |         if (!(typeof idx === 'number') || idx < 0 || idx > choices.length) { | ||||||
|  |             console.log(`invalid vote ${idx}`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (voted.has(socket)) { | ||||||
|  |             console.log(`double vote`); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log(`user voted ${idx}`) | ||||||
|  |         voted.add(socket); | ||||||
|  |         if (idx > 0) { | ||||||
|  |             results[idx-1] += 1; | ||||||
|  |         } | ||||||
|  |         console.log(`results: ${results}`); | ||||||
|  | 
 | ||||||
|  |         for (const socket of voted) { | ||||||
|  |             socket.emit('results', results); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     socket.emit('choices', choices, question); | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | server.listen(3000, () => { | ||||||
|  |     console.log('Server running at http://localhost:3000'); | ||||||
|  | }) | ||||||
							
								
								
									
										9
									
								
								login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								login.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  |     <body> | ||||||
|  |         <form method="post" action="/login"> | ||||||
|  |         <input type="password" name="password"/> | ||||||
|  |         <input type="submit" value="Log in"/> | ||||||
|  |         </form> | ||||||
|  |     </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										1075
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1075
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | { | ||||||
|  |     "name": "liliquiz", | ||||||
|  |     "version": "0.0.1", | ||||||
|  |     "description": "Minimalistic live quiz", | ||||||
|  |     "type": "module", | ||||||
|  |     "dependencies": { | ||||||
|  |         "express": "^4.21.2", | ||||||
|  |         "express-formidable": "^1.2.0", | ||||||
|  |         "express-session": "^1.18.1", | ||||||
|  |         "socket.io": "^4.8.1" | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user