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 "/.npm-cache/" \
--exclude "/node_modules/" \ --exclude "/node_modules/" \
--exclude "/config/settings.php" \ --exclude "/config/settings.php" \
--exclude "/log.html" \
--exclude "/files/" \ --exclude "/files/" \
--exclude "/geo/" \ --exclude "/geo/" \
--exclude "/gaia/" \ --exclude "/gaia/" \

View File

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

View File

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

115
package-lock.json generated
View File

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

View File

@@ -2,31 +2,29 @@
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl } from 'maplibre-gl'; import { Map, Marker, LngLatBounds, LngLat, Popup, ScaleControl } from 'maplibre-gl';
import { createApp } from 'vue'; import { createApp } from 'vue';
import Simplebar from 'simplebar-vue';
import Lightbox from '@scripts/lightbox'; import Lightbox from '@scripts/lightbox';
import SpotIcon from '@components/spotIcon'; import SpotIcon from '@components/spotIcon';
import SpotIconStack from '@components/spotIconStack'; import SpotIconStack from '@components/spotIconStack';
import ProjectPost from '@components/projectPost';
import ProjectPopup from '@components/projectPopup'; import ProjectPopup from '@components/projectPopup';
import ProjectNewsletter from '@components/projectNewsletter'; import ProjectFeed from '@components/projectFeed';
import ProjectSettings from '@components/projectSettings';
export default { export default {
components: { components: {
SpotIcon, SpotIcon,
ProjectPost, ProjectFeed,
ProjectNewsletter, ProjectSettings
Simplebar
}, },
data() { data() {
return { return {
feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true}, panels: {
refreshRate: 60, feedOpen: false,
lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''}, settingsOpen: false
feedPanelOpen: false, },
feedSwipe: {x: null, y: null}, feed: null,
settingsPanelOpen: false, settings: null,
track: null, track: null,
markers: [], markers: [],
markerProps: { markerProps: {
@@ -35,29 +33,24 @@ export default {
video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'}, video: {mainClasses: 'media', iconMain: 'marker', iconSub: 'video'},
message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'} message: {mainClasses: 'message', iconMain: 'marker', iconSub: 'footprint', iconSubTransform: 'rotate-270'}
}, },
currProject: null, project: null,
modeHisto: null, modeHisto: null,
posts: [],
baseMaps: [], baseMaps: [],
baseMap: null, baseMap: null,
map: null, map: null,
mapInitializing: false, mapInitializing: false,
markerHeight: 32, //FIXME
mapPadding: 16 + 32, //1rem + marker height
lightbox: null, lightbox: null,
hikes: { hikes: {
colors: {}, colors: {},
width: 4 width: 4
}, },
popup: {content: null, element: null}, 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: { computed: {
projectClasses() {
return [
this.feedPanelOpen?'with-feed':'',
this.settingsPanelOpen?'with-settings':''
].filter(n => n).join(' ');
},
projectOptions() { projectOptions() {
return [ return [
this.overview, this.overview,
@@ -75,7 +68,7 @@ export default {
'hash.items.0'(newProjectCodename, oldProjectCodename) { 'hash.items.0'(newProjectCodename, oldProjectCodename) {
if(newProjectCodename != oldProjectCodename) { if(newProjectCodename != oldProjectCodename) {
this.hash.items = [newProjectCodename]; this.hash.items = [newProjectCodename];
this.toggleSettingsPanel(false, 'none'); this.settings.toggle(false, 0);
this.init(); this.init();
} }
} }
@@ -113,11 +106,8 @@ export default {
}; };
//Reset values //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.track = null;
this.currProject = null; this.project = null;
this.removeMapContent(); this.removeMapContent();
//Build Map //Build Map
@@ -128,28 +118,27 @@ export default {
}, },
quit() { quit() {
this.lightbox.end(); this.lightbox.end();
this.$refs.feedSimpleBar?.scrollElement.removeEventListener('scroll', this.onFeedScroll);
this.setFeedUpdateTimer(-1);
this.removeMap(); this.removeMap();
}, },
async initOverview() { async initOverview() {
this.modeHisto = true; this.modeHisto = true;
this.hash.items = [this.overview.codename]; this.hash.items = [this.overview.codename];
this.toggleFeedPanel(false, 'none'); this.feed.toggle(false, 0);
await this.initOverviewMap(); await this.initOverviewMap();
}, },
async initProject(iProjectId) { async initProject(sProjectCodeName) {
this.currProject = this.projects[iProjectId]; this.project = this.projects[sProjectCodeName];
this.modeHisto = (this.currProject.mode == this.consts.modes.histo); this.modeHisto = (this.project.mode == this.consts.modes.histo);
await this.$nextTick();
await Promise.all([ await Promise.all([
this.initFeed(), this.feed.init(),
this.initProjectMap() this.initProjectMap()
]); ]);
//Direct link post action //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() { initLightbox() {
if(!this.lightbox) { if(!this.lightbox) {
@@ -162,11 +151,11 @@ export default {
resizeDuration: 400, resizeDuration: 400,
hasVideo: true, hasVideo: true,
onMediaChange: async (oMedia) => { 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') { if(oMedia.set == 'post-medias') {
this.goToPost('media', oMedia.id)?.panMapToMarker(); (await this.feed.goToPost('media', oMedia.id))?.panMapToMarker();
if(!this.lightbox.hasMediaAfterCurrent()) { if(!this.lightbox.hasMediaAfterCurrent()) {
await this.getNextFeed(); await this.feed.getNextFeed();
await this.$nextTick(); await this.$nextTick();
this.lightbox.refreshAlbum(); 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() { async initProjectMap() {
[ [
{ {maps: this.baseMaps, markers: this.markers},
maps: this.baseMaps,
markers: this.markers,
last_update: this.lastUpdate
},
this.track this.track
] = await Promise.all([ ] = await Promise.all([
this.api.get('markers', {id_project: this.currProject.id}), this.api.get('markers', {id_project: this.project.id}),
this.api.get('geojson', {id_project: this.currProject.id}) this.api.get('geojson', {id_project: this.project.id})
]); ]);
await this.initMap({ await this.initMap({
setCamera: () => { setCamera: () => {
this.map.fitBounds(this.getInitialMapBounds(), { this.map.fitBounds(this.getInitialMapBounds(), {
padding: 20, padding: this.mapPadding,
animate: false, animate: false,
maxZoom: 15 maxZoom: 15
}); });
@@ -229,7 +198,7 @@ export default {
//Center on default project //Center on default project
const oDefaultProject = this.projects.getDefaultProject(); const oDefaultProject = this.projects.getDefaultProject();
//Adapt zoom to see whole planet //Get Map / Canvas size
const $Canvas = this.map.getCanvas(); const $Canvas = this.map.getCanvas();
const oMapBounds = this.map.getContainer().getBoundingClientRect(); const oMapBounds = this.map.getContainer().getBoundingClientRect();
@@ -363,7 +332,7 @@ export default {
'source': 'track', 'source': 'track',
'paint': { 'paint': {
'line-opacity': 0, 'line-opacity': 0,
'line-width': this.hikes.width + 20 'line-width': this.hikes.width + this.mapPadding
} }
}); });
this.map.on('click', 'track-hitbox', this.openTrackPopup); this.map.on('click', 'track-hitbox', this.openTrackPopup);
@@ -435,7 +404,7 @@ export default {
this.openPopup({ this.openPopup({
lnglat: [oProject.longitude, oProject.latitude], lnglat: [oProject.longitude, oProject.latitude],
options: oProject, options: oProject,
offset: [0, -32] //FIXME offset: [0, -1 * this.markerHeight]
}); });
}, },
openMarkerPopup(iMarkerId, sMarkerType) { openMarkerPopup(iMarkerId, sMarkerType) {
@@ -443,7 +412,7 @@ export default {
this.openPopup({ this.openPopup({
lnglat: [oMarker.longitude, oMarker.latitude], lnglat: [oMarker.longitude, oMarker.latitude],
options: oMarker, options: oMarker,
offset: [0, -32] //FIXME offset: [0, -1 * this.markerHeight]
}); });
}, },
openTrackPopup(oEvent) { openTrackPopup(oEvent) {
@@ -466,7 +435,7 @@ export default {
this.popup.content = createApp(ProjectPopup, { this.popup.content = createApp(ProjectPopup, {
options: options, options: options,
project: this.currProject project: this.project
}); });
this.popup.content this.popup.content
.provide('lang', this.lang) .provide('lang', this.lang)
@@ -501,7 +470,7 @@ export default {
oBounds.extend(new LngLat(oHashMarker.longitude, oHashMarker.latitude)); oBounds.extend(new LngLat(oHashMarker.longitude, oHashMarker.latitude));
} }
else if( //Blog Mode: Fit to last message 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 this.markers.length > 0
) { ) {
let oLastMsg = this.markers.at(-1); let oLastMsg = this.markers.at(-1);
@@ -510,9 +479,7 @@ export default {
else { //Pre/Histo Mode: Fit to track else { //Pre/Histo Mode: Fit to track
for(const iFeatureId in this.track.features) { for(const iFeatureId in this.track.features) {
oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce( oBounds = this.track.features[iFeatureId].geometry.coordinates.reduce(
(bounds, coord) => { (bounds, coord) => bounds.extend(coord),
return bounds.extend(coord);
},
oBounds oBounds
); );
} }
@@ -520,110 +487,9 @@ export default {
return oBounds; return oBounds;
}, },
addNewMarkers(aoMarkers) { //FIXME Use its own marker update API
async findPost(sPostType, iPostId) { this.markers.push(...aoMarkers);
let vPost = this.goToPost(sPostType, iPostId); aoMarkers.forEach(this.addMarker);
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);
}, },
panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) { panToBetweenPanels(oLngLat, iZoom, iAnimDuration=500) {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -643,10 +509,10 @@ export default {
getMapPadding() { getMapPadding() {
let bIsMobile = this.isMobile(); let bIsMobile = this.isMobile();
return { return {
top: 0, top: this.mapPadding,
bottom: 0, bottom: this.mapPadding,
left: (!bIsMobile && this.settingsPanelOpen)?this.$refs.settings.getBoundingClientRect().width:0, left: this.mapPadding + ((!bIsMobile && this.panels.settingsOpen && this.settings)?this.settings.getWidth():0),
right: (!bIsMobile && this.feedPanelOpen)?this.$refs.feed.getBoundingClientRect().width:0 right: this.mapPadding + ((!bIsMobile && this.panels.feedOpen && this.feed)?this.feed.getWidth():0)
}; };
}, },
updateMapPadding(iDuration=0) { updateMapPadding(iDuration=0) {
@@ -660,58 +526,31 @@ export default {
isMarkerVisible(oLngLat){ isMarkerVisible(oLngLat){
return !!this.map && this.map.getBounds().contains(oLngLat); return !!this.map && this.map.getBounds().contains(oLngLat);
}, },
getGoogleMapsLink(asInfo) { onPanelToggle(sPanel, bNewValue, iAnimDuration=500) {
return $('<a>', { const sPanelKey = sPanel + 'Open';
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z', let bOldValue = this.panels[sPanelKey];
title: this.lang.get('map.see_on_google'), this.panels[sPanelKey] = bNewValue;
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;
if(bOldValue != this.feedPanelOpen && !this.isMobile() && this.map) { if(bOldValue != bNewValue) {
sMapAction = sMapAction || 'panTo'; //Adjust map center
switch(sMapAction) { if(!this.isMobile() && this.map) this.updateMapPadding(iAnimDuration);
case 'none':
this.updateMapPadding(); //Open Close panels
break; this.$el.classList.toggle('with-'+sPanel);
case 'panTo':
this.updateMapPadding(500);
break;
case 'panToInstant':
this.updateMapPadding();
break;
}
} }
}, },
toggleSettingsPanel(bShow, sMapAction) { setFeed(vPanel) {
let bOldValue = this.settingsPanelOpen; this.feed = vPanel;
this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow; },
setSettings(vPanel) {
if(bOldValue != this.settingsPanelOpen && !this.isMobile() && this.map) { this.settings = vPanel;
sMapAction = sMapAction || 'panTo';
switch(sMapAction) {
case 'none':
this.updateMapPadding();
break;
case 'panTo':
this.updateMapPadding(500);
break;
case 'panToInstant':
this.updateMapPadding();
break;
}
}
} }
} }
} }
</script> </script>
<template> <template>
<div id="projects" :class="projectClasses"> <div class="projects">
<div id="background"></div> <div id="background"></div>
<div id="submap"> <div id="submap">
<div class="loader"> <div class="loader">
@@ -719,87 +558,23 @@ export default {
</div> </div>
</div> </div>
<div id="map"></div> <div id="map"></div>
<div id="settings" class="map-container map-container-left" ref="settings"> <ProjectSettings
<div id="settings-panel" class="map-panel"> :ref="setSettings"
<div class="settings-header"> :projects="projectOptions"
<div class="logo"><img width="289" height="72" src="images/logo_black.png" alt="Spotty" /></div> v-model:project-code-name="hash.items[0]"
<div id="last_update" v-if="this.currProject && this.currProject.mode == this.consts.modes.blog && lastUpdate.unix_time > 0"> :base-maps="baseMaps"
<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> v-model:base-map="baseMap"
</div> :map-initializing="mapInitializing"
</div> :hikes="hikes"
<div class="settings-sections"> @toggle="(bIsOpen, iAnimDuration) => onPanelToggle('settings', bIsOpen, iAnimDuration)"
<Simplebar id="settings-sections-scrollbox"> />
<div class="settings-section"> <ProjectFeed
<h1><SpotIcon :icon="'project'" width="fixed" :text="lang.get('project.hikes')" /></h1> :ref="setFeed"
<div class="settings-section-body"> :project="project"
<div class="radio" v-for="project in projectOptions" :key="'project-'+project.id"> :mode-histo="modeHisto"
<input type="radio" :id="'project-'+project.id" :value="project.codename" v-model="hash.items[0]" :disabled="mapInitializing" /> @request-last-update="settings?.setLastUpdate"
<label :for="'project-'+project.id"> @new-markers="addNewMarkers"
<span>{{ project.name }}</span> @toggle="(bIsOpen, iAnimDuration) => onPanelToggle('feed', bIsOpen, iAnimDuration)"
<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>
</div> </div>
</template> </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 focusZoomLevel: 15
}; };
}, },
inject: ['api', 'lang', 'project', 'feed', 'user', 'map', 'hash', 'consts', 'isMobile'],
computed: { computed: {
postClass() { postClass() {
let sHeaderLess = this.options.headerless?' headerless':''; let sHeaderLess = this.options.headerless?' headerless':'';
@@ -59,10 +60,10 @@
return this.mouseOverDrill?null:'footprint'; return this.mouseOverDrill?null:'footprint';
}, },
anchorLink() { 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() { modeHisto() {
return (this.project?.currProject?.mode == this.consts.modes.histo); return (this.project?.project?.mode == this.consts.modes.histo);
}, },
relTime() { relTime() {
return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time; 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); return new LngLat(oRelatedMarker.longitude, oRelatedMarker.latitude);
} }
}, },
inject: ['api', 'lang', 'project', 'user', 'map', 'hash', 'consts', 'isMobile'],
methods: { methods: {
copyAnchor() { copyAnchor() {
copyTextToClipboard(this.consts.server+this.anchorLink); copyTextToClipboard(this.consts.server+this.anchorLink);
@@ -106,8 +106,8 @@
this.popupRequested = true; this.popupRequested = true;
if(this.isMobile()) this.project.toggleFeedPanel(false, 'panToInstant'); if(this.isMobile()) this.feed.toggle(false);
this.hash.items = [this.project.currProject.codename, this.options.type, this.options.id]; this.hash.items = [this.project.project.codename, this.options.type, this.options.id];
return this.map.panToBetweenPanels(this.relatedMarkerLatLng, this.focusZoomLevel, iAnimDuration).then(() => { return this.map.panToBetweenPanels(this.relatedMarkerLatLng, this.focusZoomLevel, iAnimDuration).then(() => {
this.openMarkerPopup(); this.openMarkerPopup();
@@ -137,14 +137,14 @@
this.api.get( this.api.get(
'add_post', 'add_post',
{ {
id_project: this.project.currProject.id, id_project: this.project.project.id,
name: this.user.name, name: this.user.name,
content: this.postMessage content: this.postMessage
} }
) )
.then(() => { .then(() => {
this.postMessage = ''; this.postMessage = '';
this.project.checkNewFeed(); this.feed.checkNewFeed();
this.sending = false; this.sending = false;
}) })
.catch((sDesc) => { .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; display: none !important;
} }
#projects { .projects {
.map-container { .map-container {
width: calc(#{$panel-width}); width: calc(#{$panel-width});
max-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-width-max: "400px + 3 * #{var.$block-spacing}";
$panel-actual-width: min($panel-width, #{$panel-width-max}); $panel-actual-width: min($panel-width, #{$panel-width-max});
#projects { .projects {
&.with-feed, &.with-settings { &.with-feed, &.with-settings {
#title { #title {
max-width: calc(100vw - var.$block-spacing - $panel-actual-width - (var.$button-width + var.$block-spacing * 2) * 2); 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.map' as map;
@use '@styles/page.project.panel' as panel; @use '@styles/page.project.panel' as panel;
@use '@styles/page.project.feed' as feed; @use '@styles/page.project.panel.feed' as feed;
@use '@styles/page.project.settings' as settings; @use '@styles/page.project.panel.settings' as settings;
#projects { .projects {
--space: #{color.$space}; --space: #{color.$space};
--horizon: #{color.$horizon}; --horizon: #{color.$horizon};
--track-main: #{color.$main-track}; --track-main: #{color.$main-track};