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:
2026-07-04 19:46:49 +02:00
co-authored by Claude Opus 4.8
parent d3a520426a
commit 87da3e4b5b
5 changed files with 205 additions and 56 deletions
+17 -15
View File
@@ -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
+64
View File
@@ -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);
+83
View File
@@ -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' });
} }