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
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:
parent
e1a16e2542
commit
f3a7f86960
@ -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() === '');
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user