feature: on session

This commit is contained in:
José Conde 2023-01-15 18:43:41 +01:00
parent 0271d06e49
commit ce697dc51f
10 changed files with 926 additions and 15 deletions

View File

@ -0,0 +1,69 @@
<template>
<section class="">
<div class="solari-container">
<SolariBoard :headers="headers" :loading="isLoading" :config="colsConfig" :data="list" :size="18" />
</div>
</section>
</template>
<style lang="scss">
.solari-container {
width: 485px;
margin: 0 auto;
}
</style>
<script>
import { getFlightplansNow } from '../data/http/listRequest.js';
import 'vue-loading-overlay/dist/css/index.css';
import SolariBoard from '../components/SolariBoard.vue';
import { hoursFromSeconds } from '../helpers/time-helpers.js';
export default {
data() {
return {
isLoading: true,
list: [],
board: undefined,
test: [['LTS016T', 'SKSM-SKBO', '']],
headers: ['Callsign', 'Ruta', 'E.T.A'],
colsConfig: [
{
align: 'left',
size: 8
},
{
align: 'right',
size: 9
},
{
align: 'right',
size: 5
},
],
}
},
async mounted() {
await this.loadData();
},
methods: {
async loadData() {
this.isLoading = true;
let data = await getFlightplansNow();
this.isLoading = false;
const rows = data.reduce((acc, d) => {
const eta = hoursFromSeconds(d.eta);
const row = [ d.callsign, `${d.departure.icao}-${d.arrival.icao}`, `${eta.h}:${eta.m}`];
acc.push(row);
return acc;
}, []);
// this.list = rows;
this.list = this.test;
}
},
components: {
SolariBoard
}
}
</script>

View File

@ -0,0 +1,191 @@
<template>
<div class="board">
<h1 v-if="title">{{ title }}</h1>
<div class="board-content" :style="getGridStyle">
<div class="board-headers" :class="{'no-headers': !(headers && headers.length) }" v-for="(header, i) in headers" :key="i">
<h2>{{ header }}</h2>
</div>
<div class="board-item" v-for="(j) in this.total" :key="j" :class="getItemClasses(j)">
<SolariBoardRow :ref="`line_${j - 1}`"
:textToShow="getValue(j - 1)"
:loops="getConfigValue(j - 1, 'loop')"
:size="getConfigValue(j - 1, 'size')"
:delay="getConfigValue(j - 1, 'delay')"
:align="getConfigValue(j - 1, 'align')"></SolariBoardRow>
</div>
</div>
</div>
</template>
<script>
import SolariBoardRow from './SolariBoardRow.vue';
import _isNil from 'lodash/isNil';
import _fill from 'lodash/fill';
export default {
props: {
title: String,
data: {
type: Array,
default: () => []
},
delay: {
type: Number,
default: () => 200
},
size: {
type: Number,
default: () => 25
},
loops: {
type: Number,
default: () => 0
},
align: {
type: String,
default: () => 'left'
},
loading: Boolean,
config: Object,
headers: Array,
rows: {
type: Number,
default: 4
}
},
data() {
return {
values: []
};
},
mounted() {
if (Array.isArray(this.data)) {
this.setData(this.data);
}
},
computed: {
getGridStyle() {
let size;
if (this.headers) {
size = this.headers && this.headers.length;
} else {
size = this.data && this.data[0] ? this.data[0].length : 1;
}
return `grid-template-columns: repeat(${size}, auto)`;
},
cols() {
if (Array.isArray(this.config)) {
return this.config.length;
}
return 0;
},
total() {
return this.cols * this.rows;
}
},
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);
},
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.align = (_isNil(r.align)) ? this.align : r.align;
return r;
},
getConfigValue(index, key) {
return this.config[(index % this.cols)][key];
},
getValue(index) {
return this.values[index];
},
getItemClasses(index) {
const classes = [];
if (index % this.cols === 0) {
classes.push('last-col');
}
if (index > ((this.rows - 1) * this.cols)) {
classes.push('last-row');
}
return classes.join(' ');
}
},
components: {
SolariBoardRow,
}
}
</script>
<style lang="scss" scoped>
.board {
min-height: 100px;
margin: 0 auto;
text-align: center;
}
.board-content {
display: inline-grid;
width: 100%
}
.board-headers{
margin-bottom: 4px;
&.no-headers {
display: none;
}
}
.board-item {
display: inline-block;
height: 2rem;
line-height: 1;
margin-bottom: 2px;
margin-right: 8px;
color: #222222;
&.last-col {
margin-right: 0;
}
&.last-row {
margin-bottom: 0;
}
}
h1 {
margin: 0;
text-align: center;
color: #eff1ed;
text-transform: uppercase;
font-size: 80%;
font-weight: 400;
margin-bottom: 8px;
}
h2 {
margin: 0;
text-align: center;
color: #eff1ed;
text-transform: uppercase;
font-size: 60%;
font-weight: 600;
margin-bottom: 4px;
}
.board {
display: inline-block;
background-color: #333;
padding: 8px;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,156 @@
<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>
</template>
<style lang="scss" scoped>
@import url('https://fonts.googleapis.com/css2?family=Inconsolata&display=swap');
* {
box-sizing: border-box;
}
.board-line {
display: inline-flex;
font-family: 'Inconsolata', monospace;
}
.board-letter {
display: inline-block;
padding: 2px;
margin-right: 2px;
border-radius: 3px;
border: 1px solid #222;
background-color: #222;
// color: #EEB868;
// color: #717744;
color: #bcbd8b;
font-size: 20px;
height: 26px;
width: 16px;
position: relative;
line-height: 1;
&::after {
content:' ';
display: inline-block;
border-bottom: 1px solid #ededed;
opacity: 0.2;
width: 100%;
height: 1px;
top: 49%;
left: 0;
position: absolute;
box-shadow: 6px 4px 8px;
}
}
.board-letter.animating {
animation: squeeze 0.075s ease-in-out infinite;
}
@keyframes squeeze {
50% {
transform: scaleY(0);
}
}
</style>
<script>
import _isInteger from 'lodash/isInteger';
import _delay from 'lodash/delay';
export default {
props: {
textToShow: {
type: String,
default: () => ''
},
delay: {
type: Number,
default: () => 0
},
size: {
type: Number,
},
loops: {
type: Number,
default: () => 0
},
align: {
type: String,
default: 'left'
}
},
data() {
return {
charsAll: ' ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789-:.>*',
charsBack: [],
isAnimating: [],
}
},
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 offset = (this.align === 'left') ? 0 : chars.length - text.length;
for (let index = 0; index < text.length; index++) {
chars[index + offset] = text.charAt(index);
}
return chars;
}
},
watch: {
chars: {
handler(newValue) {
this.charsBack = Array.apply(null, Array(newValue.length)).map(() => ' ');
if (_isInteger(this.delay) && this.delay > 0) {
_delay(this.startAnimation, this.delay);
} else {
this.startAnimation();
}
},
immediate: true
}
},
methods: {
getNextIndex(index) {
return (index >= this.charsAll.length - 1) ? 0 : index + 1;
},
getLetterIndex(letter) {
return this.charsAll.indexOf(letter);
},
getText(text) {
return (text.length > this.size) ? text.substring(0, this.size) : text;
},
async startAnimation() {
this.charsBack = this.charsBack.map(() => ' ');
this.isAnimating = [];
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))
}
},
animateLetter(index, letter) {
let showIndex = 0;
this.charsBack[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);
if (showIndex === 0) {
loop++;
}
if (loop >= this.loops && letterIndex === showIndex) {
clearInterval(interval);
this.isAnimating[index] = false;
}
}, 50);
}
}
}
</script>

View File

@ -0,0 +1,75 @@
<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">
<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>
</template>
<style lang="sass">
</style>
<script>
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 {
data() {
return {
isLoading: true,
whitelist: [],
}
},
async mounted() {
await this.loadData();
},
methods: {
async loadData() {
this.isLoading = true;
this.whitelist = await getWhitelist();
this.isLoading = false;
},
dateTime(value) {
return moment(value).fromNow(); //.format('YYYY-MM-DD HH:mm');
},
flagIcon(code) {
return new URL(`/public/flags/${code}.png`, import.meta.url).href
}
},
components: {
FormatTime,
Loading,
}
}
</script>

View File

@ -0,0 +1,135 @@
<template>
<!-- <loading v-model:active="isLoading"
:can-cancel="false"
:is-full-page="fullPage"/> -->
<section class="section">
<h1 class="title">Status mensual IVAO</h1>
<table class="table is-striped is-hoverable is-fullwidth" v-if="!$isMobile()">
<thead>
<tr>
<th class="has-text-centered">VID</th>
<th class="">Nombre</th>
<th class="has-text-centered">Horas en session</th>
<th class="has-text-centered">Horas en el aire</th>
<th class="has-text-centered">Último vuelo</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.vid">
<td class="has-text-centered">
{{ item.vid }}
</td>
<td>
{{ item.name }} ({{ item.division }} <img v-if="item.division" :src="flagIcon(item.division.toLowerCase())" :alt="item.division" :title="item.division" @error="fallbackIcon" />)
</td>
<td class="has-text-centered">
<FormatTime :value="item.sessionsTime / 60" />
</td>
<td class="has-text-centered">
<FormatTime :value="item.time / 60" />
</td>
<td class="">
<div class="is-size-6">
{{ item.lastFlight.departureId }} - {{ item.lastFlight.arrivalId }} <span class="is-size-7 ml-1 has-text-grey-light">{{ dateTime(item.lastFlightDate) }}</span>
</div>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-dark">Callsign</span>
<span class="tag">{{ item.lastCallsign }}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag is-dark">Aircraft</span>
<span class="tag">{{ item.lastFlight.aircraftId }}</span>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div v-else>
<div class="box is-shadowless mb-1" v-for="item in list" :key="item.vid">
<p class="has-text-weight-bold is-size-7 is-uppercase">{{ item.name }} ({{ item.division }} <img v-if="item.division" :src="flagIcon(item.division.toLowerCase())" :alt="item.division" :title="item.division" />)</p>
<p class="has-text-weight-light is-size-7">VID: {{ item.vid }}</p>
<p class="has-text-weight-light is-size-7">Horas en el aire : <FormatTime :value="item.time / 60" /></p>
<p class="has-text-weight-light is-size-7">Horas en session : <FormatTime :value="item.sessionsTime / 60" /></p>
<h5 class="mt-2 has-text-weight-bold is-size-7 is-uppercase">Último vuelo</h5>
<div class="is-size-7">
{{ item.lastFlight.departureId }} - {{ item.lastFlight.arrivalId }} <span class="is-size-7 ml-1 has-text-grey-light">{{ dateTime(item.lastFlightDate) }}</span>
</div>
<div class="mt-1 field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-dark">Callsign</span>
<span class="tag">{{ item.lastCallsign }}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag is-dark">Aircraft</span>
<span class="tag">{{ item.lastFlight.aircraftId }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<style lang="scss">
.box {
border: 1px solid hsl(0, 0%, 86%);
}
</style>
<script>
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 {
data() {
return {
isLoading: true,
list: [],
}
},
async mounted() {
await this.loadData();
},
methods: {
async loadData() {
this.isLoading = true;
let data = await getMonthlyList();
this.list = data.sort((a, b) => b.time - a.time).map(d => {
const val = d.time / 60;
const hours = Number.isNaN(val) ? 0 : Math.floor(val / 60);
const minutes = String(Number.isNaN(val) ? 0 :Math.round((val % 60)) + 100).substring(1);
d._formattedTime = `${hours}:${minutes}`;
this.isLoading = false;
return d;
});
},
dateTime(value) {
return moment(value).fromNow(); //.format('YYYY-MM-DD HH:mm');
},
flagIcon(code) {
return new URL(`/src/assets/images/flags/${code}.png`, import.meta.url).href
},
fallbackIcon(e) {
e.target.style.display = 'none';
}
},
components: {
FormatTime,
}
}
</script>

View File

@ -11,4 +11,16 @@ export async function getWhitelist() {
import.meta.env; import.meta.env;
const response = await request(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`); const response = await request(`${VITE_API_BASE}${VITE_API_PATH_WHITELIST}`);
return response; 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}`);
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}`);
return response;
} }

244
src/helpers/fakeData.js Normal file
View File

@ -0,0 +1,244 @@
export const onSessionFakeData = [{
'time': 34031,
'id': 51004653,
'callsign': 'GIA88',
'userId': 446688,
'connectionType': 'PILOT',
'serverId': 'WS',
'createdAt': '2023-01-13T01:35:25.000Z',
'pilotSession': {
'simulatorId': 'P3D',
'textureId': 355
},
'lastTrack': {
'altitude': 36202,
'altitudeDifference': -200,
'arrivalDistance': 2307.286865,
'departureDistance': 4086.708252,
'groundSpeed': 439,
'heading': 287,
'latitude': 28.503233,
'longitude': 45.723396,
'onGround': false,
'state': 'En Route',
'timestamp': '2023-01-13T11:02:27.000Z',
'transponder': 2000,
'transponderMode': 'N',
'time': 34023
},
'flightPlan': {
'id': 54507818,
'arrivalId': 'EHAM',
'departureId': 'WIII',
'aircraftId': 'B77W',
'aircraft': {
'icaoCode': 'B77W',
'model': '777-300ER',
'wakeTurbulence': 'H',
'isMilitary': false,
'description': 'LandPlane'
},
'departure': {
'icao': 'WIII',
'iata': 'CGK',
'name': 'Soekarno Hatta',
'countryId': 'ID',
'longitude': 106.6611111111,
'latitude': -6.1236111111,
'military': false,
'city': 'Jakarta'
},
'arrival': {
'icao': 'EHAM',
'iata': 'AMS',
'name': 'Schiphol',
'countryId': 'NL',
'longitude': 4.7641666667,
'latitude': 52.3080555556,
'military': false,
'city': 'Amsterdam'
}
},
'softwareType': {
'id': 'altitude/win',
'name': 'Altitude/win'
},
'user': {
'id': 446688,
'divisionId': 'ID',
'firstName': null,
'lastName': null,
'rating': {
'pilotRatingId': 4,
'pilotRating': {
'id': 4,
'name': 'Advanced Flight Student',
'shortName': 'FS3',
'description': 'Rating requires at least 25 hours online as a pilot<br>and a successful theoretical IvAp test<br>'
}
}
}
},
{
'time': 33149,
'id': 51004722,
'callsign': 'DLH122',
'userId': 671530,
'connectionType': 'PILOT',
'serverId': 'WS',
'createdAt': '2023-01-13T01:50:07.000Z',
'pilotSession': {
'simulatorId': 'X-Plane11',
'textureId': 1827
},
'lastTrack': {
'altitude': 34635,
'altitudeDifference': 329,
'arrivalDistance': 1223.80835,
'departureDistance': 3178.239502,
'groundSpeed': 463,
'heading': 128,
'latitude': 61.01244,
'longitude': -22.375532,
'onGround': false,
'state': 'En Route',
'timestamp': '2023-01-13T11:02:32.000Z',
'transponder': 2000,
'transponderMode': 'N',
'time': 33146
},
'flightPlan': {
'id': 54507919,
'arrivalId': 'EDDF',
'departureId': 'CYVR',
'aircraftId': 'A359',
'aircraft': {
'icaoCode': 'A359',
'model': 'A350-900 XWB',
'wakeTurbulence': 'H',
'isMilitary': false,
'description': 'LandPlane'
},
'departure': {
'icao': 'CYVR',
'iata': 'YVR',
'name': 'Vancouver',
'countryId': 'CA',
'longitude': -123.1839694444,
'latitude': 49.1946972222,
'military': false,
'city': 'Vancouver'
},
'arrival': {
'icao': 'EDDF',
'iata': 'FRA',
'name': 'Frankfurt',
'countryId': 'DE',
'longitude': 8.5704555556,
'latitude': 50.0333055556,
'military': false,
'city': 'Frankfurt/Main'
}
},
'softwareType': {
'id': 'altitude/win',
'name': 'Altitude/win'
},
'user': {
'id': 671530,
'divisionId': 'BR',
'firstName': null,
'lastName': null,
'rating': {
'pilotRatingId': 4,
'pilotRating': {
'id': 4,
'name': 'Advanced Flight Student',
'shortName': 'FS3',
'description': 'Rating requires at least 25 hours online as a pilot<br>and a successful theoretical IvAp test<br>'
}
}
}
},
{
'time': 30300,
'id': 51004897,
'callsign': 'ABS9912',
'userId': 697657,
'connectionType': 'PILOT',
'serverId': 'WS',
'createdAt': '2023-01-13T02:37:36.000Z',
'pilotSession': {
'simulatorId': 'X-Plane11',
'textureId': 67
},
'lastTrack': {
'altitude': 34959,
'altitudeDifference': 33,
'arrivalDistance': 1464.623535,
'departureDistance': 3891.043701,
'groundSpeed': 495,
'heading': 134,
'latitude': -7.272876,
'longitude': -65.504532,
'onGround': false,
'state': 'En Route',
'timestamp': '2023-01-13T11:02:27.000Z',
'transponder': 7320,
'transponderMode': 'N',
'time': 30292
},
'flightPlan': {
'id': 54508190,
'arrivalId': 'SBGR',
'departureId': 'KLAX',
'aircraftId': 'A346',
'aircraft': {
'icaoCode': 'A346',
'model': 'A340-600',
'wakeTurbulence': 'H',
'isMilitary': false,
'description': 'LandPlane'
},
'departure': {
'icao': 'KLAX',
'iata': 'LAX',
'name': 'Los Angeles International',
'countryId': 'US',
'longitude': -118.40805,
'latitude': 33.9424972222,
'military': false,
'city': 'Los Angeles'
},
'arrival': {
'icao': 'SBGR',
'iata': 'GRU',
'name': 'São Paulo/Guarulhos / Governador André Franco Montoro Intl',
'countryId': 'BR',
'longitude': -46.4730555556,
'latitude': -23.4355555556,
'military': true,
'city': 'Guarulhos'
}
},
'softwareType': {
'id': 'altitude/win',
'name': 'Altitude/win'
},
'user': {
'id': 697657,
'divisionId': 'BR',
'firstName': null,
'lastName': null,
'rating': {
'pilotRatingId': 4,
'pilotRating': {
'id': 4,
'name': 'Advanced Flight Student',
'shortName': 'FS3',
'description': 'Rating requires at least 25 hours online as a pilot<br>and a successful theoretical IvAp test<br>'
}
}
}
},
];

View File

@ -0,0 +1,12 @@
function format(num) {
return String(100 + num).substring(1);
}
export function hoursFromSeconds(seconds) {
const h = format(Math.floor(seconds / 3600));
const m = format(Math.round(seconds % 3600 * 60));
return {
h,
m
};
}

View File

@ -1,23 +1,29 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from 'vue-router';
import HomeView from "../views/HomeView.vue"; import TableAcars from '../components/TableAcars.vue';
import IvaoView from '../views/IvaoView.vue';
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(
routes: [ import.meta.env.BASE_URL),
{ routes: [{
path: "/", path: '/',
name: "home", name: 'home',
component: HomeView, component: IvaoView,
}, },
{ {
path: "/about", path: '/acars',
name: "about", name: 'acars',
// route level code-splitting component: TableAcars,
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import("../views/AboutView.vue"),
}, },
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (About.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue'),
// },
], ],
}); });
export default router; export default router;

11
src/views/IvaoView.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
import TableIvao from '../components/TableIvao.vue';
import InSessionNow from '../components/InSessionNow.vue';
</script>
<template>
<main>
<InSessionNow v-if="!$isMobile()" />
<TableIvao />
</main>
</template>