Mapa s trasami letu u eventu, drobna uprava vizualu eventu

This commit is contained in:
Michal Kváček
2024-02-28 09:41:15 +01:00
parent 92678ec829
commit 7a45a0133e
14 changed files with 211 additions and 84 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ services:
- ./src-ssr:/app/src-ssr
- ./public:/app/public
# - ./node_modules:/app/node_modules:delegated
command: quasar dev -m ssr
command: quasar dev # -m ssr
environment:
API_URL: "http://127.0.0.1:8000"
+22
View File
@@ -0,0 +1,22 @@
<script setup>
import {FORMAT_DATE, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
const props = defineProps({
event: {
type: Object,
required: true
}
})
</script>
<template>
<q-chip icon="date_range" v-if="event.dateFrom || event.dateTo">
<template v-if="event.dateFrom !== event.dateTo">
{{ formatDatetime(event.dateFrom, FORMAT_DATE) }} - {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
</template>
<template v-else>
{{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
</template>
</q-chip>
</template>
+62
View File
@@ -0,0 +1,62 @@
<script setup>
import MapWithMarkers from "components/map/MapWithMarkers.vue";
import {ref} from "vue";
import {useEventsStore} from "stores/Events";
import {minutesToHumanReadable} from "../../composables/datetimeUtils";
import {pluralize} from "../../composables/utils";
const props = defineProps({
event: {
type: Object,
required: true
},
onlyPublic: {
type: Boolean,
required: true
}
})
const tracks = ref([]);
const totalTime = ref(0);
const totalFlights = ref(0);
useEventsStore().fetchFlightsTracks({
eventId: props.event.id,
onlyPublic: props.onlyPublic
}).then((data) => {
if (!data.flights) return;
totalFlights.value = data.flights.length;
for (const flightTrack of data.flights) {
totalTime.value += flightTrack.durationMinCalculated;
if (flightTrack.gpxTrack) {
tracks.value.push(flightTrack.gpxTrack.coordinates);
}
}
});
</script>
<template>
<template v-if="tracks.length > 1">
<h3>Trasy letů</h3>
<MapWithMarkers
style="width: 100%; height: 500px;"
:tracks="tracks"
/>
<div class="text-center q-my-md">
<q-chip>
<q-avatar>{{ totalFlights }}</q-avatar>
{{ pluralize(totalFlights, "let", "lety", "letů") }}
</q-chip>
<q-chip icon="schedule">
{{ minutesToHumanReadable(totalTime) }}
</q-chip>
</div>
</template>
</template>
+14
View File
@@ -0,0 +1,14 @@
{
"name": "event",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@kvacek.cz:poletani.cz/app.git"
},
"private": true
}
+2 -9
View File
@@ -5,6 +5,7 @@ import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
import {computed} from "vue";
import LoadMoreList from "components/LoadMoreList.vue";
import {pluralize} from "../../composables/utils";
const props = defineProps({
items: {
@@ -37,16 +38,8 @@ const routeDetail = (flight) => {
else
return {name: "flightDetail", params: {id: flight.id}}
};
const pluralize = (count, singular, plural, pluralMore) => {
if (count === 1) return singular;
else if (count < 5) return plural;
else return pluralMore;
}
const loadMoreCount = computed(() => {
const remaining = props.totalItemsCount - props.items.length;
return Math.min(props.itemsPerPage, remaining);
return Math.min(props.itemsPerPage, props.totalItemsCount - props.items.length);
});
</script>
+52 -5
View File
@@ -1,14 +1,20 @@
<script setup>
import MapWrapper from "components/map/MapWrapper.vue";
import {Marker, Polyline, InfoWindow} from "vue3-google-map";
import {computed, onMounted, ref} from "vue";
import {axios} from "boot/axios";
import {computed} from "vue";
const props = defineProps({
gpxTrack: {
type: Object,
required: false,
default: () => {
}
},
tracks: {
type: Array,
required: false,
default: () => []
},
markers: {
type: Array,
@@ -26,12 +32,47 @@ const props = defineProps({
}
})
const track = ref([]);
const bounds = computed(() => {
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
const allData = [...props.markers, ...track.value, ...photosTransformed];
return allData.filter(i => !!i.position).map(i => i.position);
const coordinates = [
...[...props.markers, ...photosTransformed].filter(i => !!i.position).map(i => i.position),
...(props.gpxTrack || []),
...(props.tracks || []).flat()
];
const minLat = Math.min(...coordinates.map(i => i.lat));
const maxLat = Math.max(...coordinates.map(i => i.lat));
const minLng = Math.min(...coordinates.map(i => i.lng));
const maxLng = Math.max(...coordinates.map(i => i.lng));
return [
{lat: minLat, lng: minLng},
{lat: minLat, lng: maxLng},
{lat: maxLat, lng: minLng},
{lat: maxLat, lng: maxLng},
];
});
const getRandomColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r}, ${g}, ${b})`;
}
const getColor = (i) => {
const colors = [
'#FF0000',
'#5733FF', '#4bec41', '#8d3f25',
'#33A1FF', '#6d7e59', '#FF3333', '#0e1c48',
'#A133FF', '#FFFF33', '#c9decc'
]
return colors[i] || getRandomColor()
}
</script>
<template>
@@ -47,6 +88,12 @@ const bounds = computed(() => {
<Polyline :options="{path: gpxTrack, geodesic: true, strokeColor: '#ff0000', strokeOpacity: 0.3, strokeWeight: 5}"/>
</template>
<template v-if="tracks && tracks.length > 0">
<template v-for="(track, i) in tracks" :key="track">
<Polyline :options="{path: track, geodesic: false, strokeColor: getColor(i), strokeOpacity: 1, strokeWeight: 3}"/>
</template>
</template>
<Marker v-if="highlightPosition" :options="{position: highlightPosition, icon: '/mapIcons/currentPosition32x32.png'}"/>
</MapWrapper>
-28
View File
@@ -14,34 +14,6 @@ export const minutesToHumanReadable = (totalMinutes) => {
return `${hoursPart}${minutes}min`
}
export const formatDatetimeNative = (date, format = "DD. MM. YYYY") => {
if (typeof date === 'string') {
date = parseDate(date);
}
return format
.replace("DD", date.getDate().toString())
.replace("MM", (date.getMonth() + 1).toString())
.replace("YYYY", date.getFullYear().toString())
// todo: doplnit pocatecni nuly
.replace("hh", date.getHours().toString())
.replace("mm", date.getMinutes().toString())
.replace("ss", date.getSeconds().toString());
}
export const parseDateNative = (datetime) => {
// const [date, time] = datetime.split("T");
// const [year, month, day] = date.split("-");
// const [hour, minute] = time.split(":");
// const dateObject = new Date(year, month, day, hour, minute, 0);
// ^ tohle funguje ok
const timestamp = Date.parse(datetime);
return new Date(timestamp);
}
export const parseDate = (datetimeStr, format = FORMAT_ISO) => {
return date.extractDate(datetimeStr, format)
}
+6
View File
@@ -2,3 +2,9 @@ export const roundWithPrecision = (number, precision) => {
const factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
export const pluralize = (count, singular, plural, pluralMore) => {
if (count === 1) return singular;
else if (count < 5) return plural;
else return pluralMore;
}
+17 -1
View File
@@ -20,13 +20,29 @@ query eventsList($limit: Int!, $offset: Int) {
${FRAGMENT_EVENT_LIST_INFO}
`;
export const EVENTS_FLIGHTS_TRACKS = gql`
query eventsFlightsTracks($eventId: Int!, $onlyPublic: Boolean) {
event(id: $eventId) {
flights(onlyPublic: $onlyPublic) {
durationMinCalculated
gpxTrack {
coordinates {
lat
lng
}
}
}
}
}
`;
export const EVENTS_PUBLIC_LIST = gql`
query eventsPublicList($username: String, $limit: Int!, $offset: Int) {
events(username: $username, limit: $limit, offset: $offset, public: true) {
items {
...eventListInfo
flights {
flights(onlyPublic: true) {
titlePhoto {
thumbnailUrl
}
+7 -18
View File
@@ -5,7 +5,6 @@ import EventDetailSkeleton from "components/event/skeletons/EventDetailSkeleton.
import {useAppStore} from "stores/App";
import FAB from "components/FAB.vue";
import FlightsList from "components/flights/FlightsList.vue";
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
import {useEventsStore} from "stores/Events";
import {storeToRefs} from "pinia";
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
@@ -13,9 +12,10 @@ import {useFlightsStore} from "stores/Flights";
import PublishButton from "components/inputs/PublishButton.vue";
import {useLoggedUserStore} from "stores/LoggedUser";
import {toggleVisibility} from "src/composables/form/formHandling";
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
import {useQuasar} from "quasar";
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
import EventFlightsMap from "components/event/EventFlightsMap.vue";
import EventBasicInfo from "components/event/EventBasicInfo.vue";
const eventId = parseInt(useRoute().params.id);
onMounted(() => {
@@ -35,7 +35,6 @@ const togglePublicVisibility = (variables) => toggleVisibility({
$q, store: useEventsStore(), object: event.value, variables
});
const {isQueryRunning, event} = storeToRefs(useEventsStore());
filters.value = {eventId: eventId}
@@ -51,19 +50,7 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
<EventDetailSkeleton v-if="isQueryRunning || !event"/>
<template v-else>
<q-list bordered separator class="q-my-lg" v-if="event.dateFrom && event.dateTo">
<q-item>
<q-item-section>
Od: {{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
</q-item-section>
</q-item>
<q-item>
<q-item-section>
Do: {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
</q-item-section>
</q-item>
</q-list>
<EventBasicInfo :event="event"/>
<PublishButton
:url-prefix="`${loggedUser.publicUsername}/udalosti/`"
@@ -75,11 +62,13 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
:route="{name: 'publicEventDetail', params: {username: loggedUser.publicUsername, eventSlug: event.urlSlug}}"
/>
<article v-html="event.description"/>
<article class="q-my-lg" v-html="event.description"/>
<EventFlightsMap :event="event" :only-public="false" />
<h3>Lety</h3>
<q-banner v-if="event.flights.length === 0" class="bg-light-blue text-white">
<q-banner v-if="flights.length === 0" class="bg-light-blue text-white">
Zatím tu nemáš uložené žádné lety.
</q-banner>
@@ -94,7 +94,7 @@ const togglePublicVisibility = (variables) => toggleVisibility({
<FlightBasicInfo
:flight="flight"
:poi-detail-route="{name: 'poiDetail'}"
:event-detail-route="{name: 'eventDetail', params: {id: flight.event.id}}"
:event-detail-route="flight.event ? {name: 'eventDetail', params: {id: flight.event.id}} : null"
:aircraft-detail-route="{name: 'aircraftDetail'}"
:copilot-detail-route="{name: 'copilotDetail'}"
/>
@@ -11,7 +11,6 @@ export default {
<script setup>
import {useAppStore} from "stores/App";
import EventDetailSkeleton from "components/event/skeletons/EventDetailSkeleton.vue";
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
import FlightsList from "components/flights/FlightsList.vue";
import {storeToRefs} from "pinia";
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
@@ -19,6 +18,8 @@ import {useFlightsStore} from "stores/Flights";
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
import {useMeta} from "quasar";
import {metaData} from "src/composables/meta";
import EventBasicInfo from "components/event/EventBasicInfo.vue";
import EventFlightsMap from "components/event/EventFlightsMap.vue";
const props = defineProps({
pilot: {
@@ -55,21 +56,12 @@ const {flights, totalItemsCount, isQueryRunning: areFlightsLoading} = storeToRef
<EventDetailSkeleton v-if="isQueryRunning || !event"/>
<template v-else>
<q-list bordered separator class="q-my-lg" v-if="event.dateFrom && event.dateTo">
<q-item>
<q-item-section>
Od: {{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
</q-item-section>
</q-item>
<EventBasicInfo :event="event"/>
<q-item>
<q-item-section>
Do: {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
</q-item-section>
</q-item>
</q-list>
<article class="q-my-lg" v-html="event.description"/>
<EventFlightsMap :event="event" :only-public="true"/>
<article v-html="event.description"/>
<h3>Lety</h3>
<q-banner v-if="event.flights.length === 0" class="bg-light-blue text-white">
@@ -87,7 +79,6 @@ const {flights, totalItemsCount, isQueryRunning: areFlightsLoading} = storeToRef
:public="true"
@load-more="loadNextFlightsPage(true)"
/>
</template>
</div>
</template>
+11 -4
View File
@@ -2,7 +2,7 @@ import {defineStore} from 'pinia'
import {CREATE_EVENT, EDIT_EVENT} from "src/graphql/mutations/event";
import {mutate, query, rewriteObject, setItems, updateInItems} from "stores/helpers";
import {ref} from "vue";
import {EVENT_DETAIL, EVENT_PUBLIC_DETAIL, EVENTS_LIST, EVENTS_PUBLIC_LIST} from "src/graphql/queries/event";
import {EVENT_DETAIL, EVENT_PUBLIC_DETAIL, EVENTS_FLIGHTS_TRACKS, EVENTS_LIST, EVENTS_PUBLIC_LIST} from "src/graphql/queries/event";
export const useEventsStore = defineStore('Events', () => {
const event = ref(null);
@@ -19,7 +19,7 @@ export const useEventsStore = defineStore('Events', () => {
}).then(item => event.value = item);
};
const fetchPublicDetail = (urlSlug, username) => {
const fetchPublicDetail = (urlSlug, username) => {
return query({
isQueryRunning,
query: EVENT_PUBLIC_DETAIL,
@@ -27,6 +27,13 @@ export const useEventsStore = defineStore('Events', () => {
}).then(item => event.value = item);
};
const fetchFlightsTracks = ({eventId, onlyPublic}) => {
return query({
query: EVENTS_FLIGHTS_TRACKS,
variables: {eventId, onlyPublic}
})
}
const fetch = ({limit = 100, offset, appendItems}) => {
return query({
isQueryRunning,
@@ -35,7 +42,7 @@ export const useEventsStore = defineStore('Events', () => {
}).then(data => setItems(data, appendItems, events, totalItemsCount));
}
const fetchPublic = (username, limit = 100, offset, appendItems) => {
const fetchPublic = (username, limit = 100, offset, appendItems) => {
return query({
isQueryRunning,
query: EVENTS_PUBLIC_LIST,
@@ -65,6 +72,6 @@ export const useEventsStore = defineStore('Events', () => {
return {
event, events, isMutationRunning, isQueryRunning, fetch, fetchDetail, create, update,
fetchPublic, fetchPublicDetail, totalItemsCount
fetchPublic, fetchPublicDetail, totalItemsCount, fetchFlightsTracks
}
})
+10 -2
View File
@@ -39,7 +39,9 @@ export const query = ({isQueryRunning, query, variables, refreshUnauthorized}) =
query._refreshUnauthorizedUser = typeof refreshUnauthorized === 'undefined' ? true : refreshUnauthorized;
isQueryRunning.value = true;
if (isQueryRunning)
isQueryRunning.value = true;
const queryName = query.definitions[0].selectionSet.selections[0].name.value;
return apolloWrapper.query(query, variables)
.then((result) => {
@@ -49,12 +51,18 @@ export const query = ({isQueryRunning, query, variables, refreshUnauthorized}) =
return result.data[queryName]
})
.catch(e => Promise.reject(e))
.finally(() => isQueryRunning.value = false);
.finally(() => {
if (isQueryRunning)
isQueryRunning.value = false
});
}
export const mutate = ({isMutationRunning, mutation, variables}) => {
isMutationRunning.value = true;
let mutationName = getOperationName(mutation);
// TODO: pridat refresh tokenu!
return apolloWrapper.mutate(mutation, variables)
.then(result => {
if (typeof result.data[mutationName] === 'undefined') {