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…
Reference in New Issue
Block a user