feat(classifieds): green ✓ on filled required fields, drop full-form red
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

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) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-14 13:47:27 +02:00
parent e1a16e2542
commit f3a7f86960
2 changed files with 73 additions and 11 deletions

View File

@ -51,11 +51,13 @@
.quill-container .ql-toolbar { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); } .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-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; } .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; border: 2px solid #dc2626 !important;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); 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 attachments */
.existing-attachment { position: relative; border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-xs); background: var(--surface); } .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() { (function() {
var qc = document.getElementById('quill-editor'); 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 === '<p><br></p>' || 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) { document.getElementById('classifiedForm').addEventListener('submit', function(e) {
var html = quill.root.innerHTML; var html = quill.root.innerHTML;
var empty = (html === '<p><br></p>' || quill.getText().trim() === ''); var empty = (html === '<p><br></p>' || quill.getText().trim() === '');

View File

@ -25,9 +25,8 @@
.quill-container .ql-editor { .quill-container .ql-editor {
min-height: 150px; min-height: 150px;
} }
/* Highlight required fields that failed validation. Applied by server /* Red border on required fields that failed validation. Applied by
on POST validation error and by client JS on Quill empty submit. */ server on POST error and by client JS on submit-with-empty Quill. */
.field-error,
input.field-error, input.field-error,
select.field-error, select.field-error,
.type-selector.field-error, .type-selector.field-error,
@ -35,9 +34,13 @@
border: 2px solid #dc2626 !important; border: 2px solid #dc2626 !important;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
} }
:invalid:not(:focus):not(:placeholder-shown), /* Green checkmark next to label of filled required fields. JS toggles
select:invalid:not(:focus) { .field-valid on the form-group as the user types/picks. */
border: 2px solid #dc2626; .form-group.field-valid > label::after {
content: " ✓";
color: #16a34a;
font-weight: 700;
margin-left: 4px;
} }
.form-container { .form-container {
max-width: 700px; max-width: 700px;
@ -390,8 +393,47 @@ var quill = new Quill('#quill-editor', {
// Restore from server-rendered textarea (e.g. after POST validation error) // Restore from server-rendered textarea (e.g. after POST validation error)
var initialDesc = document.getElementById('description').value; var initialDesc = document.getElementById('description').value;
if (initialDesc) { quill.root.innerHTML = initialDesc; } 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 === '<p><br></p>' || 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) { document.getElementById('classifiedForm').addEventListener('submit', function(e) {
var html = quill.root.innerHTML; var html = quill.root.innerHTML;