1.0.1
This commit is contained in:
parent
09fd8f477a
commit
f3690a8b42
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user