feat(ui): green map pin for applied flats, hide map reject after apply, lightbox image viewer
Map: replace Leaflet's default marker with a divIcon SVG pin coloured per state — green when the user has already successfully applied (status.chip === "ok"), brand-blue otherwise. Same condition also hides the action buttons in the popup, matching the list view, which already hid both Bewerben and Ablehnen on success — so the only remaining action on an applied flat is opening the original ad link. Image gallery: clicks now open a global lightbox modal instead of a new tab. The viewer fits each image into the viewport via max-width/height + object-fit: contain (uniform sizing regardless of source aspect), shows × top-right, prev/next arrows on the sides, ←/→/Esc keyboard nav, and click-on-backdrop to close. Prev arrow is hidden on the first image and next on the last. Tile changes from <a target="_blank"> to <button> since the new-tab fallback is no longer wanted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe43a402d8
commit
787f848aba
5 changed files with 173 additions and 21 deletions
|
|
@ -64,31 +64,56 @@ function popupHtml(f, csrf) {
|
|||
`<span class="chip chip-${escHtml(f.status.chip)}">${escHtml(f.status.label)}</span>` +
|
||||
`</div>`;
|
||||
}
|
||||
html += `<div class="map-popup-actions">`;
|
||||
if (f.can_apply) {
|
||||
const confirm = `Bewerbung für ${addrText.replace(/"/g, "'")} starten?`;
|
||||
// Once the user has successfully applied, both action buttons disappear —
|
||||
// matches the list view, which hides them on success too.
|
||||
const applied = !!(f.status && f.status.chip === "ok");
|
||||
if (!applied) {
|
||||
html += `<div class="map-popup-actions">`;
|
||||
if (f.can_apply) {
|
||||
const confirm = `Bewerbung für ${addrText.replace(/"/g, "'")} starten?`;
|
||||
html +=
|
||||
`<form hx-post="/actions/apply" hx-target="#wohnungen-body" hx-swap="outerHTML">` +
|
||||
`<input type="hidden" name="csrf" value="${escHtml(csrf)}">` +
|
||||
`<input type="hidden" name="flat_id" value="${escHtml(f.id)}">` +
|
||||
`<button class="btn btn-primary text-xs" type="submit"` +
|
||||
(f.is_running ? " disabled" : "") +
|
||||
` hx-confirm="${escHtml(confirm)}">` +
|
||||
(f.is_running ? "läuft…" : "Bewerben") +
|
||||
`</button>` +
|
||||
`</form>`;
|
||||
}
|
||||
html +=
|
||||
`<form hx-post="/actions/apply" hx-target="#wohnungen-body" hx-swap="outerHTML">` +
|
||||
`<form hx-post="/actions/reject" hx-target="#wohnungen-body" hx-swap="outerHTML">` +
|
||||
`<input type="hidden" name="csrf" value="${escHtml(csrf)}">` +
|
||||
`<input type="hidden" name="flat_id" value="${escHtml(f.id)}">` +
|
||||
`<button class="btn btn-primary text-xs" type="submit"` +
|
||||
(f.is_running ? " disabled" : "") +
|
||||
` hx-confirm="${escHtml(confirm)}">` +
|
||||
(f.is_running ? "läuft…" : "Bewerben") +
|
||||
`</button>` +
|
||||
`<button class="btn btn-ghost text-xs" type="submit"` +
|
||||
` hx-confirm="Ablehnen und aus der Liste entfernen?">Ablehnen</button>` +
|
||||
`</form>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html +=
|
||||
`<form hx-post="/actions/reject" hx-target="#wohnungen-body" hx-swap="outerHTML">` +
|
||||
`<input type="hidden" name="csrf" value="${escHtml(csrf)}">` +
|
||||
`<input type="hidden" name="flat_id" value="${escHtml(f.id)}">` +
|
||||
`<button class="btn btn-ghost text-xs" type="submit"` +
|
||||
` hx-confirm="Ablehnen und aus der Liste entfernen?">Ablehnen</button>` +
|
||||
`</form>`;
|
||||
html += `</div></div>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
// Custom pin icon — divIcon with inline SVG so we can color it per-state
|
||||
// without shipping marker images. Successful apply → green; everything else
|
||||
// → brand blue. Sized + anchored to roughly match Leaflet's default pin.
|
||||
function pinIcon(color) {
|
||||
const svg =
|
||||
`<svg width="28" height="38" viewBox="0 0 28 38" xmlns="http://www.w3.org/2000/svg">` +
|
||||
`<path d="M14 1C7 1 1 6.6 1 13.4c0 9.5 13 23.6 13 23.6s13-14.1 13-23.6C27 6.6 21 1 14 1z" ` +
|
||||
`fill="${color}" stroke="rgba(0,0,0,0.25)" stroke-width="1"/>` +
|
||||
`<circle cx="14" cy="13.5" r="4.5" fill="white"/>` +
|
||||
`</svg>`;
|
||||
return L.divIcon({
|
||||
className: "lazyflat-pin",
|
||||
html: svg,
|
||||
iconSize: [28, 38],
|
||||
iconAnchor: [14, 38],
|
||||
popupAnchor: [0, -34],
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarkers(payload) {
|
||||
if (!mapInstance) return;
|
||||
const { csrf, flats } = payload;
|
||||
|
|
@ -106,7 +131,9 @@ function renderMarkers(payload) {
|
|||
|
||||
flats.forEach((f) => {
|
||||
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
|
||||
L.marker([f.lat, f.lng])
|
||||
const applied = !!(f.status && f.status.chip === "ok");
|
||||
const color = applied ? "#1f8a4a" : "#2f8ae0";
|
||||
L.marker([f.lat, f.lng], { icon: pinIcon(color) })
|
||||
.addTo(markerLayer)
|
||||
.bindPopup(popupHtml(f, csrf), { minWidth: 240, closeButton: true });
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue