templates/index.html (view raw)
1<!DOCTYPE html>
2<html>
3
4<head>
5 <title>Ricorrenze</title>
6 <link rel="icon"
7 href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>📅</text></svg>">
8 </link>
9 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
10 <style>
11 :root {
12 color-scheme: light dark;
13 --text: #000;
14 --bg: #fff;
15 --green: #4CAF50;
16 --hover-green: #45a049;
17 --red: #af4c4c;
18 --hover-red: #a14545;
19 }
20
21 @media (prefers-color-scheme: dark) {
22 :root {
23 color-scheme: dark light;
24 --text: #fff;
25 --bg: #121212;
26 --green: #265929;
27 --hover-green: #214d23;
28 --red: #592626;
29 --hover-red: #4d2121;
30 }
31 }
32
33 body {
34 font-family: Arial, sans-serif;
35 background-color: var(--bg);
36 color: var(--text);
37 }
38
39 table {
40 width: 100%;
41 border-collapse: collapse;
42 }
43
44 table,
45 th,
46 td {
47 border: 1px solid var(--text);
48 }
49
50 th,
51 td {
52 padding: 10px;
53 text-align: left;
54 }
55
56 th {
57 background-color: var(--bg);
58 }
59
60 .next {
61 background-color: var(--red);
62 color: white;
63 }
64
65 .date-inputs {
66 display: flex;
67 align-items: center;
68 border: none;
69 gap: 2px;
70 }
71
72 .small-input {
73 width: 32px;
74 }
75
76 .big-input {
77 width: 100%;
78 }
79
80 .actions i {
81 cursor: pointer;
82 padding-right: 5px;
83 }
84
85 .cute-button {
86 margin-top: 20px;
87 background-color: var(--green);
88 border: none;
89 color: white;
90 padding: 10px 20px;
91 text-align: center;
92 text-decoration: none;
93 display: inline-block;
94 font-size: 16px;
95 margin: 4px 2px;
96 transition-duration: 0.4s;
97 cursor: pointer;
98 border-radius: 12px;
99 }
100
101 .cute-button:hover {
102 background-color: var(--hover-green);
103 }
104
105 .cute-button-red {
106 background-color: var(--red);
107 }
108
109 .cute-button-red:hover {
110 background-color: var(--hover-red);
111 }
112
113 .hidden {
114 display: none;
115 }
116 </style>
117</head>
118
119<body>
120 <h1>Ricorrenzes</h1>
121 <table id="main-table">
122 <tr>
123 <th data-field="name">Nome</th>
124 <th data-field="description">Descrizione</th>
125 <th data-field="date">Data (gg/mm[/yyyy])</th>
126 <th data-field="notify">Notifica</th>
127 <th data-field="notified">Inviata</th>
128 <th data-field="actions">Azioni</th>
129 </tr>
130 {{ range .Occurrences }}
131 <tr id="occurrence-{{ .ID }}">
132 <td data-field="name" data-value="{{ .Name }}">{{ .Name }}</td>
133 <td data-field="description" data-value="{{ .Description }}">{{ .Description }}</td>
134 <td data-field="date" data-value="{{ padZero .Day }}/{{ padZero .Month }}{{if .Year }}/{{ .Year }}{{end}}">{{ padZero .Day }}/{{ padZero .Month }}{{if .Year }}/{{ .Year }} (+{{ calcYear $.CurrentYear .Year }}){{end}}</td>
135 <td data-field="notify" data-value="{{ .Notify }}"><input type="checkbox" {{if .Notify}}checked{{end}} disabled></td>
136 <td data-field="notified" data-value="{{ .Notified }}"><input type="checkbox" {{if .Notified}}checked{{end}} disabled></td>
137 <td data-field="actions" class="actions">
138 <i class="fas fa-edit" title="Modifica" onclick="editOccurrence('{{ .ID }}')"></i>
139 <i class="fas fa-trash-alt" title="Elimina" onclick="deleteOccurrence('{{ .ID }}')"></i>
140 </td>
141 </tr>
142 {{ end }}
143 <tr id="occurrence-none" class="hidden">
144 <td colspan="6">Nessuna ricorrenza.</td>
145 </tr>
146 </table>
147 <div style="margin-top: 10px; text-align: center;">
148 <button id="add-row-button" class="cute-button" onclick="addNewOccurrenceRow()">
149 <i class="fas fa-plus"></i> Aggiungi</button>
150 <button id="save-row-button" class="cute-button hidden" onclick="saveOccurrence(0)">
151 <i class="fas fa-save"></i> Salva
152 </button>
153 <button id="cancel-row-button" class="cute-button cute-button-red hidden" onclick="cancelNewOccurrence()">
154 <i class="fas fa-times"></i> Annulla
155 </button>
156 </div>
157
158 <script>
159 const hiddenClass = 'hidden';
160 const addButton = document.getElementById('add-row-button');
161 const saveButton = document.getElementById('save-row-button');
162 const cancelButton = document.getElementById('cancel-row-button');
163 const mainTable = document.getElementById('main-table');
164 const noneRow = document.getElementById('occurrence-none');
165
166 const now = new Date();
167 const currentYear = now.getFullYear();
168 const dataError = 'Controlla che i dati (e le date) siano corretti.';
169
170 let currentNext = null;
171
172 function updateRowDisplay() {
173 const tbody = mainTable.querySelector('tbody');
174 const rows = Array.from(tbody.querySelectorAll('tr[id]:not(#occurrence-none):not(#new-occurrence)'));
175
176 if (rows.length === 0) {
177 noneRow.classList.remove(hiddenClass);
178 return;
179 }
180 noneRow.classList.add(hiddenClass);
181
182 const valueRows = rows.filter((row) => !row.classList.contains('editing'));
183
184 // Sort rows by date
185 valueRows.sort((a, b) => {
186 const dateA = parseDateString(getValueFromRow(a, 'date'));
187 const dateB = parseDateString(getValueFromRow(b, 'date'));
188 return dateA - dateB;
189 });
190
191 // Re-append sorted rows to tbody
192 valueRows.forEach(row => tbody.appendChild(row));
193
194 findNextOccurrence();
195 }
196
197 function deleteOccurrence(id) {
198 if (confirm('Sei sicuro di voler eliminare questa ricorrenza?')) {
199 fetch(`/occurrences/${id}`, {
200 method: 'DELETE'
201 })
202 .then(response => {
203 if (!response.ok) {
204 console.error('Error:', response.status);
205 alert('Eliminazione fallita.');
206 return;
207 }
208 const deletedRow = document.getElementById(`occurrence-${id}`);
209 deletedRow.parentElement.removeChild(deletedRow);
210 updateRowDisplay();
211 })
212 .catch(error => {
213 console.error('Error:', error);
214 alert(dataError);
215 });
216 }
217 }
218
219 function padNumber(input, n = 2) {
220 return input.toString().padStart(n, '0');
221 }
222
223 function padMax(input, max = 31, min=1) {
224 const n = Number(input);
225 if (isNaN(n)) {
226 console.log(max)
227 return max;}
228 return padNumber(Math.max(min, Math.min(n | 0, max)))
229 }
230
231 function padYear(input, max, min) {
232 if (input == '') return '';
233 return padMax(input, max, min)
234 }
235
236 function handleInputKeyDown(event, id, isNew) {
237 if (event.key !== 'Enter') return;
238 saveOccurrence(isNew ? 0 : id);
239 }
240
241 function createRow(id, name, description, day, month, year, notify, notified) {
242 return `
243 <td data-field="name" data-value="${name}">${name}</td>
244 <td data-field="description" data-value="${description}">${description}</td>
245 <td data-field="date" data-value="${padNumber(day)}/${padNumber(month)}${year ? `/${padNumber(year, 4)}` : ''}">${padNumber(day)}/${padNumber(month)}${year ? `/${year} (+${currentYear - year})` : ''}</td>
246 <td data-field="notify" data-value="${notify}"><input type="checkbox" ${notify ? 'checked' : ''} disabled></td>
247 <td data-field="notified" data-value="${notified}"><input type="checkbox" ${notified ? 'checked' : ''} disabled></td>
248 <td data-field="actions" class="actions">
249 <i class="fas fa-edit" title="Edit" onclick="editOccurrence(${id})"></i>
250 <i class="fas fa-trash-alt" title="Delete" onclick="deleteOccurrence(${id})"></i>
251 </td>
252 `;
253 }
254
255 function escapeQuotes(str) {
256 return str.replace(/'/g, "\\'");
257 }
258
259 function createInputFields(id, name, description, day, month, year, notify, notified, isNew) {
260 const myName = name || '';
261 const myDescription = description || '';
262 const myDay = day || '01';
263 const myMonth = month || '01';
264 const myYear = year || '';
265 console.log(notified);
266 console.log(typeof notified);
267 return `
268 <td data-field="name" data-value="${myName}"><input class="big-input" type="text" value="${myName}" id="name-${id}" onkeydown="handleInputKeyDown(event, ${id})" autocomplete="off"></td>
269 <td data-field="description" data-value="${myDescription}"><input class="big-input" type="text" value="${myDescription}" id="description-${id}" onkeydown="handleInputKeyDown(event, ${id})" autocomplete="off"></td>
270 <td data-field="date" data-value="${myDay}/${myMonth}" class="date-inputs">
271 <input type="number" step="1" value="${myDay}" id="day-${id}" class="small-input" min="1" max="31" onchange="this.value = padMax(this.value, this.max);" onclick="this.select()" onkeydown="handleInputKeyDown(event, ${id})"> /
272 <input type="number" step="1" value="${myMonth}" id="month-${id}" class="small-input" min="1" max="12" onchange="this.value = padMax(this.value, this.max);" onclick="this.select()" onkeydown="handleInputKeyDown(event, ${id})"> /
273 <input type="number" step="1" value="${myYear}" id="year-${id}" class="big-input" min="1000" max="${currentYear}" onchange="this.value = padYear(this.value, this.max, this.min);" onclick="this.select()" onkeydown="handleInputKeyDown(event, ${id}, ${isNew})">
274 </td>
275 <td data-field="notify" data-value="${notify}"><input type="checkbox" id="notify-${id}"${notify === 'true' ? ' checked' : ''}></td>
276 <td data-field="notified" data-value="${notified}"><input type="checkbox" id="notified-${id}"${notified === 'true' ? ' checked' : ' disabled'}></td>
277 <td class="actions">
278 ${isNew ? '' : `
279 <i class="fas fa-save" title="Save" onclick="saveOccurrence(${id})"></i>
280 <i class="fas fa-times" title="Cancel" onclick="cancelEdit(${id}, '${escapeQuotes(name)}', '${escapeQuotes(description)}', ${day}, ${month}, ${year}, ${notify}, ${notified})"></i>
281 `}
282 </td>
283 `;
284 }
285
286 function editOccurrence(id) {
287 const row = document.getElementById(`occurrence-${id}`);
288 const cells = row.getElementsByTagName('td');
289
290 const name = getValueFromRow(row, 'name');
291 const description = getValueFromRow(row, 'description');
292 const [day, month, year] = getValueFromRow(row, 'date').split('/');
293 const notify = getValueFromRow(row, 'notify');
294 const notified = getValueFromRow(row, 'notified');
295
296 row.innerHTML = createInputFields(id, name, description, day, month, year, notify, notified, false);
297 row.classList.add('editing');
298 }
299
300 function cancelEdit(id, name, description, day, month, year, notify, notified) {
301 const row = document.getElementById(`occurrence-${id}`);
302 row.innerHTML = createRow(id, name, description, day, month, year, notify, notified);
303 }
304
305 function saveOccurrence(id) {
306 const name = document.getElementById(`name-${id}`).value;
307 const description = document.getElementById(`description-${id}`).value;
308 const day = parseInt(document.getElementById(`day-${id}`).value);
309 const month = parseInt(document.getElementById(`month-${id}`).value);
310 const year = parseInt(document.getElementById(`year-${id}`).value);
311 const notify = document.getElementById(`notify-${id}`).checked;
312 const notified = document.getElementById(`notified-${id}`).checked;
313
314 const isNew = id === 0;
315 const updatedData = {
316 id: isNew ? undefined : id,
317 name: name,
318 description: description,
319 day: day,
320 month: month,
321 year: isNaN(year) ? null : year,
322 notify: notify,
323 notified: notified
324 };
325
326 fetch('/occurrences', {
327 method: 'POST',
328 headers: {
329 'Content-Type': 'application/json'
330 },
331 body: JSON.stringify(updatedData)
332 })
333 .then(response => {
334 if (!response.ok) {
335 throw new Error(response.status)
336 }
337 if (isNew) {
338 cancelNewOccurrence();
339 mainTable.insertRow(-1).id = `occurrence-${id}`
340 }
341 updateRow(`occurrence-${id}`, response);
342 })
343 .catch(error => {
344 console.error('Error:', error);
345 alert(dataError);
346 });
347 }
348
349 function addNewOccurrenceRow() {
350 const newRow = mainTable.insertRow(-1);
351 newRow.id = 'new-occurrence';
352 newRow.innerHTML = createInputFields('0', '', '', '', '', '', "true", "false", true);
353
354 hideAddButton();
355 }
356 function hideAddButton() {
357 addButton.classList.add(hiddenClass);
358 saveButton.classList.remove(hiddenClass);
359 cancelButton.classList.remove(hiddenClass);
360 }
361
362 function showAddButton() {
363 addButton.classList.remove(hiddenClass);
364 saveButton.classList.add(hiddenClass);
365 cancelButton.classList.add(hiddenClass);
366 }
367
368 function updateRow(rowElementId, response) {
369 const newRow = document.getElementById(rowElementId);
370 response.json().then((res) => {
371 newRow.id = `occurrence-${res.id}`;
372 newRow.innerHTML = createRow(res.id, res.name, res.description, res.day, res.month, res.year, res.notify, res.notified);
373 newRow.classList.remove('editing');
374 updateRowDisplay()
375 });
376 }
377
378 function cancelNewOccurrence() {
379 const newRow = document.getElementById('new-occurrence');
380 newRow.parentNode.removeChild(newRow);
381 showAddButton();
382 updateRowDisplay();
383 }
384
385 function getValueFromRow(row, field) {
386 return row.querySelector(`td[data-field="${field}"]`).getAttribute('data-value');
387 }
388
389 function parseDateString(dateString) {
390 const [day, month, year] = dateString.split('/').map((x) => Number(x));
391 return new Date(currentYear, month - 1, day, 23, 59, 59);
392 }
393
394 function findNextOccurrence() {
395 if (currentNext !== null) {
396 currentNext.classList.remove('next');
397 }
398 const occurrenceRows = Array.from(mainTable.querySelectorAll("tr[id]:not(#occurrence-none):not(#new-occurrence)"));
399 const occurrences = occurrenceRows.map((row) => {
400 const id = row.id.split('-')[1];
401 const dateString = getValueFromRow(row, 'date');
402 const date = parseDateString(dateString);
403
404 return { id, date };
405 });
406
407 const deltas = occurrences.map((x) => ({ ...x, distance: x.date - now })).filter((x) => x.distance > 0);
408 if (deltas.length == 0) return;
409
410 const distances = deltas.map((x) => x.distance);
411 const minDistance = Math.min(...distances);
412 const minDelta = deltas.find((x) => x.distance == minDistance);
413
414 currentNext = occurrenceRows.find((row) => row.id === `occurrence-${minDelta.id}`);
415 currentNext.classList.add('next');
416 }
417
418 updateRowDisplay();
419 </script>
420
421</body>
422
423</html>