Compare commits

..

No commits in common. "cff0585bd45462b4a4bc2e9cb796c20e892d17d3" and "02ef91088022cb234bb9ceee15f6f3e844be43f5" have entirely different histories.

9 changed files with 32 additions and 385 deletions

2
Cargo.lock generated
View file

@ -654,7 +654,7 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]] [[package]]
name = "huellas" name = "huellas"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"rocket", "rocket",
"rocket_db_pools", "rocket_db_pools",

View file

@ -1,5 +0,0 @@
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
}

View file

@ -1 +0,0 @@
UPDATE places SET longitude=latitude, latitude=longitude;

View file

@ -43,7 +43,7 @@ enum UpsertResponse {
NotFound(NotFound<Json<Place>>), NotFound(NotFound<Json<Place>>),
} }
#[put("/places", format = "json", data = "<place>")] #[post("/places", format = "json", data = "<place>")]
async fn upsert_place(db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> { async fn upsert_place(db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> {
if place.id.is_some() { if place.id.is_some() {
update_place(db, place).await update_place(db, place).await
@ -79,15 +79,14 @@ async fn insert_place(mut db: Connection<Db>, mut place: Json<Place>) -> Result<
async fn update_place(mut db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> { async fn update_place(mut db: Connection<Db>, place: Json<Place>) -> Result<UpsertResponse> {
let result = ::sqlx::query!( let result = ::sqlx::query!(
"UPDATE places SET (name, address, open_hours, icon, description, longitude, latitude) = (?, ?, ?, ?, ?, ?, ?) WHERE id = ?", "UPDATE places SET (name, address, open_hours, icon, description, longitude, latitude) = (?, ?, ?, ?, ?, ?, ?)",
place.name, place.name,
place.address, place.address,
place.open_hours, place.open_hours,
place.icon, place.icon,
place.description, place.description,
place.longitude, place.longitude,
place.latitude, place.latitude
place.id,
) )
.execute(&mut *db) .execute(&mut *db)
.await?; .await?;

View file

@ -4,18 +4,12 @@
<meta charset=utf-8> <meta charset=utf-8>
<title>👣 Huellas 🐾</title> <title>👣 Huellas 🐾</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" <link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
crossorigin=""/> crossorigin=""/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet-contextmenu/1.4.0/leaflet.contextmenu.min.css" <script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
integrity="sha512-AiWBM2PiPZkogKBj8Jss3MahJ+bRbSMebXUBwZMg+83vJTnZT/FXoxZrmpL+x9GbAYLWRuBZDqzhDt0Dk73qhw==" integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
crossorigin=""></script> crossorigin=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-contextmenu/1.4.0/leaflet.contextmenu.min.js"
integrity="sha512-8sfQf8cr0KjCeN32YPfjvLU2cMvyY1lhCXTMfpTZ16CvwIzeVQtwtKlxeSqFs/TpXjKhp1Dcv77LQmn1VFaOZg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style type="text/css" media="screen"> <style type="text/css" media="screen">
body { body {
@ -37,115 +31,10 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
#modal {
display: none;
position: fixed;
z-index: 400;
left: 0;
top: 0;
height: 100vh;
width: 100vw;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
}
#modal-form {
margin: 15vh auto;
color: #333;
background-color: white;
padding: 2em;
border: 1px solid rgb(25, 25, 25);
width: 80vw;
border-radius: 12px;
}
#close {
color: #333;
float: right;
font-size: 28px;
font-weight: bold;
}
#close:hover,
#close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
form {
display: table;
}
p {
display: table-row;
}
label {
font-weight: bold;
display: table-cell;
vertical-align: top;
padding-right: 2em;
}
input {
display: table-cell;
}
</style> </style>
</head> </head>
<body> <body>
<div id="map"></div> <div id="map"></div>
<div id="modal">
<div id="modal-form">
<span id="close">&times;</span>
<h1>Título</h1>
<form>
<p>
<label for="id"> Id:</label>
<input type="text" id="id" name="id" size="30" readonly>
</p>
<p>
<label for="name"> Longitud:</label>
<input type="text" id="long" name="long" size="30" readonly>
</p>
<p>
<label for="name"> Latitud:</label>
<input type="text" id="lat" name="lat" size="30" readonly>
</p>
<p>
<label for="name"> Nombre:</label>
<input type="text" id="name" name="name" size="30">
<p>
<label for="address"> Dirección:</label>
<input type="text" id="address" name="address" size="30">
</p>
<p>
<label for="open_hours"> Horario:</label>
<textarea id="open_hours" name="open_hours"
cols="30"></textarea>
</p>
<p>
<label for="icon"> Ícono:</label>
<select id="icon" name="icon">
<option value="bar">Bar</option>
<option value="coffee">Café</option>
<option value="cinema">Cine</option>
<option value="food">Comida</option>
<option value="jazz">Jazz</option>
<option value="library">Librería</option>
<option value="marker" selected>Marcador</option>
<option value="museum">Museo</option>
<option value="dining">Restaurant</option>
<option value="mask">Teatro</option>
<option value="shop">Tienda</option>
</select>
</p>
<p>
<label for="description"> Descripción:</label>
<textarea id="description" name="description"
cols="30" rows="5"></textarea>
</p>
<p>
<button type="button" id="button">Enviar </button>
</p>
</form>
</div>
</div>
<script src="client.js" onload="setupMap()"></script> <script src="client.js" onload="setupMap()"></script>
</body> </body>
</html> </html>

View file

@ -1,3 +0,0 @@
all: client.ts
tsc
sed -i '1d' build/client.js

View file

@ -1,8 +1,7 @@
import * as L from 'leaflet-contextmenu'; import * as L from 'leaflet';
import { Feature, FeatureCollection } from 'geojson'; import { Feature, FeatureCollection, GeoJSON } from 'geojson';
interface PlaceModel { interface PlaceModel {
id: number | null;
name: string; name: string;
address: string; address: string;
open_hours: string; open_hours: string;
@ -10,253 +9,43 @@ interface PlaceModel {
description: string; description: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
} }
async function loadPlaces(): Promise<Array<PlaceModel>> { async function loadPlaces(): Promise<Array<PlaceModel>> {
return await fetch('places').then(response => response.json()); return await fetch('places/').then(response => response.json());
} }
function toFeature(place: PlaceModel): Feature { function toLeafletPlaces(backendPlaces: Array<PlaceModel>): GeoJSON {
return {
"type": "Feature",
"properties": {
"name": place.name,
"address": place.address,
"open_hours": place.open_hours,
"icon": place.icon,
"description": place.description
},
"geometry": {
"type": "Point",
"coordinates": [place.longitude, place.latitude]
}
}
}
function toLeafletPlaces(backendPlaces: Array<PlaceModel>): L.GeoJSON {
let result: FeatureCollection = { let result: FeatureCollection = {
type: "FeatureCollection", type: "FeatureCollection",
features: new Array<Feature>(), features: new Array<Feature>(),
} }
for (const place of backendPlaces) { for (const place of backendPlaces) {
result.features.push(toFeature(place)); result.features.push({
"type": "Feature",
"properties": {
"name": place.name,
"address": place.address,
"open_hours": place.open_hours,
"icon": place.icon,
"description": place.description
},
"geometry": {
"type": "Point",
"coordinates": [place.latitude, place.longitude]
}
});
} }
return result; return result;
} }
let placesLayer: L.GeoJSON;
let places = new Map<string, PlaceModel>();
function toStr(latlng: L.LatLngLiteral) {
return latlng.lng + "|" + latlng.lat;
}
function getPlaceFromForm(): PlaceModel {
const idStr = (document.getElementById("id") as HTMLInputElement).value;
const id = idStr == "" ? null : parseInt(idStr);
const name =
(document.getElementById("name") as HTMLInputElement).value;
const address =
(document.getElementById("address") as HTMLInputElement).value;
const open_hours =
(document.getElementById("open_hours") as HTMLTextAreaElement).value;
const icon =
(document.getElementById("icon") as HTMLSelectElement).value;
const description =
(document.getElementById("description") as HTMLTextAreaElement).value;
const latitude = parseFloat(
(document.getElementById("lat") as HTMLInputElement).value);
const longitude = parseFloat(
(document.getElementById("long") as HTMLSelectElement).value);
return {
id: id,
name: name,
address: address,
open_hours: open_hours,
icon: icon,
description: description,
latitude: latitude,
longitude: longitude,
};
}
function clearForm(): void {
/* Get the form elements*/
const idInput = (document.getElementById("id") as HTMLInputElement);
const latInput = (document.getElementById("lat") as HTMLInputElement);
const longInput = (document.getElementById("long") as HTMLInputElement);
const nameInput = (document.getElementById("name") as HTMLInputElement);
const addressInput = (document.getElementById("address") as HTMLInputElement);
const openHoursArea = (document.getElementById("open_hours") as HTMLTextAreaElement);
const descriptionArea = (document.getElementById("description") as HTMLTextAreaElement);
/* Clear them */
idInput.value = "";
latInput.value = "";
longInput.value = "";
nameInput.value = "";
addressInput.value = "";
openHoursArea.value = "";
descriptionArea.value = "";
/* Clear the callback */
document.getElementById("button").onclick = null;
/* Now you see it, now you don't*/
document.getElementById("modal").style.display = "none";
}
async function createPlace(): Promise<void> {
const newPlace = getPlaceFromForm();
await fetch('places', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPlace),
})
.then((response) => response.json())
.then((place: PlaceModel) => {
places.set(
toStr({ lat: place.latitude, lng: place.longitude }),
place
);
placesLayer.addData(toFeature(place));
clearForm();
alert("Lugar añadido exitosamente");
})
.catch((error) => alert(error));
}
async function editPlace(): Promise<void> {
const newPlace = getPlaceFromForm();
await fetch('places', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPlace),
})
.then((response) => response.json())
.then((place: PlaceModel) => {
places.set(
toStr({ lat: place.latitude, lng: place.longitude }),
place
);
clearForm();
alert("Lugar guardado exitosamente. Para ver los cambios, recargue la página.");
})
.catch((error) => alert(error));
}
const enum Operation {
Create = "create",
Edit = "edit",
}
interface MapEvent {
latlng: L.LatLng;
relatedTarget: L.Marker | L.Path | undefined;
}
async function setupMap(): Promise<void> { async function setupMap(): Promise<void> {
/* Create/Edit form */
const modal = document.getElementById("modal");
const closeButton = document.getElementById("close");
closeButton.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(e: Event) {
if (e.target == modal) {
modal.style.display = "none";
}
}
function openForm(op: Operation, lat: number, long: number): void {
/* Fill the form for us */
const h1 = modal.getElementsByTagName("h1")[0];
if (op == Operation.Create) {
clearForm()
h1.innerText = "Añadir lugar nuevo";
} else {
h1.innerText = "Editar lugar";
}
const latInput = (document.getElementById("lat") as HTMLInputElement);
const longInput = (document.getElementById("long") as HTMLInputElement);
latInput.value = lat.toString();
longInput.value = long.toString();
if (op == Operation.Edit) {
const place = places.get(toStr({ lat: lat, lng: long }));
console.log(toStr({ lat: lat, lng: long }));
/*Get the form elements*/
const idInput = (document.getElementById("id") as HTMLInputElement);
const nameInput = (document.getElementById("name") as HTMLInputElement);
const addressInput = (document.getElementById("address") as HTMLInputElement);
const openHoursArea = (document.getElementById("open_hours") as HTMLTextAreaElement);
const iconSelect = (document.getElementById("icon") as HTMLSelectElement);
const descriptionArea = (document.getElementById("description") as HTMLTextAreaElement);
/* And set them */
idInput.value = place.id.toString();
nameInput.value = place.name;
addressInput.value = place.address;
openHoursArea.value = place.open_hours;
iconSelect.value = place.icon;
descriptionArea.value = place.description;
}
/* Plug callbacks */
if (op == Operation.Create) {
document.getElementById("button").onclick = createPlace;
} else {
document.getElementById("button").onclick = editPlace;
}
/* Make it appear */
modal.style.display = "block";
}
function openCreateForm(e: MapEvent) {
const lat = e.latlng.lat;
const long = e.latlng.lng;
openForm(Operation.Create, lat, long);
}
function openEditForm(e: MapEvent) {
const marker = (e.relatedTarget as L.Marker);
const lat = marker.getLatLng().lat;
const long = marker.getLatLng().lng;
openForm(Operation.Edit, lat, long);
}
/* Get places from backend */
const backendPlaces = await loadPlaces(); const backendPlaces = await loadPlaces();
const leafletPlaces = toLeafletPlaces(backendPlaces); const leafletPlaces = toLeafletPlaces(backendPlaces);
for (const place of backendPlaces) {
places.set(
toStr({ lat: place.latitude, lng: place.longitude }),
place
);
}
/* Set up the map*/ /* Set up the map*/
const map = new L.map('map', { const map = new L.Map('map');
contextmenu: true,
contextmenuWidth: 140,
contextmenuItems: [
{
text: 'Añadir lugar',
callback: openCreateForm
}
]
});
/* Create the tile layer with correct attribution*/ /* Create the tile layer with correct attribution*/
const osmUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; const osmUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
@ -285,14 +74,6 @@ async function setupMap(): Promise<void> {
popupStr += "</ul>"; popupStr += "</ul>";
layer.bindPopup(popupStr); layer.bindPopup(popupStr);
layer.bindContextMenu({
contextmenu: true,
contextmenuInheritItems: false,
contextmenuItems: [{
text: 'Editar',
callback: openEditForm,
}]
});
} }
} }
@ -326,9 +107,8 @@ async function setupMap(): Promise<void> {
return L.marker(latlng); return L.marker(latlng);
} }
placesLayer = L.geoJSON(leafletPlaces, { map.addLayer(L.geoJSON(leafletPlaces, {
onEachFeature: onEachFeature, onEachFeature: onEachFeature,
pointToLayer: pointToLayer pointToLayer: pointToLayer
}) }));
map.addLayer(placesLayer);
} }

View file

@ -6,8 +6,7 @@
"": { "": {
"dependencies": { "dependencies": {
"geojson": "^0.5.0", "geojson": "^0.5.0",
"leaflet": "^1.8.0", "leaflet": "^1.8.0"
"leaflet-contextmenu": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.7.11" "@types/leaflet": "^1.7.11"
@ -40,11 +39,6 @@
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==" "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
},
"node_modules/leaflet-contextmenu": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/leaflet-contextmenu/-/leaflet-contextmenu-1.4.0.tgz",
"integrity": "sha512-BXASCmJ5bLkuJGDCpWmvGqhZi5AzeOY0IbQalfkgBcMAMfAOFSvD4y0gIQxF/XzEyLkjXaRiUpibVj4+Cf3tUA=="
} }
}, },
"dependencies": { "dependencies": {
@ -72,11 +66,6 @@
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
"integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==" "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
},
"leaflet-contextmenu": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/leaflet-contextmenu/-/leaflet-contextmenu-1.4.0.tgz",
"integrity": "sha512-BXASCmJ5bLkuJGDCpWmvGqhZi5AzeOY0IbQalfkgBcMAMfAOFSvD4y0gIQxF/XzEyLkjXaRiUpibVj4+Cf3tUA=="
} }
} }
} }

View file

@ -4,7 +4,6 @@
}, },
"dependencies": { "dependencies": {
"geojson": "^0.5.0", "geojson": "^0.5.0",
"leaflet": "^1.8.0", "leaflet": "^1.8.0"
"leaflet-contextmenu": "^1.4.0"
} }
} }