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-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 === '<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) {
var html = quill.root.innerHTML;
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');

View File

@ -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 === '<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) {
var html = quill.root.innerHTML;