diff --git a/src/components/flights/FlightBasicInfo.vue b/src/components/flights/FlightBasicInfo.vue index a9bd532..ff91960 100644 --- a/src/components/flights/FlightBasicInfo.vue +++ b/src/components/flights/FlightBasicInfo.vue @@ -1,6 +1,7 @@ @@ -144,14 +150,31 @@ const props = defineProps({ - + 3 - - {{ copilot.name }} - + diff --git a/src/components/flights/form/FlightFormPhotos.vue b/src/components/flights/form/FlightFormPhotos.vue index 393ccb1..e7604f1 100644 --- a/src/components/flights/form/FlightFormPhotos.vue +++ b/src/components/flights/form/FlightFormPhotos.vue @@ -1,8 +1,6 @@ + + diff --git a/src/composables/breadcrumbs.js b/src/composables/breadcrumbs.js new file mode 100644 index 0000000..caefae2 --- /dev/null +++ b/src/composables/breadcrumbs.js @@ -0,0 +1,44 @@ +import {flightName} from "src/composables/flight"; + +const homepageBreadcrumbs = () => { + return [{to: {name: 'homepage'}, label: "Polétání.cz"}] +} + + +// TODO: dodelat + +const publicProfileBreadcrumbs = (pilot) => { + return [ + ...homepageBreadcrumbs(), + {to: {name: "publicProfileDetail", params: {username: pilot.publicUsername}}, label: `@${pilot.publicUsername}`}, + ] +} + +const publicEventBreadcrumbs = (pilot, event) => { + +} + +export const flightBreadcrumbs = (pilot, flight, event) => { + const breadcrumbs = publicProfileBreadcrumbs(pilot) + + if (event && event.isPublic) { + breadcrumbs.push({to: {name: 'publicEventsList', username: pilot.publicUsername}, label: "Události"}); + breadcrumbs.push({ + to: { + name: 'publicEventDetail', + params: {eventSlug: event.urlSlug, username: pilot.publicUsername} + }, label: event.name + }); + } + + breadcrumbs.push({label: flightName(flight)}) + + return breadcrumbs; +} + +export const copilotBreadcrumbs = (pilot, flight, event, copilot) => { + return [ + ...flightBreadcrumbs(pilot, flight, event), + {label: copilot.name} + ] +} diff --git a/src/composables/photos/basicOperations.js b/src/composables/photos/basicOperations.js new file mode 100644 index 0000000..4d4918e --- /dev/null +++ b/src/composables/photos/basicOperations.js @@ -0,0 +1,89 @@ +import {ref} from "vue"; +import {usePhotosStore} from "stores/Photos"; +import {errorObject, successObject, warningObject} from "src/composables/dialog"; +import {useQuasar} from "quasar"; +import {usePointsOfInterestStore} from "stores/PointsOfInterest"; +import {storeToRefs} from "pinia"; +import {useFlightsStore} from "stores/Flights"; + +export const useBasicPhotoOperations = ({copilotUploadToken}) => { + const $q = useQuasar(); + + const {flight} = storeToRefs(useFlightsStore()); + const {photos} = storeToRefs(usePhotosStore()); + + // TODO: V PRIVATNI CASTI JE TO ZATIM ROZBITE!!! + + + const uploadInProgress = ref(false); + const handlePhotosUpload = (data) => { + const promises = []; + uploadInProgress.value = true; + + for (const photo of data.photos) { + promises.push(usePhotosStore().upload(flight.value.id, {photo, copilotUploadToken}).then((data) => { + photos.value.push(data); + photo._uploaded = true; + })); + } + + Promise.allSettled(promises).then((values) => { + const success = values.filter(v => v.status === 'fulfilled'); + const failed = values.filter(v => v.status === 'rejected'); // TODO: nebo vse - success? + + if (success.length > 0) + $q.notify(successObject(`Úspěšně nahráno ${success.length} fotek`)); + + if (failed.length > 0) + $q.notify(warningObject(`Nepovedlo se nahrát ${failed.length} fotek. Zde jsou důvody: ${failed.map(f => f.reason)}`)); + + data.photos = []; + }).finally(() => uploadInProgress.value = false); + } + + const _findPhotoIndex = (photoId) => photos.value.findIndex((object) => object.id === photoId); + const deletePhoto = (id) => { + usePhotosStore().destroy(id, copilotUploadToken).then((data) => { + photos.value.splice(_findPhotoIndex(data.id), 1); + }); + } + + const batchRotatePhotos = (data) => { + const selectedPhotos = data.photos; + const direction = data.direction; + + const promises = []; + for (const photo of selectedPhotos) { + promises.push(usePhotosStore().changeOrientation(photo.id, direction, copilotUploadToken).then(data => { + const idx = _findPhotoIndex(data.id); + data.thumbnailUrl += "?t=" + (new Date()).getTime(); + photos.value[idx] = data; + })); + } + + let success = 0; + Promise.allSettled(promises).then(result => { + for (const item of result) { + if (item.status === 'fulfilled') success++; + else console.error(item.reason) + } + const error = result.length - success; + if (success > 0) $q.notify(successObject("Otočeno " + success + " fotografií")); + if (error > 0) $q.notify(errorObject(`Nepovedlo se otočit ${error} fotografi`)) + }); + } + + const updateSinglePhoto = (id, formData) => { + formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name})); + + return usePhotosStore().update(id, formData).then((data) => { + const idx = _findPhotoIndex(data.id); + photos.value[idx] = data; + usePointsOfInterestStore().fetch({}); + }); +} + + return { + uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto + } +} diff --git a/src/composables/photos/batch.js b/src/composables/photos/batch.js new file mode 100644 index 0000000..c839779 --- /dev/null +++ b/src/composables/photos/batch.js @@ -0,0 +1,96 @@ +import {computed, ref, watch} from "vue"; +import {confirmObject} from "src/composables/dialog"; +import {useQuasar} from "quasar"; + + +export const usePhotoBatchManagement = ({photos, emits}) => { + const batchEdit = ref({}); + const $q = useQuasar(); + + // console.log(photos); + for (const photo of photos) { + batchEdit.value[photo.id] = false; + } + + const handleImageClick = (photo, idx) => { + batchEdit.value[photo.id] = !batchEdit.value[photo.id]; + } + + const selectedPhotos = computed(() => { + const selected = []; + for (const i in batchEdit.value) { + const photo = photos.find(p => p.id === parseInt(i)); + if (photo && batchEdit.value[i]) selected.push(photo); + } + + return selected; + }); + + const confirmBatchPhotoDelete = () => { + $q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?")) + .onOk(() => { + for (const id in batchEdit.value) { + if (!batchEdit.value[id]) continue; + emits("delete", parseInt(id)); + batchEdit.value[id] = false; + } + }); + } + + + const openBatchEdit = () => { + const photos = selectedPhotos.value; + for (const i in batchEdit.value) { + batchEdit.value[i] = false; + } + emits('batch-edit', photos); + } + + const batchRotate = (direction) => { + const photos = selectedPhotos.value; + emits("batch-rotate", {photos, direction}); + } + + const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0); + + const checkAll = ref(false); + watch(() => checkAll.value, (newVal) => { + if (newVal === null) return; + + for (const photo of photos) { + batchEdit.value[photo.id] = newVal; + } + }); + + const batchOptionsProps = computed(() => ({ + round: true, + flat: true, + class: "q-ml-lg", + disable: !isBatchEditAllowed.value + })); + + watch(() => batchEdit.value, () => { + let checkedAll = true; + let uncheckedAll = true; + + for (const i of Object.keys(batchEdit.value)) { + if (typeof batchEdit.value[i] === 'undefined') continue; + + if (batchEdit.value[i]) uncheckedAll = false; + if (!batchEdit.value[i]) checkedAll = false; + } + + if (checkedAll) { + checkAll.value = true; + } else if (uncheckedAll) { + checkAll.value = false; + } else { + checkAll.value = null; + } + }, {deep: true}); + + return { + batchEdit, handleImageClick, selectedPhotos, confirmBatchPhotoDelete, + openBatchEdit, batchRotate, checkAll, isBatchEditAllowed, batchOptionsProps + } +}; diff --git a/src/graphql/fragments/copilot.js b/src/graphql/fragments/copilot.js index b18e13b..9b17db9 100644 --- a/src/graphql/fragments/copilot.js +++ b/src/graphql/fragments/copilot.js @@ -11,18 +11,26 @@ fragment copilotListInfo on Copilot { } `; -export const FRAGMENT_COPILOT_FULL_INFO = gql` -fragment copilotFullInfo on Copilot { +export const FRAGMENT_COPILOT_PUBLIC_INFO = gql` +fragment copilotPublicInfo on Copilot { id name isPublic urlSlug +} +` + +export const FRAGMENT_COPILOT_FULL_INFO = gql` +fragment copilotFullInfo on Copilot { + ...copilotPublicInfo titlePhoto { id url } } + +${FRAGMENT_COPILOT_PUBLIC_INFO} `; export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql` diff --git a/src/graphql/fragments/flight.js b/src/graphql/fragments/flight.js index 873fa53..78c2a44 100644 --- a/src/graphql/fragments/flight.js +++ b/src/graphql/fragments/flight.js @@ -3,6 +3,7 @@ import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft"; import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport"; import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo"; import {FRAGMENT_POI_FULL_INFO} from "src/graphql/fragments/poi"; +import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot"; export const FRAGMENT_WEATHER_FULL_INFO = gql` fragment weatherFullInfo on WeatherInfo { @@ -51,13 +52,6 @@ fragment flightFullInfo on Flight { ...weatherFullInfo } - copilots { - id - name - isPublic - urlSlug - } - event { id isPublic @@ -107,6 +101,11 @@ fragment flightFullPrivateInfo on Flight { ...flightFullInfo debriefingNotes + copilots { + ...copilotPublicInfo + token + } + photos { ...photoFullInfo @@ -124,4 +123,5 @@ fragment flightFullPrivateInfo on Flight { ${FRAGMENT_FLIGHT_FULL_INFO} ${FRAGMENT_PHOTO_FULL_INFO} +${FRAGMENT_COPILOT_PUBLIC_INFO} `; diff --git a/src/graphql/mutations/photo.js b/src/graphql/mutations/photo.js index 4bbd39e..bd000f9 100644 --- a/src/graphql/mutations/photo.js +++ b/src/graphql/mutations/photo.js @@ -23,8 +23,8 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) { `; export const CHANGE_PHOTO_ORIENTATION = gql` -mutation changeOrientation($id: Int!, $direction: String!) { - changeOrientation(id: $id, direction: $direction) { +mutation changeOrientation($id: Int!, $direction: String!, $copilotUploadToken: String) { + changeOrientation(id: $id, direction: $direction, copilotUploadToken: $copilotUploadToken) { ...photoFullInfo flight { @@ -67,8 +67,8 @@ mutation adjustPhoto($id: Int!, $adjustment: AdjustmentInput!) { // TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO export const DELETE_PHOTO = gql` -mutation deletePhoto($id: Int!) { - deletePhoto(id: $id) { +mutation deletePhoto($id: Int!, $copilotUploadToken: String) { + deletePhoto(id: $id, copilotUploadToken: $copilotUploadToken) { id } } diff --git a/src/graphql/queries/copilots.js b/src/graphql/queries/copilots.js index 74da6fa..43d6ba8 100644 --- a/src/graphql/queries/copilots.js +++ b/src/graphql/queries/copilots.js @@ -34,8 +34,14 @@ ${FRAGMENT_FLIGHT_LIST_INFO} `; export const COPILOT_PUBLIC_DETAIL = gql` -query copilot($urlSlug: String!, $pilotUsername: String) { - copilot(urlSlug: $urlSlug, pilotUsername: $pilotUsername, public: true) { +query copilot($urlSlug: String, $uploadToken: String, $uploadFlightSlug: String, $pilotUsername: String!) { + copilot( + urlSlug: $urlSlug, + uploadToken: $uploadToken, + uploadFlightSlug: $uploadFlightSlug, + pilotUsername: $pilotUsername, + public: true + ) { ...copilotFullInfo } } diff --git a/src/graphql/queries/flights.js b/src/graphql/queries/flights.js index 14c6db8..cb04db4 100644 --- a/src/graphql/queries/flights.js +++ b/src/graphql/queries/flights.js @@ -2,6 +2,7 @@ import gql from "graphql-tag"; import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight"; import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list"; import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo"; +import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot"; export const FLIGHTS_LIST = gql` query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) { @@ -58,12 +59,17 @@ ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} `; export const FLIGHT_PUBLIC_DETAIL = gql` -query FlightPublicDetail($urlSlug: String!, $username: String) { +query FlightPublicDetail($urlSlug: String, $username: String) { flight(urlSlug: $urlSlug, username: $username, public: true) { ...flightFullInfo + + copilots { + ...copilotPublicInfo + } } } ${FRAGMENT_FLIGHT_FULL_INFO} +${FRAGMENT_COPILOT_PUBLIC_INFO} `; diff --git a/src/graphql/queries/photo.js b/src/graphql/queries/photo.js index c3b4cba..f5ca896 100644 --- a/src/graphql/queries/photo.js +++ b/src/graphql/queries/photo.js @@ -39,8 +39,21 @@ ${FRAGMENT_FLIGHT_LIST_INFO} `; export const PHOTOS_PUBLIC_LIST = gql` -query publicPhotos($flightId: Int, $pointOfInterestId: Int, $aircraftId: Int, $copilotId: Int) { - photos(public: true, flightId: $flightId, pointOfInterestId: $pointOfInterestId, aircraftId: $aircraftId, copilotId: $copilotId) { +query publicPhotos( + $flightId: Int, + $pointOfInterestId: Int, + $aircraftId: Int, + $copilotId: Int, + $uploadedByCopilotId: Int +) { + photos( + public: true, + flightId: $flightId, + pointOfInterestId: $pointOfInterestId, + aircraftId: $aircraftId, + copilotId: $copilotId, + uploadedByCopilotId: $uploadedByCopilotId + ) { ...photoFullInfo } } diff --git a/src/pages/profile/flight/EditFlightPage.vue b/src/pages/profile/flight/EditFlightPage.vue index cc774ca..2df4981 100644 --- a/src/pages/profile/flight/EditFlightPage.vue +++ b/src/pages/profile/flight/EditFlightPage.vue @@ -1,29 +1,39 @@ + + diff --git a/src/pages/public/pilotProfile/CopilotUploadPhotoPage.vue b/src/pages/public/pilotProfile/CopilotUploadPhotoPage.vue new file mode 100644 index 0000000..e1abc77 --- /dev/null +++ b/src/pages/public/pilotProfile/CopilotUploadPhotoPage.vue @@ -0,0 +1,100 @@ + + + + diff --git a/src/pages/public/pilotProfile/FlightDetailPage.vue b/src/pages/public/pilotProfile/FlightDetailPage.vue index d8de554..33e219d 100644 --- a/src/pages/public/pilotProfile/FlightDetailPage.vue +++ b/src/pages/public/pilotProfile/FlightDetailPage.vue @@ -81,7 +81,7 @@ useMeta(() => metaData({ :flight="flight" :poi-detail-route="{name: 'publicPOIDetail'}" :aircraft-detail-route="{name: 'publicAircraftDetail'}" - :event-detail-route="event.isPublic ? {name: 'publicEventDetail', params: {username: pilot.publicUsername, eventSlug: event.urlSlug}} : null" + :event-detail-route="event && event.isPublic ? {name: 'publicEventDetail', params: {username: pilot.publicUsername, eventSlug: event.urlSlug}} : null" :copilot-detail-route="{name: 'publicCopilotDetail', params: {username: pilot.publicUsername}}" /> diff --git a/src/router/routes.js b/src/router/routes.js index e0416ba..c373295 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -27,6 +27,7 @@ const routes = [ children: [ {path: '', name: "publicProfileDetail", component: () => import('pages/public/pilotProfile/FlightsListPage.vue')}, {path: ':flightSlug', name: "publicFlightDetail", component: () => import('pages/public/pilotProfile/FlightDetailPage.vue')}, + {path: ':flightSlug/nahraj', name: "publicCopilotUploadPhoto", component: () => import('pages/public/pilotProfile/CopilotUploadPhotoPage.vue')}, {path: 'a/:copilotSlug', name: "publicCopilotDetail", component: () => import('pages/public/pilotProfile/CopilotDetailPage.vue')}, {path: 'udalosti', name: "publicEventsList", component: () => import('pages/public/pilotProfile/EventsListPage.vue')}, {path: 'udalosti/:eventSlug', name: "publicEventDetail", component: () => import('pages/public/pilotProfile/EventDetailPage.vue')}, diff --git a/src/stores/Copilots.js b/src/stores/Copilots.js index a58aebe..fdaf1d5 100644 --- a/src/stores/Copilots.js +++ b/src/stores/Copilots.js @@ -18,11 +18,11 @@ export const useCopilotsStore = defineStore('Copilots', () => { }).then(item => copilot.value = item); }; - const fetchPublicDetail = (urlSlug, username) => { + const fetchPublicDetail = ({urlSlug, username, uploadToken, uploadFlightSlug}) => { return query({ isQueryRunning, query: COPILOT_PUBLIC_DETAIL, - variables: {urlSlug, pilotUsername: username}, + variables: {urlSlug, pilotUsername: username, uploadToken, uploadFlightSlug}, }).then(item => { copilot.value = item; return item; diff --git a/src/stores/Photos.js b/src/stores/Photos.js index e24791b..79c11f7 100644 --- a/src/stores/Photos.js +++ b/src/stores/Photos.js @@ -21,10 +21,10 @@ export const usePhotosStore = defineStore('Photos', () => { variables: {id} }).then(item => photo.value = item); - const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId}) => query({ + const fetchPublic = ({flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId}) => query({ isQueryRunning, query: PHOTOS_PUBLIC_LIST, - variables: {flightId, pointOfInterestId, aircraftId, copilotId} + variables: {flightId, pointOfInterestId, aircraftId, copilotId, uploadedByCopilotId} }).then(items => photos.value = items); const upload = (flightId, input) => { @@ -61,10 +61,10 @@ export const usePhotosStore = defineStore('Photos', () => { return item; }) - const changeOrientation = (id, direction) => mutate({ + const changeOrientation = (id, direction, copilotUploadToken) => mutate({ isMutationRunning, mutation: CHANGE_PHOTO_ORIENTATION, - variables: {id, direction} + variables: {id, direction, copilotUploadToken} }).then(item => { photo.value = item; return item; @@ -79,10 +79,10 @@ export const usePhotosStore = defineStore('Photos', () => { return item; }); - const destroy = (id) => mutate({ + const destroy = (id, copilotUploadToken) => mutate({ isMutationRunning, mutation: DELETE_PHOTO, - variables: {id} + variables: {id, copilotUploadToken} }); return {