From b339d6d0683d35b1044bff193cfc8f67c1419979 Mon Sep 17 00:00:00 2001 From: Franzz Date: Sat, 25 Apr 2026 23:55:11 +0200 Subject: [PATCH] Bye bye spot.js --- lib/Project.php | 2 + lib/Spot.php | 12 +- src/Spot.vue | 46 ++--- src/components/admin.vue | 112 ++++++------ src/components/project.vue | 119 +++++++------ src/components/projectPopup.vue | 4 +- src/components/projectPost.vue | 12 +- src/components/upload.vue | 39 +++-- src/scripts/api.js | 39 +++++ src/scripts/app.js | 29 +++- src/scripts/projects.js | 17 ++ src/scripts/spot.js | 298 -------------------------------- src/scripts/user.js | 11 ++ 13 files changed, 270 insertions(+), 470 deletions(-) create mode 100644 src/scripts/api.js create mode 100644 src/scripts/projects.js delete mode 100755 src/scripts/spot.js create mode 100644 src/scripts/user.js diff --git a/lib/Project.php b/lib/Project.php index e3a9c25..04754de 100644 --- a/lib/Project.php +++ b/lib/Project.php @@ -120,6 +120,7 @@ class Project extends PhpObject { public function getProjects($iProjectId=0) { $bSpecificProj = ($iProjectId > 0); + $sDefaultProjectCodeName = $this->getProjectCodeName(); $asInfo = array( 'select'=> array( Db::getId(self::PROJ_TABLE)." AS id", @@ -145,6 +146,7 @@ class Project extends PhpObject { //$asProject['geofilepath'] = Spot::addTimestampToFilePath(GeoJson::getDistFilePath($sCodeName)); $asProject['gpxfilepath'] = Spot::addTimestampToFilePath(Gpx::getDistFilePath($sCodeName)); $asProject['codename'] = $sCodeName; + $asProject['default'] = ($sCodeName == $sDefaultProjectCodeName); } return $bSpecificProj?$asProject:$asProjects; } diff --git a/lib/Spot.php b/lib/Spot.php index 9acccce..bfc500b 100755 --- a/lib/Spot.php +++ b/lib/Spot.php @@ -173,16 +173,16 @@ class Spot extends Main return parent::getMainPage( array( - 'vars' => array( - 'default_project_codename' => $this->oProject->getProjectCodeName(), - 'projects' => $this->oProject->getProjects(), - 'user' => $this->oUser->getUserInfo() - ), + 'projects' => $this->oProject->getProjects(), + 'user' => $this->oUser->getUserInfo(), 'consts' => array( 'modes' => Project::MODES, 'clearances' => User::CLEARANCES, 'default_timezone' => Settings::TIMEZONE, - 'chunk_size' => self::FEED_CHUNK_SIZE + 'chunk_size' => self::FEED_CHUNK_SIZE, + 'hash_sep' => '-', + 'title' => 'Spotty', + 'default_page' => 'project' ) ), self::MAIN_PAGE, diff --git a/src/Spot.vue b/src/Spot.vue index 3422e8b..6e20722 100644 --- a/src/Spot.vue +++ b/src/Spot.vue @@ -13,59 +13,59 @@ export default { data() { return { hash: {page: '', items: []}, - consts: this.spot.consts, - user: this.spot.vars('user'), - routes: aoRoutes + consts: this.appConfig.consts, + mobile: false }; }, provide() { return { hash: this.hash, - projects: this.spot.vars('projects'), consts: this.consts, - user: this.user + isMobile: () => this.isMobile() }; }, computed: { route() { - return this.routes[this.hash.page]; + return aoRoutes[this.hash.page]; }, hashSnapshot() { return JSON.stringify(this.hash); } }, - inject: ['spot'], + inject: ['appConfig'], created() { - //User - this.user.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone; - //Set initial page let asInitHash = this.getBrowserHash(); - if(!asInitHash.page) asInitHash.page = this.spot.consts.default_page; + if(!asInitHash.page) asInitHash.page = this.consts.default_page; this.setVarHash(asInitHash); }, mounted() { //Catch browser hash change window.addEventListener('hashchange', this.onBrowserHashChange); + window.addEventListener('resize', this.updateMobile); + this.updateMobile(); }, watch: { hashSnapshot(jNewHash, jOldHash) { const asNewHash = JSON.parse(jNewHash); - const asOldHash = JSON.parse(jOldHash); - this.spot.vars('page', this.hash.page); //FIXME remove - //Sync variable -> #hash if(asNewHash != this.getBrowserHash()) { this.setBrowserHash(asNewHash.page, asNewHash.items); } - //Same Page change - if(asNewHash != asOldHash && asNewHash.page == asOldHash.page) { - this.spot.onSamePageMove(asNewHash, asOldHash); - } + this.setPageTitle(asNewHash.page); } }, methods: { + isMobile() { + return this.mobile; + }, + updateMobile() { + this.mobile = getComputedStyle(this.$refs.mobile).display !== 'none'; + }, + setPageTitle(sTitle) { + document.title = this.consts.title + ' - ' + sTitle.trim(); + }, setVarHash(asHash) { this.hash.page = asHash.page || ''; this.hash.items = Array.isArray(asHash.items) ? [...asHash.items.filter(n => n)] : []; @@ -73,22 +73,22 @@ export default { onBrowserHashChange() { //Sync #hash -> variable let asHash = this.getBrowserHash(); if(asHash != this.hash) this.setVarHash(asHash); - this.spot.vars('page', this.hash.page); //FIXME remove }, getBrowserHash() { let sHash = window.location.hash.slice(1); - let asHash = sHash.split(this.spot.consts.hash_sep).filter(n => n); + let asHash = sHash.split(this.consts.hash_sep).filter(n => n); let sPage = asHash.shift() || ''; return {page: sPage, items: asHash}; }, setBrowserHash(sPage = '', asItems = []) { if(typeof asItems == 'string' && asItems != '') asItems = [asItems]; - const sItems = (asItems.length > 0)?(this.spot.consts.hash_sep + asItems.join(this.spot.consts.hash_sep)):''; + const sItems = (asItems.length > 0)?(this.consts.hash_sep + asItems.join(this.consts.hash_sep)):''; window.location.hash = '#' + sPage + sItems; } }, beforeUnmount() { - window.removeEventListener('hashchange', this.onBrowserHashChange) + window.removeEventListener('hashchange', this.onBrowserHashChange); + window.removeEventListener('resize', this.updateMobile); } } @@ -96,5 +96,5 @@ export default {
-
+
diff --git a/src/components/admin.vue b/src/components/admin.vue index 9b0e082..1203789 100644 --- a/src/components/admin.vue +++ b/src/components/admin.vue @@ -9,38 +9,37 @@ export default { SpotButton, AdminInput }, - inject: ['spot', 'lang'], - data() { - return { - elems: {}, - feedbacks: [] - }; - }, - mounted() { - this.setEvents(); - this.setProjects(); - }, + inject: ['api', 'lang'], + data() { + return { + elems: {}, + feedbacks: [], + saveTimer: null + }; + }, + beforeUnmount() { + if(this.saveTimer) clearTimeout(this.saveTimer); + }, + mounted() { + this.setProjects(); + }, methods: { l(id) { - return this.lang.get(id); + return this.lang.get(id); }, - setEvents() { - this.spot.addPage('admin', { - onFeedback: (sType, sMsg, asContext) => { - delete asContext.a; - delete asContext.t; - sMsg += ' ('; - for(const [sKey, sElem] of Object.entries(asContext)) { - sMsg += sKey+'='+sElem+' / ' ; - } - sMsg = sMsg.slice(0, -3)+')'; + addFeedback(sType, sMsg, asContext = {}) { + delete asContext.a; + delete asContext.t; + sMsg += ' ('; + for(const [sKey, sElem] of Object.entries(asContext)) { + sMsg += sKey+'='+sElem+' / ' ; + } + sMsg = sMsg.slice(0, -3)+')'; - this.feedbacks.push({type:sType, msg:sMsg}); - } - }); + this.feedbacks.push({type:sType, msg:sMsg}); }, async setProjects() { - let aoElemTypes = await this.spot.get2('admin_get'); + let aoElemTypes = await this.api.get('admin_get'); for(const [sType, aoElems] of Object.entries(aoElemTypes)) { this.elems[sType] = {}; @@ -51,17 +50,17 @@ export default { } }, createElem(sType) { - this.spot.get2('admin_create', {type: sType}) + this.api.get('admin_create', {type: sType}) .then((aoNewElemTypes) => { for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) { for(const [iKey, oNewElem] of Object.entries(aoNewElems)) { oNewElem.type = sType; this.elems[sType][oNewElem.id] = oNewElem; - this.spot.onFeedback('success', this.lang.get('admin_create_success'), {'create':sType}); + this.addFeedback('success', this.lang.get('admin_create_success'), {'create':sType}); } } }) - .catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'create':sType});}); + .catch((sMsg) => {this.addFeedback('error', sMsg, {'create':sType});}); }, deleteElem(oElem) { const asInputs = { @@ -69,23 +68,20 @@ export default { id: oElem.id }; - this.spot.get( - 'admin_delete', - (asData) => { - delete this.elems[asInputs.type][asInputs.id]; - this.spot.onFeedback('success', this.lang.get('admin_delete_success'), asInputs); - }, - asInputs, - (sError) => { - this.spot.onFeedback('error', sError, asInputs); - } - ); - }, - updateElem(oElem, oEvent) { - if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait')); - - let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name]; - let sNewVal = oEvent.target.value; + this.api.get('admin_delete', asInputs) + .then((asData) => { + delete this.elems[asInputs.type][asInputs.id]; + this.addFeedback('success', this.lang.get('admin_delete_success'), asInputs); + }) + .catch((sError) => { + this.addFeedback('error', sError, asInputs); + }); + }, + updateElem(oElem, oEvent) { + if(this.saveTimer) clearTimeout(this.saveTimer); + + let sOldVal = this.elems[oElem.type][oElem.id][oEvent.target.name]; + let sNewVal = oEvent.target.value; if(sOldVal != sNewVal) { let asInputs = { type: oElem.type, @@ -94,25 +90,25 @@ export default { value: sNewVal }; - this.spot.get2('admin_set', asInputs) + this.api.get('admin_set', asInputs) .then((asData) => { this.elems[oElem.type][oElem.id][oEvent.target.name] = sNewVal; - this.spot.onFeedback('success', this.lang.get('admin_save_success'), asInputs); + this.addFeedback('success', this.lang.get('admin_save_success'), asInputs); }) .catch((sError) => { oEvent.target.value = sOldVal; - this.spot.onFeedback('error', sError, asInputs); + this.addFeedback('error', sError, asInputs); }); } - }, - queue(oElem, oEvent) { - if(typeof this.spot.tmp('wait') != 'undefined') clearTimeout(this.spot.tmp('wait')); - this.spot.tmp('wait', setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000)); - }, + }, + queue(oElem, oEvent) { + if(this.saveTimer) clearTimeout(this.saveTimer); + this.saveTimer = setTimeout(() => {this.updateElem(oElem, oEvent);}, 2000); + }, updateProject() { - this.spot.get2('update_project') - .then((asData, sMsg) => {this.spot.onFeedback('success', sMsg, {'update':'project'});}) - .catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'update':'project'});}); + this.api.get('update_project') + .then((asData, sMsg) => {this.addFeedback('success', sMsg, {'update':'project'});}) + .catch((sMsg) => {this.addFeedback('error', sMsg, {'update':'project'});}); } } } @@ -232,4 +228,4 @@ export default {

{{ feedback.msg }}

- + diff --git a/src/components/project.vue b/src/components/project.vue index 0845de1..5fb2ec5 100644 --- a/src/components/project.vue +++ b/src/components/project.vue @@ -22,7 +22,7 @@ export default { }, data() { return { - server: this.spot.consts.server, + server: this.consts.server, feed: {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true}, refreshRate: 60, lastUpdate: { unix_time: 0, relative_time: '', formatted_time: ''}, @@ -63,9 +63,6 @@ export default { }, nlAction() { return this.subscribed?'unsubscribe':'subscribe'; - }, - mobile() { - return this.spot.isMobile(); } }, watch: { @@ -75,6 +72,12 @@ export default { if(sNewBaseMap && this.map.getLayer(sNewBaseMap)) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible'); } }, + 'hash.items.0'(newProjectCodename, oldProjectCodename) { + if(newProjectCodename && newProjectCodename != oldProjectCodename) { + this.hash.items = [newProjectCodename]; + this.init(); + } + } }, provide() { return { @@ -87,42 +90,28 @@ export default { project: this }; }, - inject: ['spot', 'lang', 'hash', 'projects', 'user'], + inject: ['api', 'lang', 'hash', 'projects', 'user', 'consts', 'isMobile'], beforeMount() { - if(this.hash.items.length == 0) this.hash.items[0] = this.spot.vars('default_project_codename'); + if(this.hash.items.length == 0) this.hash.items[0] = this.projects.getDefaultCodeName(); }, mounted() { - this.spot.addPage('project', { - onResize: () => { - //this.spot.tmp('map_offset', -1 * (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0) / getOuterWidth(window)); - - /* TODO - if(typeof this.spot.tmp('elev') != 'undefined' && this.spot.tmp('elev')._showState) { - this.spot.tmp('elev').resize({width:this.getElevWidth()}); - } - */ - }, - onSamePageMove: (asNewHash, asOldHash) => { - //this.toggleSettingsPanel(false); - //this.quit(); - - //Check for project change - if(asNewHash.items[0] != asOldHash.items[0]) { - this.hash.items = [asNewHash.items[0]]; - this.init(); - } - }, - onQuitPage: () => { - this.quit(); - } - }); - + window.addEventListener('resize', this.handleResize); this.init(); }, beforeUnmount() { + window.removeEventListener('resize', this.handleResize); this.quit(); }, methods: { + handleResize() { + //this.spot.tmp('map_offset', -1 * (this.feedPanelOpen?getOuterWidth(this.$refs.feed):0) / getOuterWidth(window)); + + /* TODO + if(typeof this.spot.tmp('elev') != 'undefined' && this.spot.tmp('elev')._showState) { + this.spot.tmp('elev').resize({width:this.getElevWidth()}); + } + */ + }, async init() { let bFirstLoad = (typeof this.currProject.codename == 'undefined'); this.initProject(); @@ -142,9 +131,25 @@ export default { this.setFeedUpdateTimer(-1); this.map.remove(); }, + getNaturalDuration(iHours) { + var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day + if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day + else { + iTimeDays = 0; + iTimeHours = Math.floor(iHours); + iHours -= iTimeHours; + + iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes + } + return '~ ' + +(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+this.lang.get(iTimeDays>1?'unit_days':'unit_day')):'') //Days + +((iTimeHours>0 || iTimeDays==0)?iTimeHours+this.lang.get('unit_hour'):'') //Hours + +((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes); //Minutes + + }, initProject() { this.currProject = this.projects[this.hash.items[0]]; - this.modeHisto = (this.currProject.mode == this.spot.consts.modes.histo); + this.modeHisto = (this.currProject.mode == this.consts.modes.histo); this.feed = {loading:false, updatable:true, outOfData:false, refIdFirst:0, refIdLast:0, firstChunk:true}; this.posts = []; //this.baseMap = null; @@ -185,8 +190,8 @@ export default { }, async initMap() { //Start async calls - const oMarkersPromise = this.spot.get2('markers', {id_project: this.currProject.id}); - const oTrackPromise = this.spot.get2('geojson', {id_project: this.currProject.id}); + const oMarkersPromise = this.api.get('markers', {id_project: this.currProject.id}); + const oTrackPromise = this.api.get('geojson', {id_project: this.currProject.id}); //Build Map if(this.map) this.map.remove(); @@ -324,11 +329,11 @@ export default { }); }, async positionMap(oTrack) { - let bOpenFeedPanel = !this.mobile; + let bOpenFeedPanel = !this.isMobile(); let oBounds = new LngLatBounds(); if( //Blog Mode: Fit to last message - this.currProject.mode == this.spot.consts.modes.blog && + this.currProject.mode == this.consts.modes.blog && this.markers.messages.length > 0 && this.hash.items[2] != 'message' ) { @@ -399,7 +404,11 @@ export default { medias: [oMedia], project: this.currProject }); - this.popup.content.provide('spot', this.spot).provide('lang', this.lang).mount($Popup); + this.popup.content + .provide('spot', this.spot) + .provide('lang', this.lang) + .provide('consts', this.consts) + .mount($Popup); }, openMarkerPopup(oFeature) { this.closeMarkerPopup(); @@ -434,7 +443,11 @@ export default { medias: JSON.parse(oFeature.properties.medias || '[]'), project: this.currProject }); - this.popup.content.provide('spot', this.spot).provide('lang', this.lang).mount($Popup); + this.popup.content + .provide('spot', this.spot) + .provide('lang', this.lang) + .provide('consts', this.consts) + .mount($Popup); }, closeMarkerPopup() { if(this.popup.content) { @@ -450,11 +463,11 @@ export default { if(!this.feed.outOfData && !this.feed.loading) { //Get next chunk this.feed.loading = true; - let aoData = await this.spot.get2('next_feed', {id_project: this.currProject.id, id: this.feed.refIdLast}); + 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.spot.consts.chunk_size); + 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; @@ -480,7 +493,7 @@ export default { if(iSeconds >= 0) this.feedTimer = setTimeout(this.checkNewFeed, iSeconds * 1000); }, async checkNewFeed() { - let aoData = await this.spot.get2('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst}); + let aoData = await this.api.get('new_feed', {id_project: this.currProject.id, id: this.feed.refIdFirst}); if(Object.keys(aoData.feed).length > 0) { //Update pointer @@ -533,10 +546,10 @@ export default { var regexEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if(!regexEmail.test(this.user.email)) this.nlFeedbacks.push({type:'error', 'msg':this.lang.get('nl_invalid_email')}); else { - this.spot.get2(this.nlAction, {'email': this.user.email, 'name': this.user.name}, this.nlLoading) + this.api.get(this.nlAction, {'email': this.user.email, 'name': this.user.name}) .then((asUser, sDesc) => { this.nlFeedbacks.push('success', sDesc); - this.user = asUser; + Object.assign(this.user, asUser); }) .catch((sDesc) => {this.nlFeedbacks.push('error', sDesc);}); } @@ -545,8 +558,8 @@ export default { let bOldValue = this.feedPanelOpen; this.feedPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.feedPanelOpen):bShow; - if(bOldValue != this.feedPanelOpen && !this.mobile) { - this.spot.onResize(); + if(bOldValue != this.feedPanelOpen && !this.isMobile()) { + this.handleResize(); sMapAction = sMapAction || 'panTo'; switch(sMapAction) { @@ -579,8 +592,8 @@ export default { let bOldValue = this.settingsPanelOpen; this.settingsPanelOpen = (typeof bShow === 'object' || typeof bShow === 'undefined')?(!this.settingsPanelOpen):bShow; - if(bOldValue != this.settingsPanelOpen && !this.mobile) { - this.spot.onResize(); + if(bOldValue != this.settingsPanelOpen && !this.isMobile()) { + this.handleResize(); sMapAction = sMapAction || 'panTo'; switch(sMapAction) { @@ -640,7 +653,7 @@ export default {
-
+

{{ lang.get('last_update')+' '+lastUpdate.relative_time }}

@@ -680,7 +693,7 @@ export default {
{{ lang.get(subscribed?'nl_subscribed_desc':'nl_unsubscribed_desc') }}
-
+

@@ -692,16 +705,16 @@ export default { {{ lang.get('credits_license') }}
-
+
-
+
{{ lang.get('track_'+hikeType) }}
-
+
{{ currProject.name }}
@@ -718,7 +731,7 @@ export default {
-
+
diff --git a/src/components/projectPopup.vue b/src/components/projectPopup.vue index 928f0c2..dd715d7 100644 --- a/src/components/projectPopup.vue +++ b/src/components/projectPopup.vue @@ -18,7 +18,7 @@ export default { medias: Array, project: Object }, - inject: ['spot', 'lang'], + inject: ['lang', 'consts'], computed: { timeIcon() { return (this.type == 'media')?'image-shot':'time'; @@ -45,7 +45,7 @@ export default {

- ({{ options.relative_time }}) + ({{ options.relative_time }})

diff --git a/src/components/projectPost.vue b/src/components/projectPost.vue index b60294e..0c0095a 100644 --- a/src/components/projectPost.vue +++ b/src/components/projectPost.vue @@ -52,10 +52,10 @@ return this.options.displayed_id?(this.lang.get('counter', this.options.displayed_id)):''; }, anchorLink() { - return '#'+[this.hash.page, this.hash.items[0], this.options.type, this.options.id].join(this.spot.consts.hash_sep); + return '#'+[this.hash.page, this.hash.items[0], this.options.type, this.options.id].join(this.consts.hash_sep); }, modeHisto() { - return (this.project.currProject.mode == this.spot.consts.modes.histo); + return (this.project.currProject.mode == this.consts.modes.histo); }, relTime() { return this.modeHisto?(this.options.formatted_time || '').substr(0, 10):this.options.relative_time; @@ -69,10 +69,10 @@ } }, - inject: ['spot', 'lang', 'project', 'user', 'map', 'hash'], + inject: ['api', 'lang', 'project', 'user', 'map', 'hash', 'consts', 'isMobile'], methods: { copyAnchor() { - copyTextToClipboard(this.spot.consts.server+this.anchorLink); + copyTextToClipboard(this.consts.server+this.anchorLink); this.anchorTitle = this.lang.get('link_copied'); this.anchorIcon = 'copied'; setTimeout(()=>{ //TODO animation @@ -82,7 +82,7 @@ }, panMapToMessage() { this.popupRequested = true; - if(this.spot.isMobile()) this.project.toggleFeedPanel(false, 'panToInstant'); + if(this.isMobile()) this.project.toggleFeedPanel(false, 'panToInstant'); this.map.panToBetweenPanels( this.lngLat, this.focusZoomLevel, @@ -100,7 +100,7 @@ send() { if(this.postMessage != '' && this.user.name != '') { this.sending = true; - this.spot.get2( + this.api.get( 'add_post', { id_project: this.project.currProject.id, diff --git a/src/components/upload.vue b/src/components/upload.vue index ea8080f..6fc3493 100644 --- a/src/components/upload.vue +++ b/src/components/upload.vue @@ -10,10 +10,10 @@ import SpotButton from './spotButton.vue'; export default { name: 'upload', components: { SpotButton, SpotIcon }, - inject: ['spot', 'lang', 'projects', 'consts', 'user'], + inject: ['api', 'lang', 'projects', 'consts', 'user'], data() { return { - project: this.projects[this.spot.vars('default_project_codename')], + project: this.projects.getDefaultProject(), files: [], logs: [], progress: 0, @@ -21,8 +21,6 @@ export default { }; }, mounted() { - this.spot.addPage('upload', {}); - if(!this.project.editable) { this.logs = [this.lang.get('upload_mode_archived', [this.project.name])]; return; @@ -87,7 +85,10 @@ export default { event.target.value = ''; }, addComment(oFile) { - this.spot.get2('add_comment', {id: oFile.id, content: oFile.content}) + this.api.get('add_comment', { + id: oFile.id, + content: oFile.content + }) .then((asData) => {this.logs.push(this.lang.get('media_comment_update', asData.filename));}) .catch((sMsgId) => {this.logs.push(this.lang.get(sMsgId));}); }, @@ -97,7 +98,11 @@ export default { navigator.geolocation.getCurrentPosition( (position) => { this.logs.push('Sending position...'); - this.spot.get2('add_position', {'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)}) + this.api.get('add_position', { + 'latitude': position.coords.latitude, + 'longitude': position.coords.longitude, + 'timestamp': Math.round(position.timestamp / 1000) + }) .then((asData) => {this.logs.push(this.lang.get('success'));}) .catch((sMsgId) => {this.logs.push(this.lang.get(sMsgId));}); }, @@ -122,20 +127,20 @@ export default {

-
- -
- - +
+ +
+ + -
-
+
+

{{ lang.get('upload_pos_title') }}

-
-

{{ log }}.

-
-
+
+

{{ log }}.

+
+ diff --git a/src/scripts/api.js b/src/scripts/api.js new file mode 100644 index 0000000..a405e94 --- /dev/null +++ b/src/scripts/api.js @@ -0,0 +1,39 @@ +export default class Api { + + constructor({server, processPage, timezone, errorCode, lang}) { + this.server = server; + this.processPage = processPage; + this.timezone = timezone; + this.errorCode = errorCode; + this.lang = lang; + } + + async get(action, params = {}) { + const requestParams = { + ...params, + a: action, + t: this.timezone + }; + + const url = new URL(this.processPage, this.server); + url.search = new URLSearchParams(requestParams).toString(); + + const request = await fetch(url, { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }); + + if(!request.ok) { + throw new Error('Error HTTP ' + request.status + ': ' + request.statusText); + } + + const response = await request.json(); + response.desc = this.lang.parse(response.desc); + + if(response.result == this.errorCode) { + throw response.desc; + } + + return response.data; + } +} diff --git a/src/scripts/app.js b/src/scripts/app.js index 3156d10..a411674 100644 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -1,8 +1,12 @@ //Librairies +import Api from './api.js'; import Lang from './lang.js'; -import Spot from './spot.js'; +import Projects from './projects.js'; +import User from './user.js'; import { createApp } from 'vue'; -import SpotVue from '../Spot.vue'; + +//Main template +import Spot from '../Spot.vue'; //Style import Css from './../styles/spot.scss'; @@ -11,11 +15,22 @@ import Css from './../styles/spot.scss'; const appConfig = JSON.parse(document.getElementById('app-config').textContent); //Instances +const oProjects = new Projects(appConfig.projects); +const oUser = new User(appConfig.user, appConfig.consts.default_timezone); const oLang = new Lang({translations: appConfig.consts.lang, prefix: appConfig.consts.lang_prefix}); -const oSpot = new Spot(appConfig, oLang); +const oApi = new Api({ + server: appConfig.consts.server, + processPage: appConfig.consts.process_page, + timezone: oUser.timezone, + errorCode: appConfig.consts.error, + lang: oLang +}); //Mount app -const oSpotVue = createApp(SpotVue); -oSpotVue.provide('spot', oSpot); -oSpotVue.provide('lang', oLang); -oSpotVue.mount('#container'); +const oSpot = createApp(Spot); +oSpot.provide('appConfig', appConfig); +oSpot.provide('api', oApi); +oSpot.provide('lang', oLang); +oSpot.provide('projects', oProjects); +oSpot.provide('user', oUser); +oSpot.mount('#container'); diff --git a/src/scripts/projects.js b/src/scripts/projects.js new file mode 100644 index 0000000..60875f0 --- /dev/null +++ b/src/scripts/projects.js @@ -0,0 +1,17 @@ +export default class Projects { + + constructor(asProjects = {}) { + Object.assign(this, asProjects); + } + + getDefaultCodeName() { + for(const [sCodeName, asProject] of Object.entries(this)) { + if(asProject.default) return sCodeName; + } + } + + getDefaultProject() { + const sCodeName = this.getDefaultCodeName(); + return this[this.getDefaultCodeName()]; + } +} diff --git a/src/scripts/spot.js b/src/scripts/spot.js deleted file mode 100755 index 2de5a70..0000000 --- a/src/scripts/spot.js +++ /dev/null @@ -1,298 +0,0 @@ -import { copyArray, getElem, setElem } from './common.js'; - -export default class Spot { - - constructor(asGlobals, oLang) { - this.consts = asGlobals.consts; - this.consts.hash_sep = '-'; - this.consts.title = 'Spotty'; - this.consts.default_page = 'project'; - //this.consts.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone; - this.lang = oLang; - - this.pages = {}; - - //Variables & constants from php - this.vars('tmp', 'object'); - this.vars('page', 'string'); - Object.entries(asGlobals.vars).forEach(([sKey, oValue]) => {this.vars(sKey, oValue);}); - - //page elem - this.elem = {}; - } - - /* Initialization */ - - /* - init() { - this.elem.container = $('#container'); - this.elem.main = $('#main'); - - //On Key down - $('html').on('keydown', (oEvent) => {this.onKeydown(oEvent);}); - - //on window resize - $(window).on('resize', () => {this.onResize();}); - - //Hash management - $(window) - .on('hashchange', () => {this.onHashChange();}) - .trigger('hashchange'); - } - */ - - /* Variable Management */ - - vars(oVarName, oValue) { - var asVarName = (typeof oVarName == 'object')?oVarName:[oVarName]; - - //Set, name & type / default value (init) - if(typeof oValue !== 'undefined') setElem(this.vars, copyArray(asVarName), oValue); - - //Get, only name parameter - return getElem(this.vars, asVarName); - } - - tmp(sVarName, oValue) { - var asVarName = (typeof sVarName == 'object')?sVarName:[sVarName]; - asVarName.unshift('tmp'); - return this.vars(asVarName, oValue); - } - - /* Interface with server */ - - /* - get(sAction, fOnSuccess, oVars, fOnError, fonProgress) { - oVars = oVars || {}; - fOnError = fOnError || function(sError) {console.log(sError);}; - fonProgress = fonProgress || function(sState){}; - fonProgress('start'); - - oVars['a'] = sAction; - oVars['t'] = this.consts.timezone; - return $.ajax( - { - url: this.consts.process_page, - data: oVars, - dataType: 'json' - }) - .done((oData) => { - fonProgress('done'); - if(oData.desc.substr(0, this.consts.lang_prefix.length)==this.consts.lang_prefix) oData.desc = this.lang(oData.desc.substr(5)); - - if(oData.result==this.consts.error) fOnError(oData.desc); - else if(fOnSuccess) fOnSuccess(oData.data, oData.desc); - }) - .fail((jqXHR, textStatus, errorThrown) => { - fonProgress('fail'); - fOnError(textStatus+' '+errorThrown); - }); - } - */ - - async get2(sAction, oVars, bLoading) { - oVars = oVars || {}; - oVars['a'] = sAction; - oVars['t'] = this.consts.timezone; - bLoading = true; - - try { - let oUrl = new URL(this.consts.process_page, this.consts.server); - oUrl.search = new URLSearchParams(oVars).toString(); - const oRequest = await fetch(oUrl, {method: 'GET', /*body: JSON.stringify(oVars),*/ headers: {"Content-Type": "application/json"}}); - if(!oRequest.ok) { - bLoading = false; - throw new Error('Error HTTP '+oRequest.status+': '+oRequest.statusText); - } - else { - let oResponse = await oRequest.json(); - bLoading = false; - oResponse.desc = this.lang.parse(oResponse.desc); - - if(oResponse.result == this.consts.error) return Promise.reject(oResponse.desc); - else return oResponse.data; - } - } - catch(oError) { - bLoading = false; - throw oError; - } - } - - isMobile() { - const mobileElem = document.getElementById('mobile'); - return !!mobileElem && getComputedStyle(mobileElem).display !== 'none'; - } - - /* Page Switch - Trigger & Event catching */ - - /* - onHashChange() { - var asHash = this.getHash(); - if(asHash.hash !='' && asHash.page != '') this.switchPage(asHash); //page switching - else if(this.vars('page')=='') this.setHash(this.consts.default_page); //first page - } - - getHash() { - var sHash = this.hash(); - var asHash = sHash.split(this.consts.hash_sep); - var sPage = asHash.shift() || ''; - return {hash:sHash, page:sPage, items:asHash}; - } - - setHash(sPage, asItems, bReboot) { - bReboot = bReboot || false; - sPage = sPage || ''; - asItems = asItems || []; - if(typeof asItems == 'string') asItems = [asItems]; - - if(sPage != '') { - var sItems = (asItems.length > 0)?this.consts.hash_sep+asItems.join(this.consts.hash_sep):''; - this.hash(sPage+sItems, bReboot); - } - } - - hash(hash, bReboot) { - bReboot = bReboot || false; - if(!hash) return window.location.hash.slice(1); - else window.location.hash = '#'+hash; - - if(bReboot) location.reload(); - } - - updateHash(sType, iId) { - sType = sType || ''; - iId = iId || 0; - - var asHash = this.getHash(); - if(iId) this.setHash(asHash.page, [asHash.items[0], sType, iId]); - } - - flushHash(asTypes) { - asTypes = asTypes || []; - var asHash = this.getHash(); - if(asHash.items.length > 1 && (asTypes.length == 0 || asTypes.indexOf(asHash.items[1]) != -1)) this.setHash(asHash.page, [asHash.items[0]]); - } - */ - - /* Page Events */ - - pageInit(asHash) { - let sPage = this.vars('page'); - if(this.pages[sPage].pageInit) this.pages[sPage].pageInit(asHash); - else console.log('no init for the page: '+asHash.page); - } - - onSamePageMove(asNewHash, asOldHash) { - let sPage = this.vars('page'); - return (this.pages[sPage] && this.pages[sPage].onSamePageMove)?this.pages[sPage].onSamePageMove(asNewHash, asOldHash):false; - } - - onQuitPage() { - let sPage = this.vars('page'); - return (this.pages[sPage] && this.pages[sPage].onQuitPage)?this.pages[sPage].onQuitPage():true; - } - - onResize() { - let sPage = this.vars('page'); - if(this.pages[sPage].onResize) this.pages[sPage].onResize(); - } - - onFeedback(sType, sMsg, asContext) { - asContext = asContext || {}; - let sPage = this.vars('page'); - if(this.pages[sPage].onFeedback) this.pages[sPage].onFeedback(sType, sMsg, asContext); - else console.log({type:sType, msg:sMsg, context:asContext}); - } - - onKeydown(oEvent) { - let sPage = this.vars('page'); - if(this.pages[sPage].onKeydown) this.pages[sPage].onKeydown(oEvent); - } - - /* Page Switch - DOM Replacement */ - - /* - getActionLink(sAction, oVars) { - if(!oVars) oVars = {}; - let sVars = ''; - - for(i in oVars) sVars += '&'+i+'='+oVars[i]; - - return this.consts.process_page+'?a='+sAction+sVars; - } - */ - - addPage(sPage, oPage) { - this.pages[sPage] = oPage; - } - - /* - switchPage(asHash) { - var sPageName = asHash.page; - var bSamePage = (this.vars('page') == sPageName); - var bFirstPage = (this.vars('page') == ''); - - if(!this.consts.pages[sPageName]) { //Page does not exist - if(bFirstPage) this.setHash(this.consts.default_page); - else this.setHash(this.vars('page'), this.vars(['hash', 'items'])); - } - else if(this.onQuitPage(bSamePage) && !bSamePage || this.onSamePageMove(asHash)) - { - //Delete tmp variables - this.vars('tmp', {}); - - //Officially a new page - this.vars('page', sPageName); - this.vars('hash', asHash); - - //Update Page Title - this.setPageTitle(sPageName+' '+(asHash.items[0] || '')); - - //Replacing DOM - var $Dom = $(this.consts.pages[sPageName]); - if(bFirstPage) this.splash($Dom, asHash, bFirstPage); //first page - else this.elem.main.stop().fadeTo('fast', 0, () => {this.splash($Dom, asHash, bFirstPage);}); //Switching page - } - else if(bSamePage) this.vars('hash', asHash); - } - - splash($Dom, asHash, bFirstPage) - { - //Switch main content - this.elem.main.empty().html($Dom); - - //Page Bootstrap - this.pageInit(asHash, bFirstPage); - - //Show main - var $FadeInElem = bFirstPage?this.elem.container:this.elem.main; - $FadeInElem.hide().fadeTo('slow', 1); - } - */ - - setPageTitle(sTitle) { - document.title = this.consts.title+' - '+sTitle; - } - - getNaturalDuration(iHours) { - var iTimeMinutes = 0, iTimeHours = 0, iTimeDays = Math.floor(iHours/8); //8 hours a day - if(iTimeDays > 1) iTimeDays = Math.round(iTimeDays * 2) / 2; //Round down to the closest half day - else { - iTimeDays = 0; - iTimeHours = Math.floor(iHours); - iHours -= iTimeHours; - - iTimeMinutes = Math.floor(iHours * 4) * 15; //Round down to the closest 15 minutes - } - return '~ ' - +(iTimeDays>0?(iTimeDays+(iTimeDays%2==0?'':'½')+' '+this.lang.get(iTimeDays>1?'unit_days':'unit_day')):'') //Days - +((iTimeHours>0 || iTimeDays==0)?iTimeHours+this.lang.get('unit_hour'):'') //Hours - +((iTimeDays>0 || iTimeMinutes==0)?'':iTimeMinutes) //Minutes - - } - - checkClearance(sClearance) { - return (this.vars(['user', 'clearance']) >= sClearance); - } -} diff --git a/src/scripts/user.js b/src/scripts/user.js new file mode 100644 index 0000000..2b22cb9 --- /dev/null +++ b/src/scripts/user.js @@ -0,0 +1,11 @@ +export default class User { + + constructor(asUserInfo = {}, sDefaultTimeZone = '') { + Object.assign(this, asUserInfo); + this.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.timezone || sDefaultTimeZone; + } + + hasClearance(sClearance) { + return this.clearance >= sClearance; + } +}