Files
geovisio-website/src/views/MySequencesView.vue
2024-01-31 13:39:11 +00:00

923 lines
25 KiB
Vue

<template>
<main class="entry-page">
<section :style="{ width: `${mapWidth}px` }" class="section-viewer">
<vue-draggable-resizable
:style="{ width: `${mapWidth}px` }"
:w="mapWidth"
:h="windowHeight"
:max-width="windowWidth"
:handles="['mr']"
:draggable="false"
class-name-active="resize-active-map"
class-name-handle="resize-handle-map"
@resizing="onResizeMap"
>
<Viewer
v-if="collectionBbox.length"
:fetch-options="{
fetchOptions: {
credentials: 'include'
}
}"
:geovisio-viewer="false"
:user-id="getUserId"
:bbox="collectionBbox"
ref="viewerRef"
/>
<div v-else class="no-map">
<img
src="@/assets/images/how-to-share-map.png"
:alt="$t('pages.share_pictures.alt_img_map')"
class="no-map-img"
/>
</div>
</vue-draggable-resizable>
<div class="bbox-filter-button">
<Button
look="button--white background-white"
icon="bi bi-search"
:text="$t('pages.sequences.filter_bbox_button')"
@trigger="triggerBboxFilter"
/>
<div class="reset-bbox">
<Button
v-if="filterBbox"
look="no-text-blue-dark"
icon="bi bi-x-circle-fill"
@trigger="triggerResetBbox"
/>
</div>
</div>
</section>
<section
:style="{ width: `${listWidth}px` }"
class="section-sequence"
@scroll="handleScroll"
>
<div class="header-title">
<h1 id="sequenceTitle" class="sequences-title">
{{ $t('pages.sequences.title') }}
</h1>
<Button
v-if="
filterDate.start || filterDate.end || sortDate.sortBy || filterBbox
"
look="button-border--black"
:text="$t('pages.sequences.reset_filter_button')"
@trigger="resetAllFilter"
/>
</div>
<div
ref="headerList"
:class="['sequence-item sequence-item-head', headerListClass]"
:style="{ width: `${listWidth}px`, borderRadius: '0px' }"
>
<div class="sequence-header-item"></div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_name') }}</span>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_photos') }}</span>
</div>
<div class="sequence-header-item">
<Button
look="link--black no-text"
:icon="iconButtonSort('datetime')"
data-test="button-sort-date"
@trigger="sortList('datetime')"
/>
<span class="title">{{ $t('pages.sequences.sequence_date') }}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('datetime')"
:tooltip="$t('pages.sequences.sequence_date_tooltip')"
@trigger="displayModal('datetime')"
/>
</div>
</div>
<div class="sequence-header-item">
<Button
look="link--black no-text"
:icon="iconButtonSort('created')"
data-test="button-sort-date"
@trigger="sortList('created')"
/>
<span class="title">{{
$t('pages.sequences.sequence_creation')
}}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('created')"
:tooltip="$t('pages.sequences.sequence_creation_tooltip')"
@trigger="displayModal('created')"
/>
</div>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_status') }}</span>
</div>
</div>
<ul v-if="!isLoading" ref="list" class="sequence-list">
<li
v-if="userSequences.length"
v-for="item in userSequences"
:class="['sequence-item', { 'sequence-selected': item.id === seqId }]"
@mouseover="goToSequence(item)"
>
<router-link
class="button-item"
:to="{
name: 'sequence',
params: { id: item.id }
}"
>
<div class="wrapper-thumb">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb"
/>
<div class="wrapper-thumb-hover">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb-hover"
/>
</div>
</div>
<div class="sequence-title">
<span>
{{ item.title }}
</span>
</div>
<div>
<i class="bi bi-images"></i>
<span>
{{ item['stats:items'].count }}
</span>
</div>
<div>
<span>
{{
formatDate(
item.extent.temporal.interval[0][0],
'Do MMM YYYY HH:mm:ss'
)
}}
</span>
</div>
<div>
<span>
{{ formatDate(item.created, 'Do MMM YYYY HH:mm:ss') }}
</span>
</div>
<div>
<span :class="item['geovisio:status']">{{
sequenceStatus(item['geovisio:status'])
}}</span>
</div>
</router-link>
<div class="wrapper-button">
<Button
:text="sequenceButtonText(item['geovisio:status'])"
look="link--blue row-reverse"
:icon="
item['geovisio:status'] === 'ready'
? 'bi bi-eye-slash'
: 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection(item)"
/>
<Button
:text="$t('pages.sequences.delete_button')"
look="link--red row-reverse"
icon="bi bi-trash"
@trigger="deleteCollection(item)"
/>
</div>
</li>
<div v-else-if="!userSequences.length && noSequencesFound">
<p class="no-sequence-found">
{{ $t('pages.sequences.no_sequence_found') }}
</p>
</div>
<div v-else class="no-sequence">
<p class="no-sequence-text">
{{ $t('pages.sequences.no_sequences_text') }}
</p>
<Link
:text="$t('general.header.upload_text')"
look="button button--blue"
:route="{ name: 'why-contribute' }"
/>
</div>
</ul>
<div v-else class="loader">
<Loader look="sm" :is-loaded="false" />
</div>
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="goToNextPage"
/>
</div>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</section>
<Modal ref="modal" :title="modalTitle">
<template v-slot:body>
<CalendarFilter
v-if="calendarType === filterDate.type"
:type="calendarType"
:range-selected="{
start: filterDate.start,
end: filterDate.end,
type: filterDate.type
}"
@triggerCloseModal="closeModal"
@triggerDate="updateFilters"
/>
<CalendarFilter
v-else
:type="calendarType"
:range-selected="{ start: null, end: null, type: '' }"
@triggerCloseModal="closeModal"
@triggerDate="updateFilters"
/>
</template>
</Modal>
</main>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect, computed, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSequenceStore } from '@/store/sequence'
import { storeToRefs } from 'pinia'
import {
scrollIntoSelected,
formatPaginationItems
} from '@/views/utils/sequence/index'
import { useCookies } from 'vue3-cookies'
import axios from 'axios'
import Viewer from '@/components/Viewer.vue'
import type ViewerType from '@/components/Viewer.vue'
import Button from '@/components/Button.vue'
import Link from '@/components/Link.vue'
import Toast from '@/components/Toast.vue'
import Loader from '@/components/Loader.vue'
import Modal from '@/components/Modal.vue'
import CalendarFilter from '@/components/filters/CalendarFilter.vue'
import Pagination from '@/components/Pagination.vue'
import type { SequenceLinkInterface } from './interfaces/MySequencesView'
import type { ResponseUserPhotoLinksInterface } from './interfaces/MySequenceView'
import { formatDate } from '@/utils/dates'
import {
deleteACollection,
patchACollection
} from '@/views/utils/sequence/request'
const { t } = useI18n()
const { cookies } = useCookies()
const sequenceStore = useSequenceStore()
const { toastText, toastLook } = storeToRefs(sequenceStore)
interface PositionInterface {
bottom: number
top: number
right: number
left: number
y: number
x: number
}
let userSequences = ref<SequenceLinkInterface[]>([])
let paginationLinks = ref<ResponseUserPhotoLinksInterface[] | []>([])
let selfLink = ref<ResponseUserPhotoLinksInterface[] | []>([])
let collectionBbox = ref<number[]>([])
let isLoading = ref<boolean>(false)
let sortedBy = ref<string>('')
let isSorted = ref<boolean>(false)
let noSequencesFound = ref<boolean>(false)
let seqId = ref<string>('')
let calendarType = ref<string>('')
let width = ref<number>(0)
let mapWidth = ref<number>(window.innerWidth / 3)
let listWidth = ref<number>(window.innerWidth / 1.5)
let filterDate = ref<{
end: string | null
start: string | null
type: string
}>({
start: null,
end: null,
type: ''
})
let filterBbox = ref<number[] | null>(null)
let sortDate = ref<{ sortBy: string | null }>({ sortBy: null })
let uri = ref<string>('api/users/me/collection?limit=50')
let modal = ref()
const windowWidth = ref<number>(window.innerWidth)
const windowHeight = ref<number>(window.innerHeight - 80)
const viewerRef = ref<InstanceType<typeof ViewerType>>()
const headerList = ref<HTMLDivElement | null>(null)
const list = ref<HTMLDListElement | null>(null)
const listPos = ref<PositionInterface | null>(null)
const headerLisPos = ref<PositionInterface | null>(null)
async function resetAllFilter(): Promise<void> {
filterDate.value = { start: null, end: null, type: '' }
sortDate.value = { sortBy: null }
filterBbox.value = null
formatUri()
await updateSequence(uri.value)
}
async function triggerResetBbox(): Promise<void> {
filterBbox.value = null
formatUri()
await updateSequence(uri.value)
}
function triggerBboxFilter(): void {
if (viewerRef.value) {
isLoading.value = true
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
]
filterBbox.value = currentBbox
formatUri()
updateSequence(uri.value)
}
}
function displayModal(type: string): void {
calendarType.value = type
if (modal.value) modal.value.show()
}
function closeModal(): void {
if (modal.value) modal.value.close()
}
function iconButtonSort(type: string): string {
if (isSorted.value && sortedBy.value === type) return 'bi bi-sort-numeric-up'
return 'bi bi-sort-numeric-down'
}
function iconButtonFilter(type: string): string {
if (
filterDate.value.start &&
filterDate.value.end &&
filterDate.value.type === type
) {
return 'bi bi-funnel-fill'
}
return 'bi bi-funnel'
}
async function fetchAndFormatSequence(): Promise<void> {
const { data } = await axios.get('api/users/me/collection?limit=50')
collectionBbox.value = data.extent.spatial.bbox[0]
userSequences.value = getLinkByRel(data.links, 'child')
}
async function patchCollection(sequence: SequenceLinkInterface): Promise<void> {
isLoading.value = true
let visible
if (sequence['geovisio:status'] === 'ready') visible = 'false'
else visible = 'true'
await patchACollection(sequence.id, { visible: visible })
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
isLoading.value = false
}
async function deleteCollection(
sequence: SequenceLinkInterface
): Promise<void> {
isLoading.value = true
if (confirm(t('pages.sequence.confirm_sequence_dialog'))) {
await deleteACollection(sequence.id)
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
}
isLoading.value = false
}
function sequenceStatus(status: string): string {
if (status === 'ready') return t('pages.sequences.sequence_published')
if (status === 'hidden') return t('pages.sequences.sequence_hidden')
return t('pages.sequences.sequence_waiting')
}
function sequenceButtonText(status: string): string {
if (status === 'hidden') return t('pages.sequences.show_button')
return t('pages.sequences.hide_button')
}
function onResizeMap(width: any): void {
if (width && collectionBbox.value.length) {
width.value = width
mapWidth.value = width.value.width
listWidth.value = window.innerWidth - width.value.width
}
}
const handleScroll = async () => {
await nextTick()
if (headerList.value && headerList.value.getBoundingClientRect()) {
headerLisPos.value = headerList.value.getBoundingClientRect()
}
if (list.value && list.value.getBoundingClientRect()) {
listPos.value = list.value.getBoundingClientRect()
}
}
async function updateSequence(uri: string) {
try {
const { data } = await axios.get(uri)
selfLink.value = getLinkByRel(data.links, 'self')
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getLinkByRel(data.links, 'child')
scrollToElement()
isLoading.value = false
if (!userSequences.value.length) noSequencesFound.value = true
} catch (e) {
userSequences.value = []
noSequencesFound.value = true
isLoading.value = false
}
}
async function sortList(dateToSort: string): Promise<void> {
isLoading.value = true
sortedBy.value = dateToSort
let sortBy = `+${dateToSort}`
if (isSorted.value) sortBy = `-${dateToSort}`
sortDate.value.sortBy = encodeURIComponent(sortBy)
formatUri()
await updateSequence(uri.value)
isSorted.value = !isSorted.value
}
function bboxIsInsideOther(mainBbox: number[], bboxInside: number[]): boolean {
return (
bboxInside[0] <= mainBbox[0] &&
bboxInside[1] <= mainBbox[1] &&
bboxInside[2] >= mainBbox[2] &&
bboxInside[3] >= mainBbox[3]
)
}
function goToSequence(sequence: SequenceLinkInterface): void {
if (!viewerRef.value) return
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
]
if (
seqId.value === sequence.id &&
bboxIsInsideOther(currentBbox, sequence.extent.spatial.bbox[0])
) {
return
}
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
viewerRef.value.viewer._map.flyTo({
center: [
sequence.extent.spatial.bbox[0][0],
sequence.extent.spatial.bbox[0][1]
],
duration: 0
})
}
function getLinkByRel(
sequences: SequenceLinkInterface[],
rel: string
): SequenceLinkInterface[] {
return sequences.filter((el: SequenceLinkInterface) => el.rel === rel)
}
function scrollToElement(): void {
const elementTarget = document.querySelector('#sequenceTitle')
if (elementTarget) elementTarget.scrollIntoView({ behavior: 'smooth' })
}
async function goToNextPage(value: string): Promise<void> {
isLoading.value = true
await updateSequence(value)
}
const filterClass = computed<string>((): string => {
return 'no-text-green'
})
const modalTitle = computed<string>((): string => {
if (calendarType.value === 'datetime') {
return t('pages.sequences.filter_date_title')
}
return t('pages.sequences.filter_date_upload_title')
})
const getUserId = computed<string>((): string => cookies.get('user_id'))
const headerListClass = computed<string>((): string => {
if (headerLisPos.value && listPos.value) {
const classCondition = headerLisPos.value.y != 0 && listPos.value.top < 174
return classCondition ? 'item-head-fixed' : ''
}
return ''
})
onMounted(async () => {
isLoading.value = true
try {
const { data } = await axios.get('api/users/me/collection?limit=50')
selfLink.value = getLinkByRel(data.links, 'self')
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getLinkByRel(data.links, 'child')
if (userSequences.value[0]) {
collectionBbox.value = [
userSequences.value[0].extent.spatial.bbox[0][0],
userSequences.value[0].extent.spatial.bbox[0][1],
userSequences.value[0].extent.spatial.bbox[0][2],
userSequences.value[0].extent.spatial.bbox[0][3]
]
}
isLoading.value = false
} catch (err) {
isLoading.value = false
console.log(err)
}
})
function formatUri(): void {
let params: string[] = []
if (filterBbox && filterBbox.value) {
const bboxFilter = `${filterBbox.value[0]},${filterBbox.value[1]},${filterBbox.value[2]},${filterBbox.value[3]}`
params = [...params, `&bbox=${bboxFilter}`]
}
if (filterDate.value.start && filterDate.value.end) {
const rangeFilter = encodeURIComponent(
`${filterDate.value.type} BETWEEN '${filterDate.value.start}' AND '${filterDate.value.end}'`
)
params = [...params, `&filter=${rangeFilter}`]
}
if (sortDate.value.sortBy) {
params = [...params, `&sortby=${sortDate.value.sortBy}`]
}
if (params.length) {
const constructParams = params.join('')
uri.value = `api/users/me/collection?limit=50${constructParams}`
} else uri.value = 'api/users/me/collection?limit=50'
}
async function updateFilters(value: {
start: null
end: null
type: ''
}): Promise<void> {
isLoading.value = true
noSequencesFound.value = false
filterDate.value = { start: value.start, end: value.end, type: value.type }
formatUri()
await updateSequence(uri.value)
}
watchEffect(() => {
if (viewerRef.value && viewerRef.value.viewer) {
viewerRef.value.viewer.addEventListener(
'select',
(e: { detail: { seqId: string } }) => {
if (seqId.value === e.detail.seqId) return
seqId.value = e.detail.seqId
scrollIntoSelected(
e.detail.seqId,
userSequences.value.map((e) => e.id)
)
if (viewerRef.value) viewerRef.value.viewer.select(e.detail.seqId)
}
)
}
})
</script>
<style lang="scss">
.resize-handle-map-mr {
top: 0;
right: toRem(0);
cursor: e-resize;
background-color: var(--black);
display: block !important;
}
.resize-handle-map {
z-index: 999999;
box-sizing: border-box;
position: absolute;
height: 100%;
width: toRem(0.5);
&:hover {
cursor: col-resize;
}
}
</style>
<style lang="scss" scoped>
.entry-page {
display: flex;
height: calc(100vh - #{toRem(8)});
overflow: hidden;
position: relative;
}
.section-viewer {
height: 100%;
position: relative;
}
.bbox-filter-button {
padding: toRem(2);
width: fit-content;
height: fit-content;
position: relative;
}
.reset-bbox {
position: absolute;
right: 0;
top: toRem(0.5);
}
.no-map {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background-color: var(--blue-pale);
}
.section-sequence {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
position: relative;
}
.header-title {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
position: relative;
margin: toRem(3);
}
.sequences-title {
@include text(h1);
color: var(--blue-dark);
}
.date-filter-title {
@include text(s-r-regular);
}
.wrapper-date-filter-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.sequence-list {
box-shadow: 0px 2px 30px 0px #0000000f;
padding: 0;
}
.sequence-item {
@include text(s-regular);
position: relative;
border: none;
display: flex;
justify-content: center;
align-items: center;
margin: auto;
background-color: var(--blue-pale);
padding-right: toRem(2);
padding-left: toRem(2);
&:nth-child(2n) {
background-color: var(--white);
}
&:hover,
&.sequence-selected {
background-color: var(--blue-semi);
}
}
.wrapper-button {
position: absolute;
right: toRem(1);
bottom: 0;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
height: 100%;
.button--white:first-child {
margin-bottom: toRem(1);
}
}
.item-head-fixed {
position: fixed;
top: toRem(8);
width: 100%;
z-index: 2;
}
.sequence-item-head {
margin-bottom: toRem(1);
padding: toRem(1) toRem(2);
border-bottom: toRem(0.1) solid var(--grey);
border-radius: toRem(2) toRem(2) 0rem 0rem;
background-color: var(--white);
}
.sequence-item-head:hover {
background-color: var(--white);
color: initial;
}
.wrapper-title {
display: flex;
align-items: center;
}
.wrapper-thumb {
position: relative;
}
.wrapper-thumb-hover {
display: none;
border-radius: toRem(0.3);
border: toRem(1) solid var(--grey);
position: absolute;
bottom: 0;
height: toRem(15);
z-index: 1;
}
.sequence-title {
text-decoration: underline;
}
.ready {
color: var(--green);
}
.hidden {
color: var(--red-pale);
}
.waiting-for-process {
color: var(--yellow);
}
.thumb-hover {
height: 100%;
}
.wrapper-thumb:hover {
.wrapper-thumb-hover {
display: block;
}
}
.thumb {
height: 100%;
width: 100%;
object-fit: cover;
border-radius: toRem(0.5);
position: relative;
}
.button-item {
display: flex;
align-items: center;
width: 100%;
background-color: transparent;
border: none;
text-decoration: none;
& > * {
padding: toRem(1);
text-align: initial;
width: calc(31% - #{toRem(4.75)});
color: var(--black);
}
> :first-child {
color: var(--blue);
width: toRem(6);
}
& > :first-child {
padding: 0;
margin-right: toRem(2);
}
& > :nth-child(3) {
width: toRem(13);
}
& > :nth-child(2) {
color: var(--blue-dark);
}
}
.bi-images {
margin-right: toRem(0.5);
}
.sequence-header-item {
display: flex;
align-items: center;
width: calc(31% - #{toRem(4.75)});
color: var(--grey-semi-dark);
&:first-child {
margin-right: toRem(2);
}
&:first-child {
width: toRem(6);
}
&:nth-child(3) {
width: toRem(13);
}
.title {
color: var(--black);
}
}
.button-filter {
margin-left: toRem(-1);
.icon {
font-size: toRem(1);
}
}
.no-sequence {
padding-top: toRem(2);
padding-bottom: toRem(4);
width: fit-content;
@include text(m-regular);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: auto;
}
.no-sequence-text {
margin-bottom: toRem(4);
}
.no-sequence-found {
text-align: center;
padding: toRem(2);
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.ay11-link {
padding: toRem(2);
margin-left: auto;
width: fit-content;
}
.entry-pagination {
margin-top: toRem(4);
margin-bottom: toRem(4);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: toRem(102.4)) {
.section-viewer {
width: 30%;
}
.section-sequence {
width: 70%;
}
}
@media (max-width: toRem(76.8)) {
.entry-page {
padding-right: toRem(2);
padding-left: toRem(2);
padding-top: toRem(14);
height: 100%;
}
.section-viewer {
display: none;
}
.section-sequence {
width: 100% !important;
}
.sequence-item-head {
width: 100% !important;
flex-direction: row !important;
padding-right: 0;
padding-left: 0;
}
.sequence-header-item {
width: 50%;
justify-content: center;
&:first-child,
&:last-child,
&:nth-child(2),
&:nth-child(3) {
display: none;
}
}
.button-item {
flex-direction: column;
align-items: center;
padding-right: toRem(1);
padding-left: toRem(1);
& > * {
text-align: center;
width: 100%;
}
.wrapper-thumb {
margin-right: 0;
}
}
.sequence-item {
border-top-right-radius: toRem(1);
border-top-left-radius: toRem(1);
flex-direction: column;
}
.wrapper-button {
flex-direction: row;
position: relative;
right: 0;
bottom: 0;
justify-content: center;
margin-top: toRem(1);
margin-bottom: 0;
.button--white:first-child {
margin-right: toRem(1);
margin-bottom: 0;
}
}
}
</style>