Compare commits
3 Commits
ce697dc51f
...
map
Author | SHA1 | Date | |
---|---|---|---|
|
01f945fe49 | ||
|
3191fe5c04 | ||
|
b1207d0b6f |
3
.env
@@ -3,4 +3,5 @@ VITE_API_BASE=http://localhost:3000/api/v1
|
||||
VITE_API_PATH_LIST=/list/today
|
||||
VITE_API_PATH_WHITELIST=/whitelist
|
||||
VITE_API_PATH_NOW_SESSIONS=/ivao/sessions/now
|
||||
VITE_API_PATH_NOW_FLIGHTPLANS=/ivao/flightplans/latest
|
||||
VITE_API_PATH_NOW_FLIGHTPLANS=/ivao/flightplans/latest
|
||||
VITE_API_PATH_AUTHENTICATION=/admin/user/authenticate
|
182
package-lock.json
generated
@@ -8,13 +8,18 @@
|
||||
"name": "lts-stats-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@headlessui/vue": "^1.7.7",
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"axios": "^1.2.2",
|
||||
"bootstrap": "^5.2.3",
|
||||
"leaflet-rotatedmarker": "^0.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.0.29",
|
||||
"redis": "^4.5.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-loading-overlay": "^6.0.2",
|
||||
@@ -24,11 +29,13 @@
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue-leaflet/vue-leaflet": "^0.8.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"bulma": "^0.9.4",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"leaflet": "^1.9.3",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.57.1",
|
||||
"vite": "^4.0.0"
|
||||
@@ -420,6 +427,48 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
|
||||
"integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz",
|
||||
"integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz",
|
||||
"integrity": "sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/vue-fontawesome": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.2.tgz",
|
||||
"integrity": "sha512-xHVtVY8ASUeEvgcA/7vULUesENhD+pi/EirRHdMBqooHlXBqK+yrV6d8tUye1m5UKQKVgYAHMhUBfOnoiwvc8Q==",
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"vue": ">= 3.0.0 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/vue": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.7.tgz",
|
||||
@@ -666,6 +715,16 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-leaflet/vue-leaflet": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.8.0.tgz",
|
||||
"integrity": "sha512-oHPdD4zq243NvK98T+HiRT9qf1zvRys5KO0eDdRkZKd09i9Epj9ALPoRqF2NFUPcS8G2yEO1qCLiKAgFQ26ARQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.6.0",
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.2.45",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
|
||||
@@ -1958,6 +2017,17 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
||||
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/leaflet-rotatedmarker": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz",
|
||||
"integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg=="
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -2269,6 +2339,56 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.0.29",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.29.tgz",
|
||||
"integrity": "sha512-5z/KpFecq/cIgfeTnulJXldiLcTITRkTe3N58RKYSj0Pc1EdR6oyCdnf5A9jLoVwBqX5LtHhd0kGlpzWvk9oiQ==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.4.5",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.6.14 || ^3.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.32",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||
@@ -3172,6 +3292,33 @@
|
||||
"strip-json-comments": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
|
||||
"integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz",
|
||||
"integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz",
|
||||
"integrity": "sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
}
|
||||
},
|
||||
"@fortawesome/vue-fontawesome": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.2.tgz",
|
||||
"integrity": "sha512-xHVtVY8ASUeEvgcA/7vULUesENhD+pi/EirRHdMBqooHlXBqK+yrV6d8tUye1m5UKQKVgYAHMhUBfOnoiwvc8Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"@headlessui/vue": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.7.tgz",
|
||||
@@ -3360,6 +3507,13 @@
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue-leaflet/vue-leaflet": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.8.0.tgz",
|
||||
"integrity": "sha512-oHPdD4zq243NvK98T+HiRT9qf1zvRys5KO0eDdRkZKd09i9Epj9ALPoRqF2NFUPcS8G2yEO1qCLiKAgFQ26ARQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.2.45",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz",
|
||||
@@ -4335,6 +4489,17 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"leaflet": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
||||
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"leaflet-rotatedmarker": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz",
|
||||
"integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg=="
|
||||
},
|
||||
"levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -4559,6 +4724,23 @@
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"pinia": {
|
||||
"version": "2.0.29",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.29.tgz",
|
||||
"integrity": "sha512-5z/KpFecq/cIgfeTnulJXldiLcTITRkTe3N58RKYSj0Pc1EdR6oyCdnf5A9jLoVwBqX5LtHhd0kGlpzWvk9oiQ==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.4.5",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.32",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||
|
@@ -11,13 +11,18 @@
|
||||
"docker-build": "docker build -t arhuako/ltsweb ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@headlessui/vue": "^1.7.7",
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"axios": "^1.2.2",
|
||||
"bootstrap": "^5.2.3",
|
||||
"leaflet-rotatedmarker": "^0.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.0.29",
|
||||
"redis": "^4.5.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-loading-overlay": "^6.0.2",
|
||||
@@ -27,11 +32,13 @@
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue-leaflet/vue-leaflet": "^0.8.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"bulma": "^0.9.4",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"leaflet": "^1.9.3",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.57.1",
|
||||
"vite": "^4.0.0"
|
||||
|
172
src/App.vue
@@ -1,18 +1,62 @@
|
||||
<script>
|
||||
// import { RouterLink, RouterView } from "vue-router";
|
||||
// import { useAuth0 } from '@auth0/auth0-vue';
|
||||
|
||||
import { useSessionStore } from './stores/session-store';
|
||||
import { authenticate, isAlive, logout } from './data/http/auth';
|
||||
import { mapState } from 'pinia';
|
||||
export default {
|
||||
setup() {
|
||||
const store = useSessionStore();
|
||||
|
||||
const { setUser, logoutUser, hasRoles } = store;
|
||||
|
||||
return {
|
||||
logoutUser,
|
||||
setUser,
|
||||
hasRoles
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const user = await isAlive();
|
||||
if (user) {
|
||||
this.setUser(user);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
loading: true,
|
||||
list: [],
|
||||
whitelist: [],
|
||||
isNavMobileOpen: false,
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
...mapState(useSessionStore, ['user', 'isAuthenticated']),
|
||||
},
|
||||
methods: {
|
||||
toggleNavMobile() {
|
||||
this.isNavMobileOpen = !this.isNavMobileOpen;
|
||||
},
|
||||
async login() {
|
||||
if (!this.username.trim() || !this.password.trim()) {
|
||||
return;
|
||||
}
|
||||
const user = await authenticate(this.username, this.password);
|
||||
this.setUser(user);
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
window.location.reload();
|
||||
},
|
||||
async logout() {
|
||||
await logout();
|
||||
this.logoutUser();
|
||||
this.$router.push('/');
|
||||
},
|
||||
async check() {
|
||||
await isAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,41 +81,47 @@ export default {
|
||||
<div class="navbar-start">
|
||||
<router-link class="navbar-item" to="/">ICAO</router-link>
|
||||
<router-link class="navbar-item" to="/acars">Acars</router-link>
|
||||
<router-link class="navbar-item" to="/map">Live Map</router-link>
|
||||
|
||||
<router-link v-if="hasRoles(['admin', 'cabal'])" class="navbar-item" to="/cabal">Capt Cabal</router-link>
|
||||
<router-link v-if="hasRoles(['admin'])" class="navbar-item" to="/admin">Admin</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="field is-horizontal">
|
||||
<div v-if="!isAuthenticated" class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small input-login" @keyup.enter="login" v-model="username" type="text" placeholder="Username">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small input-login" @keyup.enter="login" v-model="password" type="password" placeholder="Password">
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-small is-light" @click="login">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAuthenticated" class="field-body">
|
||||
<div class="is-6 mr-2 mt-1">{{ user.firstname }} {{ user.lastname }}</div>
|
||||
<p class="control">
|
||||
<a class="button is-small is-light" @click="logout">
|
||||
Logout
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div> <RouterView /> </div>
|
||||
<!-- <section class="section">
|
||||
<h1 class="title">Pilotos activos con mas de 200 horas en ACARS</h1>
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-centered">VID</th>
|
||||
<th class="">Nombre</th>
|
||||
<th class="has-text-centered">Total Vuelos</th>
|
||||
<th class="has-text-centered">Horas totales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in whitelist" :key="item.vid">
|
||||
<td class="has-text-centered">
|
||||
{{ item.vid }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.name }}
|
||||
</td>
|
||||
<td class="has-text-centered">
|
||||
{{ item.flights }}
|
||||
</td>
|
||||
<td class="has-text-centered">
|
||||
<FormatTime :value="item.flightTime" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -80,63 +130,13 @@ export default {
|
||||
.logo {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
.tag {
|
||||
font-size: 60% !important;
|
||||
.input-login {
|
||||
width: 100px;
|
||||
}
|
||||
/* header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
nav {
|
||||
.loading-icon {
|
||||
margin-top: 24px;
|
||||
color: #ddd;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
text-align: center;;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
|
15
src/assets/images/coloring.svg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/map/location-blue.png
Normal file
After Width: | Height: | Size: 653 B |
BIN
src/assets/images/map/location-gray.png
Normal file
After Width: | Height: | Size: 199 B |
BIN
src/assets/images/map/location-green.png
Normal file
After Width: | Height: | Size: 638 B |
BIN
src/assets/images/map/location-lightblue.png
Normal file
After Width: | Height: | Size: 589 B |
BIN
src/assets/images/map/location-red.png
Normal file
After Width: | Height: | Size: 1023 B |
BIN
src/assets/images/map/location-yellow.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
src/assets/images/map/marker-here.png
Normal file
After Width: | Height: | Size: 987 B |
BIN
src/assets/images/map/marker-landing.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/map/marker-takeoff.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/map/plane.png
Normal file
After Width: | Height: | Size: 498 B |
BIN
src/assets/images/ranks/01.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/ranks/02.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/images/ranks/03.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/images/ranks/04.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/ranks/05.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/ranks/06.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/ranks/07.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/images/ranks/08.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/images/ranks/09.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/images/ranks/10.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/images/ranks/11.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/ranks/12.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/ranks/13.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/ranks/14.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/ranks/15.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/ranks/16.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="">
|
||||
<div class="solari-container">
|
||||
<SolariBoard :headers="headers" :loading="isLoading" :config="colsConfig" :data="list" :size="18" />
|
||||
<SolariBoard :headers="headers" :loading="isLoading" :config="colsConfig" :data="list" :length="18" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -22,6 +22,7 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
interval: undefined,
|
||||
isLoading: true,
|
||||
list: [],
|
||||
board: undefined,
|
||||
@@ -30,23 +31,30 @@
|
||||
colsConfig: [
|
||||
{
|
||||
align: 'left',
|
||||
size: 8
|
||||
length: 8
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
size: 9
|
||||
length: 9
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
size: 5
|
||||
length: 5
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadData();
|
||||
this.startLoop();
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
methods: {
|
||||
startLoop() {
|
||||
this.interval = setInterval(this.loadData, 1000 * 30)
|
||||
},
|
||||
async loadData() {
|
||||
this.isLoading = true;
|
||||
let data = await getFlightplansNow();
|
||||
@@ -58,8 +66,7 @@
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// this.list = rows;
|
||||
this.list = this.test;
|
||||
this.list = rows;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<SolariBoardRow :ref="`line_${j - 1}`"
|
||||
:textToShow="getValue(j - 1)"
|
||||
:loops="getConfigValue(j - 1, 'loop')"
|
||||
:size="getConfigValue(j - 1, 'size')"
|
||||
:length="getConfigValue(j - 1, 'length')"
|
||||
:delay="getConfigValue(j - 1, 'delay')"
|
||||
:align="getConfigValue(j - 1, 'align')"></SolariBoardRow>
|
||||
</div>
|
||||
@@ -22,7 +22,8 @@
|
||||
<script>
|
||||
import SolariBoardRow from './SolariBoardRow.vue';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import _fill from 'lodash/fill';
|
||||
import _times from 'lodash/times';
|
||||
import _constant from 'lodash/constant';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -35,7 +36,7 @@ export default {
|
||||
type: Number,
|
||||
default: () => 200
|
||||
},
|
||||
size: {
|
||||
length: {
|
||||
type: Number,
|
||||
default: () => 25
|
||||
},
|
||||
@@ -62,9 +63,9 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
|
||||
if (Array.isArray(this.data)) {
|
||||
this.setData(this.data);
|
||||
}
|
||||
// if (Array.isArray(this.data)) {
|
||||
// this.setData(this.data);
|
||||
// }
|
||||
|
||||
},
|
||||
computed: {
|
||||
@@ -87,23 +88,26 @@ export default {
|
||||
return this.cols * this.rows;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler(newData) {
|
||||
this.setData(newData);
|
||||
},
|
||||
immediate: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setData(data) {
|
||||
const plainData = (data || []).reduce((acc, row) => {
|
||||
console.log('row :>> ', row);
|
||||
acc = acc.concat(row);
|
||||
return acc;
|
||||
}, []);
|
||||
console.log('plainData :>> ', plainData);
|
||||
console.log('this.total :>> ', this.total);
|
||||
this.values = _fill(Array(this.total), ' ').map((d, i) => plainData[i] || d);
|
||||
console.log('this.values :>> ', this.values);
|
||||
|
||||
this.values = _times(this.total, _constant(' ')).map((d, i) => plainData[i] || d);
|
||||
},
|
||||
mapRow(r, i) {
|
||||
r.loops = (_isNil(r.loops)) ? this.loops : r.loops;
|
||||
r.delay = ((_isNil(r.delay)) ? this.delay : r.delay) * i;
|
||||
r.size = (_isNil(r.size)) ? this.size : r.size;
|
||||
r.length = (_isNil(r.length)) ? this.length : r.length;
|
||||
r.align = (_isNil(r.align)) ? this.align : r.align;
|
||||
return r;
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="board-line">
|
||||
<div class="board-letter" :ref="'letter' + $index" :class="{'animating': isAnimating[$index]}" v-for="(char, $index) in charsBack" :key="$index">{{ char }}</div>
|
||||
<div class="board-line" :class="size">
|
||||
<div class="board-letter" :ref="'letter' + $index" :class="{'animating': isAnimating[$index]}" v-for="(char, $index) in charsToShow" :key="$index">{{ char }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
|
||||
.big & {
|
||||
font-size: 32px;
|
||||
height: 38px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content:' ';
|
||||
display: inline-block;
|
||||
@@ -56,9 +62,14 @@
|
||||
<script>
|
||||
import _isInteger from 'lodash/isInteger';
|
||||
import _delay from 'lodash/delay';
|
||||
import _clone from 'lodash/clone';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
textToShow: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
@@ -67,7 +78,7 @@
|
||||
type: Number,
|
||||
default: () => 0
|
||||
},
|
||||
size: {
|
||||
length: {
|
||||
type: Number,
|
||||
},
|
||||
loops: {
|
||||
@@ -80,17 +91,19 @@
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const newArray = Array.apply(null, Array(this.length)).map(() => ' ');
|
||||
const newArrayAnimating = Array.apply(null, Array(this.length)).map(() => false);
|
||||
return {
|
||||
charsAll: ' ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789-:.>*',
|
||||
charsBack: [],
|
||||
isAnimating: [],
|
||||
charsAll: ' ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789-:.>*¡!¿?@#',
|
||||
charsReference: _clone(newArray),
|
||||
charsToShow: _clone(newArray),
|
||||
isAnimating: newArrayAnimating,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
chars() {
|
||||
const text = this.getText(this.textToShow);
|
||||
const size = _isInteger(this.size) ? this.size : text.length;
|
||||
const chars = Array.apply(null, Array(size)).map(() => ' ');
|
||||
const chars = Array.apply(null, Array(this.length)).map(() => ' ');
|
||||
const offset = (this.align === 'left') ? 0 : chars.length - text.length;
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
@@ -101,8 +114,7 @@
|
||||
},
|
||||
watch: {
|
||||
chars: {
|
||||
handler(newValue) {
|
||||
this.charsBack = Array.apply(null, Array(newValue.length)).map(() => ' ');
|
||||
handler() {
|
||||
if (_isInteger(this.delay) && this.delay > 0) {
|
||||
_delay(this.startAnimation, this.delay);
|
||||
} else {
|
||||
@@ -120,27 +132,29 @@
|
||||
return this.charsAll.indexOf(letter);
|
||||
},
|
||||
getText(text) {
|
||||
return (text.length > this.size) ? text.substring(0, this.size) : text;
|
||||
return (text.length > this.length) ? text.substring(0, this.length) : text;
|
||||
},
|
||||
async startAnimation() {
|
||||
this.charsBack = this.charsBack.map(() => ' ');
|
||||
this.isAnimating = [];
|
||||
const cts = _clone(this.charsReference);
|
||||
for (let index = 0; index < this.chars.length; index++) {
|
||||
const char = this.chars[index];
|
||||
this.animateLetter(index, char);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
if (char !== cts[index]) {
|
||||
this.animateLetter(index, char);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
this.charsReference = this.chars;
|
||||
},
|
||||
animateLetter(index, letter) {
|
||||
let showIndex = 0;
|
||||
this.charsBack[index] = this.charsAll[showIndex];
|
||||
this.charsToShow[index] = this.charsAll[showIndex];
|
||||
this.isAnimating[index] = true;
|
||||
let loop = 0;
|
||||
const letterIndex = this.getLetterIndex(letter);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
showIndex = this.getNextIndex(showIndex);
|
||||
this.charsBack[index] = this.charsAll.charAt(showIndex);
|
||||
this.charsToShow[index] = this.charsAll.charAt(showIndex);
|
||||
if (showIndex === 0) {
|
||||
loop++;
|
||||
}
|
||||
|
@@ -1,7 +1,4 @@
|
||||
<template>
|
||||
<loading v-model:active="isLoading"
|
||||
:can-cancel="false"
|
||||
:is-full-page="fullPage"/>
|
||||
<section class="section">
|
||||
<h1 class="title">Pilotos activos con mas de 200 horas en ACARS</h1>
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
@@ -13,6 +10,15 @@
|
||||
<th class="has-text-centered">Horas totales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="isLoading">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="loading-icon">
|
||||
<font-awesome-icon spin icon="fa-solid fa-spinner" size="2x" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-for="item in whitelist" :key="item.vid">
|
||||
<td class="has-text-centered">
|
||||
@@ -41,7 +47,6 @@
|
||||
import moment from 'moment';
|
||||
import { getWhitelist } from '../data/http/listRequest.js';
|
||||
import FormatTime from './FormatTime.vue';
|
||||
import Loading from 'vue-loading-overlay';
|
||||
import 'vue-loading-overlay/dist/css/index.css';
|
||||
|
||||
export default {
|
||||
@@ -69,7 +74,6 @@
|
||||
},
|
||||
components: {
|
||||
FormatTime,
|
||||
Loading,
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<!-- <loading v-model:active="isLoading"
|
||||
:can-cancel="false"
|
||||
:is-full-page="fullPage"/> -->
|
||||
<section class="section">
|
||||
<!-- <loading class="mt-6" v-model:active="isLoading"
|
||||
:can-cancel="false"/> -->
|
||||
<h1 class="title">Status mensual IVAO</h1>
|
||||
<table class="table is-striped is-hoverable is-fullwidth" v-if="!$isMobile()">
|
||||
<thead>
|
||||
@@ -14,6 +13,15 @@
|
||||
<th class="has-text-centered">Último vuelo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="isLoading">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="loading-icon">
|
||||
<font-awesome-icon spin icon="fa-solid fa-spinner" size="2x" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-for="item in list" :key="item.vid">
|
||||
<td class="has-text-centered">
|
||||
@@ -81,7 +89,11 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.tag {
|
||||
font-size: 60% !important;
|
||||
}
|
||||
.box {
|
||||
border: 1px solid hsl(0, 0%, 86%);
|
||||
}
|
||||
@@ -91,7 +103,6 @@
|
||||
import moment from 'moment';
|
||||
import { getMonthlyList } from '../data/http/listRequest.js';
|
||||
import FormatTime from './FormatTime.vue';
|
||||
import Loading from 'vue-loading-overlay';
|
||||
import 'vue-loading-overlay/dist/css/index.css';
|
||||
|
||||
export default {
|
||||
|
22
src/data/http/auth.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {get, post } from './requests.js';
|
||||
|
||||
export async function isAlive() {
|
||||
const { VITE_API_BASE } =
|
||||
import.meta.env;
|
||||
const response = await get(`${VITE_API_BASE}/admin/user/alive`);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function authenticate(username, password) {
|
||||
const { VITE_API_BASE, VITE_API_PATH_AUTHENTICATION } =
|
||||
import.meta.env;
|
||||
const response = await post(`${VITE_API_BASE}${VITE_API_PATH_AUTHENTICATION}`, { username, password });
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const { VITE_API_BASE } =
|
||||
import.meta.env;
|
||||
const response = await get(`${VITE_API_BASE}/admin/user/logout`);
|
||||
return response;
|
||||
}
|
@@ -1,26 +1,32 @@
|
||||
import { request } from './requests.js';
|
||||
import {get } from './requests.js';
|
||||
|
||||
export async function getMonthlyList() {
|
||||
const { VITE_API_BASE, VITE_API_PATH_LIST } =
|
||||
import.meta.env;
|
||||
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_LIST}`);
|
||||
const response = await get(`${VITE_API_BASE}${VITE_API_PATH_LIST}`);
|
||||
return response;
|
||||
}
|
||||
export async function getWhitelist() {
|
||||
const { VITE_API_BASE, VITE_API_PATH_WHITELIST } =
|
||||
import.meta.env;
|
||||
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`);
|
||||
const response = await get(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`);
|
||||
return response;
|
||||
}
|
||||
export async function getInSessionNow() {
|
||||
const { VITE_API_BASE, VITE_API_PATH_NOW_SESSIONS } =
|
||||
import.meta.env;
|
||||
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_NOW_SESSIONS}`);
|
||||
const response = await get(`${VITE_API_BASE}${VITE_API_PATH_NOW_SESSIONS}`);
|
||||
return response;
|
||||
}
|
||||
export async function getFlightplansNow() {
|
||||
const { VITE_API_BASE, VITE_API_PATH_NOW_FLIGHTPLANS } =
|
||||
import.meta.env;
|
||||
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_NOW_FLIGHTPLANS}`);
|
||||
const response = await get(`${VITE_API_BASE}${VITE_API_PATH_NOW_FLIGHTPLANS}`);
|
||||
return response;
|
||||
}
|
||||
export async function getWazzup() {
|
||||
const { VITE_API_BASE } =
|
||||
import.meta.env;
|
||||
const response = await get(`${VITE_API_BASE}/ivao/wazzup`);
|
||||
return response;
|
||||
}
|
@@ -1,6 +1,15 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const request = async(url, options) => {
|
||||
const response = await axios.get(url, options);
|
||||
export const get = async(url) => {
|
||||
const response = await axios.get(url, { withCredentials: true });
|
||||
return response.data;
|
||||
};
|
||||
};
|
||||
|
||||
export const post = async(url, data) => {
|
||||
try {
|
||||
const response = await axios.post(url, data, { withCredentials: true });
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log('err :>> ', err.response);
|
||||
}
|
||||
}
|
82
src/helpers/MapHelper.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { icon } from 'leaflet';
|
||||
const popupAnchor = [0, -30];
|
||||
export const customIcons = {
|
||||
iconHere: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/marker-here.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [21, 32],
|
||||
iconAnchor: [10, 32],
|
||||
popupAnchor,
|
||||
}),
|
||||
iconLanding: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/marker-landing.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [21, 32],
|
||||
iconAnchor: [10, 32],
|
||||
popupAnchor,
|
||||
}),
|
||||
iconTakeoff: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/marker-takeoff.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [21, 32],
|
||||
iconAnchor: [10, 32],
|
||||
popupAnchor,
|
||||
}),
|
||||
iconPlane: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/plane.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [15, 15],
|
||||
iconAnchor: [15, 15],
|
||||
popupAnchor: [0, -20],
|
||||
className: 'plane'
|
||||
}),
|
||||
ownPlane: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/plane.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [15, 15],
|
||||
iconAnchor: [15, 15],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationRed: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-red.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationYellow: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-yellow.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationBlue: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-blue.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationLightBlue: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-lightblue.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationGreen: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-green.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
iconLocationGray: icon({
|
||||
iconUrl: new URL('/src/assets/images/map/location-gray.png',
|
||||
import.meta.url).href,
|
||||
iconSize: [12, 16],
|
||||
iconAnchor: [6, 16],
|
||||
popupAnchor: [0, -20],
|
||||
}),
|
||||
};
|
@@ -4,7 +4,7 @@ function format(num) {
|
||||
|
||||
export function hoursFromSeconds(seconds) {
|
||||
const h = format(Math.floor(seconds / 3600));
|
||||
const m = format(Math.round(seconds % 3600 * 60));
|
||||
const m = format(Math.round(seconds % 3600 / 60));
|
||||
return {
|
||||
h,
|
||||
m
|
||||
|
14
src/main.js
@@ -1,13 +1,27 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import VueMobileDetection from 'vue-mobile-detection';
|
||||
|
||||
import './assets/css/main.css';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { faSpinner, faPaperPlane, faDeleteLeft, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
library.add(faPaperPlane);
|
||||
library.add(faDeleteLeft);
|
||||
library.add(faXmark);
|
||||
library.add(faSpinner);
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(VueMobileDetection);
|
||||
app.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
|
||||
app.mount('#app');
|
@@ -1,10 +1,21 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
import { useSessionStore } from '../stores/session-store';
|
||||
import TableAcars from '../components/TableAcars.vue';
|
||||
import IvaoView from '../views/IvaoView.vue';
|
||||
import CabalView from '../views/CabalView.vue';
|
||||
import AdminView from '../views/AdminView.vue';
|
||||
import MapView from '../views/MapView/MapView.vue';
|
||||
|
||||
const checkRolesFn = (roles) => {
|
||||
return () => {
|
||||
const store = useSessionStore();
|
||||
return store.isAuthenticated && store.hasRoles(roles);
|
||||
}
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(
|
||||
import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(),
|
||||
routes: [{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
@@ -15,6 +26,23 @@ const router = createRouter({
|
||||
name: 'acars',
|
||||
component: TableAcars,
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: AdminView,
|
||||
beforeEnter: checkRolesFn(['admin']),
|
||||
},
|
||||
{
|
||||
path: '/cabal',
|
||||
name: 'cabal',
|
||||
component: CabalView,
|
||||
beforeEnter: checkRolesFn(['admin', 'cabal']),
|
||||
},
|
||||
{
|
||||
path: '/map',
|
||||
name: 'map',
|
||||
component: MapView
|
||||
}
|
||||
// {
|
||||
// path: '/about',
|
||||
// name: 'about',
|
||||
|
26
src/stores/session-store.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useSessionStore = defineStore('user', {
|
||||
state: () => ({ _user: {}, _isAuthenticated: false }),
|
||||
getters: {
|
||||
user: (state) => state._user,
|
||||
isAuthenticated: (state) => state._isAuthenticated,
|
||||
},
|
||||
actions: {
|
||||
setUser(user) {
|
||||
this._isAuthenticated = !!user;
|
||||
this._user = user;
|
||||
},
|
||||
logoutUser() {
|
||||
this._isAuthenticated = false;
|
||||
this._user = '';
|
||||
},
|
||||
hasRoles(roles = []) {
|
||||
let hasRole = false;
|
||||
roles.forEach(role => {
|
||||
hasRole = hasRole || (this._user.roles || []).indexOf(role) !== -1;
|
||||
})
|
||||
return hasRole;
|
||||
}
|
||||
}
|
||||
});
|
3
src/views/AdminView.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>TODO: Admin page</h1>
|
||||
</template>
|
286
src/views/CabalView.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<script>
|
||||
import SolariBoardRow from '../components/SolariBoardRow.vue';
|
||||
export default {
|
||||
mounted() {
|
||||
const sts = localStorage.getItem('defined_statuses');
|
||||
if (sts !== null) {
|
||||
this.addedStatuses = JSON.parse(sts);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
route: '',
|
||||
status: '',
|
||||
routeAlign: 'left',
|
||||
statusAlign: 'left',
|
||||
routeToSend: '',
|
||||
routeError: '',
|
||||
statusError: '',
|
||||
addingError: '',
|
||||
statusToSend: '',
|
||||
statusToAdd: '',
|
||||
charRegexp: /[a-zA-Z0-9Ññ\-:.>*¡!¿?@#]/g,
|
||||
defaultStatuses: [
|
||||
'BIENVENIDOS!',
|
||||
'BOARDING',
|
||||
'TAXI OUT',
|
||||
'TAKEOFF',
|
||||
'CLIMB',
|
||||
'DESCENT',
|
||||
'APPROACH',
|
||||
'LANDING',
|
||||
'TAXI TO GATE'
|
||||
],
|
||||
addedStatuses: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setRoute(val = '') {
|
||||
const newVal = val.trim();
|
||||
if (newVal && !this.charRegexp.test(newVal)) {
|
||||
this.routeError = 'Hay carácteres inválidos';
|
||||
return;
|
||||
}
|
||||
this.route = newVal.toUpperCase();
|
||||
this.routeError = '';
|
||||
},
|
||||
setStatus(val = '') {
|
||||
const newVal = val.trim();
|
||||
if (newVal && !this.charRegexp.test(newVal)) {
|
||||
this.statusError = 'Hay carácteres inválidos';
|
||||
return;
|
||||
}
|
||||
this.status = newVal.toUpperCase();
|
||||
this.statusError = '';
|
||||
},
|
||||
deleteStatus() {
|
||||
this.setStatus(this.statusToSend = '');
|
||||
},
|
||||
deleteRoute() {
|
||||
this.setRoute(this.routeToSend = '');
|
||||
},
|
||||
setQuickStatus(value) {
|
||||
this.setStatus(this.statusToSend = value);
|
||||
},
|
||||
addQuickStatus() {
|
||||
const newStatus = this.statusToAdd.trim();
|
||||
if (!newStatus) {
|
||||
return;
|
||||
}
|
||||
if (newStatus && !this.charRegexp.test(newStatus)) {
|
||||
this.addingError = 'Hay carácteres inválidos';
|
||||
return;
|
||||
}
|
||||
if (this.defaultStatuses.indexOf(newStatus) === -1 && this.addedStatuses.indexOf(newStatus) === -1) {
|
||||
this.addedStatuses.push(newStatus.toUpperCase());
|
||||
this.updateStatuses();
|
||||
this.statusToAdd = '';
|
||||
this.addingError = '';
|
||||
}
|
||||
},
|
||||
deleteQuickStatus(val) {
|
||||
const index = this.addedStatuses.indexOf(val);
|
||||
this.addedStatuses.splice(index, 1);
|
||||
this.updateStatuses();
|
||||
},
|
||||
updateStatuses() {
|
||||
localStorage.setItem('defined_statuses', JSON.stringify(this.addedStatuses));
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SolariBoardRow
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="section">
|
||||
<h1 class="title is-4"> Capitán Cabal Hub</h1>
|
||||
<div class="board">
|
||||
<div class="board-content">
|
||||
<h2>Ruta</h2>
|
||||
<h2>Estado</h2>
|
||||
<div class="solari-row">
|
||||
<SolariBoardRow ref="solari" :length="9" size="big" :align="routeAlign"
|
||||
:textToShow="route"></SolariBoardRow>
|
||||
</div>
|
||||
<div class="solari-row">
|
||||
<SolariBoardRow ref="solari" :length="15" :align="statusAlign" size="big"
|
||||
:textToShow="status"></SolariBoardRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-board">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<p class="title is-5">Quick Statuses</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label"></div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="tags">
|
||||
<span v-for="s in defaultStatuses" @click="setQuickStatus(s)" :key="s" class="tag is-rounded is-medium is-info is-light is-tag-clickable">{{ s }}</span>
|
||||
<span v-for="s in addedStatuses" @click="setQuickStatus(s)" :key="s" class="tag is-rounded is-medium is-info is-tag-clickable">{{ s }}
|
||||
<button @click.prevent.stop="deleteQuickStatus(s)" class="delete is-small"></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-6">
|
||||
<div class="field-label">
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<p class="title is-6">Carácteres aceptados: (espacio)ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789-:.>*¡!¿?@#</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="field">Ruta</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" @keydown.enter="setRoute(routeToSend)" maxlength="9" v-model="routeToSend" type="text" placeholder="Route">
|
||||
</div>
|
||||
<p v-if="routeError" class="help is-danger">{{ routeError }}</p>
|
||||
<p v-if="!routeError" class="help">9 carácteres</p>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="routeAlign">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="buttons">
|
||||
<button class="button is-info" @click="setRoute(routeToSend)"><span class="icon"><font-awesome-icon :icon="['fas', 'paper-plane']" /></span></button>
|
||||
<button class="button is-danger" @click="deleteRoute"><span class="icon"><font-awesome-icon :icon="['fas', 'delete-left']" /></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="field">Estado</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" @keydown.enter="setStatus(statusToSend)" maxlength="15" v-model="statusToSend" type="text" placeholder="Status">
|
||||
</div>
|
||||
<p v-if="statusError" class="help is-danger">{{ statusError }}</p>
|
||||
<p v-if="!statusError" class="help">15 carácteres</p>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="statusAlign">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="buttons">
|
||||
<button class="button is-info" @click="setStatus(statusToSend)"><span class="icon"><font-awesome-icon :icon="['fas', 'paper-plane']" /></span></button>
|
||||
<button class="button is-danger" @click="deleteStatus"><span class="icon"><font-awesome-icon :icon="['fas', 'delete-left']" /></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-6">
|
||||
<div class="field-label"></div>
|
||||
<div class="field-body">
|
||||
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<input @keydown.enter="addQuickStatus" :class="{ 'is-danger': addingError }" class="input" maxlength="15" v-model="statusToAdd" type="text" placeholder="Add a Quick Status">
|
||||
</div>
|
||||
<p v-if="addingError" class="help is-danger">{{ addingError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-light is-info" @click="addQuickStatus">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-board{
|
||||
border: 1px solid #dedede;
|
||||
margin-top: 130px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.board {
|
||||
margin: 16px auto;
|
||||
width: 100%;
|
||||
background-color: #333;
|
||||
padding: 32px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
.board-content{
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.is-tag-clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&.is-info:hover {
|
||||
background-color: #3488ce;
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
&.is-light.is-info:hover {
|
||||
background-color: #e4eff9;
|
||||
border-color: transparent;
|
||||
color: #296fa8;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #eff1ed;
|
||||
text-transform: uppercase;
|
||||
font-size: 60%;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.solari-row {
|
||||
margin-right: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,9 +0,0 @@
|
||||
<script setup>
|
||||
import TheWelcome from "../components/TheWelcome.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
@@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<main class="section">
|
||||
<InSessionNow v-if="!$isMobile()" />
|
||||
<TableIvao />
|
||||
</main>
|
||||
|
8
src/views/MapView/MapView.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<h1>Map</h1>
|
||||
<section class="hero">
|
||||
<div class="livemap-wrapper">
|
||||
<l-map ref="map" :zoom="zoom" :center="[51, -0.09]">
|
||||
<l-tile-layer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" layer-type="base" name="OpenStreetMap"></l-tile-layer>
|
||||
</l-map>
|
||||
</div>
|
||||
</section>
|
14
src/views/MapView/MapView.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
h1 {
|
||||
color: green
|
||||
}
|
||||
|
||||
.livemap-wrapper {
|
||||
// position: absolute;
|
||||
// bottom: 0;
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
img.leaflet-marker-icon.plane {
|
||||
opacity: 0.5 !important;
|
||||
}
|
97
src/views/MapView/MapView.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Map</h1>
|
||||
<div id="map" class="livemap-wrapper" ref="map"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <template src="./MapView.html">
|
||||
</template> -->
|
||||
|
||||
<style src="./MapView.scss" lang="scss" scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import leaflet from 'leaflet'
|
||||
import 'leaflet-rotatedmarker';
|
||||
// import { LatLng, latLngBounds, latLng, point } from 'leaflet';
|
||||
import { customIcons } from '../../helpers/MapHelper';
|
||||
import { getWazzup } from '../../data/http/listRequest';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tilesUrl: {
|
||||
type: String,
|
||||
default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
// default: 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png',
|
||||
},
|
||||
attribution: {
|
||||
type: String,
|
||||
default: '© <a href=\'https://www.openstreetmap.org/copyright\'>OpenStreetMap</a> contributors',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
zoom: 13.5,
|
||||
map: null,
|
||||
tileLayer: null,
|
||||
markers: {},
|
||||
testPilot: null,
|
||||
updateInterval: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.start();
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.updateInterval);
|
||||
},
|
||||
methods: {
|
||||
createMap() {
|
||||
this.map = leaflet.map(this.$refs.map).setView([0,0], 4);
|
||||
this.tileLayer = leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(this.map);
|
||||
},
|
||||
async start() {
|
||||
this.createMap();
|
||||
const wazzup = await getWazzup();
|
||||
const pilots= wazzup.clients.pilots;
|
||||
|
||||
const latitudes = [];
|
||||
const longitudes = [];
|
||||
|
||||
pilots.forEach(pilot => {
|
||||
if (pilot.lastTrack) {
|
||||
const { latitude, longitude, heading } = pilot.lastTrack;
|
||||
this.markers[pilot.userId] = leaflet.marker([latitude, longitude], {
|
||||
icon: customIcons.iconPlane
|
||||
}).addTo(this.map).setRotationAngle(heading);
|
||||
latitudes.push(latitude);
|
||||
longitudes.push(longitude);
|
||||
}
|
||||
});
|
||||
this.updateInterval = setInterval(this.update, 5000);
|
||||
},
|
||||
async update() {
|
||||
console.log('update');
|
||||
const wazzup = await getWazzup();
|
||||
const pilots= wazzup.clients.pilots;
|
||||
pilots.forEach(pilot => {
|
||||
if (pilot.lastTrack) {
|
||||
const { latitude, longitude, heading } = pilot.lastTrack;
|
||||
let marker = this.markers[pilot.userId];
|
||||
if (marker) {
|
||||
marker.setLatLng([latitude, longitude]).setRotationAngle(heading);
|
||||
} else {
|
||||
this.markers[pilot.userId] = leaflet.marker([latitude, longitude], {
|
||||
icon: customIcons.iconPlane
|
||||
}).addTo(this.map).setRotationAngle(heading);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|