You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

407 lines
15 KiB

  1. <!DOCTYPE html>
  2. <html lang="it">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Consiglio Comunale</title>
  6. <style>
  7. body { font-family: Arial, sans-serif; background: #f0f2f5; padding: 20px; }
  8. .container { background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 1000px; margin: auto; }
  9. h1 { text-align: center; color: #333; }
  10. .input-group { display: flex; justify-content: center; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
  11. label { font-size: 18px; margin-right: 10px; }
  12. 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; }
  13. button { padding: 12px 24px; font-size: 16px; border: none; border-radius: 8px; cursor: pointer; margin: 5px; }
  14. #confirm { background-color: #28a745; color: white; }
  15. #start { background-color: #007bff; color: white; }
  16. #configure { background-color: #17a2b8; color: white; }
  17. #mapConfig { background-color: #fd7e14; color: white; }
  18. #stop { background-color: #dc3545; color: white; }
  19. #exportLog, #clearLog, #exportXML, #importXML { background-color: #6c757d; color: white; }
  20. #buttons, #config-buttons { margin-top: 20px; text-align: center; }
  21. #buttons button { background-color: #6f42c1; color: white; margin: 5px; position: relative; }
  22. #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; }
  23. #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; }
  24. .map-button { position: absolute; padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 8px; user-select: none; }
  25. .hidden { display: none; }
  26. #saveMapBtn { display: block; margin: 20px auto; }
  27. button.clicked-feedback { animation: buttonFlash 0.3s ease-in-out; }
  28. @keyframes buttonFlash {
  29. 0% { transform: scale(1); filter: brightness(1); }
  30. 50% { transform: scale(1.1); filter: brightness(1.5); }
  31. 100% { transform: scale(1); filter: brightness(1); }
  32. }
  33. </style>
  34. </head>
  35. <body>
  36. <div class="container">
  37. <h1>Consiglio Comunale</h1>
  38. <div class="input-group">
  39. <label for="numPartecipanti">Numero partecipanti:</label>
  40. <input type="number" id="numPartecipanti" min="1" />
  41. <button id="confirm">Conferma</button>
  42. <button id="importXML">Importa Partecipanti (XML)</button>
  43. <input type="file" id="fileInput" accept=".xml" style="display:none">
  44. </div>
  45. <div class="input-group hidden" id="sessionControls">
  46. <button id="start">Avvia</button>
  47. <button id="configure">Assegna nomi</button>
  48. <button id="mapConfig">Configura mappa</button>
  49. <button id="stop" class="hidden">Termina</button>
  50. <button id="exportLog">Esporta Log</button>
  51. <button id="clearLog">Pulisci Log</button>
  52. <button id="exportXML" class="hidden">Esporta Elenco Partecipanti (XML)</button>
  53. </div>
  54. <div id="buttons" class="hidden"></div>
  55. <div id="config-buttons" class="hidden"></div>
  56. <div id="mapContainer" class="hidden"></div>
  57. <button id="saveMapBtn" class="hidden">Salva Mappa</button>
  58. <h2 style="text-align: center;">Chi sta parlando:</h2>
  59. <div id="currentSpeaker">Nessuno</div>
  60. <script>
  61. let buttonLabels = [];
  62. let buttonCoords = [];
  63. let sessionLog = '';
  64. let sessionStart = null;
  65. let sessionState = {
  66. active: false,
  67. startTime: null,
  68. currentSpeaker: 'Nessuno',
  69. mapVisible: false
  70. };
  71. const exportXMLBtn = document.getElementById('exportXML');
  72. const saveBtn = document.getElementById('saveMapBtn');
  73. const map = document.getElementById('mapContainer');
  74. function giveButtonFeedback(btn) {
  75. btn.classList.add('clicked-feedback');
  76. setTimeout(() => btn.classList.remove('clicked-feedback'), 300);
  77. }
  78. function applyButtonFeedback() {
  79. document.querySelectorAll('button').forEach(btn => {
  80. btn.addEventListener('click', () => giveButtonFeedback(btn));
  81. });
  82. }
  83. function saveSessionLog() {
  84. localStorage.setItem('sessionLog', sessionLog);
  85. }
  86. function saveSessionData() {
  87. localStorage.setItem('buttonLabels', JSON.stringify(buttonLabels));
  88. localStorage.setItem('buttonCoords', JSON.stringify(buttonCoords));
  89. localStorage.setItem('sessionState', JSON.stringify(sessionState));
  90. }
  91. function logSpeech(index) {
  92. const now = new Date();
  93. const time = sessionStart ? new Date(now - sessionStart).toISOString().substr(11, 8) : now.toLocaleTimeString();
  94. const entry = `[${time}] Parla: ${buttonLabels[index]}\n`;
  95. sessionLog += entry;
  96. sessionState.currentSpeaker = buttonLabels[index];
  97. saveSessionLog();
  98. saveSessionData();
  99. document.getElementById('currentSpeaker').textContent = buttonLabels[index];
  100. }
  101. function createButtons() {
  102. const div = document.getElementById('buttons');
  103. div.innerHTML = '<hr style="margin: 20px 0;">';
  104. buttonLabels.forEach((name, i) => {
  105. const btn = document.createElement('button');
  106. btn.textContent = name;
  107. btn.addEventListener('click', () => logSpeech(i));
  108. div.appendChild(btn);
  109. });
  110. div.classList.remove('hidden');
  111. applyButtonFeedback();
  112. saveSessionData();
  113. }
  114. function renderMapConfig() {
  115. if (buttonLabels.length === 0) return;
  116. map.innerHTML = '';
  117. map.classList.remove('hidden');
  118. saveBtn.classList.remove('hidden');
  119. saveBtn.style.display = 'block';
  120. const cols = Math.ceil(Math.sqrt(buttonLabels.length));
  121. const spacingX = map.clientWidth / (cols + 1);
  122. const spacingY = map.clientHeight / (cols + 1);
  123. buttonLabels.forEach((name, i) => {
  124. const x = buttonCoords[i].x;
  125. const y = buttonCoords[i].y;
  126. const b = document.createElement('button');
  127. b.textContent = name;
  128. b.className = 'map-button';
  129. b.style.left = `${x}px`;
  130. b.style.top = `${y}px`;
  131. b.setAttribute('data-index', i);
  132. b.onmousedown = startDrag;
  133. map.appendChild(b);
  134. });
  135. }
  136. function renderMapView() {
  137. if (buttonLabels.length === 0) return;
  138. map.innerHTML = '';
  139. map.classList.remove('hidden');
  140. saveBtn.classList.add('hidden');
  141. buttonLabels.forEach((name, i) => {
  142. const b = document.createElement('button');
  143. b.textContent = name;
  144. b.className = 'map-button';
  145. b.style.left = `${buttonCoords[i].x}px`;
  146. b.style.top = `${buttonCoords[i].y}px`;
  147. b.style.cursor = 'pointer';
  148. b.onclick = () => {
  149. giveButtonFeedback(b);
  150. logSpeech(i);
  151. };
  152. map.appendChild(b);
  153. });
  154. }
  155. document.getElementById('confirm').addEventListener('click', () => {
  156. const n = parseInt(document.getElementById('numPartecipanti').value);
  157. if (!n || n < 1) return alert('Inserisci un numero valido');
  158. const gruppi = ['Centro', 'Sinistra', 'Destra'];
  159. buttonLabels = Array.from({ length: n }, (_, i) => {
  160. const gruppo = gruppi[i % gruppi.length];
  161. const numero = Math.floor(i / gruppi.length) + 1;
  162. return `${gruppo} ${numero}`;
  163. });
  164. buttonCoords = Array.from({ length: n }, () => ({ x: 0, y: 0 }));
  165. createButtons();
  166. document.getElementById('sessionControls').classList.remove('hidden');
  167. });
  168. document.getElementById('start').addEventListener('click', () => {
  169. if (!sessionStart) {
  170. sessionStart = new Date();
  171. sessionState.active = true;
  172. sessionState.startTime = sessionStart.getTime();
  173. sessionLog += `[00:00:00] Sessione avviata (${sessionStart.toLocaleString('it-IT')})\n`;
  174. saveSessionLog();
  175. saveSessionData();
  176. document.getElementById('stop').classList.remove('hidden');
  177. document.getElementById('start').classList.add('hidden');
  178. document.getElementById('configure').classList.add('hidden');
  179. }
  180. });
  181. document.getElementById('stop').addEventListener('click', () => {
  182. if (sessionStart) {
  183. const end = new Date();
  184. const duration = new Date(end - sessionStart).toISOString().substr(11, 8);
  185. sessionLog += `[${duration}] Sessione terminata (${end.toLocaleString('it-IT')})\n`;
  186. sessionState.active = false;
  187. sessionState.startTime = null;
  188. saveSessionLog();
  189. saveSessionData();
  190. document.getElementById('stop').classList.add('hidden');
  191. document.getElementById('start').classList.remove('hidden');
  192. document.getElementById('configure').classList.remove('hidden');
  193. }
  194. });
  195. document.getElementById('configure').addEventListener('click', () => {
  196. const configDiv = document.getElementById('config-buttons');
  197. configDiv.innerHTML = '';
  198. saveBtn.classList.add('hidden');
  199. buttonLabels.forEach((name, i) => {
  200. const input = document.createElement('input');
  201. input.type = 'text';
  202. input.value = name;
  203. input.dataset.index = i;
  204. input.style.margin = '5px';
  205. input.style.padding = '8px';
  206. configDiv.appendChild(input);
  207. });
  208. const saveBtnNames = document.createElement('button');
  209. saveBtnNames.textContent = 'Salva Nomi';
  210. saveBtnNames.style.backgroundColor = '#28a745';
  211. saveBtnNames.style.color = 'white';
  212. saveBtnNames.style.marginTop = '20px';
  213. saveBtnNames.style.display = 'block';
  214. saveBtnNames.onclick = () => {
  215. configDiv.querySelectorAll('input').forEach(input => {
  216. const idx = parseInt(input.dataset.index);
  217. buttonLabels[idx] = input.value;
  218. });
  219. saveSessionData();
  220. configDiv.classList.add('hidden');
  221. createButtons();
  222. };
  223. configDiv.appendChild(saveBtnNames);
  224. configDiv.classList.remove('hidden');
  225. });
  226. document.getElementById('clearLog').addEventListener('click', () => {
  227. if (confirm('Sei sicuro di voler cancellare il log e i dati?')) {
  228. localStorage.clear();
  229. location.reload();
  230. }
  231. });
  232. document.getElementById('exportLog').addEventListener('click', () => {
  233. const blob = new Blob([sessionLog], { type: 'text/plain' });
  234. const link = document.createElement('a');
  235. link.href = URL.createObjectURL(blob);
  236. link.download = `log_consiglio_${new Date().toLocaleDateString('it-IT').replace(/\//g, '-')}.txt`;
  237. link.click();
  238. });
  239. exportXMLBtn.addEventListener('click', () => {
  240. let xml = '<?xml version="1.0" encoding="UTF-8"?><participants>';
  241. buttonLabels.forEach((name, i) => {
  242. const { x, y } = buttonCoords[i] || { x: 0, y: 0 };
  243. xml += `<participant><name>${name}</name><x>${x}</x><y>${y}</y></participant>`;
  244. });
  245. xml += '</participants>';
  246. const blob = new Blob([xml], { type: 'application/xml' });
  247. const link = document.createElement('a');
  248. link.href = URL.createObjectURL(blob);
  249. link.download = `partecipanti_consiglio_${new Date().toLocaleDateString('it-IT').replace(/\//g, '-')}.xml`;
  250. link.click();
  251. });
  252. document.getElementById('importXML').addEventListener('click', () => document.getElementById('fileInput').click());
  253. document.getElementById('fileInput').addEventListener('change', e => {
  254. const file = e.target.files[0];
  255. if (!file) return;
  256. const reader = new FileReader();
  257. reader.onload = evt => {
  258. const xml = new DOMParser().parseFromString(evt.target.result, 'application/xml');
  259. const participants = xml.getElementsByTagName('participant');
  260. buttonLabels = [];
  261. buttonCoords = [];
  262. for (const p of participants) {
  263. buttonLabels.push(p.getElementsByTagName('name')[0].textContent);
  264. buttonCoords.push({
  265. x: parseInt(p.getElementsByTagName('x')[0]?.textContent || 0),
  266. y: parseInt(p.getElementsByTagName('y')[0]?.textContent || 0)
  267. });
  268. }
  269. createButtons();
  270. document.getElementById('sessionControls').classList.remove('hidden');
  271. };
  272. reader.readAsText(file);
  273. });
  274. let dragEl = null, offsetX = 0, offsetY = 0;
  275. function startDrag(e) {
  276. dragEl = e.target;
  277. offsetX = e.offsetX;
  278. offsetY = e.offsetY;
  279. document.onmousemove = drag;
  280. document.onmouseup = stopDrag;
  281. }
  282. function drag(e) {
  283. if (!dragEl) return;
  284. const rect = map.getBoundingClientRect();
  285. const x = e.clientX - rect.left - offsetX;
  286. const y = e.clientY - rect.top - offsetY;
  287. dragEl.style.left = `${x}px`;
  288. dragEl.style.top = `${y}px`;
  289. }
  290. function stopDrag() {
  291. if (dragEl) {
  292. const i = dragEl.dataset.index;
  293. buttonCoords[i] = {
  294. x: parseInt(dragEl.style.left),
  295. y: parseInt(dragEl.style.top)
  296. };
  297. dragEl = null;
  298. document.onmousemove = null;
  299. document.onmouseup = null;
  300. saveSessionData();
  301. }
  302. }
  303. document.getElementById('mapConfig').addEventListener('click', () => {
  304. sessionState.mapVisible = true;
  305. saveSessionData();
  306. renderMapConfig();
  307. });
  308. saveBtn.addEventListener('click', (event) => {
  309. if (map.classList.contains('hidden')) return;
  310. giveButtonFeedback(event.currentTarget);
  311. document.querySelectorAll('.map-button').forEach(b => {
  312. b.onmousedown = null;
  313. b.style.cursor = 'pointer';
  314. b.onclick = () => {
  315. giveButtonFeedback(b);
  316. logSpeech(b.dataset.index);
  317. };
  318. });
  319. sessionState.mapVisible = true;
  320. saveSessionData();
  321. saveBtn.style.display = 'none';
  322. saveBtn.classList.add('hidden');
  323. exportXMLBtn.classList.remove('hidden');
  324. });
  325. window.addEventListener('load', () => {
  326. map.classList.add('hidden');
  327. saveBtn.classList.add('hidden');
  328. saveBtn.style.display = 'none';
  329. const savedLog = localStorage.getItem('sessionLog');
  330. if (savedLog) sessionLog = savedLog;
  331. const savedLabels = localStorage.getItem('buttonLabels');
  332. const savedCoords = localStorage.getItem('buttonCoords');
  333. const savedState = localStorage.getItem('sessionState');
  334. if (savedLabels && savedCoords) {
  335. buttonLabels = JSON.parse(savedLabels);
  336. buttonCoords = JSON.parse(savedCoords);
  337. createButtons();
  338. document.getElementById('sessionControls').classList.remove('hidden');
  339. }
  340. if (savedState) {
  341. sessionState = JSON.parse(savedState);
  342. if (sessionState.active && sessionState.startTime) {
  343. sessionStart = new Date(sessionState.startTime);
  344. document.getElementById('start').classList.add('hidden');
  345. document.getElementById('stop').classList.remove('hidden');
  346. document.getElementById('configure').classList.add('hidden');
  347. }
  348. document.getElementById('currentSpeaker').textContent = sessionState.currentSpeaker || 'Nessuno';
  349. if (sessionState.mapVisible) {
  350. renderMapView();
  351. }
  352. }
  353. applyButtonFeedback();
  354. document.getElementById('numPartecipanti').value = '';
  355. });
  356. </script>
  357. </div>
  358. </body>
  359. </html>