|
|
@@ -0,0 +1,406 @@ |
|
|
|
<!DOCTYPE html> |
|
|
|
<html lang="it"> |
|
|
|
<head> |
|
|
|
<meta charset="UTF-8" /> |
|
|
|
<title>Consiglio Comunale</title> |
|
|
|
<style> |
|
|
|
body { font-family: Arial, sans-serif; background: #f0f2f5; padding: 20px; } |
|
|
|
.container { background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 1000px; margin: auto; } |
|
|
|
h1 { text-align: center; color: #333; } |
|
|
|
.input-group { display: flex; justify-content: center; align-items: center; margin-bottom: 20px; flex-wrap: wrap; } |
|
|
|
label { font-size: 18px; margin-right: 10px; } |
|
|
|
input[type="number"], input[type="file"], input[type="text"] { width: 250px; padding: 12px; font-size: 18px; border: 1px solid #ccc; border-radius: 8px; margin: 5px; } |
|
|
|
button { padding: 12px 24px; font-size: 16px; border: none; border-radius: 8px; cursor: pointer; margin: 5px; } |
|
|
|
#confirm { background-color: #28a745; color: white; } |
|
|
|
#start { background-color: #007bff; color: white; } |
|
|
|
#configure { background-color: #17a2b8; color: white; } |
|
|
|
#mapConfig { background-color: #fd7e14; color: white; } |
|
|
|
#stop { background-color: #dc3545; color: white; } |
|
|
|
#exportLog, #clearLog, #exportXML, #importXML { background-color: #6c757d; color: white; } |
|
|
|
#buttons, #config-buttons { margin-top: 20px; text-align: center; } |
|
|
|
#buttons button { background-color: #6f42c1; color: white; margin: 5px; position: relative; } |
|
|
|
#currentSpeaker { margin-top: 20px; font-size: 24px; font-weight: bold; border: 2px solid #007bff; padding: 20px; height: 100px; background: #e9ecef; display: flex; align-items: center; justify-content: center; border-radius: 8px; } |
|
|
|
#mapContainer { position: relative; width: 800px; height: 600px; background-image: url('mappa.jpg'); background-size: cover; background-position: center; border: 2px solid #ccc; margin: 20px auto; } |
|
|
|
.map-button { position: absolute; padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 8px; user-select: none; } |
|
|
|
.hidden { display: none; } |
|
|
|
#saveMapBtn { display: block; margin: 20px auto; } |
|
|
|
button.clicked-feedback { animation: buttonFlash 0.3s ease-in-out; } |
|
|
|
@keyframes buttonFlash { |
|
|
|
0% { transform: scale(1); filter: brightness(1); } |
|
|
|
50% { transform: scale(1.1); filter: brightness(1.5); } |
|
|
|
100% { transform: scale(1); filter: brightness(1); } |
|
|
|
} |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
<body> |
|
|
|
<div class="container"> |
|
|
|
<h1>Consiglio Comunale</h1> |
|
|
|
|
|
|
|
<div class="input-group"> |
|
|
|
<label for="numPartecipanti">Numero partecipanti:</label> |
|
|
|
<input type="number" id="numPartecipanti" min="1" /> |
|
|
|
<button id="confirm">Conferma</button> |
|
|
|
<button id="importXML">Importa Partecipanti (XML)</button> |
|
|
|
<input type="file" id="fileInput" accept=".xml" style="display:none"> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="input-group hidden" id="sessionControls"> |
|
|
|
<button id="start">Avvia</button> |
|
|
|
<button id="configure">Assegna nomi</button> |
|
|
|
<button id="mapConfig">Configura mappa</button> |
|
|
|
<button id="stop" class="hidden">Termina</button> |
|
|
|
<button id="exportLog">Esporta Log</button> |
|
|
|
<button id="clearLog">Pulisci Log</button> |
|
|
|
<button id="exportXML" class="hidden">Esporta Elenco Partecipanti (XML)</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div id="buttons" class="hidden"></div> |
|
|
|
<div id="config-buttons" class="hidden"></div> |
|
|
|
<div id="mapContainer" class="hidden"></div> |
|
|
|
<button id="saveMapBtn" class="hidden">Salva Mappa</button> |
|
|
|
|
|
|
|
<h2 style="text-align: center;">Chi sta parlando:</h2> |
|
|
|
<div id="currentSpeaker">Nessuno</div> |
|
|
|
|
|
|
|
<script> |
|
|
|
let buttonLabels = []; |
|
|
|
let buttonCoords = []; |
|
|
|
let sessionLog = ''; |
|
|
|
let sessionStart = null; |
|
|
|
|
|
|
|
let sessionState = { |
|
|
|
active: false, |
|
|
|
startTime: null, |
|
|
|
currentSpeaker: 'Nessuno', |
|
|
|
mapVisible: false |
|
|
|
}; |
|
|
|
|
|
|
|
const exportXMLBtn = document.getElementById('exportXML'); |
|
|
|
const saveBtn = document.getElementById('saveMapBtn'); |
|
|
|
const map = document.getElementById('mapContainer'); |
|
|
|
|
|
|
|
function giveButtonFeedback(btn) { |
|
|
|
btn.classList.add('clicked-feedback'); |
|
|
|
setTimeout(() => btn.classList.remove('clicked-feedback'), 300); |
|
|
|
} |
|
|
|
|
|
|
|
function applyButtonFeedback() { |
|
|
|
document.querySelectorAll('button').forEach(btn => { |
|
|
|
btn.addEventListener('click', () => giveButtonFeedback(btn)); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function saveSessionLog() { |
|
|
|
localStorage.setItem('sessionLog', sessionLog); |
|
|
|
} |
|
|
|
|
|
|
|
function saveSessionData() { |
|
|
|
localStorage.setItem('buttonLabels', JSON.stringify(buttonLabels)); |
|
|
|
localStorage.setItem('buttonCoords', JSON.stringify(buttonCoords)); |
|
|
|
localStorage.setItem('sessionState', JSON.stringify(sessionState)); |
|
|
|
} |
|
|
|
|
|
|
|
function logSpeech(index) { |
|
|
|
const now = new Date(); |
|
|
|
const time = sessionStart ? new Date(now - sessionStart).toISOString().substr(11, 8) : now.toLocaleTimeString(); |
|
|
|
const entry = `[${time}] Parla: ${buttonLabels[index]}\n`; |
|
|
|
sessionLog += entry; |
|
|
|
sessionState.currentSpeaker = buttonLabels[index]; |
|
|
|
saveSessionLog(); |
|
|
|
saveSessionData(); |
|
|
|
document.getElementById('currentSpeaker').textContent = buttonLabels[index]; |
|
|
|
} |
|
|
|
|
|
|
|
function createButtons() { |
|
|
|
const div = document.getElementById('buttons'); |
|
|
|
div.innerHTML = '<hr style="margin: 20px 0;">'; |
|
|
|
buttonLabels.forEach((name, i) => { |
|
|
|
const btn = document.createElement('button'); |
|
|
|
btn.textContent = name; |
|
|
|
btn.addEventListener('click', () => logSpeech(i)); |
|
|
|
div.appendChild(btn); |
|
|
|
}); |
|
|
|
div.classList.remove('hidden'); |
|
|
|
applyButtonFeedback(); |
|
|
|
saveSessionData(); |
|
|
|
} |
|
|
|
|
|
|
|
function renderMapConfig() { |
|
|
|
if (buttonLabels.length === 0) return; |
|
|
|
|
|
|
|
map.innerHTML = ''; |
|
|
|
map.classList.remove('hidden'); |
|
|
|
saveBtn.classList.remove('hidden'); |
|
|
|
saveBtn.style.display = 'block'; |
|
|
|
|
|
|
|
const cols = Math.ceil(Math.sqrt(buttonLabels.length)); |
|
|
|
const spacingX = map.clientWidth / (cols + 1); |
|
|
|
const spacingY = map.clientHeight / (cols + 1); |
|
|
|
|
|
|
|
buttonLabels.forEach((name, i) => { |
|
|
|
const x = buttonCoords[i].x; |
|
|
|
const y = buttonCoords[i].y; |
|
|
|
const b = document.createElement('button'); |
|
|
|
b.textContent = name; |
|
|
|
b.className = 'map-button'; |
|
|
|
b.style.left = `${x}px`; |
|
|
|
b.style.top = `${y}px`; |
|
|
|
b.setAttribute('data-index', i); |
|
|
|
b.onmousedown = startDrag; |
|
|
|
map.appendChild(b); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function renderMapView() { |
|
|
|
if (buttonLabels.length === 0) return; |
|
|
|
|
|
|
|
map.innerHTML = ''; |
|
|
|
map.classList.remove('hidden'); |
|
|
|
saveBtn.classList.add('hidden'); |
|
|
|
|
|
|
|
buttonLabels.forEach((name, i) => { |
|
|
|
const b = document.createElement('button'); |
|
|
|
b.textContent = name; |
|
|
|
b.className = 'map-button'; |
|
|
|
b.style.left = `${buttonCoords[i].x}px`; |
|
|
|
b.style.top = `${buttonCoords[i].y}px`; |
|
|
|
b.style.cursor = 'pointer'; |
|
|
|
b.onclick = () => { |
|
|
|
giveButtonFeedback(b); |
|
|
|
logSpeech(i); |
|
|
|
}; |
|
|
|
map.appendChild(b); |
|
|
|
}); |
|
|
|
} |
|
|
|
document.getElementById('confirm').addEventListener('click', () => { |
|
|
|
const n = parseInt(document.getElementById('numPartecipanti').value); |
|
|
|
if (!n || n < 1) return alert('Inserisci un numero valido'); |
|
|
|
const gruppi = ['Centro', 'Sinistra', 'Destra']; |
|
|
|
buttonLabels = Array.from({ length: n }, (_, i) => { |
|
|
|
const gruppo = gruppi[i % gruppi.length]; |
|
|
|
const numero = Math.floor(i / gruppi.length) + 1; |
|
|
|
return `${gruppo} ${numero}`; |
|
|
|
}); |
|
|
|
buttonCoords = Array.from({ length: n }, () => ({ x: 0, y: 0 })); |
|
|
|
createButtons(); |
|
|
|
document.getElementById('sessionControls').classList.remove('hidden'); |
|
|
|
}); |
|
|
|
|
|
|
|
document.getElementById('start').addEventListener('click', () => { |
|
|
|
if (!sessionStart) { |
|
|
|
sessionStart = new Date(); |
|
|
|
sessionState.active = true; |
|
|
|
sessionState.startTime = sessionStart.getTime(); |
|
|
|
sessionLog += `[00:00:00] Sessione avviata (${sessionStart.toLocaleString('it-IT')})\n`; |
|
|
|
saveSessionLog(); |
|
|
|
saveSessionData(); |
|
|
|
document.getElementById('stop').classList.remove('hidden'); |
|
|
|
document.getElementById('start').classList.add('hidden'); |
|
|
|
document.getElementById('configure').classList.add('hidden'); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
document.getElementById('stop').addEventListener('click', () => { |
|
|
|
if (sessionStart) { |
|
|
|
const end = new Date(); |
|
|
|
const duration = new Date(end - sessionStart).toISOString().substr(11, 8); |
|
|
|
sessionLog += `[${duration}] Sessione terminata (${end.toLocaleString('it-IT')})\n`; |
|
|
|
sessionState.active = false; |
|
|
|
sessionState.startTime = null; |
|
|
|
saveSessionLog(); |
|
|
|
saveSessionData(); |
|
|
|
document.getElementById('stop').classList.add('hidden'); |
|
|
|
document.getElementById('start').classList.remove('hidden'); |
|
|
|
document.getElementById('configure').classList.remove('hidden'); |
|
|
|
} |
|
|
|
}); |
|
|
|
document.getElementById('configure').addEventListener('click', () => { |
|
|
|
const configDiv = document.getElementById('config-buttons'); |
|
|
|
configDiv.innerHTML = ''; |
|
|
|
saveBtn.classList.add('hidden'); |
|
|
|
|
|
|
|
buttonLabels.forEach((name, i) => { |
|
|
|
const input = document.createElement('input'); |
|
|
|
input.type = 'text'; |
|
|
|
input.value = name; |
|
|
|
input.dataset.index = i; |
|
|
|
input.style.margin = '5px'; |
|
|
|
input.style.padding = '8px'; |
|
|
|
configDiv.appendChild(input); |
|
|
|
}); |
|
|
|
|
|
|
|
const saveBtnNames = document.createElement('button'); |
|
|
|
saveBtnNames.textContent = 'Salva Nomi'; |
|
|
|
saveBtnNames.style.backgroundColor = '#28a745'; |
|
|
|
saveBtnNames.style.color = 'white'; |
|
|
|
saveBtnNames.style.marginTop = '20px'; |
|
|
|
saveBtnNames.style.display = 'block'; |
|
|
|
saveBtnNames.onclick = () => { |
|
|
|
configDiv.querySelectorAll('input').forEach(input => { |
|
|
|
const idx = parseInt(input.dataset.index); |
|
|
|
buttonLabels[idx] = input.value; |
|
|
|
}); |
|
|
|
saveSessionData(); |
|
|
|
configDiv.classList.add('hidden'); |
|
|
|
createButtons(); |
|
|
|
}; |
|
|
|
|
|
|
|
configDiv.appendChild(saveBtnNames); |
|
|
|
configDiv.classList.remove('hidden'); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('clearLog').addEventListener('click', () => { |
|
|
|
if (confirm('Sei sicuro di voler cancellare il log e i dati?')) { |
|
|
|
localStorage.clear(); |
|
|
|
location.reload(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
document.getElementById('exportLog').addEventListener('click', () => { |
|
|
|
const blob = new Blob([sessionLog], { type: 'text/plain' }); |
|
|
|
const link = document.createElement('a'); |
|
|
|
link.href = URL.createObjectURL(blob); |
|
|
|
link.download = `log_consiglio_${new Date().toLocaleDateString('it-IT').replace(/\//g, '-')}.txt`; |
|
|
|
link.click(); |
|
|
|
}); |
|
|
|
|
|
|
|
exportXMLBtn.addEventListener('click', () => { |
|
|
|
let xml = '<?xml version="1.0" encoding="UTF-8"?><participants>'; |
|
|
|
buttonLabels.forEach((name, i) => { |
|
|
|
const { x, y } = buttonCoords[i] || { x: 0, y: 0 }; |
|
|
|
xml += `<participant><name>${name}</name><x>${x}</x><y>${y}</y></participant>`; |
|
|
|
}); |
|
|
|
xml += '</participants>'; |
|
|
|
const blob = new Blob([xml], { type: 'application/xml' }); |
|
|
|
const link = document.createElement('a'); |
|
|
|
link.href = URL.createObjectURL(blob); |
|
|
|
link.download = `partecipanti_consiglio_${new Date().toLocaleDateString('it-IT').replace(/\//g, '-')}.xml`; |
|
|
|
link.click(); |
|
|
|
}); |
|
|
|
|
|
|
|
document.getElementById('importXML').addEventListener('click', () => document.getElementById('fileInput').click()); |
|
|
|
|
|
|
|
document.getElementById('fileInput').addEventListener('change', e => { |
|
|
|
const file = e.target.files[0]; |
|
|
|
if (!file) return; |
|
|
|
const reader = new FileReader(); |
|
|
|
reader.onload = evt => { |
|
|
|
const xml = new DOMParser().parseFromString(evt.target.result, 'application/xml'); |
|
|
|
const participants = xml.getElementsByTagName('participant'); |
|
|
|
buttonLabels = []; |
|
|
|
buttonCoords = []; |
|
|
|
for (const p of participants) { |
|
|
|
buttonLabels.push(p.getElementsByTagName('name')[0].textContent); |
|
|
|
buttonCoords.push({ |
|
|
|
x: parseInt(p.getElementsByTagName('x')[0]?.textContent || 0), |
|
|
|
y: parseInt(p.getElementsByTagName('y')[0]?.textContent || 0) |
|
|
|
}); |
|
|
|
} |
|
|
|
createButtons(); |
|
|
|
document.getElementById('sessionControls').classList.remove('hidden'); |
|
|
|
}; |
|
|
|
reader.readAsText(file); |
|
|
|
}); |
|
|
|
|
|
|
|
let dragEl = null, offsetX = 0, offsetY = 0; |
|
|
|
function startDrag(e) { |
|
|
|
dragEl = e.target; |
|
|
|
offsetX = e.offsetX; |
|
|
|
offsetY = e.offsetY; |
|
|
|
document.onmousemove = drag; |
|
|
|
document.onmouseup = stopDrag; |
|
|
|
} |
|
|
|
|
|
|
|
function drag(e) { |
|
|
|
if (!dragEl) return; |
|
|
|
const rect = map.getBoundingClientRect(); |
|
|
|
const x = e.clientX - rect.left - offsetX; |
|
|
|
const y = e.clientY - rect.top - offsetY; |
|
|
|
dragEl.style.left = `${x}px`; |
|
|
|
dragEl.style.top = `${y}px`; |
|
|
|
} |
|
|
|
|
|
|
|
function stopDrag() { |
|
|
|
if (dragEl) { |
|
|
|
const i = dragEl.dataset.index; |
|
|
|
buttonCoords[i] = { |
|
|
|
x: parseInt(dragEl.style.left), |
|
|
|
y: parseInt(dragEl.style.top) |
|
|
|
}; |
|
|
|
dragEl = null; |
|
|
|
document.onmousemove = null; |
|
|
|
document.onmouseup = null; |
|
|
|
saveSessionData(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
document.getElementById('mapConfig').addEventListener('click', () => { |
|
|
|
sessionState.mapVisible = true; |
|
|
|
saveSessionData(); |
|
|
|
renderMapConfig(); |
|
|
|
}); |
|
|
|
|
|
|
|
saveBtn.addEventListener('click', (event) => { |
|
|
|
if (map.classList.contains('hidden')) return; |
|
|
|
giveButtonFeedback(event.currentTarget); |
|
|
|
|
|
|
|
document.querySelectorAll('.map-button').forEach(b => { |
|
|
|
b.onmousedown = null; |
|
|
|
b.style.cursor = 'pointer'; |
|
|
|
b.onclick = () => { |
|
|
|
giveButtonFeedback(b); |
|
|
|
logSpeech(b.dataset.index); |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
sessionState.mapVisible = true; |
|
|
|
saveSessionData(); |
|
|
|
|
|
|
|
saveBtn.style.display = 'none'; |
|
|
|
saveBtn.classList.add('hidden'); |
|
|
|
exportXMLBtn.classList.remove('hidden'); |
|
|
|
}); |
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
|
map.classList.add('hidden'); |
|
|
|
saveBtn.classList.add('hidden'); |
|
|
|
saveBtn.style.display = 'none'; |
|
|
|
|
|
|
|
const savedLog = localStorage.getItem('sessionLog'); |
|
|
|
if (savedLog) sessionLog = savedLog; |
|
|
|
|
|
|
|
const savedLabels = localStorage.getItem('buttonLabels'); |
|
|
|
const savedCoords = localStorage.getItem('buttonCoords'); |
|
|
|
const savedState = localStorage.getItem('sessionState'); |
|
|
|
|
|
|
|
if (savedLabels && savedCoords) { |
|
|
|
buttonLabels = JSON.parse(savedLabels); |
|
|
|
buttonCoords = JSON.parse(savedCoords); |
|
|
|
createButtons(); |
|
|
|
document.getElementById('sessionControls').classList.remove('hidden'); |
|
|
|
} |
|
|
|
|
|
|
|
if (savedState) { |
|
|
|
sessionState = JSON.parse(savedState); |
|
|
|
if (sessionState.active && sessionState.startTime) { |
|
|
|
sessionStart = new Date(sessionState.startTime); |
|
|
|
document.getElementById('start').classList.add('hidden'); |
|
|
|
document.getElementById('stop').classList.remove('hidden'); |
|
|
|
document.getElementById('configure').classList.add('hidden'); |
|
|
|
} |
|
|
|
document.getElementById('currentSpeaker').textContent = sessionState.currentSpeaker || 'Nessuno'; |
|
|
|
|
|
|
|
if (sessionState.mapVisible) { |
|
|
|
renderMapView(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
applyButtonFeedback(); |
|
|
|
document.getElementById('numPartecipanti').value = ''; |
|
|
|
}); |
|
|
|
</script> |
|
|
|
</div> |
|
|
|
</body> |
|
|
|
</html> |
|
|
|
|