| @@ -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> | |||