all repos — cameraman @ 347af7a085d07e23fe44d7a1366e71d8af37cece

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>