From 06adc0f3f5f08c22a95958df37006aaf1d38e527 Mon Sep 17 00:00:00 2001 From: Ulas Kalayci Date: Wed, 29 Apr 2026 10:37:16 +0200 Subject: [PATCH 1/2] feat: add edit button for ICS subscriptions (#100) Adds a pencil-icon edit button to each ICS subscription row. Clicking it opens a modal to update name, color, and shared visibility via PATCH /calendar/subscriptions/:id. Adds updatedToast i18n key to all 15 locales. Co-Authored-By: Claude Sonnet 4.6 --- public/locales/ar.json | 5 ++-- public/locales/de.json | 5 ++-- public/locales/el.json | 5 ++-- public/locales/en.json | 5 ++-- public/locales/es.json | 5 ++-- public/locales/fr.json | 5 ++-- public/locales/hi.json | 5 ++-- public/locales/it.json | 5 ++-- public/locales/ja.json | 5 ++-- public/locales/pt.json | 5 ++-- public/locales/ru.json | 5 ++-- public/locales/sv.json | 5 ++-- public/locales/tr.json | 5 ++-- public/locales/uk.json | 5 ++-- public/locales/zh.json | 5 ++-- public/pages/settings.js | 59 ++++++++++++++++++++++++++++++++++++++++ 16 files changed, 104 insertions(+), 30 deletions(-) diff --git a/public/locales/ar.json b/public/locales/ar.json index 7c5ebd3..5a2fafb 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -732,7 +732,8 @@ "badges": { "private": "خاص", "shared": "مشترك" - } + }, + "updatedToast": "تم تحديث الاشتراك." }, "memberPhoneLabel": "رقم الهاتف (اختياري)", "memberEmailLabel": "البريد الإلكتروني (اختياري)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "أنشئ وصفات واربطها بمخطط الوجبات." } -} +} \ No newline at end of file diff --git a/public/locales/de.json b/public/locales/de.json index 7836d42..7a6e61e 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -757,7 +757,8 @@ "empty": "Noch keine Abonnements.", "addedToast": "Abonnement hinzugefügt.", "syncedToast": "Abonnement synchronisiert.", - "deletedToast": "Abonnement gelöscht." + "deletedToast": "Abonnement gelöscht.", + "updatedToast": "Abonnement aktualisiert." }, "memberPhoneLabel": "Telefonnummer (optional)", "memberEmailLabel": "E-Mail (optional)", @@ -935,4 +936,4 @@ "goShop": "Einkaufsliste", "goNotes": "Notizen" } -} +} \ No newline at end of file diff --git a/public/locales/el.json b/public/locales/el.json index 2fc71ba..612be4a 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -732,7 +732,8 @@ "badges": { "private": "Ιδιωτικό", "shared": "Κοινόχρηστο" - } + }, + "updatedToast": "Η συνδρομή ενημερώθηκε." }, "memberPhoneLabel": "Αριθμός τηλεφώνου (προαιρετικό)", "memberEmailLabel": "Email (προαιρετικό)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Δημιουργήστε συνταγές και συνδέστε τις με τον προγραμματισμό γευμάτων." } -} +} \ No newline at end of file diff --git a/public/locales/en.json b/public/locales/en.json index 3bcf10b..bd783ec 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -732,7 +732,8 @@ "badges": { "private": "Private", "shared": "Shared" - } + }, + "updatedToast": "Subscription updated." }, "memberPhoneLabel": "Phone number (optional)", "memberEmailLabel": "Email (optional)", @@ -916,4 +917,4 @@ "birthdays": "Add birthdays — you will receive a reminder in time.", "recipes": "Create recipes and link them to your meal planner." } -} +} \ No newline at end of file diff --git a/public/locales/es.json b/public/locales/es.json index b2b70f8..2be26bd 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -732,7 +732,8 @@ "badges": { "private": "Privado", "shared": "Compartido" - } + }, + "updatedToast": "Suscripción actualizada." }, "memberPhoneLabel": "Número de teléfono (opcional)", "memberEmailLabel": "Correo electrónico (opcional)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Crea recetas y vincúlalas con tu planificador de comidas." } -} +} \ No newline at end of file diff --git a/public/locales/fr.json b/public/locales/fr.json index adb1b26..bd2e99d 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -732,7 +732,8 @@ "badges": { "private": "Privé", "shared": "Partagé" - } + }, + "updatedToast": "Abonnement mis à jour." }, "memberPhoneLabel": "Numéro de téléphone (facultatif)", "memberEmailLabel": "E-mail (facultatif)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Créez des recettes et associez-les à votre planification des repas." } -} +} \ No newline at end of file diff --git a/public/locales/hi.json b/public/locales/hi.json index 19c96d4..7f7bc16 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -732,7 +732,8 @@ "badges": { "private": "निजी", "shared": "साझा" - } + }, + "updatedToast": "सदस्यता अपडेट की गई।" }, "memberPhoneLabel": "फ़ोन नंबर (वैकल्पिक)", "memberEmailLabel": "ईमेल (वैकल्पिक)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "रेसिपी बनाएं और उन्हें अपने भोजन योजनाकार से जोड़ें।" } -} +} \ No newline at end of file diff --git a/public/locales/it.json b/public/locales/it.json index 20bf470..2dff300 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -732,7 +732,8 @@ "badges": { "private": "Privato", "shared": "Condiviso" - } + }, + "updatedToast": "Abbonamento aggiornato." }, "memberPhoneLabel": "Numero di telefono (opzionale)", "memberEmailLabel": "E-mail (opzionale)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Crea ricette e collegale al tuo piano pasti." } -} +} \ No newline at end of file diff --git a/public/locales/ja.json b/public/locales/ja.json index 2588864..c983173 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -732,7 +732,8 @@ "badges": { "private": "プライベート", "shared": "共有" - } + }, + "updatedToast": "サブスクリプションが更新されました。" }, "memberPhoneLabel": "電話番号(任意)", "memberEmailLabel": "メールアドレス(任意)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "レシピを作成して、食事プランに関連付けましょう。" } -} +} \ No newline at end of file diff --git a/public/locales/pt.json b/public/locales/pt.json index 9a85cea..ae55e25 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -732,7 +732,8 @@ "badges": { "private": "Privado", "shared": "Partilhado" - } + }, + "updatedToast": "Subscrição atualizada." }, "memberPhoneLabel": "Telefone (opcional)", "memberEmailLabel": "E-mail (opcional)", @@ -898,4 +899,4 @@ "emptyHint": { "recipes": "Crie receitas e vincule-as ao seu planejador de refeições." } -} +} \ No newline at end of file diff --git a/public/locales/ru.json b/public/locales/ru.json index 053d761..2e5881a 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -732,7 +732,8 @@ "badges": { "private": "Личное", "shared": "Общее" - } + }, + "updatedToast": "Подписка обновлена." }, "memberPhoneLabel": "Номер телефона (необязательно)", "memberEmailLabel": "Электронная почта (необязательно)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Создавайте рецепты и связывайте их с вашим планом питания." } -} +} \ No newline at end of file diff --git a/public/locales/sv.json b/public/locales/sv.json index c1a06a7..342d0f0 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -732,7 +732,8 @@ "badges": { "private": "Privat", "shared": "Delad" - } + }, + "updatedToast": "Prenumeration uppdaterad." }, "memberPhoneLabel": "Telefonnummer (valfritt)", "memberEmailLabel": "E-post (valfritt)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Skapa recept och koppla dem till din måltidsplanering." } -} +} \ No newline at end of file diff --git a/public/locales/tr.json b/public/locales/tr.json index 9c705c4..6088b79 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -732,7 +732,8 @@ "badges": { "private": "Özel", "shared": "Paylaşımlı" - } + }, + "updatedToast": "Abonelik güncellendi." }, "memberPhoneLabel": "Telefon numarası (isteğe bağlı)", "memberEmailLabel": "E-posta (isteğe bağlı)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "Tarifler oluşturun ve yemek planlayıcınıza bağlayın." } -} +} \ No newline at end of file diff --git a/public/locales/uk.json b/public/locales/uk.json index 5ab2109..5333a59 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -732,7 +732,8 @@ "badges": { "private": "Приватне", "shared": "Спільне" - } + }, + "updatedToast": "Підписку оновлено." }, "memberPhoneLabel": "Номер телефону (необов'язково)", "memberEmailLabel": "Електронна пошта (необов'язково)", @@ -905,4 +906,4 @@ "birthdays": "Додайте дні народження — ви отримаєте нагадування завчасно.", "recipes": "Створюйте рецепти та пов'язуйте їх із планувальником харчування." } -} +} \ No newline at end of file diff --git a/public/locales/zh.json b/public/locales/zh.json index d425200..cf396f7 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -732,7 +732,8 @@ "badges": { "private": "私人", "shared": "共享" - } + }, + "updatedToast": "订阅已更新。" }, "memberPhoneLabel": "电话号码(可选)", "memberEmailLabel": "电子邮件(可选)", @@ -897,4 +898,4 @@ "emptyHint": { "recipes": "创建食谱并将其关联到你的膳食计划。" } -} +} \ No newline at end of file diff --git a/public/pages/settings.js b/public/pages/settings.js index 08891a5..2f7d76f 100644 --- a/public/pages/settings.js +++ b/public/pages/settings.js @@ -1644,6 +1644,19 @@ function renderIcsList(container, subs, user) { syncBtn.appendChild(syncIcon); actions.appendChild(syncBtn); + const editBtn = document.createElement('button'); + editBtn.className = 'btn btn--icon btn--ghost'; + editBtn.title = t('settings.ics.actions.edit'); + editBtn.setAttribute('aria-label', t('settings.ics.actions.edit')); + editBtn.dataset.action = 'ics-edit'; + editBtn.dataset.id = sub.id; + const editIcon = document.createElement('i'); + editIcon.setAttribute('data-lucide', 'pencil'); + editIcon.style.cssText = 'width:14px;height:14px'; + editIcon.setAttribute('aria-hidden', 'true'); + editBtn.appendChild(editIcon); + actions.appendChild(editBtn); + const delBtn = document.createElement('button'); delBtn.className = 'btn btn--icon btn--danger-outline'; delBtn.title = t('settings.ics.actions.delete'); @@ -1756,6 +1769,52 @@ function bindIcsEvents(container, user, initialSubs) { } } + if (action === 'ics-edit') { + const sub = subs.find((s) => s.id === id); + if (!sub) return; + openModal({ + title: t('settings.ics.actions.edit'), + size: 'sm', + content: ` +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ `, + onSave: async (modalEl, close) => { + const name = modalEl.querySelector('#ics-edit-name').value.trim(); + const color = modalEl.querySelector('#ics-edit-color').value; + const shared = modalEl.querySelector('#ics-edit-shared').checked ? 1 : 0; + const errEl = modalEl.querySelector('#ics-edit-error'); + errEl.hidden = true; + try { + const res = await api.patch(`/calendar/subscriptions/${id}`, { name, color, shared }); + const idx = subs.findIndex((s) => s.id === id); + if (idx >= 0) subs[idx] = res.data; + renderIcsList(container, subs, user); + window.oikos?.showToast(t('settings.ics.updatedToast'), 'success'); + close(); + } catch (err) { + errEl.textContent = err.message ?? t('common.errorGeneric'); + errEl.hidden = false; + } + }, + }); + } + if (action === 'ics-delete') { const name = target.dataset.name; if (!await confirmModal(t('settings.ics.confirm_delete'), { danger: true, confirmLabel: t('common.delete') })) return; From 99783ca600b85cd7e014e33b769e56f56739fc1f Mon Sep 17 00:00:00 2001 From: Ulas Kalayci Date: Wed, 29 Apr 2026 10:39:17 +0200 Subject: [PATCH 2/2] fix: correct onSave pattern for ICS edit modal onSave(panel) is a setup hook, not a submit handler. Bind the form's submit event inside it and call closeModal({ force: true }) on success. Also add explicit submit/cancel buttons to the modal content. Co-Authored-By: Claude Sonnet 4.6 --- public/pages/settings.js | 47 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/public/pages/settings.js b/public/pages/settings.js index 2f7d76f..976ab2a 100644 --- a/public/pages/settings.js +++ b/public/pages/settings.js @@ -1791,26 +1791,37 @@ function bindIcsEvents(container, user, initialSubs) { - + +
+ + +
`, - onSave: async (modalEl, close) => { - const name = modalEl.querySelector('#ics-edit-name').value.trim(); - const color = modalEl.querySelector('#ics-edit-color').value; - const shared = modalEl.querySelector('#ics-edit-shared').checked ? 1 : 0; - const errEl = modalEl.querySelector('#ics-edit-error'); - errEl.hidden = true; - try { - const res = await api.patch(`/calendar/subscriptions/${id}`, { name, color, shared }); - const idx = subs.findIndex((s) => s.id === id); - if (idx >= 0) subs[idx] = res.data; - renderIcsList(container, subs, user); - window.oikos?.showToast(t('settings.ics.updatedToast'), 'success'); - close(); - } catch (err) { - errEl.textContent = err.message ?? t('common.errorGeneric'); - errEl.hidden = false; - } + onSave(panel) { + panel.querySelector('#ics-edit-cancel')?.addEventListener('click', () => closeModal()); + panel.querySelector('#ics-edit-form')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = panel.querySelector('[type=submit]'); + const errEl = panel.querySelector('#ics-edit-error'); + const name = panel.querySelector('#ics-edit-name').value.trim(); + const color = panel.querySelector('#ics-edit-color').value; + const shared = panel.querySelector('#ics-edit-shared').checked ? 1 : 0; + errEl.hidden = true; + submitBtn.disabled = true; + try { + const res = await api.patch(`/calendar/subscriptions/${id}`, { name, color, shared }); + const idx = subs.findIndex((s) => s.id === id); + if (idx >= 0) subs[idx] = res.data; + renderIcsList(container, subs, user); + window.oikos?.showToast(t('settings.ics.updatedToast'), 'success'); + closeModal({ force: true }); + } catch (err) { + errEl.textContent = err.message ?? t('common.errorGeneric'); + errEl.hidden = false; + submitBtn.disabled = false; + } + }); }, }); }