From f3a7f869604d91593eecf9a06948c715a010f873 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Tue, 14 Apr 2026 13:47:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(classifieds):=20green=20=E2=9C=93=20on=20f?= =?UTF-8?q?illed=20required=20fields,=20drop=20full-form=20red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The :invalid CSS without scoping was making the entire form-container draw a red border (the form is :invalid as long as any inner field is). Removed it. Replaced with positive feedback: a green ✓ appears next to the label of each required field as soon as it is filled. Tracks title, category, listing_type radios and Quill description (new.html) plus title and description (edit.html). Initial pass at load sets the check on values restored after a POST validation error. Co-Authored-By: Claude Opus 4.6 (1M context) --- templates/classifieds/edit.html | 26 +++++++++++++-- templates/classifieds/new.html | 58 ++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/templates/classifieds/edit.html b/templates/classifieds/edit.html index aa2c4ae..d23ecfb 100644 --- a/templates/classifieds/edit.html +++ b/templates/classifieds/edit.html @@ -51,11 +51,13 @@ .quill-container .ql-toolbar { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); } .quill-container .ql-container { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); font-size: var(--font-size-base); } .quill-container .ql-editor { min-height: 150px; } - .field-error, input.field-error, .quill-container.field-error { + input.field-error, .quill-container.field-error { border: 2px solid #dc2626 !important; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); } - :invalid:not(:focus):not(:placeholder-shown) { border: 2px solid #dc2626; } + .form-group.field-valid > label::after { + content: " ✓"; color: #16a34a; font-weight: 700; margin-left: 4px; + } /* Existing attachments */ .existing-attachment { position: relative; border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-xs); background: var(--surface); } @@ -192,7 +194,25 @@ quill.root.innerHTML = {{ classified.description|tojson }}; (function() { var qc = document.getElementById('quill-editor'); - quill.on('text-change', function() { qc && qc.classList.remove('field-error'); }); + function setValid(g, ok) { + if (!g) return; + if (ok) { g.classList.add('field-valid'); g.classList.remove('field-error'); } + else g.classList.remove('field-valid'); + } + var titleEl = document.getElementById('title'); + var titleGrp = titleEl.closest('.form-group'); + var descGrp = qc ? qc.closest('.form-group') : null; + function refreshTitle() { setValid(titleGrp, titleEl.value.trim().length > 0); } + function refreshDesc() { + var html = quill.root.innerHTML; + var ok = !(html === '


' || quill.getText().trim() === ''); + setValid(descGrp, ok); + if (ok && qc) qc.classList.remove('field-error'); + } + titleEl.addEventListener('input', refreshTitle); + quill.on('text-change', refreshDesc); + refreshTitle(); refreshDesc(); + document.getElementById('classifiedForm').addEventListener('submit', function(e) { var html = quill.root.innerHTML; var empty = (html === '


' || quill.getText().trim() === ''); diff --git a/templates/classifieds/new.html b/templates/classifieds/new.html index 41b2c5f..81df2f2 100755 --- a/templates/classifieds/new.html +++ b/templates/classifieds/new.html @@ -25,9 +25,8 @@ .quill-container .ql-editor { min-height: 150px; } - /* Highlight required fields that failed validation. Applied by server - on POST validation error and by client JS on Quill empty submit. */ - .field-error, + /* Red border on required fields that failed validation. Applied by + server on POST error and by client JS on submit-with-empty Quill. */ input.field-error, select.field-error, .type-selector.field-error, @@ -35,9 +34,13 @@ border: 2px solid #dc2626 !important; box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); } - :invalid:not(:focus):not(:placeholder-shown), - select:invalid:not(:focus) { - border: 2px solid #dc2626; + /* Green checkmark next to label of filled required fields. JS toggles + .field-valid on the form-group as the user types/picks. */ + .form-group.field-valid > label::after { + content: " ✓"; + color: #16a34a; + font-weight: 700; + margin-left: 4px; } .form-container { max-width: 700px; @@ -390,8 +393,47 @@ var quill = new Quill('#quill-editor', { // Restore from server-rendered textarea (e.g. after POST validation error) var initialDesc = document.getElementById('description').value; if (initialDesc) { quill.root.innerHTML = initialDesc; } - // Clear error highlight as soon as user starts typing - quill.on('text-change', function() { qc && qc.classList.remove('field-error'); }); + + // Toggle .field-valid on a .form-group based on a "is filled" check. + function setValid(group, ok) { + if (!group) return; + if (ok) { + group.classList.add('field-valid'); + group.classList.remove('field-error'); + } else { + group.classList.remove('field-valid'); + } + } + + var titleEl = document.getElementById('title'); + var titleGrp = titleEl.closest('.form-group'); + var catEl = document.getElementById('category'); + var catGrp = catEl.closest('.form-group'); + var radios = document.querySelectorAll('input[name="listing_type"]'); + var radiosGrp = radios.length ? radios[0].closest('.form-group') : null; + var descGrp = qc ? qc.closest('.form-group') : null; + + function refreshTitle() { setValid(titleGrp, titleEl.value.trim().length > 0); } + function refreshCat() { setValid(catGrp, catEl.value !== ''); } + function refreshRadios() { + var any = Array.from(radios).some(function(r) { return r.checked; }); + setValid(radiosGrp, any); + } + function refreshDesc() { + var html = quill.root.innerHTML; + var ok = !(html === '


' || quill.getText().trim() === ''); + setValid(descGrp, ok); + if (ok && qc) qc.classList.remove('field-error'); + } + + titleEl.addEventListener('input', refreshTitle); + catEl.addEventListener('change', refreshCat); + radios.forEach(function(r) { r.addEventListener('change', refreshRadios); }); + quill.on('text-change', refreshDesc); + + // Initial pass — pre-filled fields (after POST validation error) get + // their green check immediately on page load. + refreshTitle(); refreshCat(); refreshRadios(); refreshDesc(); document.getElementById('classifiedForm').addEventListener('submit', function(e) { var html = quill.root.innerHTML;