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
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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); }
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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); }
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user