Mapa s trasami letu u eventu, drobna uprava vizualu eventu
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user