This commit is contained in:
Flatlogic Bot 2026-06-04 16:43:04 +00:00
parent 09fd8f477a
commit f3690a8b42
10 changed files with 77 additions and 18 deletions

View File

@ -27,7 +27,7 @@ class PropertyLocationForm(BootstrapFormMixin, forms.ModelForm):
model = PropertyEntry model = PropertyEntry
fields = ["address", "latitude", "longitude", "phone", "email", "listing_type"] fields = ["address", "latitude", "longitude", "phone", "email", "listing_type"]
widgets = { widgets = {
"address": forms.TextInput(attrs={"placeholder": "Street, area, city"}), "address": forms.TextInput(attrs={"placeholder": "Type or paste the full address", "autocomplete": "street-address", "data-manual-address": "true"}),
"phone": forms.TextInput(attrs={"placeholder": "+34 600 000 000"}), "phone": forms.TextInput(attrs={"placeholder": "+34 600 000 000"}),
"email": forms.EmailInput(attrs={"placeholder": "owner@example.com"}), "email": forms.EmailInput(attrs={"placeholder": "owner@example.com"}),
"listing_type": forms.Select(), "listing_type": forms.Select(),
@ -39,7 +39,7 @@ class PropertyLocationForm(BootstrapFormMixin, forms.ModelForm):
latitude = cleaned.get("latitude") latitude = cleaned.get("latitude")
longitude = cleaned.get("longitude") longitude = cleaned.get("longitude")
if not address and (latitude is None or longitude is None): if not address and (latitude is None or longitude is None):
raise forms.ValidationError("Add an address or allow location so this property can be placed on the pinboard.") raise forms.ValidationError("Type or paste an address, or allow location, so this property can be placed on the pinboard.")
return cleaned return cleaned

View File

@ -31,7 +31,7 @@
<span class="badge rounded-pill text-bg-light">Live MVP</span> <span class="badge rounded-pill text-bg-light">Live MVP</span>
<h2>Public nearby list</h2> <h2>Public nearby list</h2>
<p>{{ total_entries }} entries · {{ photo_entries }} photo uploads</p> <p>{{ total_entries }} entries · {{ photo_entries }} photo uploads</p>
<a href="{% url 'add_location_property' %}">Add current location</a> <a href="{% url 'add_location_property' %}">Add by location/address</a>
</div> </div>
</div> </div>
</div> </div>
@ -47,7 +47,7 @@
<article class="feature-card h-100"> <article class="feature-card h-100">
<span class="feature-icon">📍</span> <span class="feature-icon">📍</span>
<h2>Pin by location</h2> <h2>Pin by location</h2>
<p>Grant browser location permission, then submit an address or GPS coordinates with optional contact details.</p> <p>Grant browser location permission, or type/paste an address if permission is unavailable, then submit optional contact details.</p>
<a href="{% url 'add_location_property' %}">Pin a property</a> <a href="{% url 'add_location_property' %}">Pin a property</a>
</article> </article>
</div> </div>

View File

@ -4,11 +4,19 @@
<main class="page-shell"> <main class="page-shell">
<div class="container narrow-container"> <div class="container narrow-container">
<div class="form-card"> <div class="form-card">
<p class="eyebrow">Create from current location</p> <p class="eyebrow">Create from location or address</p>
<h1>Add a property pin</h1> <h1>Add a property pin</h1>
<p class="text-muted">Address or GPS is required. Contact details and sale/rental type help others recognize the listing.</p> <p class="text-muted">Use your current GPS position when available, or type/paste the address manually if browser location is blocked, denied, or unsupported.</p>
<button class="btn btn-ghost mb-3" type="button" data-action="fill-current-location">Use my current location</button> <div class="location-choice-panel mb-3" aria-label="Choose how to add location information">
<small id="form-location-status" class="status-text d-block mb-3">Location not captured yet.</small> <button class="btn btn-ghost" type="button" data-action="fill-current-location">Use my current location</button>
<button class="btn btn-outline-dark rounded-pill" type="button" data-action="manual-address">Type or paste address</button>
</div>
<small id="form-location-status" class="status-text d-block mb-3">Choose current location or enter the address below.</small>
<div id="manual-address-tip" class="manual-address-tip mb-3" data-manual-address-tip hidden>
<span>Manual fallback</span>
<strong>Location permission is optional.</strong>
<p>Paste the full street address into the Address field below. GPS coordinates can stay empty when an address is provided.</p>
</div>
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %} {% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
@ -17,6 +25,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label> <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }} {{ field }}
{% if field.name == "address" %}<div class="form-text">Required if you do not grant current-location access.</div>{% endif %}
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %} {% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %} {% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
</div> </div>

View File

@ -103,8 +103,8 @@ def add_location_property(request):
else: else:
form = PropertyLocationForm() form = PropertyLocationForm()
context = { context = {
"page_title": "Add current-location property — NearbyNest", "page_title": "Add property by location or address — NearbyNest",
"meta_description": "Add a property sighting from your current location, with optional contact details.", "meta_description": "Add a property sighting from current GPS location or a typed/pasted address, with optional contact details.",
"form": form, "form": form,
} }
return render(request, "core/property_form_location.html", context) return render(request, "core/property_form_location.html", context)

View File

@ -53,6 +53,7 @@ a { color: var(--nest-primary); text-decoration: none; } a:hover { color: var(--
.permission-wizard { display: grid; gap: 1rem; padding: 1rem; } .permission-step { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; padding: 1.2rem; border-radius: 1.4rem; background: rgba(255,255,255,.56); } .permission-wizard { display: grid; gap: 1rem; padding: 1rem; } .permission-step { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; padding: 1.2rem; border-radius: 1.4rem; background: rgba(255,255,255,.56); }
.permission-step h2 { font-size: 1.25rem; margin: .3rem 0; } .status-text { color: var(--nest-muted); font-weight: 700; } .action-dock { display: flex; flex-wrap: wrap; justify-content: center; gap: .8rem; margin-top: 1.4rem; } .permission-step h2 { font-size: 1.25rem; margin: .3rem 0; } .status-text { color: var(--nest-muted); font-weight: 700; } .action-dock { display: flex; flex-wrap: wrap; justify-content: center; gap: .8rem; margin-top: 1.4rem; }
.form-card { padding: clamp(1.2rem, 4vw, 2rem); } .form-card h1 { letter-spacing: -.055em; } .form-card { padding: clamp(1.2rem, 4vw, 2rem); } .form-card h1 { letter-spacing: -.055em; }
.location-choice-panel { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; } .manual-address-tip { padding: 1rem; border-radius: 1.25rem; background: rgba(215,255,241,.76); border: 1px solid rgba(15,118,110,.2); } .manual-address-tip span { display: inline-flex; margin-bottom: .35rem; color: var(--nest-primary); text-transform: uppercase; letter-spacing: .12em; font-size: .72rem; font-weight: 900; } .manual-address-tip strong { display: block; } .manual-address-tip p { margin: .25rem 0 0; color: var(--nest-muted); }
.form-control, .form-select { border-radius: 1rem; border-color: rgba(20,33,61,.16); padding: .82rem 1rem; } .form-control:focus, .form-select:focus { border-color: var(--nest-primary); box-shadow: 0 0 0 .25rem rgba(15,118,110,.12); } .form-control, .form-select { border-radius: 1rem; border-color: rgba(20,33,61,.16); padding: .82rem 1rem; } .form-control:focus, .form-select:focus { border-color: var(--nest-primary); box-shadow: 0 0 0 .25rem rgba(15,118,110,.12); }
.invalid-copy { color: #b42318; font-weight: 700; font-size: .9rem; margin-top: .35rem; } .invalid-copy { color: #b42318; font-weight: 700; font-size: .9rem; margin-top: .35rem; }
.list-hero { display: flex; justify-content: space-between; align-items: end; gap: 1.5rem; margin-bottom: 1.25rem; } .sort-panel { display: flex; flex-wrap: wrap; gap: .8rem; align-items: end; padding: 1rem; margin-bottom: 1rem; } .sort-panel > div { min-width: min(100%, 16rem); } .list-hero { display: flex; justify-content: space-between; align-items: end; gap: 1.5rem; margin-bottom: 1.25rem; } .sort-panel { display: flex; flex-wrap: wrap; gap: .8rem; align-items: end; padding: 1rem; margin-bottom: 1rem; } .sort-panel > div { min-width: min(100%, 16rem); }

View File

@ -3,9 +3,25 @@ function setText(id, text) {
if (el) el.textContent = text; if (el) el.textContent = text;
} }
function requestLocation(callback, statusId) { function showManualAddressTip(message) {
const tip = document.querySelector("[data-manual-address-tip]");
const addressInput = document.querySelector("[data-manual-address]") || document.getElementById("id_address");
if (tip) {
tip.hidden = false;
}
if (message) {
setText("form-location-status", message);
}
if (addressInput) {
addressInput.focus({ preventScroll: true });
addressInput.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
function requestLocation(callback, statusId, unavailableCallback) {
if (!navigator.geolocation) { if (!navigator.geolocation) {
setText(statusId, "Location is not supported by this browser."); setText(statusId, "Location is not supported by this browser. You can type or paste the address instead.");
if (unavailableCallback) unavailableCallback();
return; return;
} }
setText(statusId, "Requesting location…"); setText(statusId, "Requesting location…");
@ -16,7 +32,10 @@ function requestLocation(callback, statusId) {
setText(statusId, `Captured ${lat}, ${lng}`); setText(statusId, `Captured ${lat}, ${lng}`);
callback(lat, lng); callback(lat, lng);
}, },
() => setText(statusId, "Location permission was denied or unavailable."), () => {
setText(statusId, "Location permission was denied or unavailable. Type or paste the address instead.");
if (unavailableCallback) unavailableCallback();
},
{ enableHighAccuracy: true, timeout: 10000 } { enableHighAccuracy: true, timeout: 10000 }
); );
} }
@ -35,13 +54,18 @@ document.addEventListener("click", (event) => {
setText("notification-status", `Notification permission: ${permission}`); setText("notification-status", `Notification permission: ${permission}`);
}); });
} }
if (action === "manual-address") {
showManualAddressTip("Manual address mode: paste or type the property address below.");
}
if (action === "fill-current-location") { if (action === "fill-current-location") {
requestLocation((lat, lng) => { requestLocation((lat, lng) => {
const latInput = document.getElementById("id_latitude"); const latInput = document.getElementById("id_latitude");
const lngInput = document.getElementById("id_longitude"); const lngInput = document.getElementById("id_longitude");
if (latInput) latInput.value = lat; if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng; if (lngInput) lngInput.value = lng;
}, "form-location-status"); }, "form-location-status", () => {
showManualAddressTip();
});
} }
if (action === "use-location-for-list") { if (action === "use-location-for-list") {
requestLocation((lat, lng) => { requestLocation((lat, lng) => {

View File

@ -53,6 +53,7 @@ a { color: var(--nest-primary); text-decoration: none; } a:hover { color: var(--
.permission-wizard { display: grid; gap: 1rem; padding: 1rem; } .permission-step { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; padding: 1.2rem; border-radius: 1.4rem; background: rgba(255,255,255,.56); } .permission-wizard { display: grid; gap: 1rem; padding: 1rem; } .permission-step { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; padding: 1.2rem; border-radius: 1.4rem; background: rgba(255,255,255,.56); }
.permission-step h2 { font-size: 1.25rem; margin: .3rem 0; } .status-text { color: var(--nest-muted); font-weight: 700; } .action-dock { display: flex; flex-wrap: wrap; justify-content: center; gap: .8rem; margin-top: 1.4rem; } .permission-step h2 { font-size: 1.25rem; margin: .3rem 0; } .status-text { color: var(--nest-muted); font-weight: 700; } .action-dock { display: flex; flex-wrap: wrap; justify-content: center; gap: .8rem; margin-top: 1.4rem; }
.form-card { padding: clamp(1.2rem, 4vw, 2rem); } .form-card h1 { letter-spacing: -.055em; } .form-card { padding: clamp(1.2rem, 4vw, 2rem); } .form-card h1 { letter-spacing: -.055em; }
.location-choice-panel { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; } .manual-address-tip { padding: 1rem; border-radius: 1.25rem; background: rgba(215,255,241,.76); border: 1px solid rgba(15,118,110,.2); } .manual-address-tip span { display: inline-flex; margin-bottom: .35rem; color: var(--nest-primary); text-transform: uppercase; letter-spacing: .12em; font-size: .72rem; font-weight: 900; } .manual-address-tip strong { display: block; } .manual-address-tip p { margin: .25rem 0 0; color: var(--nest-muted); }
.form-control, .form-select { border-radius: 1rem; border-color: rgba(20,33,61,.16); padding: .82rem 1rem; } .form-control:focus, .form-select:focus { border-color: var(--nest-primary); box-shadow: 0 0 0 .25rem rgba(15,118,110,.12); } .form-control, .form-select { border-radius: 1rem; border-color: rgba(20,33,61,.16); padding: .82rem 1rem; } .form-control:focus, .form-select:focus { border-color: var(--nest-primary); box-shadow: 0 0 0 .25rem rgba(15,118,110,.12); }
.invalid-copy { color: #b42318; font-weight: 700; font-size: .9rem; margin-top: .35rem; } .invalid-copy { color: #b42318; font-weight: 700; font-size: .9rem; margin-top: .35rem; }
.list-hero { display: flex; justify-content: space-between; align-items: end; gap: 1.5rem; margin-bottom: 1.25rem; } .sort-panel { display: flex; flex-wrap: wrap; gap: .8rem; align-items: end; padding: 1rem; margin-bottom: 1rem; } .sort-panel > div { min-width: min(100%, 16rem); } .list-hero { display: flex; justify-content: space-between; align-items: end; gap: 1.5rem; margin-bottom: 1.25rem; } .sort-panel { display: flex; flex-wrap: wrap; gap: .8rem; align-items: end; padding: 1rem; margin-bottom: 1rem; } .sort-panel > div { min-width: min(100%, 16rem); }

View File

@ -3,9 +3,25 @@ function setText(id, text) {
if (el) el.textContent = text; if (el) el.textContent = text;
} }
function requestLocation(callback, statusId) { function showManualAddressTip(message) {
const tip = document.querySelector("[data-manual-address-tip]");
const addressInput = document.querySelector("[data-manual-address]") || document.getElementById("id_address");
if (tip) {
tip.hidden = false;
}
if (message) {
setText("form-location-status", message);
}
if (addressInput) {
addressInput.focus({ preventScroll: true });
addressInput.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
function requestLocation(callback, statusId, unavailableCallback) {
if (!navigator.geolocation) { if (!navigator.geolocation) {
setText(statusId, "Location is not supported by this browser."); setText(statusId, "Location is not supported by this browser. You can type or paste the address instead.");
if (unavailableCallback) unavailableCallback();
return; return;
} }
setText(statusId, "Requesting location…"); setText(statusId, "Requesting location…");
@ -16,7 +32,10 @@ function requestLocation(callback, statusId) {
setText(statusId, `Captured ${lat}, ${lng}`); setText(statusId, `Captured ${lat}, ${lng}`);
callback(lat, lng); callback(lat, lng);
}, },
() => setText(statusId, "Location permission was denied or unavailable."), () => {
setText(statusId, "Location permission was denied or unavailable. Type or paste the address instead.");
if (unavailableCallback) unavailableCallback();
},
{ enableHighAccuracy: true, timeout: 10000 } { enableHighAccuracy: true, timeout: 10000 }
); );
} }
@ -35,13 +54,18 @@ document.addEventListener("click", (event) => {
setText("notification-status", `Notification permission: ${permission}`); setText("notification-status", `Notification permission: ${permission}`);
}); });
} }
if (action === "manual-address") {
showManualAddressTip("Manual address mode: paste or type the property address below.");
}
if (action === "fill-current-location") { if (action === "fill-current-location") {
requestLocation((lat, lng) => { requestLocation((lat, lng) => {
const latInput = document.getElementById("id_latitude"); const latInput = document.getElementById("id_latitude");
const lngInput = document.getElementById("id_longitude"); const lngInput = document.getElementById("id_longitude");
if (latInput) latInput.value = lat; if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng; if (lngInput) lngInput.value = lng;
}, "form-location-status"); }, "form-location-status", () => {
showManualAddressTip();
});
} }
if (action === "use-location-for-list") { if (action === "use-location-for-list") {
requestLocation((lat, lng) => { requestLocation((lat, lng) => {