VueJS rewrite;
This commit is contained in:
parent
97b62ca02c
commit
fd4ba2a8bb
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# poi dist
|
||||
dist
|
47
README.md
Normal file
47
README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# app
|
||||
|
||||
> My posh Vue project
|
||||
|
||||
## Commands
|
||||
|
||||
You can replace `yarn` with `npm run` here.
|
||||
|
||||
```bash
|
||||
# build for production
|
||||
yarn build
|
||||
|
||||
# development mode
|
||||
yarn dev
|
||||
|
||||
# run unit tests
|
||||
yarn test
|
||||
|
||||
# serve the bundled dist folder in production mode
|
||||
yarn serve
|
||||
```
|
||||
|
||||
## Polyfills
|
||||
|
||||
By default we only polyfill `window.Promise` and `Object.assign`. You can add more polyfills in `./src/polyfills.js`.
|
||||
|
||||
## Analyze bundle size
|
||||
|
||||
Run `yarn report` to get a report of bundle size which helps you:
|
||||
|
||||
- Realize what's really inside your bundle
|
||||
- Find out what modules make up the most of it's size
|
||||
- Find modules that got there by mistake
|
||||
- Optimize it!
|
||||
|
||||
## Progress Web App
|
||||
|
||||
Your app is now offline-ready (only in production bundle), which means you can visit it without network.
|
||||
|
||||
Here we use a default [manifest.json](./static/manifest.json) to configurure your pwa, for example, to enable *Add to Home Screen* feature on Android. It will be copied directly to `./dist/manifest.json`.
|
||||
|
||||
|
||||
For all the available options, please head to [poi-preset-offline](https://github.com/egoist/poi/tree/master/packages/poi-preset-offline#api).
|
||||
|
||||
---
|
||||
|
||||
This project is generated by [create-vue-app](https://github.com/vue-land/create-vue-app).
|
29
index.ejs
Normal file
29
index.ejs
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#4DBA87">
|
||||
|
||||
|
||||
<% if (htmlWebpackPlugin.options.description) { %>
|
||||
<meta name="description" content="<%= htmlWebpackPlugin.options.description %>"/>
|
||||
<% } %>
|
||||
|
||||
<% for (var chunk of webpack.chunks) {
|
||||
for (var file of chunk.files) {
|
||||
if (file.match(/\.(js|css)$/)) { %>
|
||||
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="<%= htmlWebpackPlugin.files.publicPath + file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
37
index.html
37
index.html
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Cool places in Lyon 7</title>
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
|
||||
integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
|
||||
crossorigin=""/>
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css' rel='stylesheet' />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Magellan</h1>
|
||||
<form class="form" id="form">
|
||||
<div class="form-group">
|
||||
<input id="address" class="form-control" type="text" name="title" placeholder="Adresse">
|
||||
<input id="submit" class="btn btn-primary" type="submit" value="Search">
|
||||
</div>
|
||||
</form>
|
||||
<ul id="results"></ul>
|
||||
<hr>
|
||||
<div id="the-map" style="width: 100%; height: 800px"></div>
|
||||
</div>
|
||||
|
||||
<script src="//unpkg.com/kinto/dist/kinto.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
|
||||
integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script src='https://api.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.js'></script>
|
||||
|
||||
<script src="magellan.js"></script>
|
||||
</body>
|
||||
</html>
|
151
magellan.js
151
magellan.js
@ -1,151 +0,0 @@
|
||||
(function() {
|
||||
const MIN_ZOOM = 6;
|
||||
const MAX_ZOOM = 18;
|
||||
const INITIAL_ZOOM = 14;
|
||||
|
||||
const TILE_URL = 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png';
|
||||
|
||||
const STATE = {
|
||||
locations: new Map(),
|
||||
};
|
||||
|
||||
let db = new Kinto({
|
||||
bucket: "magellan"
|
||||
});
|
||||
let kintoLocations = db.collection("locations");
|
||||
let syncOptions = {
|
||||
remote: "https://kinto.b.delire.party/v1",
|
||||
//headers: {Authorization: "Basic " + btoa('user:pass')}
|
||||
};
|
||||
|
||||
async function sync() {
|
||||
try {
|
||||
await kintoLocations.sync(syncOptions);
|
||||
} catch (err){
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
function addLocation(loc) {
|
||||
if (!STATE.locations.has(loc.label)) {
|
||||
let marker = L.marker(loc.coordinates)
|
||||
.addTo(map)
|
||||
.bindPopup(loc.label);
|
||||
STATE.locations.set(loc.label, marker);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
let locations = await kintoLocations.list();
|
||||
locations = locations.data;
|
||||
console.log(locations)
|
||||
locations.forEach(addLocation);
|
||||
} catch (err) {
|
||||
alert(`on init: ${err.toString()}`)
|
||||
}
|
||||
}
|
||||
init();
|
||||
|
||||
async function addNewLocation(loc) {
|
||||
if (addLocation(loc)) {
|
||||
let marker = STATE.locations.get(loc.label);
|
||||
marker.openPopup();
|
||||
|
||||
// Add it to kinto.
|
||||
try {
|
||||
await kintoLocations.create({
|
||||
label: loc.label,
|
||||
coordinates: loc.coordinates
|
||||
});
|
||||
} catch (err) {
|
||||
alert(`Error when creating new location: ${err.toString()}`);
|
||||
};
|
||||
|
||||
sync();
|
||||
} else {
|
||||
STATE.locations.get(loc.label).openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
class Location {
|
||||
constructor({ id, label, coordinates}) {
|
||||
this.id = id || null;
|
||||
this.label = label;
|
||||
this.coordinates = coordinates;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLocation(address) {
|
||||
// Alternatively, search only for addresses (specialized for Lyon 7).
|
||||
//const GEOCODING_URL = 'https://api-adresse.data.gouv.fr/search/?q={query}&postcode=69007';
|
||||
|
||||
const GEOCODING_URL = 'https://demo.addok.xyz/search/?q={query}&limit=20';
|
||||
let resp = await fetch(GEOCODING_URL.replace('{query}', address));
|
||||
let results = await resp.json();
|
||||
|
||||
results = results.features.map(feature => {
|
||||
let { properties: {label: label}, geometry: { coordinates: coordinates }} = feature;
|
||||
coordinates.unshift(coordinates.pop());
|
||||
return new Location({label, coordinates});
|
||||
});
|
||||
|
||||
console.log(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
let map = L.map('the-map').setView([45.751591, 4.845695], INITIAL_ZOOM);
|
||||
|
||||
L.tileLayer(TILE_URL, {minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM, attribution: 'Carte données © <a href="https://osm.org/copyright/">OpenStreetMap (ODbL)</a> / fond OSM-FR (CC-by-SA), <a href="https://www.data.gouv.fr/fr/datasets/points-dinterets-openstreetmap/">POI: OpenStreetMap (ODbL)</a>, <a href="https://www.data.gouv.fr/fr/datasets/base-d-adresses-nationale-ouverte-bano/">adresses BANO (ODbL)</a>, <a href="https://github.com/addok/addok">geocodeur addok</a>'}).addTo(map);
|
||||
|
||||
let $ = document.querySelector.bind(document);
|
||||
let $address = $('#address');
|
||||
let $submit = $('#submit');
|
||||
let $results = window.results = $('#results');
|
||||
|
||||
$('#form').onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
$submit.onclick = async function() {
|
||||
let address = $address.value.trim();
|
||||
|
||||
$results.innerHTML = `searching for ${address}...`;
|
||||
let firstResult = true;
|
||||
|
||||
let locations = await fetchLocation(address);
|
||||
locations = locations.map(loc => {
|
||||
let li = document.createElement('li');
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.href = '#';
|
||||
a.onclick = function() {
|
||||
L.popup()
|
||||
.setLatLng(loc.coordinates)
|
||||
.setContent(loc.label)
|
||||
.openOn(map);
|
||||
}
|
||||
a.appendChild(document.createTextNode(loc.label))
|
||||
li.appendChild(a);
|
||||
|
||||
li.appendChild(document.createTextNode(' '));
|
||||
|
||||
let button = document.createElement('button');
|
||||
button.appendChild(document.createTextNode('Save'));
|
||||
button.onclick = () => { addNewLocation(loc) };
|
||||
li.appendChild(button);
|
||||
|
||||
if (firstResult) {
|
||||
firstResult = false;
|
||||
$results.innerHTML = '';
|
||||
}
|
||||
$results.appendChild(li);
|
||||
return li;
|
||||
});
|
||||
}
|
||||
})();
|
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "app",
|
||||
"productName": "app",
|
||||
"description": "My posh Vue project",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "poi",
|
||||
"build": "poi build",
|
||||
"report": "poi build --bundle-report",
|
||||
"serve": "serve dist --single"
|
||||
},
|
||||
"author": {
|
||||
"name": "bnjbvr",
|
||||
"email": "public@benj.me"
|
||||
},
|
||||
"dependencies": {
|
||||
"kinto": "^12.3.0",
|
||||
"leaflet": "^1.4.0",
|
||||
"leaflet-sidebar-v2": "^3.1.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"normalize.css": "^7.0.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"offline-plugin": "^4.8.0",
|
||||
"promise-polyfill": "^6.0.2",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue2-leaflet": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.8.1",
|
||||
"poi": "^9.0.0",
|
||||
"poi-preset-bundle-report": "^2.0.0",
|
||||
"poi-preset-offline": "^9.0.0",
|
||||
"serve": "^6.1.0"
|
||||
}
|
||||
}
|
29
poi.config.js
Normal file
29
poi.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
const path = require('path');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
'src/polyfills.js',
|
||||
'src/index.js'
|
||||
],
|
||||
html: {
|
||||
title: pkg.productName,
|
||||
description: pkg.description,
|
||||
template: path.join(__dirname, 'index.ejs')
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
// Your postcss plugins
|
||||
]
|
||||
},
|
||||
presets: [
|
||||
require('poi-preset-bundle-report')(),
|
||||
require('poi-preset-offline')({
|
||||
pwa: './src/pwa.js', // Path to pwa runtime entry
|
||||
pluginOptions: {} // Additional options for offline-plugin
|
||||
})
|
||||
],
|
||||
output: {
|
||||
publicUrl: '/magellan',
|
||||
}
|
||||
};
|
35
src/backend.js
Normal file
35
src/backend.js
Normal file
@ -0,0 +1,35 @@
|
||||
import * as Kinto from 'kinto';
|
||||
|
||||
const DB = new Kinto({
|
||||
bucket: 'magellan'
|
||||
});
|
||||
|
||||
const LOCATIONS = DB.collection('locations');
|
||||
|
||||
const syncOptions = { remote: 'https://kinto.b.delire.party/v1' };
|
||||
|
||||
export async function getAll() {
|
||||
let locations = await LOCATIONS.list();
|
||||
return locations.data;
|
||||
}
|
||||
|
||||
function checkNotMissing(obj, field) {
|
||||
if (typeof obj[field] === 'undefined') {
|
||||
console.warn(`missing field ${field} on object in ${new Error().stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(loc) {
|
||||
checkNotMissing(loc, 'label');
|
||||
checkNotMissing(loc, 'coordinates');
|
||||
|
||||
let result = await LOCATIONS.create({
|
||||
label: loc.label,
|
||||
coordinates: loc.coordinates,
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
await LOCATIONS.sync(syncOptions);
|
||||
}
|
58
src/components/App.vue
Normal file
58
src/components/App.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '../store';
|
||||
import * as backend from '../backend';
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
created() {
|
||||
backend.sync().then(() => {
|
||||
return backend.getAll();
|
||||
}).then(locations => {
|
||||
store.setLocations(locations);
|
||||
}).catch(err => {
|
||||
alert(`at app startup: ${err.toString()}`);
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style src="normalize.css/normalize.css"></style>
|
||||
<style src="@fortawesome/fontawesome-free/css/all.css"></style>
|
||||
|
||||
<style>
|
||||
code {
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
code::before, code::after {
|
||||
content: '`';
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bm-burger-button {
|
||||
top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bm-menu a,
|
||||
.bm-menu a:visited {
|
||||
color: white
|
||||
}
|
||||
</style>
|
53
src/components/ListAll.vue
Normal file
53
src/components/ListAll.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<ul class="locations">
|
||||
<li
|
||||
v-for="location in store.locations"
|
||||
@click="showDetails(location)"
|
||||
>
|
||||
<router-link :to="createLink(location)">
|
||||
{{location.label}}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '../store';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
store
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createLink(loc) {
|
||||
let lat = loc.coordinates[0];
|
||||
let lng = loc.coordinates[1];
|
||||
return `/${lat}/${lng}`;
|
||||
},
|
||||
|
||||
showDetails(loc) {
|
||||
let lat = loc.coordinates[0];
|
||||
let lng = loc.coordinates[1];
|
||||
store.openPopup(loc.label, lat, lng);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.leaflet-sidebar-content ul {
|
||||
list-style-type: none;
|
||||
padding-left: 5px;
|
||||
}
|
||||
</style>
|
105
src/components/Map.vue
Normal file
105
src/components/Map.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<l-map
|
||||
id="the-map"
|
||||
ref="myMap"
|
||||
:zoom="zoom"
|
||||
:min-zoom="min_zoom"
|
||||
:max-zoom="max_zoom"
|
||||
:center="center"
|
||||
:options="{zoomControl: false}"
|
||||
@click="onClick"
|
||||
>
|
||||
<l-tile-layer
|
||||
:url="url"
|
||||
:attribution="attribution"
|
||||
></l-tile-layer>
|
||||
|
||||
<l-marker
|
||||
v-for="loc in store.locations"
|
||||
:lat-lng="loc.coordinates"
|
||||
>
|
||||
<l-popup>
|
||||
{{ loc.label }}
|
||||
</l-popup>
|
||||
</l-marker>
|
||||
|
||||
<l-control-zoom position="topright"></l-control-zoom>
|
||||
|
||||
<Sidebar></Sidebar>
|
||||
</l-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LPopup, LControl, LControlZoom, LMap, LTileLayer, LMarker } from 'vue2-leaflet';
|
||||
import * as L from 'leaflet';
|
||||
|
||||
import store from '../store';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
const TILE_URL = 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png';
|
||||
const ATTRIBUTION = 'Carte données © <a href="https://osm.org/copyright/">OpenStreetMap (ODbL)</a> / fond OSM-FR (CC-by-SA), <a href="https://www.data.gouv.fr/fr/datasets/points-dinterets-openstreetmap/">POI: OpenStreetMap (ODbL)</a>, <a href="https://www.data.gouv.fr/fr/datasets/base-d-adresses-nationale-ouverte-bano/">adresses BANO (ODbL)</a>, <a href="https://github.com/addok/addok">geocodeur addok</a>';
|
||||
|
||||
const MIN_ZOOM = 6;
|
||||
const MAX_ZOOM = 18;
|
||||
const INITIAL_ZOOM = 14;
|
||||
|
||||
export default {
|
||||
name: 'fullmap',
|
||||
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LMarker,
|
||||
LControlZoom,
|
||||
LControl,
|
||||
LPopup,
|
||||
Sidebar,
|
||||
},
|
||||
|
||||
data() {
|
||||
let lat = this.$route.params.lat;
|
||||
let lng = this.$route.params.lng;
|
||||
|
||||
return {
|
||||
center: L.latLng(lat, lng),
|
||||
zoom: INITIAL_ZOOM,
|
||||
min_zoom: MIN_ZOOM,
|
||||
max_zoom: MAX_ZOOM,
|
||||
url: TILE_URL,
|
||||
attribution: ATTRIBUTION,
|
||||
store,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(event) {
|
||||
let {lat, lng} = event.latlng;
|
||||
this.$router.replace(`/${lat}/${lng}`);
|
||||
this.$refs.myMap.mapObject.panTo(event.latlng);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.store.setMapObject(this.$refs.myMap.mapObject);
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (to.name === 'position') {
|
||||
let {lat, lng} = to.params;
|
||||
this.$refs.myMap.mapObject.panTo({ lat, lng });
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style src="leaflet/dist/leaflet.css"></style>
|
||||
|
||||
<style scoped>
|
||||
#the-map {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
119
src/components/Search.vue
Normal file
119
src/components/Search.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="form">
|
||||
<input type="text" placeholder="place" v-model="searchInput" @keydown="doSearch" />
|
||||
<button @click="clearSearch()" :disabled="searchInput.length === 0">
|
||||
<i class="fa fa-trash-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<p v-if="searchStatus">{{ searchStatus }}</p>
|
||||
<ul class="results" v-if="results.length > 0">
|
||||
<li
|
||||
v-for="result in results"
|
||||
@click="openPopup(result)"
|
||||
>
|
||||
<router-link :to="{ name: 'position', params: {
|
||||
lat: result.coordinates[0],
|
||||
lng: result.coordinates[1] } }">{{result.label}}</router-link>
|
||||
|
||||
<i class="fa fa-save" @click="save(result)"></i>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="searchInput.length > 0 && searchStatus === null && results.length == 0">
|
||||
No results found.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import store from '../store';
|
||||
|
||||
const GEOCODING_URL = 'https://demo.addok.xyz/search/?q={query}&limit=20';
|
||||
|
||||
export default {
|
||||
name: 'Menu',
|
||||
|
||||
data() {
|
||||
return {
|
||||
store,
|
||||
searchInput: '',
|
||||
searchStatus: null,
|
||||
results: [],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
debouncedSearch: debounce(async function() {
|
||||
if (this.searchInput.length === 0) {
|
||||
// Just cleared the input.
|
||||
this.searchStatus = null;
|
||||
this.results = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch search.
|
||||
this.searchStatus = "Searching...";
|
||||
|
||||
let resp = await fetch(GEOCODING_URL.replace('{query}', this.searchInput));
|
||||
let results = await resp.json();
|
||||
this.searchStatus = null;
|
||||
|
||||
this.results = (results && results.features && results.features.map(feature => {
|
||||
let { properties: {label: label}, geometry: { coordinates: coordinates }} = feature;
|
||||
coordinates.unshift(coordinates.pop());
|
||||
return {label, coordinates}
|
||||
})) || [];
|
||||
}, 300),
|
||||
|
||||
doSearch() {
|
||||
if (this.searchInput.length > 0) {
|
||||
this.searchStatus = "Waiting for more characters...";
|
||||
this.results = [];
|
||||
}
|
||||
this.debouncedSearch();
|
||||
},
|
||||
|
||||
openPopup(loc) {
|
||||
this.store.openPopup(loc.label, loc.coordinates[0], loc.coordinates[1]);
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchInput = '';
|
||||
this.searchStatus = null;
|
||||
this.results = [];
|
||||
},
|
||||
|
||||
async save(loc) {
|
||||
await store.addLocation(loc);
|
||||
this.clearSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 2;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.fa-save, .fa-trash-alt {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
66
src/components/Sidebar.vue
Normal file
66
src/components/Sidebar.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div id="sidebar" class="leaflet-sidebar collapsed">
|
||||
<div class="leaflet-sidebar-tabs">
|
||||
<ul role="tablist"> <!-- top aligned tabs -->
|
||||
<li><a href="#search" role="tab"><i class="fa fa-search"></i></a></li>
|
||||
<li><a href="#all" role="tab"><i class="fa fa-bars"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="leaflet-sidebar-content">
|
||||
<div class="leaflet-sidebar-pane" id="search">
|
||||
<h1 class="leaflet-sidebar-header">Search<div class="leaflet-sidebar-close"><i class="fa fa-caret-left"></i></div></h1>
|
||||
<Search/>
|
||||
</div>
|
||||
<div class="leaflet-sidebar-pane" id="all">
|
||||
<h1 class="leaflet-sidebar-header">All locations<div class="leaflet-sidebar-close"><i class="fa fa-caret-left"></i></div></h1>
|
||||
<ListAll></ListAll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as _ from 'leaflet-sidebar-v2';
|
||||
|
||||
import Search from './Search';
|
||||
import ListAll from './ListAll';
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
components: {
|
||||
Search,
|
||||
ListAll,
|
||||
},
|
||||
mounted() {
|
||||
this.mapObject = L.control.sidebar({
|
||||
autopan: true,
|
||||
closeButton: true,
|
||||
container: 'sidebar',
|
||||
position: 'left',
|
||||
});
|
||||
|
||||
let parentContainer = this.$parent;
|
||||
while (typeof parentContainer.mapObject === 'undefined') {
|
||||
parentContainer = parentContainer.$parent;
|
||||
}
|
||||
|
||||
this.mapObject.addTo(parentContainer.mapObject);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mapObject.remove();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style src="leaflet-sidebar-v2/css/leaflet-sidebar.min.css"></style>
|
||||
|
||||
<style>
|
||||
.leaflet-sidebar-content {
|
||||
text-align: left;
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.leaflet-sidebar-pane h1 + * {
|
||||
margin-top: 18px;
|
||||
}
|
||||
</style>
|
34
src/index.js
Normal file
34
src/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
import { Icon } from 'leaflet';
|
||||
|
||||
import App from './components/App.vue';
|
||||
import Map from './components/Map.vue';
|
||||
|
||||
delete Icon.Default.prototype._getIconUrl;
|
||||
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
|
||||
});
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{ name: "position", path: "/:lat/:lng", component: Map },
|
||||
{ path: "/", redirect: '/45.751591/4.845695/' }
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App),
|
||||
router,
|
||||
});
|
5
src/polyfills.js
Normal file
5
src/polyfills.js
Normal file
@ -0,0 +1,5 @@
|
||||
if (!window.Promise) {
|
||||
window.Promise = require('promise-polyfill');
|
||||
}
|
||||
|
||||
Object.assign = require('object-assign');
|
7
src/pwa.js
Normal file
7
src/pwa.js
Normal file
@ -0,0 +1,7 @@
|
||||
import runtime from 'offline-plugin/runtime';
|
||||
|
||||
runtime.install({
|
||||
onUpdateReady() {
|
||||
runtime.applyUpdate();
|
||||
}
|
||||
});
|
42
src/store.js
Normal file
42
src/store.js
Normal file
@ -0,0 +1,42 @@
|
||||
import * as backend from './backend';
|
||||
|
||||
const store = {
|
||||
locations: [],
|
||||
locationLabels: new Set(),
|
||||
setLocations(newLocations) {
|
||||
this.locations = newLocations;
|
||||
this.locations.sort((a, b) => a.label > b.label);
|
||||
for (let loc of newLocations) {
|
||||
this.locationLabels.add(loc.label);
|
||||
}
|
||||
},
|
||||
async addLocation(loc) {
|
||||
if (this.locationLabels.has(loc.label)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newLoc = null;
|
||||
try {
|
||||
newLoc = await backend.create(loc);
|
||||
} catch (err) {
|
||||
alert(`Error when saving a new place: ${err.toString()}`);
|
||||
return;
|
||||
}
|
||||
this.locations.push(newLoc);
|
||||
this.locations.sort((a, b) => a.label > b.label);
|
||||
this.locationLabels.add(newLoc.label);
|
||||
},
|
||||
|
||||
mapObject: null,
|
||||
setMapObject(mapObject) {
|
||||
this.mapObject = mapObject;
|
||||
},
|
||||
openPopup(label, lat, lng) {
|
||||
L.popup()
|
||||
.setLatLng([lat, lng])
|
||||
.setContent(label)
|
||||
.openOn(this.mapObject);
|
||||
},
|
||||
};
|
||||
|
||||
export default store;
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
static/icons/android-chrome-192x192.png
Normal file
BIN
static/icons/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
static/icons/android-chrome-512x512.png
Normal file
BIN
static/icons/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
20
static/manifest.json
Normal file
20
static/manifest.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "app",
|
||||
"short_name": "app",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#4DBA87"
|
||||
}
|
Loading…
Reference in New Issue
Block a user