Separate project / feed / settings
All checks were successful
Deploy Spot / deploy (push) Successful in 49s

This commit is contained in:
2026-05-22 22:17:04 +02:00
parent 3fd68fa938
commit 8a590aa2fc
13 changed files with 519 additions and 378 deletions

View File

@@ -54,6 +54,7 @@ jobs:
--exclude "/.npm-cache/" \
--exclude "/node_modules/" \
--exclude "/config/settings.php" \
--exclude "/log.html" \
--exclude "/files/" \
--exclude "/geo/" \
--exclude "/gaia/" \

View File

@@ -325,19 +325,20 @@ class Spot extends Main
$asMarkers = [...$asMessages, ...$asGeoMedias];
usort($asMarkers, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
//Spot Last Update
$asLastUpdate = array();
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
$asResult = array(
'markers' => $asMarkers,
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
'last_update' => $asLastUpdate
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId())
);
return $bInternal?$asResult:self::getJsonResult(true, '', $asResult);
}
public function getLastUpdate() {
$asLastUpdate = array();
$this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate());
return self::getJsonResult(true, '', $asLastUpdate);
}
public function subscribe($sEmail, $sNickName) {
$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get(), $sNickName);
$asUserInfo = $this->oUser->getUserInfo();

View File

@@ -42,6 +42,9 @@ if($sAction!='')
case 'markers':
$sResult = $oSpot->getMarkers();
break;
case 'last_update':
$sResult = $oSpot->getLastUpdate();
break;
case 'geojson':
$sResult = $oSpot->getProjectGeoJson();
break;

115
package-lock.json generated
View File

@@ -2131,12 +2131,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
"integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/retry": {
@@ -2680,9 +2680,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
"version": "2.10.31",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
"integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -2760,9 +2760,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001792",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
"funding": [
{
"type": "opencollective",
@@ -2797,15 +2797,15 @@
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 14.16.0"
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -2992,9 +2992,9 @@
}
},
"node_modules/css-loader/node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -3084,9 +3084,9 @@
"license": "ISC"
},
"node_modules/electron-to-chromium": {
"version": "1.5.353",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
"integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
"version": "1.5.361",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
"integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
"license": "ISC"
},
"node_modules/emojis-list": {
@@ -3099,9 +3099,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.21.5",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz",
"integrity": "sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==",
"version": "5.21.6",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -3758,9 +3758,9 @@
}
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
"license": "ISC"
},
"node_modules/kind-of": {
@@ -4015,10 +4015,13 @@
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.38",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"license": "MIT"
"version": "2.0.45",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz",
"integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -4331,9 +4334,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@@ -4350,7 +4353,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -4461,9 +4464,9 @@
"license": "ISC"
},
"node_modules/preact": {
"version": "10.29.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
"version": "10.29.2",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -4483,12 +4486,12 @@
"license": "ISC"
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
@@ -4661,12 +4664,12 @@
}
},
"node_modules/sass": {
"version": "1.99.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
"integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"chokidar": "^5.0.0",
"immutable": "^5.1.5",
"source-map-js": ">=0.6.2 <2.0.0"
},
@@ -4674,7 +4677,7 @@
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
"node": ">=20.19.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
@@ -4923,9 +4926,9 @@
}
},
"node_modules/terser": {
"version": "5.47.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz",
"integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==",
"version": "5.48.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
"integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -5035,9 +5038,9 @@
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -5204,9 +5207,9 @@
}
},
"node_modules/webpack": {
"version": "5.107.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.0.tgz",
"integrity": "sha512-PSxeHk/dmLYZlnTU+vL1Gej6Evg5RNtl3flhxBresfznFnzxinHMzHKloHnywM/3ouQv7/AlZCswWDIkNSggUA==",
"version": "5.107.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.1.tgz",
"integrity": "sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.8",

View File

@@ -2,31 +2,29 @@
import 'maplibre-gl/dist/maplibre-gl.css';
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl } from 'maplibre-gl';
import { createApp } from 'vue';
import Simplebar from 'simplebar-vue';
import Lightbox from '@scripts/lightbox';
import SpotIcon from '@components/spotIcon';
import SpotIconStack from '@components/spotIconStack';
import ProjectPost from '@components/projectPost';
import ProjectPopup from '@components/projectPopup';
import ProjectNewsletter from '@components/projectNewsletter';
import ProjectFeed from '@components/projectFeed';
import ProjectSettings from '@components/projectSettings';
export default {
components: {
SpotIcon,
ProjectPost,
ProjectNewsletter,
Simplebar
ProjectFeed,
ProjectSettings
},
data() {
return {
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true},
refreshRate: 60,
lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''},
feedPanelOpen: false,
feedSwipe: {x: null, y: null},
settingsPanelOpen: false,
panels: {
feedOpen: false,
settingsOpen: false
},
feed: null,
settings: null,
track: null,
markers: [],
markerProps: {
@@ -35,29 +33,24 @@ export default {
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
},
currProject: null,
project: null,
modeHisto: null,
posts: [],
baseMaps: [],
baseMap: null,
map: null,
mapInitializing: false,
markerHeight: 32, //FIXME
mapPadding: 16 + 32, //1rem + marker height
lightbox: null,
hikes: {
colors: {},
width: 4
},
popup: {content: null, element: null},
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')}
overview: {id: 0, codename:'overview', name: this.lang.get('project.overview')},
};
},
computed: {
projectClasses() {
return [
this.feedPanelOpen?'with-feed':'',
this.settingsPanelOpen?'with-settings':''
].filter(n => n).join(' ');
},
projectOptions() {
return [
this.overview,
@@ -75,7 +68,7 @@ export default {
'hash.items.0'(newProjectCodename, oldProjectCodename) {
if(newProjectCodename != oldProjectCodename) {
this.hash.items = [newProjectCodename];
this.toggleSettingsPanel(false, 'none');
this.settings.toggle(false, 0);
this.init();
}
}
@@ -113,11 +106,8 @@ export default {
};
//Reset values
this.setFeedUpdateTimer(-1);
this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true};
this.posts = [];
this.track = null;
this.currProject = null;
this.project = null;
this.removeMapContent();
//Build Map
@@ -128,28 +118,27 @@ export default {
},
quit() {
this.lightbox.end();
this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll);
this.setFeedUpdateTimer(-1);
this.removeMap();
},
async initOverview() {
this.modeHisto = true;
this.hash.items = [this.overview.codename];
this.toggleFeedPanel(false, 'none');
this.feed.toggle(false, 0);
await this.initOverviewMap();
},
async initProject(iProjectId) {
this.currProject = this.projects[iProjectId];
this.modeHisto = (this.currProject.mode == this.consts.modes.histo);
async initProject(sProjectCodeName) {
this.project = this.projects[sProjectCodeName];
this.modeHisto = (this.project.mode == this.consts.modes.histo);
await this.$nextTick();
await Promise.all([
this.initFeed(),
this.feed.init(),
this.initProjectMap()
]);
//Direct link post action
if(this.hash.items.length == 3) await this.findPost(this.hash.items[1], this.hash.items[2]);
if(this.hash.items.length == 3) await this.feed.findPost(this.hash.items[1], this.hash.items[2]);
},
initLightbox() {
if(!this.lightbox) {
@@ -162,11 +151,11 @@ export default {
resizeDuration: 400,
hasVideo: true,
onMediaChange: async (oMedia) => {
this.hash.items = [this.currProject.codename, 'media', oMedia.id];
this.hash.items = [this.project.codename, 'media', oMedia.id];
if(oMedia.set == 'post-medias') {
this.goToPost('media', oMedia.id)?.panMapToMarker();
(await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
if(!this.lightbox.hasMediaAfterCurrent()) {
await this.getNextFeed();
await this.feed.getNextFeed();
await this.$nextTick();
this.lightbox.refreshAlbum();
}
@@ -176,39 +165,19 @@ export default {
});
}
},
async initFeed() {
await this.$nextTick();
//Simplebar event
this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll);
this.$refs.feedSimpleBar?.scrollElement.addEventListener('scroll', this.onFeedScroll);
this.toggleFeedPanel(!this.isMobile(), 'none');
//Get first posts batch
await this.getNextFeed();
if(this.$refs.feedSimpleBar) this.$refs.feedSimpleBar.scrollElement.scrollTop = 0;
//Start auto-update
if(!this.modeHisto) this.setFeedUpdateTimer(this.refreshRate);
},
async initProjectMap() {
[
{
maps: this.baseMaps,
markers: this.markers,
last_update: this.lastUpdate
},
{maps: this.baseMaps, markers: this.markers},
this.track
] = await Promise.all([
this.api.get('markers', {id_project: this.currProject.id}),
this.api.get('geojson', {id_project: this.currProject.id})
this.api.get('markers', {id_project: this.project.id}),
this.api.get('geojson', {id_project: this.project.id})
]);
await this.initMap({
setCamera: () => {
this.map.fitBounds(this.getInitialMapBounds(), {
padding: 20,
padding: this.mapPadding,
animate: false,
maxZoom: 15
});
@@ -229,7 +198,7 @@ export default {
//Center on default project
const oDefaultProject = this.projects.getDefaultProject();
//Adapt zoom to see whole planet
//Get Map / Canvas size
const $Canvas = this.map.getCanvas();
const oMapBounds = this.map.getContainer().getBoundingClientRect();
@@ -248,7 +217,7 @@ export default {
//Build map
if(!this.map) this.addMap();
this.updateMapPadding();
setCamera();
setCamera();
//Force wait for load event
await new Promise((resolve) => {
@@ -363,7 +332,7 @@ export default {
'source': 'track',
'paint': {
'line-opacity': 0,
'line-width': this.hikes.width + 20
'line-width': this.hikes.width + this.mapPadding
}
});
this.map.on('click', 'track-hitbox', this.openTrackPopup);
@@ -435,7 +404,7 @@ export default {
this.openPopup({
lnglat: [oProject.longitude, oProject.latitude],
options: oProject,
offset: [0, -32] //FIXME
offset: [0, -1 * this.markerHeight]
});
},
openMarkerPopup(iMarkerId, sMarkerType) {
@@ -443,7 +412,7 @@ export default {
this.openPopup({
lnglat: [oMarker.longitude, oMarker.latitude],
options: oMarker,
offset: [0, -32] //FIXME
offset: [0, -1 * this.markerHeight]
});
},
openTrackPopup(oEvent) {
@@ -466,7 +435,7 @@ export default {
this.popup.content = createApp(ProjectPopup, {
options: options,
project: this.currProject
project: this.project
});
this.popup.content
.provide('lang', this.lang)
@@ -501,7 +470,7 @@ export default {
oBounds.extend(new LngLat(oHashMarker.longitude, oHashMarker.latitude));
}
else if( //Blog Mode: Fit to last message
this.currProject.mode == this.consts.modes.blog &&
this.project.mode == this.consts.modes.blog &&
this.markers.length > 0
) {
let oLastMsg = this.markers.at(-1);
@@ -510,9 +479,7 @@ export default {
else { //Pre/Histo Mode: Fit to track
for(const iFeatureId in this.track.features) {
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
(bounds, coord) => {
return bounds.extend(coord);
},
(bounds, coord) => bounds.extend(coord),
oBounds
);
}
@@ -520,110 +487,9 @@ export default {
return oBounds;
},
async findPost(sPostType, iPostId) {
let vPost = this.goToPost(sPostType, iPostId);
if(vPost) {
await vPost.executeMainAction(0);
return vPost;
}
else if(!this.feed.outOfData) {
await this.getNextFeed();
return this.findPost(sPostType, iPostId);
}
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
return null;
},
goToPost(sPostType, iPostId) {
let bFound = false;
let avPosts = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
if(avPosts.length > 0) {
let vPost = avPosts[0];
this.$refs.feedSimpleBar.scrollElement.scrollTop += Math.round(
vPost.$el.getBoundingClientRect().top
+ window.pageYOffset
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
);
return vPost;
}
},
async getNextFeed() {
if(!this.feed.outOfData && !this.feed.loading) {
//Get next chunk
this.feed.loading = true;
let aoData = await this.api.get('next_feed', {id_project: this.currProject.id, id: this.feed.refIdLast});
let iPostCount = Object.keys(aoData.feed).length;
//Update pointers
this.feed.outOfData = (iPostCount < this.consts.chunk_size);
if(iPostCount > 0) {
this.feed.refIdLast = aoData.ref_id_last;
if(this.feed.firstChunk) this.feed.refIdFirst = aoData.ref_id_first;
}
//Add posts
this.posts.push(...aoData.feed);
this.feed.loading = false;
this.feed.firstChunk = false;
}
return true;
},
onFeedScroll(oEvent) {
const box = oEvent.currentTarget
const content = box.querySelector('.simplebar-content')
if ((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
},
onFeedTouchStart(oEvent) {
if(!this.isMobile() || !this.feedPanelOpen || oEvent.touches.length != 1) return;
const oTouch = oEvent.touches[0];
this.feedSwipe = {x: oTouch.clientX, y: oTouch.clientY};
},
onFeedTouchEnd(oEvent) {
const oTouch = oEvent.changedTouches[0];
if(!oTouch || this.feedSwipe.x === null) return;
const iDeltaX = oTouch.clientX - this.feedSwipe.x;
const iDeltaY = oTouch.clientY - this.feedSwipe.y;
if(iDeltaX > 80 && Math.abs(iDeltaX) > Math.abs(iDeltaY) * 1.5) {
this.toggleFeedPanel();
}
this.feedSwipe = {x: null, y: null};
},
setFeedUpdateTimer(iSeconds) {
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
if(iSeconds >= 0) this.feedTimer = setTimeout(this.checkNewFeed, iSeconds * 1000);
},
async checkNewFeed() {
let aoData = await this.api.get('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst});
const aoFeed = aoData.feed || [];
const aoMarkers = aoData.markers || [];
if(aoFeed.length > 0) {
//Update pointer
this.feed.refIdFirst = aoData.ref_id_first;
//Add new posts
this.posts.unshift(...aoFeed);
}
//Add new Markers
if(aoMarkers.length > 0) {
this.markers.push(...aoMarkers);
aoMarkers.forEach(this.addMarker);
}
//Message Last Update
this.lastUpdate = aoData.last_update;
//Reschedule
this.setFeedUpdateTimer(this.refreshRate);
addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
this.markers.push(...aoMarkers);
aoMarkers.forEach(this.addMarker);
},
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
return new Promise((resolve) => {
@@ -643,10 +509,10 @@ export default {
getMapPadding() {
let bIsMobile = this.isMobile();
return {
top: 0,
bottom: 0,
left: (!bIsMobile && this.settingsPanelOpen)?this.$refs.settings.getBoundingClientRect().width:0,
right: (!bIsMobile && this.feedPanelOpen)?this.$refs.feed.getBoundingClientRect().width:0
top: this.mapPadding,
bottom: this.mapPadding,
left: this.mapPadding + ((!bIsMobile && this.panels.settingsOpen && this.settings)?this.settings.getWidth():0),
right: this.mapPadding + ((!bIsMobile && this.panels.feedOpen && this.feed)?this.feed.getWidth():0)
};
},
updateMapPadding(iDuration=0) {
@@ -660,58 +526,31 @@ export default {
isMarkerVisible(oLngLat){
return !!this.map && this.map.getBounds().contains(oLngLat);
},
getGoogleMapsLink(asInfo) {
return $('<a>', {
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
title: this.lang.get('map.see_on_google'),
target: '_blank',
rel: 'noreferrer noopener'
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
},
toggleFeedPanel(bShow, sMapAction) {
let bOldValue = this.feedPanelOpen;
this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow;
onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
const sPanelKey = sPanel + 'Open';
let bOldValue = this.panels[sPanelKey];
this.panels[sPanelKey] = bNewValue;
if(bOldValue != this.feedPanelOpen && !this.isMobile() && this.map) {
sMapAction = sMapAction || 'panTo';
switch(sMapAction) {
case 'none':
this.updateMapPadding();
break;
case 'panTo':
this.updateMapPadding(500);
break;
case 'panToInstant':
this.updateMapPadding();
break;
}
if(bOldValue != bNewValue) {
//Adjust map center
if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
//Open Close panels
this.$el.classList.toggle('with-'+sPanel);
}
},
toggleSettingsPanel(bShow, sMapAction) {
let bOldValue = this.settingsPanelOpen;
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow;
if(bOldValue != this.settingsPanelOpen && !this.isMobile() && this.map) {
sMapAction = sMapAction || 'panTo';
switch(sMapAction) {
case 'none':
this.updateMapPadding();
break;
case 'panTo':
this.updateMapPadding(500);
break;
case 'panToInstant':
this.updateMapPadding();
break;
}
}
setFeed(vPanel) {
this.feed = vPanel;
},
setSettings(vPanel) {
this.settings = vPanel;
}
}
}
</script>
<template>
<div id="projects" :class="projectClasses">
<div class="projects">
<div id="background"></div>
<div id="submap">
<div class="loader">
@@ -719,87 +558,23 @@ export default {
</div>
</div>
<div id="map"></div>
<div id="settings" class="map-container map-container-left" ref="settings">
<div id="settings-panel" class="map-panel">
<div class="settings-header">
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
<div id="last_update" v-if="this.currProject && this.currProject.mode == this.consts.modes.blog && lastUpdate.unix_time > 0">
<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
</div>
</div>
<div class="settings-sections">
<Simplebar id="settings-sections-scrollbox">
<div class="settings-section">
<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1>
<div class="settings-section-body">
<div class="radio" v-for="project in projectOptions" :key="'project-'+project.id">
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="hash.items[0]" :disabled="mapInitializing" />
<label :for="'project-'+project.id">
<span>{{ project.name }}</span>
<a v-if="project.gpxfilepath" class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
<SpotIcon :icon="'download'" margin="left" />
</a>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h1><SpotIcon :icon="'map'" width="fixed" :text="lang.get('map.title')" /></h1>
<div class="settings-section-body">
<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMap" :disabled="mapInitializing" />
<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
</div>
</div>
</div>
<div class="settings-section newsletter">
<ProjectNewsletter />
</div>
<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
<h1><SpotIcon :icon="'admin'" width="fixed" :text="lang.get('admin.title')" /></h1>
<div class="admin-actions">
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
</div>
</div>
</Simplebar>
</div>
<div class="settings-footer">
<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
</a>
<span>&nbsp;{{ lang.get('credits.license') }}</span>
</div>
</div>
<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleSettingsPanel">
<SpotIcon :icon="settingsPanelOpen?'prev':'menu'" />
</div>
<div v-if="currProject && !isMobile()" id="legend" class="map-control settings-control map-control-bottom">
<div v-for="(color, hikeType) in hikes.colors" class="track">
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
</div>
</div>
<div v-if="currProject" id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
<span>{{ currProject.name }}</span>
</div>
</div>
<div id="feed" class="map-container map-container-right" ref="feed" @touchstart.passive="onFeedTouchStart" @touchend.passive="onFeedTouchEnd">
<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
<div id="feed-header">
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
</div>
<div v-if="currProject" id="feed-posts">
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
</div>
<div id="feed-footer" v-if="feed.loading">
<ProjectPost :options="{type: 'loading', headerless: true}" />
</div>
</Simplebar>
<div v-if="currProject" :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggleFeedPanel">
<SpotIcon :icon="feedPanelOpen?'next':'post'" />
</div>
</div>
<ProjectSettings
:ref="setSettings"
:projects="projectOptions"
v-model:project-code-name="hash.items[0]"
:base-maps="baseMaps"
v-model:base-map="baseMap"
:map-initializing="mapInitializing"
:hikes="hikes"
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('settings', bIsOpen, iAnimDuration)"
/>
<ProjectFeed
:ref="setFeed"
:project="project"
:mode-histo="modeHisto"
@request-last-update="settings?.setLastUpdate"
@new-markers="addNewMarkers"
@toggle="(bIsOpen, iAnimDuration) => onPanelToggle('feed', bIsOpen, iAnimDuration)"
/>
</div>
</template>

View File

@@ -0,0 +1,216 @@
<script>
import Simplebar from 'simplebar-vue';
import SpotIcon from '@components/spotIcon';
import ProjectPost from '@components/projectPost';
export default {
components: {
SpotIcon,
ProjectPost,
Simplebar
},
props: {
project: Object,
modeHisto: Boolean
},
data() {
return {
loading: false,
updatable: true,
outOfData: false,
refIdFirst: 0,
refIdLast: 0,
firstChunk: true,
isOpen: false,
posts: [],
refreshRate: 60,
swipe: {x: null, y: null}
};
},
emits: ['request-last-update', 'new-markers', 'toggle'],
inject: ['api', 'lang', 'consts', 'isMobile'],
provide() {
return {
feed: {
checkNewFeed: this.checkNewFeed
}
};
},
watch: {
project() {
this.syncUpdateTimer();
},
modeHisto() {
this.syncUpdateTimer();
},
firstChunk() {
this.syncUpdateTimer();
}
},
mounted() {
this.getScrollElement()?.addEventListener('scroll', this.onFeedScroll);
this.syncUpdateTimer();
},
beforeUnmount() {
this.getScrollElement()?.removeEventListener('scroll', this.onFeedScroll);
this.setUpdateTimer(-1);
},
methods: {
async init() {
this.setUpdateTimer(-1);
this.loading = false;
this.updatable = true;
this.outOfData = false;
this.refIdFirst = 0;
this.refIdLast = 0;
this.firstChunk = true;
this.posts = [];
this.swipe = {x: null, y: null};
await this.$nextTick();
this.toggle(!this.isMobile(), 0);
await this.getNextFeed();
this.getScrollElement().scrollTop = 0;
this.syncUpdateTimer();
},
getScrollElement() {
return this.$refs.feedSimpleBar?.scrollElement;
},
async findPost(sPostType, iPostId) {
let vPost = await this.goToPost(sPostType, iPostId);
if(vPost) {
await vPost.executeMainAction(0);
return vPost;
}
else if(!this.outOfData) {
await this.getNextFeed();
await this.$nextTick();
return this.findPost(sPostType, iPostId);
}
else console.log('Missing element ID "'+iPostId+'" of type "'+sPostType+'"');
return null;
},
async goToPost(sPostType, iPostId) {
let avPosts = this.$refs.posts.filter((post) => {return post.postId == sPostType+'-'+iPostId;});
if(avPosts.length == 0) return null;
//Force next update to have enough subsequent elements to position the post on top of the page
await this.getNextFeed();
let vPost = avPosts[0];
this.getScrollElement().scrollTop += Math.round(
vPost.$el.getBoundingClientRect().top
+ window.pageYOffset
- parseFloat(getComputedStyle(this.$refs.feedSimpleBar.$el).paddingTop)
);
return vPost;
},
async getNextFeed() {
if(!this.project || this.outOfData || this.loading) return true;
//Get next chunk
this.loading = true;
let aoData = await this.api.get('next_feed', {id_project: this.project.id, id: this.refIdLast});
let iPostCount = Object.keys(aoData.feed).length;
//Update pointers
this.outOfData = (iPostCount < this.consts.chunk_size);
if(iPostCount > 0) {
this.refIdLast = aoData.ref_id_last;
if(this.firstChunk) this.refIdFirst = aoData.ref_id_first;
}
//Add posts
this.posts.push(...aoData.feed);
this.loading = false;
this.firstChunk = false;
return true;
},
onFeedScroll(oEvent) {
const box = oEvent.currentTarget;
const content = box.querySelector('.simplebar-content');
if((box.scrollTop + box.clientHeight) / (content?.offsetHeight || 1) >= 0.8) this.getNextFeed();
},
onTouchStart(oEvent) {
if(!this.isMobile() || !this.isOpen || oEvent.touches.length != 1) return;
const oTouch = oEvent.touches[0];
this.swipe = {x: oTouch.clientX, y: oTouch.clientY};
},
onTouchEnd(oEvent) {
const oTouch = oEvent.changedTouches[0];
if(!oTouch || this.swipe.x === null) return;
const iDeltaX = oTouch.clientX - this.swipe.x;
const iDeltaY = oTouch.clientY - this.swipe.y;
if(iDeltaX > 80 && Math.abs(iDeltaX) > Math.abs(iDeltaY) * 1.5) this.toggle();
this.swipe = {x: null, y: null};
},
setUpdateTimer(iSeconds) {
if(typeof this.feedTimer != 'undefined') clearTimeout(this.feedTimer);
if(iSeconds >= 0) this.feedTimer = setTimeout(this.onUpdateTimer, iSeconds * 1000);
},
syncUpdateTimer() {
this.setUpdateTimer((!!this.project && !this.modeHisto && !this.firstChunk)?this.refreshRate:-1);
},
async onUpdateTimer() {
await this.checkNewFeed();
this.syncUpdateTimer();
},
async checkNewFeed() {
if(!this.project) return;
let aoData = await this.api.get('new_feed', {id_project: this.project.id, id: this.refIdFirst});
const aoFeed = aoData.feed || [];
const aoMarkers = aoData.markers || [];
if(aoFeed.length > 0) {
//Update pointer
this.refIdFirst = aoData.ref_id_first;
//Add new posts
this.posts.unshift(...aoFeed);
}
if(aoMarkers.length > 0) this.$emit('new-markers', aoMarkers);
this.$emit('request-last-update');
},
toggle(bShow, iAnimDuration=500) {
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
this.$emit('toggle', this.isOpen, iAnimDuration);
return this.isOpen;
},
getWidth() {
return this.$el.getBoundingClientRect().width;
}
}
}
</script>
<template>
<div id="feed" class="map-container map-container-right" @touchstart.passive="onTouchStart" @touchend.passive="onTouchEnd">
<Simplebar id="feed-panel" class="map-panel" ref="feedSimpleBar">
<div id="feed-header">
<ProjectPost v-if="modeHisto" :options="{type: 'archived', headerless: true}" />
<ProjectPost v-else :options="{type: 'poster', relative_time: lang.get('post.new_message')}" />
</div>
<div v-if="project" id="feed-posts">
<ProjectPost v-for="post in posts" :options="post" ref="posts" />
</div>
<div id="feed-footer" v-if="loading">
<ProjectPost :options="{type: 'loading', headerless: true}" />
</div>
</Simplebar>
<div v-if="project" :class="'map-control map-control-icon feed-control map-control-'+(isMobile()?'bottom':'top')" @click="toggle">
<SpotIcon :icon="isOpen?'next':'post'" />
</div>
</div>
</template>

View File

@@ -38,6 +38,7 @@
focusZoomLevel: 15
};
},
inject: ['api', 'lang', 'project', 'feed', 'user', 'map', 'hash', 'consts', 'isMobile'],
computed: {
postClass() {
let sHeaderLess = this.options.headerless?' headerless':'';
@@ -59,10 +60,10 @@
return this.mouseOverDrill?null:'footprint';
},
anchorLink() {
return '#'+[this.hash.page, this.project.currProject.codename, this.options.type, this.options.id].join(this.consts.hash_sep);
return '#'+[this.hash.page, this.project.project.codename, this.options.type, this.options.id].join(this.consts.hash_sep);
},
modeHisto() {
return (this.project?.currProject?.mode == this.consts.modes.histo);
return (this.project?.project?.mode == this.consts.modes.histo);
},
relTime() {
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time;
@@ -90,7 +91,6 @@
return new LngLat(oRelatedMarker.longitude, oRelatedMarker.latitude);
}
},
inject: ['api', 'lang', 'project', 'user', 'map', 'hash', 'consts', 'isMobile'],
methods: {
copyAnchor() {
copyTextToClipboard(this.consts.server+this.anchorLink);
@@ -106,8 +106,8 @@
this.popupRequested = true;
if(this.isMobile()) this.project.toggleFeedPanel(false, 'panToInstant');
this.hash.items = [this.project.currProject.codename, this.options.type, this.options.id];
if(this.isMobile()) this.feed.toggle(false);
this.hash.items = [this.project.project.codename, this.options.type, this.options.id];
return this.map.panToBetweenPanels(this.relatedMarkerLatLng, this.focusZoomLevel, iAnimDuration).then(() => {
this.openMarkerPopup();
@@ -137,14 +137,14 @@
this.api.get(
'add_post',
{
id_project: this.project.currProject.id,
id_project: this.project.project.id,
name: this.user.name,
content: this.postMessage
}
)
.then(() => {
this.postMessage = '';
this.project.checkNewFeed();
this.feed.checkNewFeed();
this.sending = false;
})
.catch((sDesc) => {

View File

@@ -0,0 +1,142 @@
<script>
import Simplebar from 'simplebar-vue';
import SpotIcon from '@components/spotIcon';
import ProjectNewsletter from '@components/projectNewsletter';
export default {
components: {
SpotIcon,
ProjectNewsletter,
Simplebar
},
props: {
projects: Array,
projectCodeName: String,
baseMaps: Array,
baseMap: String,
mapInitializing: Boolean,
hikes: Object
},
data() {
return {
isOpen: false,
lastUpdate: {unix_time: 0, relative_time: '', formatted_time: ''}
};
},
emits: ['update:baseMap', 'update:projectCodeName', 'toggle'],
inject: ['api', 'lang', 'user', 'consts', 'isMobile'],
computed: {
project() {
return this.projects.find((project) => project.codename == this.projectCodeName);
},
projectCodeNameModel: {
get() {
return this.projectCodeName;
},
set(sProjectCodeName) {
this.$emit('update:projectCodeName', sProjectCodeName);
}
},
baseMapModel: {
get() {
return this.baseMap;
},
set(sBaseMap) {
this.$emit('update:baseMap', sBaseMap);
}
}
},
watch: {
projectCodeName() {
this.setLastUpdate();
}
},
mounted() {
this.setLastUpdate();
},
methods: {
async setLastUpdate() {
if(this.project?.mode == this.consts.modes.blog) {
this.lastUpdate = await this.api.get('last_update', {id_project: this.project.id});
}
},
toggle(bShow, iAnimDuration=500) {
this.isOpen = (typeof bShow == 'boolean')?bShow:(!this.isOpen);
this.$emit('toggle', this.isOpen, iAnimDuration);
return this.isOpen;
},
getWidth() {
return this.$el.getBoundingClientRect().width;
}
}
}
</script>
<template>
<div id="settings" class="map-container map-container-left">
<div id="settings-panel" class="map-panel">
<div class="settings-header">
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div>
<div id="last_update" v-if="project?.mode == consts.modes.blog && lastUpdate.unix_time > 0">
<p><span><img src="images/spot-logo-only.svg" alt="" /></span><abbr :title="lastUpdate.formatted_time">{{ lang.get('feed.last_update')+' '+lastUpdate.relative_time }}</abbr></p>
</div>
</div>
<div class="settings-sections">
<Simplebar id="settings-sections-scrollbox">
<div class="settings-section">
<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1>
<div class="settings-section-body">
<div class="radio" v-for="project in projects" :key="'project-'+project.id">
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="projectCodeNameModel" :disabled="mapInitializing" />
<label :for="'project-'+project.id">
<span>{{ project.name }}</span>
<a v-if="project.gpxfilepath" class="download" :href="project.gpxfilepath" :download="project.codename + '.gpx'" :title="lang.get('track.download')" @click.stop="()=>{}">
<SpotIcon :icon="'download'" margin="left" />
</a>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h1><SpotIcon :icon="'map'" width="fixed" :text="lang.get('map.title')" /></h1>
<div class="settings-section-body">
<div class="radio" v-for="bm in baseMaps" :key="'map-'+bm.id_map">
<input type="radio" :id="'map-'+bm.id_map" :value="bm.codename" v-model="baseMapModel" :disabled="mapInitializing" />
<label :for="'map-'+bm.id_map">{{ lang.get('map.'+bm.codename) }}</label>
</div>
</div>
</div>
<div class="settings-section newsletter">
<ProjectNewsletter />
</div>
<div class="settings-section admin" v-if="user.hasClearance(consts.clearances.admin)">
<h1><SpotIcon :icon="'admin'" width="fixed" :text="lang.get('admin.title')" /></h1>
<div class="admin-actions">
<a class="button" href="#admin"><SpotIcon :icon="'config'" :text="lang.get('admin.config')" /></a>
<a class="button" href="#upload"><SpotIcon :icon="'upload'" :text="lang.get('admin.upload')" /></a>
</div>
</div>
</Simplebar>
</div>
<div class="settings-footer">
<a href="https://git.lutran.fr/franzz/spot" :title="lang.get('credits.git')" target="_blank" rel="noopener">
<SpotIcon :icon="'credits'" :text="lang.get('credits.project')" />
</a>
<span>&nbsp;{{ lang.get('credits.license') }}</span>
</div>
</div>
<div :class="'map-control map-control-icon settings-control map-control-'+(isMobile()?'bottom':'top')" @click="toggle">
<SpotIcon :icon="isOpen?'prev':'menu'" />
</div>
<div v-if="project?.id && !isMobile()" id="legend" class="map-control settings-control map-control-bottom">
<div v-for="(color, hikeType) in hikes.colors" class="track">
<span class="line" :style="'background-color:'+color+'; height:'+hikes.width+'px;'"></span>
<span class="desc">{{ lang.get('track.'+hikeType) }}</span>
</div>
</div>
<div v-if="project?.id" id="title" :class="'map-control settings-control map-control-'+(isMobile()?'bottom':'top')">
<span>{{ project.name }}</span>
</div>
</div>
</template>

View File

@@ -10,7 +10,7 @@
display: none !important;
}
#projects {
.projects {
.map-container {
width: calc(#{$panel-width});
max-width: calc(#{$panel-width});

View File

@@ -6,7 +6,7 @@ $panel-width: 30vw;
$panel-width-max: "400px + 3 * #{var.$block-spacing}";
$panel-actual-width: min($panel-width, #{$panel-width-max});
#projects {
.projects {
&.with-feed, &.with-settings {
#title {
max-width: calc(100vw - var.$block-spacing - $panel-actual-width - (var.$button-width + var.$block-spacing * 2) * 2);

View File

@@ -3,10 +3,10 @@
@use '@styles/page.project.map' as map;
@use '@styles/page.project.panel' as panel;
@use '@styles/page.project.feed' as feed;
@use '@styles/page.project.settings' as settings;
@use '@styles/page.project.panel.feed' as feed;
@use '@styles/page.project.panel.settings' as settings;
#projects {
.projects {
--space: #{color.$space};
--horizon: #{color.$horizon};
--track-main: #{color.$main-track};