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
fields = ["address", "latitude", "longitude", "phone", "email", "listing_type"]
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"}),
"email": forms.EmailInput(attrs={"placeholder": "owner@example.com"}),
"listing_type": forms.Select(),
@ -39,7 +39,7 @@ class PropertyLocationForm(BootstrapFormMixin, forms.ModelForm):
latitude = cleaned.get("latitude")
longitude = cleaned.get("longitude")
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

View File

@ -31,7 +31,7 @@
<span class="badge rounded-pill text-bg-light">Live MVP</span>
<h2>Public nearby list</h2>
<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>
@ -47,7 +47,7 @@
<article class="feature-card h-100">
<span class="feature-icon">📍</span>
<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>
</article>
</div>

View File

@ -4,11 +4,19 @@
<main class="page-shell">
<div class="container narrow-container">
<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>
<p class="text-muted">Address or GPS is required. Contact details and sale/rental type help others recognize the listing.</p>
<button class="btn btn-ghost mb-3" type="button" data-action="fill-current-location">Use my current location</button>
<small id="form-location-status" class="status-text d-block mb-3">Location not captured yet.</small>
<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>
<div class="location-choice-panel mb-3" aria-label="Choose how to add location information">
<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>
{% csrf_token %}
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
@ -17,6 +25,7 @@
<div class="mb-3">
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ 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 %}
{% for error in field.errors %}<div class="invalid-copy">{{ error }}</div>{% endfor %}
</div>

View File

@ -103,8 +103,8 @@ def add_location_property(request):
else:
form = PropertyLocationForm()
context = {
"page_title": "Add current-location property — NearbyNest",
"meta_description": "Add a property sighting from your current location, with optional contact details.",
"page_title": "Add property by location or address — NearbyNest",
"meta_description": "Add a property sighting from current GPS location or a typed/pasted address, with optional contact details.",
"form": form,
}
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-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; }
.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); }
.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); }

View File

@ -3,9 +3,25 @@ function setText(id, 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) {
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;
}
setText(statusId, "Requesting location…");
@ -16,7 +32,10 @@ function requestLocation(callback, statusId) {
setText(statusId, `Captured ${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 }
);
}
@ -35,13 +54,18 @@ document.addEventListener("click", (event) => {
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") {
requestLocation((lat, lng) => {
const latInput = document.getElementById("id_latitude");
const lngInput = document.getElementById("id_longitude");
if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng;
}, "form-location-status");
}, "form-location-status", () => {
showManualAddressTip();
});
}
if (action === "use-location-for-list") {
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-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; }
.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); }
.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); }

View File

@ -3,9 +3,25 @@ function setText(id, 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) {
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;
}
setText(statusId, "Requesting location…");
@ -16,7 +32,10 @@ function requestLocation(callback, statusId) {
setText(statusId, `Captured ${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 }
);
}
@ -35,13 +54,18 @@ document.addEventListener("click", (event) => {
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") {
requestLocation((lat, lng) => {
const latInput = document.getElementById("id_latitude");
const lngInput = document.getElementById("id_longitude");
if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng;
}, "form-location-status");
}, "form-location-status", () => {
showManualAddressTip();
});
}
if (action === "use-location-for-list") {
requestLocation((lat, lng) => {