feat(post-form): photos-first order, auto-collapsing photo summary, success-only view
- Reorder blueprint to Photos → Title → Content; drop title autofocus so the
picker leads (it anchors what you write).
- After upload the photo section auto-collapses to a live summary bar
("✓ N photos ready — tap to review") and re-expands on tap; the field's
own .form-label is hidden so the <summary> is the sole header. Trailing
refreshes settle the summary past FilePond's event/DOM timing gap.
- Replace FilePond's murky completed-thumbnail overlay with a clean green ✓
badge (remove action left intact).
- On a successful post, hide the reset form + location/weather controls so
only the "✓ Saved" confirmation + View-journal CTA remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+17
-15
@@ -19,11 +19,27 @@ form:
|
|||||||
action: /post
|
action: /post
|
||||||
enctype: multipart/form-data
|
enctype: multipart/form-data
|
||||||
fields:
|
fields:
|
||||||
|
# Photos come first — they anchor what you'll write (Photos → Title →
|
||||||
|
# Content). After upload post-form.js auto-collapses this into a summary
|
||||||
|
# bar so the thumbnails don't crowd the writing area.
|
||||||
|
-
|
||||||
|
name: photos
|
||||||
|
label: Photos (max 4)
|
||||||
|
# Grav-managed filepond field. post-form.js hooks its beforeAddFile
|
||||||
|
# to convert HEIC->JPEG in the browser, then re-adds the JPEG via the
|
||||||
|
# public pond.addFile() API — FilePond keeps ownership of the upload
|
||||||
|
# and page-attach contract (U4).
|
||||||
|
type: filepond
|
||||||
|
multiple: true
|
||||||
|
destination: '@self'
|
||||||
|
limit: 4
|
||||||
|
accept:
|
||||||
|
- 'image/*'
|
||||||
|
|
||||||
-
|
-
|
||||||
name: title
|
name: title
|
||||||
label: Title
|
label: Title
|
||||||
type: text
|
type: text
|
||||||
autofocus: true
|
|
||||||
validate:
|
validate:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -44,20 +60,6 @@ form:
|
|||||||
validate:
|
validate:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
-
|
|
||||||
name: photos
|
|
||||||
label: Photos (max 4)
|
|
||||||
# Grav-managed filepond field. post-form.js hooks its beforeAddFile
|
|
||||||
# to convert HEIC->JPEG in the browser, then re-adds the JPEG via the
|
|
||||||
# public pond.addFile() API — FilePond keeps ownership of the upload
|
|
||||||
# and page-attach contract (U4).
|
|
||||||
type: filepond
|
|
||||||
multiple: true
|
|
||||||
destination: '@self'
|
|
||||||
limit: 4
|
|
||||||
accept:
|
|
||||||
- 'image/*'
|
|
||||||
|
|
||||||
-
|
-
|
||||||
name: lat
|
name: lat
|
||||||
label: Latitude
|
label: Latitude
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -195,6 +195,70 @@
|
|||||||
/* Hide FilePond's "Powered by PQINA" credit. */
|
/* Hide FilePond's "Powered by PQINA" credit. */
|
||||||
.filepond--credits { display: none !important; }
|
.filepond--credits { display: none !important; }
|
||||||
|
|
||||||
|
/* Issue #2: FilePond's completed thumbnail carries a murky gradient tint + the
|
||||||
|
filename overlay, which reads as "something's wrong". Strip those overlays
|
||||||
|
and show a single clean green ✓ badge so a finished upload is unambiguous.
|
||||||
|
The remove (×) action button is left untouched so photos stay removable. */
|
||||||
|
.filepond--image-preview-overlay { display: none !important; }
|
||||||
|
.filepond--item[data-filepond-item-state="processing-complete"] .filepond--file-info,
|
||||||
|
.filepond--item[data-filepond-item-state="processing-complete"] .filepond--file-status {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.filepond--item[data-filepond-item-state="processing-complete"]::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 5;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Issue #3: photo section auto-collapses to a summary bar after upload — same
|
||||||
|
<details> affordance as "More options" above. */
|
||||||
|
.photos-collapse {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-canvas);
|
||||||
|
}
|
||||||
|
.photos-collapse__summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-ink);
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.photos-collapse__summary::-webkit-details-marker { display: none; }
|
||||||
|
.photos-collapse__summary::before {
|
||||||
|
content: '▸';
|
||||||
|
margin-right: var(--space-2);
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
.photos-collapse[open] .photos-collapse__summary::before { transform: rotate(90deg); }
|
||||||
|
.photos-collapse[open] .photos-collapse__summary { border-bottom: 1px solid var(--color-border); }
|
||||||
|
.photos-collapse > :not(summary) { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.photos-collapse[open] .filepond--root,
|
||||||
|
.photos-collapse[open] .filepond-root { margin: var(--space-4) 0; }
|
||||||
|
.photos-collapse[open] > .photo-convert-status:last-child,
|
||||||
|
.photos-collapse[open] > .photo-reauth-hint:last-child { padding-bottom: var(--space-4); }
|
||||||
|
|
||||||
/* ── EasyMDE — Field Notes dark theme (U5) ─────────────────── */
|
/* ── EasyMDE — Field Notes dark theme (U5) ─────────────────── */
|
||||||
.EasyMDEContainer .CodeMirror {
|
.EasyMDEContainer .CodeMirror {
|
||||||
background: var(--color-canvas);
|
background: var(--color-canvas);
|
||||||
|
|||||||
@@ -101,11 +101,42 @@ function photoStatusEl() {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap the photo picker in a native <details> so it can auto-collapse to a
|
||||||
|
// live summary bar once uploads settle (issue #3: thumbnails otherwise crowd
|
||||||
|
// the writing area). The field's own <label> is hidden — the <summary> becomes
|
||||||
|
// the section header. Returns { details, summary } or null when no picker.
|
||||||
|
function buildPhotoCollapse() {
|
||||||
|
var root = document.querySelector('.filepond-root, .form-input-file');
|
||||||
|
if (!root) return null;
|
||||||
|
var wrapper = root.closest('.form-field');
|
||||||
|
if (!wrapper) return null;
|
||||||
|
|
||||||
|
// Grav wraps the field label in a .form-label div (not a bare <label>);
|
||||||
|
// hide it so the <summary> is the section's only header.
|
||||||
|
var fieldLabel = wrapper.querySelector('.form-label');
|
||||||
|
|
||||||
|
var details = document.createElement('details');
|
||||||
|
details.className = 'photos-collapse';
|
||||||
|
details.open = true;
|
||||||
|
var summary = document.createElement('summary');
|
||||||
|
summary.className = 'photos-collapse__summary';
|
||||||
|
summary.textContent = 'Photos (max 4)';
|
||||||
|
details.appendChild(summary);
|
||||||
|
|
||||||
|
// Relocate the existing control (+ label/status) into the details body.
|
||||||
|
while (wrapper.firstChild) details.appendChild(wrapper.firstChild);
|
||||||
|
wrapper.appendChild(details);
|
||||||
|
if (fieldLabel) fieldLabel.style.display = 'none';
|
||||||
|
|
||||||
|
return { details: details, summary: summary };
|
||||||
|
}
|
||||||
|
|
||||||
function initPhotoConversion() {
|
function initPhotoConversion() {
|
||||||
var form = document.querySelector('form[name="new-entry"]');
|
var form = document.querySelector('form[name="new-entry"]');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
||||||
var converting = 0; // HEIC conversions in flight (before FilePond) — gates Submit
|
var converting = 0; // HEIC conversions in flight (before FilePond) — gates Submit
|
||||||
|
var collapse = buildPhotoCollapse();
|
||||||
|
|
||||||
function setStatus(msg, kind) {
|
function setStatus(msg, kind) {
|
||||||
var el = photoStatusEl();
|
var el = photoStatusEl();
|
||||||
@@ -114,6 +145,42 @@ function initPhotoConversion() {
|
|||||||
el.className = 'photo-convert-status form-status' + (kind ? ' form-status--' + kind : '');
|
el.className = 'photo-convert-status form-status' + (kind ? ' form-status--' + kind : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live summary + auto-collapse for the photo section. Reads FilePond's DOM
|
||||||
|
// item states (robust across FilePond versions) plus the pre-FilePond HEIC
|
||||||
|
// `converting` counter, so the section stays open while anything is in
|
||||||
|
// flight and collapses to "✓ N ready" only once everything has settled.
|
||||||
|
function refreshCollapse() {
|
||||||
|
if (!collapse) return;
|
||||||
|
var items = document.querySelectorAll('.filepond--item[data-filepond-item-state]');
|
||||||
|
var total = items.length;
|
||||||
|
var done = 0;
|
||||||
|
Array.prototype.forEach.call(items, function (el) {
|
||||||
|
if (el.getAttribute('data-filepond-item-state') === 'processing-complete') done++;
|
||||||
|
});
|
||||||
|
if (converting > 0 || (total > 0 && done < total)) {
|
||||||
|
collapse.summary.textContent = converting > 0
|
||||||
|
? 'Converting ' + converting + ' photo' + (converting > 1 ? 's' : '') + '…'
|
||||||
|
: 'Uploading ' + total + ' photo' + (total > 1 ? 's' : '') + '…';
|
||||||
|
collapse.details.open = true; // keep visible while working
|
||||||
|
} else if (total > 0) {
|
||||||
|
collapse.summary.textContent = '✓ ' + total + ' photo' + (total > 1 ? 's' : '') + ' ready — tap to review';
|
||||||
|
collapse.details.open = false; // auto-collapse when settled
|
||||||
|
} else {
|
||||||
|
collapse.summary.textContent = 'Photos (max 4)';
|
||||||
|
collapse.details.open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilePond fires processfile/processfiles a tick before it flips the item's
|
||||||
|
// data-filepond-item-state to "processing-complete", so a refresh bound
|
||||||
|
// straight to the event reads a stale state. Re-check on trailing timeouts
|
||||||
|
// so the summary settles on the final DOM state.
|
||||||
|
function scheduleRefresh() {
|
||||||
|
refreshCollapse();
|
||||||
|
setTimeout(refreshCollapse, 150);
|
||||||
|
setTimeout(refreshCollapse, 500);
|
||||||
|
}
|
||||||
|
|
||||||
function updateGate() {
|
function updateGate() {
|
||||||
var btn = form.querySelector('button[type="submit"], input[type="submit"]');
|
var btn = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||||
if (btn) btn.disabled = converting > 0;
|
if (btn) btn.disabled = converting > 0;
|
||||||
@@ -123,6 +190,7 @@ function initPhotoConversion() {
|
|||||||
var el = photoStatusEl();
|
var el = photoStatusEl();
|
||||||
if (el && /Converting/.test(el.textContent)) setStatus('');
|
if (el && /Converting/.test(el.textContent)) setStatus('');
|
||||||
}
|
}
|
||||||
|
refreshCollapse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard the submit while a HEIC is still converting (it hasn't entered
|
// Guard the submit while a HEIC is still converting (it hasn't entered
|
||||||
@@ -177,6 +245,11 @@ function initPhotoConversion() {
|
|||||||
if (pond && !pond._heicHooked) {
|
if (pond && !pond._heicHooked) {
|
||||||
pond._heicHooked = true;
|
pond._heicHooked = true;
|
||||||
pond.setOptions({ beforeAddFile: makeBeforeAddFile(pond) });
|
pond.setOptions({ beforeAddFile: makeBeforeAddFile(pond) });
|
||||||
|
// Update the collapse summary as files are added/uploaded/removed.
|
||||||
|
['addfile', 'processfile', 'processfiles', 'removefile', 'error'].forEach(function (ev) {
|
||||||
|
try { pond.on(ev, scheduleRefresh); } catch (e) { /* older FilePond API */ }
|
||||||
|
});
|
||||||
|
refreshCollapse();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -496,6 +569,16 @@ function initSuccessState() {
|
|||||||
panel.appendChild(actions);
|
panel.appendChild(actions);
|
||||||
notice.parentNode.insertBefore(panel, notice.nextSibling);
|
notice.parentNode.insertBefore(panel, notice.nextSibling);
|
||||||
|
|
||||||
|
// Issue #1: after a successful post, show the confirmation *only* — hide the
|
||||||
|
// (now-reset) form and the location/weather controls so a stray empty form
|
||||||
|
// doesn't render below the success panel. The .notices message and the
|
||||||
|
// injected panel live in .form-wrapper *before* the <form>, so hiding the
|
||||||
|
// form leaves them visible.
|
||||||
|
['form', '.form-action-row', '#location-status', '#weather-status'].forEach(function (sel) {
|
||||||
|
var el = wrap.querySelector(sel);
|
||||||
|
if (el) el.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
notice.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
notice.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user