37 Commits

Author SHA1 Message Date
antoine-de
518f06c846 Release 2.5.0 2024-03-11 11:42:26 +01:00
antoine-de
50f1f7b472 Merge branch 'update_geovisio_2_5' into 'develop'
Update geovisio to 2.5

See merge request geovisio/website!128
2024-03-11 08:47:03 +00:00
antoine-de
2f4000291f Update geovisio to 2.5 2024-03-11 08:47:03 +00:00
Jean Andreani
6ad1d85604 Feat/improve meta tag 2024-03-05 13:30:13 +01:00
Jean Andreani
707420c69e Feat/add matomo analytics 2024-03-05 13:30:13 +01:00
Jean Andreani
1bd41002af Merge branch 'fix/ign-tiles-fallback' into 'main'
fix/ign-tiles-fallback

See merge request geovisio/website!125
2024-03-05 12:27:47 +00:00
Jean Andreani
c30eeb020d fix/ign-tiles-fallback 2024-03-05 12:27:47 +00:00
Andreani Jean
c37296066f Release 2.4.1 2024-02-01 10:17:17 +01:00
Andreani Jean
19f5f5a320 maj geovisio 2024-02-01 10:15:00 +01:00
Andreani Jean
589f7c64c0 Release 2.4.0 2024-01-31 15:24:10 +01:00
Jean Andreani
459efcfd57 Merge branch 'fix/auth-sequence-list' into 'develop'
Fix/auth sequence list

See merge request geovisio/website!122
2024-01-31 13:39:11 +00:00
Jean Andreani
966be42f19 Fix/auth sequence list 2024-01-31 13:39:11 +00:00
Jean Andreani
c4066930ba Merge branch 'fix/auth-sequence-list' into 'develop'
Fix/auth sequence list

See merge request geovisio/website!121
2024-01-31 09:00:25 +00:00
Jean Andreani
6322a75f42 Fix/auth sequence list 2024-01-31 09:00:25 +00:00
Andreani Jean
c37f814dc3 Release 2.3.1 2024-01-29 17:23:12 +01:00
Jean Andreani
fe68941ea9 Merge branch 'fix/ui-ux-fixs' into 'develop'
fix : UI / UX

See merge request geovisio/website!120
2024-01-29 16:20:50 +00:00
Jean Andreani
e2df50e18f fix : UI / UX 2024-01-29 16:20:50 +00:00
Jean Andreani
5d83e9df13 Merge branch 'feat-fullscreen-viewer' into 'develop'
feat: add fullscreen button to home

Closes #60

See merge request geovisio/website!118
2024-01-29 15:16:38 +00:00
Jean Andreani
d849f95013 feat: add fullscreen button to home 2024-01-29 15:16:38 +00:00
Jean Andreani
f53adeea28 Merge branch 'fix/pagination' into 'develop'
fix: remove condition pagination

See merge request geovisio/website!119
2024-01-24 13:56:08 +00:00
Jean Andreani
7685993710 fix: remove condition pagination 2024-01-24 13:56:08 +00:00
Jean Andreani
47dfd9bddc Merge branch 'feat/bbox-filter-sequence-list' into 'develop'
feat : bbox filter sequence

See merge request geovisio/website!117
2024-01-23 22:41:52 +00:00
Jean Andreani
b72fb7a0df feat : bbox filter sequence 2024-01-23 22:41:52 +00:00
Jean Andreani
136b3d629f Merge branch 'feat/filters-sequence-list' into 'develop'
Feat/filters sequence list

See merge request geovisio/website!116
2024-01-23 16:33:57 +00:00
Jean Andreani
9d82d73ca8 Feat/filters sequence list 2024-01-23 16:33:57 +00:00
Jean Andreani
503443f458 Merge branch 'feat/filters-sequence-list' into 'develop'
Feat/filters sequence list

Closes #57

See merge request geovisio/website!115
2024-01-22 14:08:46 +00:00
Jean Andreani
9b6bdc394b Feat/filters sequence list 2024-01-22 14:08:46 +00:00
Jean Andreani
cec383e424 Merge branch 'feat/link-to-sequence-from-viewer' into 'develop'
feat: add button to viewer to link to seuqence + refacto

Closes #48

See merge request geovisio/website!114
2023-12-19 12:53:06 +00:00
Jean Andreani
7e20788591 feat: add button to viewer to link to seuqence + refacto 2023-12-19 12:53:06 +00:00
Jean Andreani
390343916e Merge branch 'tech/add-more-test-e2e' into 'develop'
add e2e for upload test

See merge request geovisio/website!113
2023-12-18 10:27:31 +00:00
Jean Andreani
c768b714b9 add e2e for upload test 2023-12-18 10:27:31 +00:00
Jean Andreani
bf0bc4d91c Merge branch 'tech/perf-hover-sequence-list' into 'develop'
Tech/perf hover sequence list

See merge request geovisio/website!110
2023-12-12 15:55:46 +00:00
Jean Andreani
5d292b186c Tech/perf hover sequence list 2023-12-12 15:55:46 +00:00
Jean Andreani
93e434ecf9 Merge branch 'fix-e2e' into 'develop'
fix duration gitlab for e2e

See merge request geovisio/website!112
2023-12-12 15:43:21 +00:00
Jean Andreani
721bafbd3e fix duration gitlab for e2e 2023-12-12 15:43:21 +00:00
Jean Andreani
127550a19f Merge branch 'tech-add-test-e2e' into 'develop'
Tech add test e2e

See merge request geovisio/website!89
2023-12-12 15:01:50 +00:00
Jean Andreani
85abc46038 Tech add test e2e 2023-12-12 15:01:50 +00:00
47 changed files with 5697 additions and 2399 deletions

View File

@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Before _0.1.0_, website development was on rolling release, meaning there are no version tags.
## [2.5.0] - 2024-03-11
### Changed
- GeoVisio web viewer updated to [2.5.0](https://gitlab.com/geovisio/web-viewer/-/compare/2.4.0...2.5.0) to reduce tiles size.
## [2.4.1] - 2024-02-01
### Fixed
- Fix geovisio version yarn.lock
## [2.4.0] - 2024-01-31
### Added
- Possibility to edit a sequence title in the sequence page
### Changed
- GeoVisio web viewer updated to [2.4.0](https://gitlab.com/geovisio/web-viewer/-/compare/2.3.1...2.4.0) to manage sequence by user
### Fixed
- Fix filter reset button to include bbox filter
- Fix fullscreen button added by the widget viewer
- Some UI and UX fixes before user tests
## [2.3.1] - 2024-01-29
### Added
- Add the possibility to fullscreen the viewer in the homepage : https://gitlab.com/geovisio/website/-/issues/60
- In the sequence list page add a filter by bbox in the map : https://gitlab.com/geovisio/website/-/issues/61
- In the sequence list page add a filter by date in the list : https://gitlab.com/geovisio/website/-/issues/57
- Add a cancel button to when a sequence is uploading to cancel the upload
### Changed
- GeoVisio web viewer updated to [2.3.1](https://gitlab.com/geovisio/web-viewer/-/compare/2.3.0...2.3.1)
### Fixed
- Some UI and UX fixes before user tests
## [2.3.0] - 2023-12-06
### Added
@@ -147,10 +192,14 @@ Before _0.1.0_, website development was on rolling release, meaning there are no
- Header have now a new entry `Mes photos` when the user is logged to access to the sequence list
- The router guard for logged pages has been changed to not call the api to check the token
[unreleased]: https://gitlab.com/geovisio/website/-/compare/2.3.0...develop
[2.2.1]: https://gitlab.com/geovisio/website/-/compare/2.2.3...2.3.0
[2.2.1]: https://gitlab.com/geovisio/website/-/compare/2.2.2...2.2.3
[2.2.1]: https://gitlab.com/geovisio/website/-/compare/2.2.1...2.2.2
[unreleased]: https://gitlab.com/geovisio/website/-/compare/2.5.0...develop
[2.5.0]: https://gitlab.com/geovisio/website/-/compare/2.4.1...2.5.0
[2.4.1]: https://gitlab.com/geovisio/website/-/compare/2.4.0...2.4.1
[2.4.0]: https://gitlab.com/geovisio/website/-/compare/2.3.1...2.4.0
[2.3.1]: https://gitlab.com/geovisio/website/-/compare/2.3.0...2.3.1
[2.3.0]: https://gitlab.com/geovisio/website/-/compare/2.2.3...2.3.0
[2.2.3]: https://gitlab.com/geovisio/website/-/compare/2.2.2...2.2.3
[2.2.2]: https://gitlab.com/geovisio/website/-/compare/2.2.1...2.2.2
[2.2.1]: https://gitlab.com/geovisio/website/-/compare/2.2.0...2.2.1
[2.2.0]: https://gitlab.com/geovisio/website/-/compare/2.1.3...2.2.0
[2.1.3]: https://gitlab.com/geovisio/website/-/compare/2.1.2...2.1.3

View File

@@ -19,6 +19,7 @@ COPY *.json *.js *.ts *.html ./
# Replace env variables by placeholder for dynamic change on container start
ENV VITE_INSTANCE_NAME=DOCKER_VITE_INSTANCE_NAME
ENV VITE_RASTER_TILE=DOCKER_VITE_RASTER_TILE
ENV VITE_API_URL=DOCKER_VITE_API_URL
ENV VITE_TILES=DOCKER_VITE_TILES
ENV VITE_MAX_ZOOM=DOCKER_VITE_MAX_ZOOM
@@ -50,6 +51,7 @@ ENV VITE_INSTANCE_NAME="GeoVisio/Docker"
ENV VITE_API_URL="https://panoramax.openstreetmap.fr"
ENV VITE_TILES="https://tile-vect.openstreetmap.fr/styles/basic/style.json"
ENV VITE_MAX_ZOOM=""
ENV VITE_RASTER_TILE=""
ENV VITE_ZOOM=""
ENV VITE_CENTER=""

View File

@@ -20,6 +20,10 @@ describe('In the contribute page', () => {
})
interface contributeInterface {
textButtonContribute: string
textButtonDocPython: string
textButtonCli: string
textButtonDocCli: string
textButtonTiles: string
textButtonDoc: string
}
export {}

32
cypress/e2e/upload.cy.ts Normal file
View File

@@ -0,0 +1,32 @@
describe('In the login page', () => {
it('login and go to the upload page to upload images', () => {
cy.visit(`${Cypress.env('api_url')}api/auth/login`)
cy.get('#username').type('Elysee')
cy.get('#password').type('my password')
cy.fixture('upload').then((uploadData: uploadInterface) => {
cy.contains(uploadData.textLinkLogin).click()
cy.visit('/envoyer')
cy.get('.edit-button').click()
cy.get('#upload-title').clear()
cy.get('#upload-title').type(uploadData.textTitle)
cy.contains(uploadData.textButtonTitle).click()
cy.contains(uploadData.textButtonUpload).click()
})
cy.get('.input-file').selectFile(
[
'/src/cypress/fixtures/images/image1.jpg',
'/src/cypress/fixtures/images/image2.jpg',
'/src/cypress/fixtures/images/image3.jpg'
],
{ force: true }
)
})
})
interface uploadInterface {
textLinkLogin: string
textLinkUpload: string
textButtonUpload: string
textTitle: string
textButtonTitle: string
}
export {}

View File

@@ -0,0 +1,7 @@
{
"textLinkLogin": "Sign In",
"textLinkUpload": "Mes photos",
"textTitle": "My title",
"textButtonTitle": "Valider",
"textButtonUpload": "Glissez vos images ici ou cliquez sur"
}

View File

@@ -14,6 +14,7 @@ Available parameters are:
- `VITE_MAX_ZOOM`: the max zoom to use on the map (defaults to 24).
- `VITE_ZOOM`: the zoom to use at the initialization of the map (defaults to 0).
- `VITE_CENTER`: the center position to use at the initialization of the map (defaults to 0).
- `VITE_RASTER_TILE`: the raster tile. Example : `https://maplibre.org/maplibre-style-spec/sources/#raster`.
- Settings for the work environment:
- `NPM_CONFIG_PRODUCTION`: is it production environment (`true`, `false`)
- `YARN_PRODUCTION`: same as below, but if you use Yarn instead of NPM

View File

@@ -4,6 +4,31 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/static/favicon.ico" />
<title>
Panoramax <%- instanceName %> : photo-cartographier les territoires
</title>
<meta
name="description"
content="L'instance Panoramax <%- instanceName %> permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages."
/>
<meta
name="twitter:title"
content="Panoramax <%- instanceName %> : photo-cartographier les territoires"
/>
<meta
name="og:title"
content="Panoramax <%- instanceName %> : photo-cartographier les territoires"
/>
<meta
name="twitter:description"
content="L'instance Panoramax <%- instanceName %> permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages."
/>
<meta
name="og:description"
content="L'instance Panoramax <%- instanceName %> permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages."
/>
<meta name="og:image" content="<%- frontUrl %>/static/meta-img.jpg" />
<meta name="twitter:image" content="<%- frontUrl %>/static/meta-img.jpg" />
</head>
<body>
<div id="app"></div>

View File

@@ -1,6 +1,6 @@
{
"name": "geovisio-website",
"version": "2.3.0",
"version": "2.5.0",
"engines": {
"node": "18.16.1"
},
@@ -25,16 +25,17 @@
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"geovisio": "2.3.0",
"geovisio": "2.5.0",
"moment": "^2.29.4",
"pako": "^2.1.0",
"pinia": "^2.1.4",
"v-calendar": "^3.1.2",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-draggable-resizable-vue3": "^2.3.1-beta.13",
"vue-eslint-parser": "^9.1.0",
"vue-i18n": "9.2.2",
"vue-meta": "^3.0.0-alpha.10",
"vue-matomo": "^4.2.0",
"vue-router": "^4.1.6",
"vue3-cookies": "^1.0.6",
"vue3-smooth-scroll": "^0.8.1"
@@ -69,6 +70,7 @@
"typescript": "~4.7.4",
"vite": "^3.2.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2",
"vitest": "^0.25.3",
"vue-tsc": "^1.0.9"
},

View File

@@ -3,28 +3,14 @@ import { ref, computed } from 'vue'
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import { RouterView } from 'vue-router'
import { useMeta } from 'vue-meta'
import { useI18n } from 'vue-i18n'
import { hasASessionCookieDecoded } from '@/utils/auth'
import { title } from '@/utils/index'
import authConfig from './composables/auth'
const { authConf } = authConfig()
const { t } = useI18n()
let focusMap = ref<string>('focus-map')
useMeta({
title: title(t('general.title')),
og: {
title: title(t('general.meta.title')),
description: title(t('general.meta.description'))
},
twitter: {
title: title(t('general.meta.title')),
description: title(t('general.meta.description'))
}
})
function setFocusMap(value: string) {
focusMap.value = value
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,3 +1,45 @@
export function createLink(href: string, text: string): string {
return `<a href='mailto:signalement.ign@panoramax.fr${href}' target='_blank' title='${text}' class='gvs-btn gvs-widget-bg gvs-btn-large' style='font-size: 1.6em;display: block'><i class="bi bi-exclamation-triangle"></i></a>`
return `<a href='mailto:signalement.ign@panoramax.fr${href}' target='_blank' title='${text}' class='gvs-btn gvs-widget-bg gvs-btn-large' style='font-size: 1.6em;display: block; margin-top: 0.5em;'><i class="bi bi-exclamation-triangle"></i></a>`
}
export function createSequenceLink(href: string, title: string): string {
return `<a href='${href}' title='${title}' class='gvs-btn gvs-widget-bg gvs-btn-large' style='font-size: 1.6em;display: block; position: relative; margin-top: 0.5em;'>
<i class="bi bi-images"></i>
</a>`
}
export function createFullScreenButton(): string {
return `<button type='button' onClick="
const header = document.getElementById('navHeader')
const footer = document.getElementById('navFooter')
const icon = document.getElementById('iconScreen')
const home = document.getElementById('homePage')
if (header) {
const isHiddenHeader = header.classList.contains('hidden')
if (isHiddenHeader) header.classList.remove('hidden')
else header.classList.add('hidden')
}
if (footer) {
const isHiddenHeader = footer.classList.contains('hidden')
if (isHiddenHeader) footer.classList.remove('hidden')
else footer.classList.add('hidden')
}
if (icon) {
const isIconNotFull = icon.classList.contains('bi-fullscreen')
if(isIconNotFull) {
icon.classList.remove('bi-fullscreen')
icon.classList.add('bi-fullscreen-exit')
} else {
icon.classList.remove('bi-fullscreen-exit')
icon.classList.add('bi-fullscreen')
}
}
if (home) {
const isHomeFull = home.classList.contains('full-viewer')
if(isHomeFull) home.classList.remove('full-viewer')
else home.classList.add('full-viewer')
}
"
class='gvs-btn gvs-widget-bg gvs-btn-large'
>
<i id='iconScreen' class="bi bi-fullscreen"></i>
</button>`
}

View File

@@ -55,10 +55,30 @@ defineProps({
visibility: visible;
}
}
.row-reverse {
flex-direction: row-reverse;
justify-content: flex-start;
width: 100%;
padding: 0;
font-size: toRem(1.2);
.icon {
margin-left: toRem(0.5);
margin-right: 0;
}
}
.button--black {
color: var(--white);
background-color: var(--black);
}
.button-border--black {
font-size: toRem(1.4);
background-color: var(--white);
border: toRem(0.1) solid var(--black);
.icon {
font-size: toRem(1.4);
color: var(--black);
}
}
.button--blue {
color: var(--white);
background-color: var(--blue);
@@ -75,28 +95,37 @@ defineProps({
color: var(--white);
}
.button--red {
color: var(--red);
background-color: transparent;
border: toRem(0.1) solid var(--red);
color: var(--white);
background-color: var(--red-pale);
border: toRem(0.1) solid var(--red-pale);
.icon {
margin-right: 0;
font-size: toRem(1.4);
color: var(--red);
color: var(--white);
}
.text {
margin-left: toRem(1);
}
&.background-white {
color: var(--red-pale);
.icon {
color: var(--red-pale);
}
&.disabled {
.icon {
color: var(--grey-pale);
}
}
}
}
.button--white {
color: var(--blue);
background-color: var(--white);
border: toRem(0.1) solid var(--blue);
.icon {
font-size: toRem(1.4);
color: var(--blue);
margin-right: 0;
}
.text {
margin-left: toRem(1);
margin-right: toRem(1);
}
}
.no-text {
@@ -104,7 +133,6 @@ defineProps({
width: toRem(3);
padding: 0;
.icon {
color: var(--black);
font-size: toRem(1.8);
margin-right: 0;
}
@@ -113,9 +141,27 @@ defineProps({
color: var(--white);
margin-right: 0;
}
.no-text-blue-dark .icon {
color: var(--blue-dark);
margin-right: 0;
font-size: toRem(1.4);
}
.no-text-green .icon {
color: var(--blue);
margin-right: 0;
font-size: toRem(1.6);
}
.background-white {
background-color: var(--white);
}
.link--blue {
color: var(--blue);
text-decoration: underline;
.icon {
font-size: toRem(1.4);
color: var(--blue);
}
}
.link--grey {
color: var(--grey-semi-dark);
.icon {
@@ -123,12 +169,19 @@ defineProps({
color: var(--grey-semi-dark);
}
}
.link--red {
color: var(--red);
background-color: var(--white);
.link--black {
color: var(--black);
.icon {
font-size: toRem(1.4);
color: var(--red);
color: var(--black);
}
}
.link--red {
color: var(--red-pale);
text-decoration: underline;
.icon {
font-size: toRem(1.4);
color: var(--red-pale);
}
}
@@ -143,8 +196,10 @@ defineProps({
align-items: center;
border-radius: 50%;
padding: 0;
height: toRem(2.5);
width: toRem(2.5);
height: toRem(4);
width: toRem(4);
background-color: var(--white);
border: toRem(0.1) solid var(--grey-pale);
.icon {
color: var(---black);
font-size: toRem(1.8);
@@ -162,6 +217,7 @@ defineProps({
visibility: hidden;
width: toRem(20);
right: 0;
z-index: 9;
@include text(xss-regular);
}

View File

@@ -1,15 +1,28 @@
<template>
<div class="entry-edit">
<form
v-if="isEditTitle && !isDisabled"
@submit.prevent="isEditTitle = false"
class="edit-form"
>
<div class="wrapper-input">
<Input
:text="text || ''"
:placeholder="$t('pages.upload.edit_placeholder_input')"
@input="changeTextValue"
<div :class="['entry-edit', { 'edit-mode': isEditTitle }]">
<span v-if="isEditTitle && formTitle" class="form-title">{{
formTitle
}}</span>
<div class="entry-form">
<form
v-if="isEditTitle && !isDisabled"
@submit.prevent="isEditTitle = false"
class="edit-form"
>
<div class="wrapper-input">
<Input
id="upload-title"
:text="text || ''"
:placeholder="$t('pages.upload.edit_placeholder_input')"
@input="changeTextValue"
/>
</div>
<Button
id="valid-button"
:text="$t('pages.upload.ok_button')"
type="submit"
look="button--white"
@trigger="validNewName"
/>
<div class="close-button">
<Button
@@ -19,24 +32,17 @@
@trigger="closeEdition"
/>
</div>
</form>
<span v-else class="title">{{ text }}</span>
<div v-if="!isEditTitle" class="edit-button">
<Button
look="no-text-blue-dark"
icon="bi bi-pen"
:tooltip="$t('pages.upload.edit_title_tooltip')"
:disabled="isDisabled"
@trigger="goToEditMode"
/>
</div>
<Button
id="valid-button"
:text="$t('pages.upload.ok_button')"
type="submit"
look="button button--blue"
@trigger="validNewName"
/>
</form>
<span v-else class="title">{{ text }}</span>
<div v-if="!isEditTitle" class="edit-button">
<Button
look="no-text"
icon="bi bi-pen"
:tooltip="$t('pages.upload.edit_title_tooltip')"
:disabled="isDisabled"
@trigger="goToEditMode"
/>
</div>
</div>
</template>
@@ -52,7 +58,8 @@ const emit = defineEmits<{
const props = defineProps({
defaultText: { type: String, default: null },
isLoading: { type: Boolean, default: false },
isLoaded: { type: Boolean, default: false }
isLoaded: { type: Boolean, default: false },
formTitle: { type: String, default: null }
})
let titleToEdit = ref<string | null>(null)
let isEditTitle = ref<boolean>(false)
@@ -85,13 +92,27 @@ const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
<style scoped lang="scss">
.title {
color: var(--blue-dark);
text-align: left;
}
.form-title {
text-align: left;
width: 100%;
@include text(xs-r-regular);
margin-bottom: toRem(0.3);
}
.entry-edit {
display: flex;
flex-direction: column;
position: relative;
}
.entry-form {
display: flex;
align-items: center;
margin-bottom: toRem(2);
width: 100%;
height: toRem(4.7);
}
.edit-mode {
background-color: var(--blue-pale);
padding: toRem(1);
border-radius: toRem(0.4);
}
.wrapper-edit {
display: flex;
@@ -102,13 +123,14 @@ const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
.edit-button {
background-color: var(--grey);
border-radius: 50%;
height: toRem(3.5);
width: toRem(3.5);
height: toRem(2.5);
width: toRem(2.5);
padding: toRem(1);
display: flex;
justify-content: center;
align-items: center;
margin-left: toRem(1.5);
z-index: 2;
}
.wrapper-input {
position: relative;

View File

@@ -1,5 +1,5 @@
<template>
<footer class="footer">
<footer id="navFooter" class="footer">
<ul class="link-list">
<li class="link-item">
<div class="link">
@@ -62,6 +62,9 @@ ul {
padding: toRem(1.5) toRem(3);
border-top: toRem(0.1) solid var(--grey);
}
.hidden {
display: none;
}
.link-list {
display: flex;
justify-content: center;

View File

@@ -1,5 +1,5 @@
<template>
<header class="header">
<header id="navHeader" class="header">
<div class="responsive entry-instance">
<InstanceName />
</div>
@@ -128,7 +128,6 @@ onClickOutside(list, () => closeModal())
function closeModal(): void {
menuIsClosed.value = true
}
function toggleMenu(): void {
menuIsClosed.value = !menuIsClosed.value
}
@@ -161,6 +160,9 @@ const userName = computed((): string => {
background-color: var(--white);
border-bottom: toRem(0.1) solid var(--grey-pale);
}
.hidden {
display: none;
}
.nav {
width: 100%;
display: flex;

View File

@@ -24,10 +24,11 @@ function emitValue(event: Event): void {
<style scoped lang="scss">
.input {
padding: toRem(1);
padding: toRem(0.5) toRem(1);
border-radius: toRem(0.5);
border: toRem(0.1) solid var(--blue-dark);
color: var(--blue-dark);
width: 100%;
@include text(s-r-regular);
}
</style>

View File

@@ -117,6 +117,12 @@ function triggerButton() {
color: var(--grey-semi-dark);
text-decoration: underline;
font-weight: inherit;
font-size: toRem(1.4);
.icon {
color: var(--grey-semi-dark);
font-size: toRem(1.4);
margin-right: toRem(0.5);
}
}
.link--blue-dark {
color: var(--blue-dark);

View File

@@ -13,15 +13,10 @@
<i class="bi bi-x-circle-fill"></i>
</button>
<div class="modal-header">
<h5>{{ $t('pages.upload.modal_error_title') }}</h5>
<h5>{{ title }}</h5>
</div>
<div class="modal-body">
<ul>
<li v-for="item in uploadErrors" class="error-item">
<span>{{ item.name }} - </span>
<span>{{ item.details.error }}</span>
</li>
</ul>
<slot name="body"></slot>
</div>
</div>
</div>
@@ -30,15 +25,13 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { PropType } from 'vue'
import { Modal } from 'bootstrap'
let bsModal = ref()
import type { uploadErrorInterface } from '@/views/interfaces/UploadPicturesView'
defineProps({
uploadErrors: {
type: Array as PropType<uploadErrorInterface[]>,
default: []
title: {
type: String,
default: ''
}
})
@@ -62,12 +55,6 @@ ul {
.modal {
background: rgba(10, 31, 105, 0.6);
}
.error-item {
padding: toRem(1);
&:nth-child(odd) {
background-color: var(--grey);
}
}
.modal-content {
border-radius: toRem(1.5);
}

View File

@@ -3,13 +3,21 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import type { ViewerInterface, MapInterface } from '@/views/interfaces/common'
import { getIgnTiles } from '@/utils/mapAndViewer'
import axios from 'axios'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useSequenceStore } from '@/store/sequence'
import { Viewer, StandaloneMap } from 'geovisio'
import { getIgnTiles } from '@/utils/mapAndViewer'
import { createUrlLink } from '@/utils'
import { createLink } from '@/components-viewer/reportLink'
import {
createLink,
createSequenceLink,
createFullScreenButton
} from '@/components-viewer/reportLink'
import { useI18n } from 'vue-i18n'
import { hasASessionCookieDecoded } from '@/utils/auth'
import type { ViewerInterface, MapInterface } from '@/views/interfaces/common'
const sequenceStore = useSequenceStore()
const { t } = useI18n()
let mapIsLoaded = ref<boolean>(false)
let viewer = ref()
@@ -20,119 +28,170 @@ const props = defineProps({
bbox: { type: Array, default: null },
userId: { type: String, default: '' }
})
const isLogged = computed((): boolean => {
const cookie = hasASessionCookieDecoded()
return !!(cookie && cookie.account)
})
const userName = computed((): string => {
const cookie = hasASessionCookieDecoded()
if (cookie && cookie.account) return cookie.account.name
return ''
})
defineExpose({
viewer
})
onMounted(async () => {
const tiles = import.meta.env.VITE_TILES
async function getSequenceId(imgId: string): Promise<{
sequenceId: string
username: string
}> {
const { data } = await axios.get(`api/search?ids=${imgId}`)
return {
sequenceId: data.features[0].collection,
username: data.features[0].properties['geovisio:producer']
}
}
function createViewerButton(link: HTMLDivElement): void {
link.innerHTML = `<div>${createFullScreenButton()}</div>`
viewer.value.addEventListener(
'picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
const sequenceInformation = await getSequenceId(e.detail.picId)
let href: string
if (isLogged.value && sequenceInformation.username === userName.value) {
href = `${window.location.origin}/sequence/${sequenceInformation.sequenceId}?currentPic=${e.detail.picId}`
link.innerHTML = `<div>
${createFullScreenButton()}
${createSequenceLink(
href,
t('pages.home.sequence_title')
)}
</div>`
sequenceStore.addSequence(e.detail.picId)
} else {
href = t('pages.home.report_mail', {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
link.innerHTML = `<div>
${createFullScreenButton()}
${createLink(
href,
t('pages.home.report_button_text')
)}
</div>`
}
}
)
}
async function setupViewerMap(tiles: string): Promise<void> {
const maxZoom = import.meta.env.VITE_MAX_ZOOM
const zoom = import.meta.env.VITE_ZOOM
const center = import.meta.env.VITE_CENTER
let paramsViewer: ViewerInterface
let paramsMap: MapInterface
try {
if (props.geovisioViewer) {
paramsViewer = { map: { startWide: true } }
if (center && center !== '') {
const centerMap = center.split(',').map((el: string) => parseInt(el))
paramsViewer = {
map: {
...paramsViewer.map,
center: centerMap
}
}
const raster = import.meta.env.VITE_RASTER_TILE
let paramsViewer: ViewerInterface = { map: { startWide: true } }
if (raster && raster !== '') {
paramsViewer = {
map: {
...paramsViewer.map,
raster: JSON.parse(raster)
}
if (zoom && zoom !== '') {
paramsViewer = {
map: {
...paramsViewer.map,
zoom: parseFloat(zoom)
}
}
}
if (maxZoom && maxZoom !== '') {
paramsViewer = {
map: {
...paramsViewer.map,
maxZoom: parseInt(maxZoom)
}
}
}
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsViewer = {
map: {
...paramsViewer.map,
style
}
}
}
if (props.fetchOptions) {
paramsViewer = {
...paramsViewer,
...props.fetchOptions
}
}
const reportLink = document.createElement('div')
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
viewer.value = new Viewer(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsViewer,
widgets: { customWidget: reportLink }
}
)
if (viewer.value && viewer.value.addEventListener) {
viewer.value.addEventListener(
'picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
const href = t('pages.home.report_mail', {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
reportLink.innerHTML = createLink(
href,
t('pages.home.report_button_text')
)
}
)
}
} else {
paramsMap = { minZoom: 7 }
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsMap = {
...paramsMap,
style
}
}
const bbox = [props.bbox[0], props.bbox[1], props.bbox[2], props.bbox[3]]
viewer.value = new StandaloneMap(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsMap,
bounds: bbox,
zoom: 14
}
)
viewer.value.addEventListener('ready', () => {
viewer.value.setFilters({ user: props.userId }, true)
viewer.value.fitBounds(bbox, {
padding: { top: 70, bottom: 70, left: 70, right: 70 },
maxZoom: 14,
speed: 10
})
})
}
mapIsLoaded.value = true
}
if (center && center !== '') {
const centerMap = center.split(',').map((el: string) => parseInt(el))
paramsViewer = {
map: {
...paramsViewer.map,
center: centerMap
}
}
}
if (zoom && zoom.length) {
paramsViewer = {
map: {
...paramsViewer.map,
zoom: parseFloat(zoom)
}
}
}
if (maxZoom && maxZoom.length) {
paramsViewer = {
map: {
...paramsViewer.map,
maxZoom: parseInt(maxZoom)
}
}
}
if (tiles && tiles.length) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsViewer = {
map: {
...paramsViewer.map,
style
}
}
}
if (props.fetchOptions) {
paramsViewer = {
...paramsViewer,
...props.fetchOptions
}
}
const reportLink = document.createElement('div')
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
viewer.value = new Viewer(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsViewer,
widgets: { customWidget: reportLink }
}
)
if (viewer.value && viewer.value.addEventListener) {
createViewerButton(reportLink)
}
mapIsLoaded.value = true
}
async function setupMap(tiles: string): Promise<void> {
let paramsMap: MapInterface
paramsMap = { users: [props.userId], minZoom: 7 }
if (tiles && tiles.length) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsMap = {
...paramsMap,
style
}
}
const bbox = [props.bbox[0], props.bbox[1], props.bbox[2], props.bbox[3]]
viewer.value = new StandaloneMap(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsMap,
bounds: bbox,
zoom: 14
}
)
viewer.value.addEventListener('ready', () => {
viewer.value.fitBounds(bbox, {
padding: { top: 70, bottom: 70, left: 70, right: 70 },
maxZoom: 14,
speed: 10
})
})
mapIsLoaded.value = true
}
onMounted(async (): Promise<void> => {
const tiles = import.meta.env.VITE_TILES
try {
if (props.geovisioViewer) return await setupViewerMap(tiles)
return await setupMap(tiles)
} catch (err) {
mapIsLoaded.value = true
console.log(err)
}
})
onUnmounted(() => {
onUnmounted((): void => {
if (viewer.value && props.geovisioViewer) viewer.value.destroy()
})
</script>

View File

@@ -0,0 +1,166 @@
<template>
<div class="wrapper-calendar">
<div class="inputs-wrapper">
<div class="input-wrapper">
<i class="bi bi-calendar-plus"></i>
<input
:value="
range && range.start
? formatDate(new Date(range.start), 'YYYY-MM-DD')
: null
"
:placeholder="$t('pages.sequences.radio_date_placeholder')"
@input="setStartValue"
class="input"
/>
</div>
<i class="bi bi-arrow-right"></i>
<div class="input-wrapper">
<i class="bi bi-calendar-plus"></i>
<input
:value="
range && range.end
? formatDate(new Date(range.end), 'YYYY-MM-DD')
: null
"
:placeholder="$t('pages.sequences.radio_date_placeholder')"
@input="setEndValue"
class="input"
/>
</div>
</div>
</div>
<v-date-picker
ref="datePicker"
v-model="range"
mode="date"
:masks="{
input: 'YYYY-MM-DD'
}"
is-range
expanded
:max-date="new Date()"
/>
<div class="footer-modal">
<Button
:text="$t('pages.sequences.filter_date_close_button')"
look="button--transparent"
@trigger="$emit('triggerCloseModal')"
/>
<Button
v-if="range && (range.start || range.end)"
:text="$t('pages.sequences.filter_date_reset_button')"
icon="bi bi-trash"
look="button--red"
@trigger="resetCalendar"
/>
</div>
</template>
<script setup lang="ts">
import moment from 'moment'
import { ref, watch } from 'vue'
import type { PropType } from 'vue'
import { formatDate } from '@/utils/dates'
import Button from '@/components/Button.vue'
interface CalendarDateInterface {
start: Date | string | null
end: Date | string | null
type: string
}
const emit = defineEmits(['triggerDate', 'triggerCloseModal'])
const props = defineProps({
type: { type: String, default: '' },
rangeSelected: {
type: Object as PropType<CalendarDateInterface>,
default: { start: null, end: null, type: '' }
}
})
let range = ref<CalendarDateInterface>({
start: props.rangeSelected.start,
end: props.rangeSelected.end,
type: props.rangeSelected.type
})
const datePicker = ref()
function checkValidityDate(dateToValid: string): boolean {
const date = moment(dateToValid, 'YYYY-MM-DD', true)
return date.isValid() && date.format('YYYY-MM-DD') === dateToValid
}
function setStartValue(event: Event): void {
const value = (event.target as HTMLInputElement).value
if (checkValidityDate(value)) {
range.value.start = new Date(value)
const startDate = `${formatDate(new Date(value), 'YYYY-MM-DD')} 12:00 AM`
if (range && range.value.end && range.value.start) {
range.value = {
start: new Date(startDate),
end: range.value.end,
type: props.type
}
datePicker.value.updateValue(range.value)
}
}
}
function setEndValue(event: Event): void {
const value = (event.target as HTMLInputElement).value
if (checkValidityDate(value)) {
range.value.end = new Date(value)
const endDate = `${formatDate(new Date(value), 'YYYY-MM-DD')} 11:59 PM`
if (range && range.value.end && range.value.start) {
range.value = {
end: new Date(endDate),
start: range.value.start,
type: props.type
}
datePicker.value.updateValue(range.value)
}
}
}
function resetCalendar(): void {
range.value = { start: null, end: null, type: '' }
emit('triggerDate', { start: null, end: null, type: props.type })
}
watch(range, (range) => {
if (range && range.start && range.end) {
const startDate = `${formatDate(range.start, 'YYYY-MM-DD')} 12:00 AM`
const endDate = `${formatDate(range.end, 'YYYY-MM-DD')} 11:59 PM`
range.type = props.type
emit('triggerDate', { start: startDate, end: endDate, type: props.type })
}
})
</script>
<style scoped lang="scss">
.wrapper-calendar {
margin-bottom: toRem(2);
}
.inputs-wrapper {
display: flex;
align-items: center;
@include text(xs-r-regular);
}
.input-wrapper {
position: relative;
width: 100%;
}
.footer-modal {
margin-top: toRem(2);
display: flex;
justify-content: space-between;
}
.bi-arrow-right {
margin-right: toRem(1);
margin-left: toRem(1);
}
.bi-calendar-plus {
position: absolute;
left: 5%;
top: 20%;
}
.input {
padding: toRem(0.5) toRem(0.5) toRem(0.5) toRem(2.5);
border-radius: toRem(0.3);
border: toRem(0.1) solid var(--grey-pale);
width: 100%;
}
</style>

View File

@@ -37,7 +37,10 @@
"pages": {
"home": {
"report_mail": "?subject=⚠️ Report on picture {picId}&body=HEllo, %0D%0A%0D%0A Problem on image (keep type of problem reported) : %0D%0A%0D%0A %0D%0A%0D%0A inappropriate content / lack of blurring on an element to be anonymized or blurred for security reasons / overblurring (too much blurring) %0D%0A%0D%0A Link to affected photo : {link} %0D%0A%0D%0A Details of affected elements (especially for blurring problems - what should be blurred or unblurred?) :",
"report_button_text": "Report this picture"
"report_button_text": "Report this picture",
"sequence_title": "See the séquence",
"open_fullscreen": "Fullscreen mode",
"close_fullscreen": "Normal mode"
},
"settings": {
"title": "My tokens",
@@ -47,7 +50,9 @@
"sequence_published": "Published",
"sequence_waiting": "Still processing",
"sequence_hidden": "Hidden",
"sequence_form_title": "Edit the title",
"hide_sequence_tooltip": "Hide this sequences",
"back_button": "Back to my sequence list",
"delete_sequence_tooltip": "Permanently delete this sequence",
"hide_photo_tooltip": "Hide selected pictures",
"delete_photo_tooltip": "Permanently delete selected pictures",
@@ -75,10 +80,24 @@
},
"sequences": {
"title": "My sequences",
"filter_date_upload_title": "Filter by upload date",
"filter_date_title": "Filter by shooting date :",
"radio_date_placeholder": "03/01/2024",
"radio_datetime_placeholder": "03/01/2024 12:00 AM",
"radio_date": "date",
"hide_button": "Hide",
"show_button": "Show",
"delete_button": "Delete",
"filter_date_reset_button": "Reset",
"filter_date_close_button": "Close",
"no_sequence_found": "No sequence found",
"sequence_name": "Name",
"sequence_photos": "Photos",
"sequence_date": "Shot on",
"sequence_creation": "Upload",
"sequence_creation_tooltip": "Filter by uploaded date",
"sequence_date_tooltip": "Filter by shooting date",
"filter_bbox_button": "Search on this area",
"sequence_status": "Status",
"sequence_published": "Published",
"sequence_waiting": "Still processing",
@@ -138,6 +157,8 @@
"text_import": "Upload your jpg files here. Each picture or series of pictures constitutes a \"sequence\". You can then find them in the \"my pictures\" section and choose to hide, show or delete them.",
"subtitle_process": "Upload processing",
"uploading_process": "Upload in progress...",
"uploading_cancel": "Cancel sending photos",
"cancel_message": "⚠️ Please note, the download will be interrupted if you validate and the sequence will be deleted.",
"sequence_title": "Sequence ",
"import": "Uploads",
"upload_pending": "Upload in progress...",

View File

@@ -1,6 +1,7 @@
{
"general": {
"title": "Instance Panoramax",
"title": "DESC Panoramax {instanceName}: photo-cartographier les territoires",
"description": "DESC L'instance Panoramax {instanceName} permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages.",
"meta": {
"title": "Instance Panoramax",
"description": "Panoramax, lalternative libre pour photo-cartographier les territoires"
@@ -37,7 +38,10 @@
"pages": {
"home": {
"report_mail": "?subject=⚠️ Signalement sur l`image {picId}&body=Bonjour, %0D%0A%0D%0A Problème sur l`image (garder le type de problème signalé) : %0D%0A%0D%0A contenu inapproprié / absence de floutage sur un élément à anonymiser ou flouter pour des raisons de sécurité /surfloutage (floutage en trop) %0D%0A%0D%0A Lien vers la photo concernée : {link} %0D%0A%0D%0A Précision sur les éléments concernés (en particulier pour les problèmes de floutage - que faut-il flouter ou déflouter?) :",
"report_button_text": "Signaler la photo"
"report_button_text": "Signaler la photo",
"sequence_title": "Voir la séquence",
"open_fullscreen": "Mode plein écran",
"close_fullscreen": "Mode normal"
},
"settings": {
"title": "Mes Tokens",
@@ -47,7 +51,9 @@
"sequence_published": "Publiée",
"sequence_waiting": "En cours de publication",
"sequence_hidden": "Masquée",
"sequence_form_title": "Modifier le titre",
"hide_sequence_tooltip": "Masque la séquence sur la carte",
"back_button": "Retourner à la liste de mes séquences",
"delete_sequence_tooltip": "Supprime définitivement la séquence",
"hide_photo_tooltip": "Masque les photos sur la carte",
"delete_photo_tooltip": "Supprime définitivement les photos",
@@ -75,10 +81,24 @@
},
"sequences": {
"title": "Mes séquences de photos",
"filter_date_upload_title": "Filtrer par date de versement :",
"filter_date_title": "Filtrer par date de prise de vue :",
"radio_date_placeholder": "2024-01-03",
"radio_date": "date",
"hide_button": "Masquer",
"show_button": "Afficher",
"delete_button": "Supprimer",
"filter_date_reset_button": "Réinitialiser",
"filter_date_close_button": "Fermer",
"no_sequence_found": "Aucune séquence trouvée",
"sequence_name": "Nom",
"sequence_photos": "Photos",
"sequence_date": "Prise de vue",
"sequence_creation": "Versement",
"sequence_creation_tooltip": "Filtre par date de versement",
"sequence_date_tooltip": "Filtre par date de prise de vue",
"reset_filter_button": "Réinitialiser les filtres",
"filter_bbox_button": "Chercher dans cette zone",
"sequence_status": "Statut",
"sequence_published": "Publiée",
"sequence_waiting": "En cours de publication",
@@ -138,6 +158,8 @@
"text_import": "Déposez ici vos fichiers jpg. Chaque image ou série dimages constitue une « séquence ». Vous pourrez ensuite les retrouver dans la section « mes images » et choisir de les masquer, les afficher ou les supprimer.",
"subtitle_process": "Traitements de l'import",
"uploading_process": "Envoi en cours...",
"uploading_cancel": "Annuler l'envoi des photos",
"cancel_message": "⚠️ Attention, le téléchargement sera interrompu si vous validez et la séquence sera supprimée.",
"sequence_title": "Séquence du ",
"import": "Imports",
"upload_pending": "Transfert en cours...",

View File

@@ -37,7 +37,10 @@
"pages": {
"home": {
"report_mail": "?subject=⚠️ Report on picture {picId}&body=Hello, %0D%0A%0D%0A Problem on image (keep type of problem reported) : %0D%0A%0D%0A %0D%0A%0D%0A inappropriate content / lack of blurring on an element to be anonymized or blurred for security reasons / overblurring (too much blurring) %0D%0A%0D%0A Link to affected photo : {link} %0D%0A%0D%0A Details of affected elements (especially for blurring problems - what should be blurred or unblurred?) :",
"report_button_text": "Fénykép jelentése"
"report_button_text": "Fénykép jelentése",
"sequence_title": "lásd a sorrendet",
"open_fullscreen": "Teljes képernyős mód",
"close_fullscreen": "Normál mód"
},
"settings": {
"title": "Saját tokenek",
@@ -47,7 +50,9 @@
"sequence_published": "Közzétéve",
"sequence_waiting": "Feldolgozás alatt",
"sequence_hidden": "Rejtett",
"sequence_form_title": "Szerkessze a címet",
"hide_sequence_tooltip": "A sorozat elrejtése",
"back_button": "Vissza a sorozatlistámhoz",
"delete_sequence_tooltip": "A sorozat végleges törlése",
"hide_photo_tooltip": "A kiválasztott fényképek elrejtése",
"delete_photo_tooltip": "A kiválasztottt fényképek végleges törlése",
@@ -75,10 +80,24 @@
},
"sequences": {
"title": "Saját fényképsorozatok",
"filter_date_upload_title": "Szűrés feltöltés dátuma szerint :",
"filter_date_title": "Szűrés forgatás dátuma szerint :",
"radio_date_placeholder": "03/01/2024",
"radio_datetime_placeholder": "03/01/2024 12:00 AM",
"radio_date": "dátum",
"hide_button": "Elrejt",
"show_button": "Előadás",
"delete_button": "Töröl",
"filter_date_reset_button": "Visszaállítás",
"filter_date_close_button": "Bezárás",
"no_sequence_found": "Nem található felvétel",
"sequence_name": "Név",
"sequence_photos": "Fényképek",
"sequence_date": "Elkészítés ideje",
"sequence_creation": "Feltöltés ideje",
"sequence_creation_tooltip": "Szűrés feltöltés dátuma szerint",
"sequence_date_tooltip": "Szűrés forgatás dátuma szerint",
"filter_bbox_button": "Keresés ezen a területen",
"sequence_status": "Állapot",
"sequence_published": "Közzétéve",
"sequence_waiting": "Feldolgozás alatt",
@@ -138,6 +157,8 @@
"text_import": "Ide töltse fel a JPG-fájljait. Minden kép vagy képsorozat egy „sorozatot” alkot. Megtalálhatja azokat a „saját fényképek” szakaszban, és elrejtheti, megjelenítheti vagy törölheti azokat.",
"subtitle_process": "Feltöltés feldolgozása",
"uploading_process": "Feltöltés folyamatban…",
"uploading_cancel": "Fényképek küldésének megszakítása",
"cancel_message": "⚠️ Felhívjuk figyelmét, hogy a letöltés megszakad, ha érvényesíti, és a sorozat törlődik.",
"sequence_title": "Sorozat ",
"import": "Feltöltések",
"upload_pending": "Feltöltés folyamatban…",

View File

@@ -1,11 +1,13 @@
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import VueMatomo from 'vue-matomo'
import App from './App.vue'
import router from './router'
import axios from 'axios'
import VueAxios from 'vue-axios'
import { createMetaManager } from 'vue-meta'
import { VueDraggableResizable } from 'vue-draggable-resizable-vue3'
import VCalendar from 'v-calendar'
import 'v-calendar/style.css'
import { pinia } from './store'
import fr from './locales/fr.json'
import en from './locales/en.json'
@@ -15,8 +17,17 @@ import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
import 'geovisio/build/index.css'
declare global {
interface Window {
_paq: any[]
}
}
axios.defaults.baseURL = import.meta.env.VITE_API_URL
axios.defaults.withCredentials = true
const matomoHost = import.meta.env.VITE_MATOMO_HOST
const matomoSiteId = import.meta.env.VITE_MATOMO_SITE_ID
const matomoExist = matomoHost && matomoSiteId
const i18n = createI18n({
locale: navigator.language.split('-')[0],
@@ -32,12 +43,20 @@ const i18n = createI18n({
})
const app = createApp(App)
app.use(pinia)
app.use(i18n)
app.use(router)
app.use(VueAxios, axios)
app.provide('axios', app.config.globalProperties.axios)
app.use(createMetaManager())
app.use(VueDraggableResizable)
app.use(VCalendar)
if (matomoExist) {
app.use(VueMatomo, {
host: matomoHost,
siteId: matomoExist
})
}
app.mount('#app')
if (matomoExist) {
window._paq.push(['trackPageView']) // Pour suivre les visites sur vos pages
}

View File

@@ -3,7 +3,8 @@ import { defineStore } from 'pinia'
export const useSequenceStore = defineStore('sequence', {
state: () => ({
toastText: <string>'',
toastLook: <string>''
toastLook: <string>'',
picId: <string>''
}),
actions: {
addToastText(text: string, look: string): void {
@@ -12,6 +13,9 @@ export const useSequenceStore = defineStore('sequence', {
setTimeout(() => {
this.toastText = ''
}, 3000)
},
addSequence(id: string): void {
this.picId = id
}
}
})

View File

@@ -26,17 +26,10 @@ describe('Template', () => {
}
}
})
expect(wrapper.vm.uploadErrors).toEqual([])
expect(wrapper.vm.title).toEqual('')
})
it('should render the props filled', async () => {
document.body.innerHTML = '<div id="bs-modal"></div>'
const uploadErrors = [
{
details: { error: 'my error' },
message: 'my message',
name: 'my name'
}
]
const wrapper = shallowMount(Modal, {
global: {
plugins: [i18n],
@@ -45,13 +38,12 @@ describe('Template', () => {
}
},
props: {
uploadErrors
title: 'My title'
}
})
expect(wrapper.vm.uploadErrors).toEqual(uploadErrors)
expect(wrapper.html()).contains('my name - ')
expect(wrapper.html()).contains('my error')
expect(wrapper.vm.title).toEqual('My title')
expect(wrapper.html()).contains('My title')
})
})
})

View File

@@ -12,7 +12,6 @@ import {
hasASessionCookieDecoded
} from '../../utils/auth'
import { img } from '../../utils/image'
import { title } from '../../utils/index'
import { useCookies } from 'vue3-cookies'
vi.mock('vue3-cookies', () => {
const mockCookies = {
@@ -185,19 +184,6 @@ describe('img', () => {
})
})
describe('title', () => {
it('should return the formated title with instance name', () => {
import.meta.env.VITE_INSTANCE_NAME = 'my instance'
const myTitle = 'my title'
expect(title(myTitle)).toEqual('my title my instance')
})
it('should return the formated title without instance name', () => {
import.meta.env.VITE_INSTANCE_NAME = ''
const myTitle = 'my title'
expect(title(myTitle)).toEqual('my title')
})
})
describe('sortByName', () => {
it('should return the the list sorted by name', () => {
const list1 = [

View File

@@ -81,7 +81,7 @@ describe('Template', () => {
spyPicture.mockReturnValue({ data: {} })
const sequenceTitle = `Séquence du ${formatDate(
new Date(),
'Do MMMM YYYY, hh:mm:ss'
'Do MMMM YYYY, HH:mm:ss'
)}`
const body = new FormData()
body.append('position', '1')

View File

@@ -1,7 +1,7 @@
import moment from 'moment'
import 'moment/dist/locale/fr'
function formatDate(date: Date, formatType: string): string {
function formatDate(date: Date | null | string, formatType: string): string {
const formatDate = moment(date)
return formatDate.locale(window.navigator.language).format(formatType)
}

View File

@@ -1,9 +1,3 @@
export function title(title: string): string {
const instanceName = import.meta.env.VITE_INSTANCE_NAME
if (instanceName) return `${title} ${instanceName}`
return title
}
export function createUrlLink(picId: string): string {
return encodeURIComponent(`${window.location.origin}/#focus=pic&pic=${picId}`)
}

View File

@@ -1,21 +1,29 @@
import axios from 'axios'
async function getIgnTiles(): Promise<object> {
const { data } = await axios.get(
'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json'
)
data.sources.plan_ign.scheme = 'xyz'
data.sources.plan_ign.attribution = 'Données cartographiques : © IGN'
const objIndex = data.layers.findIndex(
(el: any) => el.id === 'toponyme - parcellaire - adresse'
)
data.layers[objIndex].layout = {
...data.layers[objIndex].layout,
'text-field': ['concat', ['get', 'numero'], ['get', 'indice_de_repetition']]
async function getIgnTiles(): Promise<object | string> {
try {
const { data } = await axios.get(
'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json'
)
data.sources.plan_ign.scheme = 'xyz'
data.sources.plan_ign.attribution = 'Données cartographiques : © IGN'
const objIndex = data.layers.findIndex(
(el: { id: string }) => el.id === 'toponyme - parcellaire - adresse'
)
data.layers[objIndex].layout = {
...data.layers[objIndex].layout,
'text-field': [
'concat',
['get', 'numero'],
['get', 'indice_de_repetition']
]
}
// Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS
// Patch num_repetition
return data
} catch (error) {
return 'https://tile-vect.openstreetmap.fr/styles/basic/style.json'
}
// Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS
// Patch num_repetition
return data
}
export { getIgnTiles }

View File

@@ -1,7 +1,7 @@
<template>
<main class="entry-page">
<main id="homePage" class="entry-page">
<section class="entry-section">
<Viewer ref="viewerRef" />
<Viewer ref="viewerRef"> </Viewer>
</section>
</main>
</template>
@@ -9,8 +9,8 @@
<script lang="ts" setup>
import { ref } from 'vue'
import Viewer from '@/components/Viewer.vue'
const viewerRef = ref<unknown>(null)
import type ViewerType from '@/components/Viewer.vue'
const viewerRef = ref<InstanceType<typeof ViewerType>>()
</script>
<style scoped lang="scss">
.entry-page {
@@ -20,10 +20,18 @@ const viewerRef = ref<unknown>(null)
width: 100%;
height: calc(100vh - #{toRem(13.2)});
}
.mobile {
display: none;
}
.focus-pic,
.logged .entry-section {
height: calc(100vh - #{toRem(8)});
}
.full-viewer {
.entry-section {
height: 100vh;
}
}
.gvs-focus-map .entry-report-button {
display: none;
}
@@ -35,5 +43,17 @@ const viewerRef = ref<unknown>(null)
.entry-section {
height: calc(100dvh - #{toRem(18)});
}
.full-viewer {
height: 100vh;
padding-top: 0;
}
}
@media (max-width: toRem(50)) {
.mobile {
display: block;
}
.desktop {
display: none;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<main :class="['entry-page', { 'menu-is-open': menuIsOpen }]">
<div class="button-close">
<Button
look="no-text"
look="no-text-blue-dark"
:icon="menuIsOpen ? 'bi bi-chevron-right' : 'bi bi-chevron-left'"
@trigger="menuIsOpen = !menuIsOpen"
/>
@@ -14,95 +14,107 @@
/>
</section>
<div v-if="sequence && !isLoading" class="menu-right">
<div class="menu-top" ref="collapseMenu">
<div class="header-menu">
<button
data-bs-target="#collapseTarget"
data-bs-toggle="collapse"
class="button-collapse"
@click="toggleMenu"
>
<div ref="collapseMenu">
<div class="back-button">
<Link
icon="bi bi-arrow-left"
:text="$t('pages.sequence.back_button')"
:route="{ name: 'my-sequences' }"
look="link--grey"
/>
</div>
<div class="menu-top">
<div class="header-menu">
<div class="wrapper-title">
<span :class="[sequence.status, 'sequence-status']">{{
sequenceStatus
}}</span>
<h1 class="title desktop">
{{ sequence.title }}
</h1>
<div @click.stop class="entry-edit-text">
<EditText
:default-text="sequence.title"
:is-loading="isLoadingTitle"
:form-title="$t('pages.sequence.sequence_form_title')"
@triggerNewText="setNewSequenceTitle"
/>
</div>
</div>
<i :class="headerPanelIsOpen ? 'bi bi-dash' : 'bi bi-plus'"></i>
</button>
<h1 class="title responsive">
{{ sequence.title }}
</h1>
</div>
<div v-if="isSequenceOwner" class="wrapper-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
:text="
sequence.status === 'ready'
? $t('pages.sequence.button_disable')
: $t('pages.sequence.button_enable')
"
look="button--white background-white"
:icon="
sequence.status === 'ready' ? 'bi bi-eye-slash' : 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection"
/>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
:text="$t('pages.sequence.button_delete')"
look="button--red background-white"
icon="bi bi-trash"
@trigger="deleteCollection"
/>
</div>
<div
:class="[sequence.status, 'collapse py-2 show']"
id="collapseTarget"
>
<div class="block-collapse">
<div class="wrapper-info-top">
<span v-if="sequence.created"
>{{ $t('pages.sequence.created') }}
{{
formatDate(new Date(sequence.created), 'Do MMMM YYYY')
}}</span
>
<span v-if="sequence.duration"
>{{ $t('pages.sequence.duration') }}
{{ sequence.duration }}</span
>
<span v-if="sequence.taken"
>{{ $t('pages.sequence.taken') }}
{{ formatDate(sequence.taken, 'Do MMMM YYYY') }}</span
>
</div>
<div class="wrapper-info-top">
<span v-if="sequence.extent.temporal.interval[0][0]"
>{{ $t('pages.sequence.duration_begin') }}
{{
formatDate(
sequence.extent.temporal.interval[0][0],
'Do MMMM YYYY, HH:mm:ss'
)
}}</span
>
<span v-if="sequence.extent.temporal.interval[0][1]"
>{{ $t('pages.sequence.duration_end') }}
{{
formatDate(
sequence.extent.temporal.interval[0][1],
'Do MMMM YYYY, HH:mm:ss'
)
}}</span
>
<span v-if="sequence.camera"
>{{ $t('pages.sequence.camera') }} {{ sequence.camera }} -
{{ sequence.cameraModel }}</span
>
<button
data-bs-target="#collapseTarget"
data-bs-toggle="collapse"
class="button-collapse"
@click="toggleMenu"
>
<i :class="headerPanelIsOpen ? 'bi bi-dash' : 'bi bi-plus'"></i>
</button>
</div>
<div v-if="isSequenceOwner" class="wrapper-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
:text="
sequence.status === 'ready'
? $t('pages.sequence.button_disable')
: $t('pages.sequence.button_enable')
"
look="button--white background-white"
:icon="
sequence.status === 'ready' ? 'bi bi-eye-slash' : 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection"
/>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
:text="$t('pages.sequence.button_delete')"
look="button--red background-white"
icon="bi bi-trash"
@trigger="deleteCollection"
/>
</div>
<div
:class="[sequence.status, 'collapse py-2 show']"
id="collapseTarget"
>
<div class="block-collapse">
<div class="wrapper-info-top">
<span v-if="sequence.created"
>{{ $t('pages.sequence.created') }}
{{
formatDate(new Date(sequence.created), 'Do MMMM YYYY')
}}</span
>
<span v-if="sequence.duration"
>{{ $t('pages.sequence.duration') }}
{{ sequence.duration }}</span
>
<span v-if="sequence.taken"
>{{ $t('pages.sequence.taken') }}
{{ formatDate(sequence.taken, 'Do MMMM YYYY') }}</span
>
</div>
<div class="wrapper-info-top">
<span v-if="sequence.extent.temporal.interval[0][0]"
>{{ $t('pages.sequence.duration_begin') }}
{{
formatDate(
sequence.extent.temporal.interval[0][0],
'Do MMMM YYYY, HH:mm:ss'
)
}}</span
>
<span v-if="sequence.extent.temporal.interval[0][1]"
>{{ $t('pages.sequence.duration_end') }}
{{
formatDate(
sequence.extent.temporal.interval[0][1],
'Do MMMM YYYY, HH:mm:ss'
)
}}</span
>
<span v-if="sequence.camera"
>{{ $t('pages.sequence.camera') }} {{ sequence.camera }} -
{{ sequence.cameraModel }}</span
>
</div>
</div>
</div>
</div>
@@ -125,7 +137,7 @@
</div>
<div class="action-buttons">
<Button
look="button--white background-white"
look="button--white background-white no-text"
:icon="
picturesToDeleteStatus === 'hidden' ||
imagesSelectedHaveDifferentStatus
@@ -140,7 +152,7 @@
/>
<div class="button-hidde">
<Button
look="button--red background-white"
look="button--red background-white no-text"
icon="bi bi-trash"
:tooltip="$t('pages.sequence.delete_photo_tooltip')"
:disabled="!picturesToDelete.length"
@@ -195,12 +207,15 @@ import { useSequenceStore } from '@/store/sequence'
import { storeToRefs } from 'pinia'
import { useCookies } from 'vue3-cookies'
import Button from '@/components/Button.vue'
import Link from '@/components/Link.vue'
import Toast from '@/components/Toast.vue'
import Pagination from '@/components/Pagination.vue'
import InputCheckbox from '@/components/InputCheckbox.vue'
import Loader from '@/components/Loader.vue'
import ImageItem from '@/components/ImageItem.vue'
import EditText from '@/components/EditText.vue'
import Viewer from '@/components/Viewer.vue'
import type ViewerType from '@/components/Viewer.vue'
import { durationCalc, formatDate } from '@/utils/dates'
import {
deleteACollectionItem,
@@ -242,10 +257,11 @@ let headerPanelIsOpen = ref<boolean>(true)
let isShiftPressed = ref<boolean>(false)
let itemSelected = ref<string>('')
let isLoading = ref<boolean>(false)
let isLoadingTitle = ref<boolean>(false)
const collapseMenu = ref<HTMLDivElement>()
const deleteAll = ref<HTMLDivElement>()
const menuHeight = ref<string>()
const viewerRef = ref()
const viewerRef = ref<InstanceType<typeof ViewerType>>()
onMounted(async () => {
try {
@@ -266,37 +282,62 @@ onMounted(async () => {
)
pictures.value = collectionItems
setHeightValue()
if (itemSelected.value.length || !collectionItemsReady[0]) return
if (
itemSelected.value.length ||
!getCurrentPicId(collectionItemsReady[0].id)
) {
return
}
if (!viewerRef.value) return
viewerRef.value.viewer._api.onceReady().then(() => {
if (!viewerRef.value) return
viewerRef.value.viewer.goToPicture(
collectionItemsReady[0].id,
getCurrentPicId(collectionItemsReady[0].id),
sequence.value?.id
)
})
itemSelected.value = collectionItemsReady[0].id
scrollIntoSelected(collectionItemsReady[0].id, pictures.value)
itemSelected.value = getCurrentPicId(collectionItemsReady[0].id)
if (!pictureExistInList(getCurrentPicId(collectionItemsReady[0].id))) {
await goToTheGoodPage(getCurrentPicId(collectionItemsReady[0].id))
}
scrollIntoSelected(
itemSelected.value,
pictures.value.map((e) => e.id)
)
} catch (err) {
console.log(err)
}
})
watchEffect(async () => {
if (!viewerExist(viewerRef)) return
if (!viewerRef || !viewerRef.value || !viewerRef.value.viewer) return
viewerRef.value.viewer.addEventListener(
'picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
if (itemSelected.value === e.detail.picId) return
if (!pictureExistInList(e.detail.picId)) {
await goToTheGoodPage(e.detail.picId)
if (!pictureExistInList(getCurrentPicId(e.detail.picId))) {
await goToTheGoodPage(getCurrentPicId(e.detail.picId))
}
itemSelected.value = e.detail.picId
scrollIntoSelected(e.detail.picId, pictures.value)
itemSelected.value = getCurrentPicId(e.detail.picId)
scrollIntoSelected(
getCurrentPicId(e.detail.picId),
pictures.value.map((e) => e.id)
)
}
)
})
function viewerExist(viewerRef: any): boolean {
return !!(viewerRef && viewerRef.value && viewerRef.value.viewer)
async function setNewSequenceTitle(value: string | null): Promise<void> {
isLoadingTitle.value = true
if (value && value.length > 0) {
await patchACollection(route.params.id, { title: value })
const fetchCollectionInfo = await fetchCollection(route.params.id)
formatSequenceFetched(fetchCollectionInfo.data)
isLoadingTitle.value = false
}
}
function getCurrentPicId(id: string): string {
const parseParams = new URLSearchParams(window.location.search)
const pict = parseParams.get('currentPic')
return pict && pict.length ? pict : id
}
const isSequenceOwner = computed((): boolean => {
@@ -413,7 +454,7 @@ async function patchCollection(): Promise<void> {
let visible
if (sequence.value?.status === 'ready') visible = 'false'
else visible = 'true'
await patchACollection(route.params.id, visible)
await patchACollection(route.params.id, { visible: visible })
const fetchCollectionInfo = await fetchCollection(route.params.id)
formatSequenceFetched(fetchCollectionInfo.data)
if (visible === 'false') hiddeAllPictures()
@@ -421,7 +462,7 @@ async function patchCollection(): Promise<void> {
const { data } = await fetchCollectionItems(route.params.id, '?limit=100')
pictures.value = data.features
}
viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
isLoading.value = false
}
@@ -435,7 +476,10 @@ async function goToNextPage(value: string): Promise<void> {
selfLink.value = data.links.filter((el) => el.rel === 'self')
paginationLinks.value = formatPaginationItems(data.links)
pictures.value = data.features
scrollIntoSelected(pictures.value[0].id, pictures.value)
scrollIntoSelected(
pictures.value[0].id,
pictures.value.map((e) => e.id)
)
picturesToDelete.value = []
isLoading.value = false
setHeightValue()
@@ -484,16 +528,26 @@ function selectPhotoToDeleteOrPatch(
async function selectImageAndMove(
item: ResponseUserPhotoInterface
): Promise<void> {
const parseParams = new URLSearchParams(window.location.search)
const pict = parseParams.get('currentPic')
if (pict && pict.length) {
await router.push({ name: 'sequence', params: { id: route.params.id } })
}
selectPhotoToDeleteOrPatch(item)
if (
picturesToDelete.value.length < 2 &&
item.properties['geovisio:status'] === 'ready'
) {
const viewerMap = await viewerRef.value.viewer
viewerMap.goToPicture(item.id, sequence.value?.id)
if (viewerRef.value) {
const viewerMap = await viewerRef.value.viewer
viewerMap.goToPicture(item.id, sequence.value?.id)
}
itemSelected.value = item.id
await goToTheGoodPage(item.id)
scrollIntoSelected(item.id, pictures.value)
scrollIntoSelected(
item.id,
pictures.value.map((e) => e.id)
)
}
}
@@ -553,9 +607,14 @@ async function patchOrDeleteCollectionItems(
const { data } = await fetchCollectionItems(route.params.id, '?limit=100')
pictures.value = data.features
isLoading.value = false
viewerRef.value.viewer.reloadVectorTiles()
viewerRef.value.viewer.goToPicture(pictures.value[0].id, route.params.id)
scrollIntoSelected(picturesToDelete.value[0], pictures.value)
if (viewerRef.value) {
viewerRef.value.viewer.reloadVectorTiles()
viewerRef.value.viewer.goToPicture(pictures.value[0].id, route.params.id)
}
scrollIntoSelected(
picturesToDelete.value[0],
pictures.value.map((e) => e.id)
)
picturesToDelete.value = []
sequenceStore.addToastText(t('general.success_text'), 'success')
} catch (e) {
@@ -581,7 +640,12 @@ async function patchOrDeleteCollectionItems(
.menu-right {
width: 50vw;
height: calc(100vh - #{toRem(8)});
overflow: hidden;
overflow: auto;
}
.back-button {
width: fit-content;
margin-left: toRem(2);
margin-top: toRem(2);
}
.wrapper-loader {
display: flex;
@@ -589,8 +653,7 @@ async function patchOrDeleteCollectionItems(
align-items: center;
}
.wrapper-title {
display: flex;
align-items: center;
min-width: 60%;
}
.wrapper-button {
display: flex;
@@ -630,6 +693,7 @@ async function patchOrDeleteCollectionItems(
}
.title {
@include text(h4);
text-align: left;
color: var(--grey-dark);
margin-right: toRem(1);
margin-bottom: 0;
@@ -638,8 +702,9 @@ async function patchOrDeleteCollectionItems(
display: none;
}
.menu-top {
position: relative;
margin: toRem(2) toRem(2) 0;
padding: toRem(1) toRem(2);
padding: toRem(2.5) toRem(2) toRem(1);
border: toRem(0.1) solid var(--grey);
border-radius: toRem(0.5);
background-color: var(--blue-semi);
@@ -647,35 +712,37 @@ async function patchOrDeleteCollectionItems(
.header-menu {
display: flex;
justify-content: space-between;
margin-bottom: toRem(1);
}
.sequence-status {
@include text(xs-r-regular);
border-radius: toRem(3);
padding: toRem(0.5) toRem(1);
margin-right: toRem(1);
position: absolute;
top: 0;
left: 0;
z-index: 1;
@include text(xss-regular);
border-radius: toRem(0.4);
padding: toRem(0.3) toRem(0.8);
color: var(--white);
&.ready {
background-color: var(--orange);
border: toRem(0.1) solid var(--orange);
background-color: var(--green);
border: toRem(0.1) solid var(--green);
}
&.waiting-for-process {
background-color: var(--yellow);
border: toRem(0.1) solid var(--yellow);
}
&.hidden {
background-color: var(--blue-geovisio);
border: toRem(0.1) solid var(--blue-geovisio);
background-color: var(--red-pale);
border: toRem(0.1) solid var(--red-pale);
}
}
.button-collapse {
border: none;
background-color: transparent;
padding: 0;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: toRem(1);
position: absolute;
top: 0;
right: toRem(2);
}
.bi-plus,
.bi-dash {
@@ -743,9 +810,8 @@ async function patchOrDeleteCollectionItems(
justify-content: center;
}
@media (max-width: toRem(102.4)) {
.header-menu {
flex-direction: column;
align-items: flex-start;
.wrapper-title {
width: 90%;
}
.block-collapse {
flex-direction: column;
@@ -794,9 +860,10 @@ async function patchOrDeleteCollectionItems(
margin-bottom: toRem(1);
}
.button-collapse {
width: 100%;
justify-content: space-between;
margin-bottom: 0;
top: toRem(-1);
right: 0;
}
.photo-item {
width: 100%;
@@ -853,6 +920,7 @@ async function patchOrDeleteCollectionItems(
}
.menu-top {
width: 0vw;
padding: toRem(2.5) toRem(1);
}
.button-close {
position: absolute;
@@ -868,9 +936,6 @@ async function patchOrDeleteCollectionItems(
background-color: var(--white);
border: toRem(0.1) solid var(--black);
}
.menu-top {
padding: toRem(1);
}
.menu-is-open {
.menu-right {
z-index: 3;

View File

@@ -24,16 +24,49 @@
: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"
>
<h1 id="sequenceTitle" class="sequences-title">
{{ $t('pages.sequences.title') }}
</h1>
<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]"
@@ -48,21 +81,39 @@
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_date')"
look="link--grey"
icon="bi bi-arrow-down-up"
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
:text="$t('pages.sequences.sequence_creation')"
look="link--grey"
icon="bi bi-arrow-down-up"
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>
@@ -72,10 +123,7 @@
<li
v-if="userSequences.length"
v-for="item in userSequences"
:class="[
'sequence-item',
item.id === seqId ? 'button-item-hover' : ''
]"
:class="['sequence-item', { 'sequence-selected': item.id === seqId }]"
@mouseover="goToSequence(item)"
>
<router-link
@@ -84,7 +132,6 @@
name: 'sequence',
params: { id: item.id }
}"
@mouseover.stop
>
<div class="wrapper-thumb">
<img
@@ -104,7 +151,7 @@
/>
</div>
</div>
<div>
<div class="sequence-title">
<span>
{{ item.title }}
</span>
@@ -120,14 +167,14 @@
{{
formatDate(
item.extent.temporal.interval[0][0],
'Do MMMM YYYY HH:mm:ss'
'Do MMM YYYY HH:mm:ss'
)
}}
</span>
</div>
<div>
<span>
{{ formatDate(item.created, 'Do MMMM YYYY HH:mm:ss') }}
{{ formatDate(item.created, 'Do MMM YYYY HH:mm:ss') }}
</span>
</div>
<div>
@@ -138,8 +185,8 @@
</router-link>
<div class="wrapper-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
look="button--white background-white"
:text="sequenceButtonText(item['geovisio:status'])"
look="link--blue row-reverse"
:icon="
item['geovisio:status'] === 'ready'
? 'bi bi-eye-slash'
@@ -149,13 +196,18 @@
@trigger="patchCollection(item)"
/>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
look="button--red background-white"
: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') }}
@@ -181,6 +233,28 @@
</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>
@@ -196,10 +270,13 @@ import {
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'
@@ -226,23 +303,86 @@ 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<any>(null)
const headerList = ref<any>(null)
const list = ref<any>(null)
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')
const { data } = await axios.get('api/users/me/collection?limit=50')
collectionBbox.value = data.extent.spatial.bbox[0]
userSequences.value = getRelChild(data.links)
userSequences.value = getLinkByRel(data.links, 'child')
}
async function patchCollection(sequence: SequenceLinkInterface): Promise<void> {
@@ -250,9 +390,9 @@ async function patchCollection(sequence: SequenceLinkInterface): Promise<void> {
let visible
if (sequence['geovisio:status'] === 'ready') visible = 'false'
else visible = 'true'
await patchACollection(sequence.id, visible)
await patchACollection(sequence.id, { visible: visible })
await fetchAndFormatSequence()
viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
isLoading.value = false
}
async function deleteCollection(
@@ -262,7 +402,7 @@ async function deleteCollection(
if (confirm(t('pages.sequence.confirm_sequence_dialog'))) {
await deleteACollection(sequence.id)
await fetchAndFormatSequence()
viewerRef.value.viewer.reloadVectorTiles()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
}
isLoading.value = false
}
@@ -271,8 +411,12 @@ function sequenceStatus(status: string): string {
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) {
if (width && collectionBbox.value.length) {
width.value = width
mapWidth.value = width.value.width
listWidth.value = window.innerWidth - width.value.width
@@ -287,21 +431,30 @@ const handleScroll = async () => {
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}`
const { data } = await axios.get(
`api/users/me/collection?limit=50&sortby=${encodeURIComponent(sortBy)}`
)
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === 'self'
)
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getRelChild(data.links)
sortDate.value.sortBy = encodeURIComponent(sortBy)
formatUri()
await updateSequence(uri.value)
isSorted.value = !isSorted.value
scrollToElement()
isLoading.value = false
}
function bboxIsInsideOther(mainBbox: number[], bboxInside: number[]): boolean {
@@ -313,28 +466,34 @@ function bboxIsInsideOther(mainBbox: number[], bboxInside: number[]): boolean {
)
}
function goToSequence(sequence: SequenceLinkInterface): void {
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
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 (bboxIsInsideOther(currentBbox, sequence.extent.spatial.bbox[0])) return
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]
],
zoom: 10,
duration: 0
})
}
function getRelChild(
sequences: SequenceLinkInterface[]
function getLinkByRel(
sequences: SequenceLinkInterface[],
rel: string
): SequenceLinkInterface[] {
return sequences.filter((el: SequenceLinkInterface) => el.rel === 'child')
return sequences.filter((el: SequenceLinkInterface) => el.rel === rel)
}
function scrollToElement(): void {
const elementTarget = document.querySelector('#sequenceTitle')
@@ -342,21 +501,22 @@ function scrollToElement(): void {
}
async function goToNextPage(value: string): Promise<void> {
isLoading.value = true
const { data } = await axios.get(value)
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === 'self'
)
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getRelChild(data.links)
scrollToElement()
isLoading.value = false
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) {
return headerLisPos.value.y != 0 && listPos.value.top < 180
? 'item-head-fixed'
: ''
const classCondition = headerLisPos.value.y != 0 && listPos.value.top < 174
return classCondition ? 'item-head-fixed' : ''
}
return ''
})
@@ -364,31 +524,66 @@ onMounted(async () => {
isLoading.value = true
try {
const { data } = await axios.get('api/users/me/collection?limit=50')
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === 'self'
)
selfLink.value = getLinkByRel(data.links, 'self')
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getRelChild(data.links)
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]
]
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(
'hover',
'select',
(e: { detail: { seqId: string } }) => {
if (seqId.value === e.detail.seqId) return
seqId.value = e.detail.seqId
scrollIntoSelected(e.detail.seqId, userSequences.value)
viewerRef.value.viewer.select(e.detail.seqId)
scrollIntoSelected(
e.detail.seqId,
userSequences.value.map((e) => e.id)
)
if (viewerRef.value) viewerRef.value.viewer.select(e.detail.seqId)
}
)
}
@@ -424,18 +619,49 @@ watchEffect(() => {
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);
margin-bottom: toRem(5);
margin-top: toRem(3);
margin-left: toRem(3);
}
.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;
@@ -450,10 +676,15 @@ watchEffect(() => {
align-items: center;
margin: auto;
background-color: var(--blue-pale);
padding: toRem(2);
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;
@@ -468,18 +699,6 @@ watchEffect(() => {
margin-bottom: toRem(1);
}
}
.button-item-hover {
background-color: var(--blue);
&:nth-child(2n) {
background-color: var(--blue);
}
.button-item {
& > *,
& > :nth-child(2) {
color: var(--white);
}
}
}
.item-head-fixed {
position: fixed;
top: toRem(8);
@@ -513,6 +732,9 @@ watchEffect(() => {
height: toRem(15);
z-index: 1;
}
.sequence-title {
text-decoration: underline;
}
.ready {
color: var(--green);
}
@@ -569,7 +791,10 @@ watchEffect(() => {
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);
}
@@ -579,6 +804,15 @@ watchEffect(() => {
&: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);
@@ -594,6 +828,10 @@ watchEffect(() => {
.no-sequence-text {
margin-bottom: toRem(4);
}
.no-sequence-found {
text-align: center;
padding: toRem(2);
}
.loader {
display: flex;
justify-content: center;
@@ -634,7 +872,20 @@ watchEffect(() => {
width: 100% !important;
}
.sequence-item-head {
display: none;
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;

View File

@@ -9,7 +9,7 @@
<template v-slot:cross>
<Button
icon="bi bi-x-lg"
look="no-text"
look="no-text-blue-dark"
@trigger="informationCardDisplayed = false"
/>
</template>
@@ -39,12 +39,14 @@
<p class="import-text">
{{ $t('pages.upload.description_title_sequence') }}
</p>
<EditText
:default-text="newSequenceTitle || sequenceTitle"
:is-loading="isLoading"
:is-loaded="isLoaded"
@triggerNewText="setNewSequenceTitle"
/>
<div class="wrapper-edit-text">
<EditText
:default-text="newSequenceTitle || sequenceTitle"
:is-loading="isLoading"
:is-loaded="isLoaded"
@triggerNewText="setNewSequenceTitle"
/>
</div>
<form>
<div class="wrapper-form">
<InputUpload
@@ -63,6 +65,13 @@
class="uploading-img"
/>
<span>{{ $t('pages.upload.uploading_process') }}</span>
<div class="cancel-button">
<Button
:text="$t('pages.upload.uploading_cancel')"
look="button--red"
@trigger="cancelUpload"
/>
</div>
<span class="loader-text-warning">{{
$t('pages.upload.leave_message')
}}</span>
@@ -90,7 +99,20 @@
v-if="uploadedSequence && uploadedSequence.picturesOnError"
ref="modal"
:upload-errors="uploadedSequence.picturesOnError"
/>
:title="$t('pages.upload.modal_error_title')"
>
<template v-slot:body>
<ul>
<li
v-for="item in uploadedSequence.picturesOnError"
class="error-item"
>
<span>{{ item.name }} - </span>
<span>{{ item.details.error }}</span>
</li>
</ul>
</template>
</Modal>
</main>
</template>
@@ -114,6 +136,7 @@ import {
} from '@/views/utils/upload/request'
import { sortByName } from '@/views/utils/upload/index'
import authConfig from '../composables/auth'
import { deleteACollection } from '@/views/utils/sequence/request'
const { authConf } = authConfig()
const { t } = useI18n()
@@ -169,7 +192,7 @@ function setNewSequenceTitle(value: string | null): void {
function formatSequenceTitle(): string {
return `${t('pages.upload.sequence_title')}${formatDate(
new Date(),
'Do MMMM YYYY, hh:mm:ss'
'Do MMMM YYYY, HH:mm:ss'
)}`
}
function picturesToUploadSizeText(): void {
@@ -200,6 +223,16 @@ function addPictures(value: FileList): void {
loadPercentage.value = '0%'
uploadPicture()
}
async function cancelUpload(): Promise<void> {
const answer = window.confirm(t('pages.upload.cancel_message'))
if (answer) {
pictures.value = []
isLoading.value = false
if (uploadedSequence.value) {
await deleteACollection(uploadedSequence.value.id)
}
}
}
async function uploadPicture(): Promise<void> {
if (!pictures.value || !pictures.value.length) {
return
@@ -223,13 +256,16 @@ async function uploadPicture(): Promise<void> {
}
let i = 0
for (let el of picturesToUpload) {
if (pictures.value.length === 0) {
break
}
const body = new FormData()
i++
body.append('position', i.toString())
body.append('picture', el)
try {
const pictureUploaded = await createAPictureToASequence(data.id, body)
const pictures = { ...pictureUploaded.data, name: el.name }
const pictures = { ...pictureUploaded, name: el.name }
picturesUploadingSize.value = picturesUploadingSize.value + el.size
uploadedSequence.value.pictures = [
...uploadedSequence.value.pictures,
@@ -267,7 +303,6 @@ h3 {
.entry-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: calc(100vh - #{toRem(13.82)});
background-color: var(--grey);
@@ -300,6 +335,12 @@ h3 {
margin-right: toRem(2);
}
}
.error-item {
padding: toRem(1);
&:nth-child(odd) {
background-color: var(--grey);
}
}
.subtitle {
@include text(h3);
color: var(--blue-dark);
@@ -307,6 +348,9 @@ h3 {
.import-text {
@include text(m-r-regular);
}
.wrapper-edit-text {
margin-bottom: toRem(2);
}
.entry-license {
margin-bottom: toRem(1);
}
@@ -325,10 +369,13 @@ h3 {
.uploading-img {
height: toRem(20);
}
.cancel-button {
margin-top: toRem(1);
}
.loader-text-warning {
text-align: center;
@include text(s-r-regular);
color: var(--orange);
color: var(--red-pale);
margin-top: toRem(1);
width: toRem(31);
}
@@ -340,6 +387,9 @@ h3 {
align-items: center;
}
@media (max-width: toRem(76.8)) {
.section {
display: none;
}
.entry-page {
padding-top: toRem(15);
}

View File

@@ -0,0 +1,5 @@
export interface ExtendedHtmlEl extends HTMLElement {
mozRequestFullScreen?: () => void
webkitRequestFullScreen?: () => void
msRequestFullscreen?: () => void
}

View File

@@ -39,5 +39,5 @@ export interface ResponseUserSequenceInterface extends UserSequenceInterface {
export interface CheckboxInterface {
isChecked: boolean
isIndeterminate: boolean
isIndeterminate?: boolean
}

View File

@@ -1,11 +1,18 @@
export interface MapInterface {
startWide?: boolean
users?: string[]
maxZoom?: number
minZoom?: number
style?: object | string
zoom?: number
center?: number[]
bounds?: number[]
raster?: {
type: string
tiles: string[]
attribution: string
tileSize: number
}
}
export interface ViewerInterface {

View File

@@ -0,0 +1,43 @@
import type { ExtendedHtmlEl } from '@/views/interfaces/HomeView'
interface ExtendedDocument extends Document {
webkitFullscreenElement?: Element
mozFullScreenElement?: Element
msFullscreenElement?: Element
mozCancelFullScreen?: () => void
webkitExitFullscreen?: () => void
msExitFullscreen?: () => void
}
export function toggleFullscreen(element: ExtendedHtmlEl) {
const documentWithExtensions = document as ExtendedDocument
const isInFullScreen =
(documentWithExtensions.fullscreenElement &&
documentWithExtensions.fullscreenElement !== null) ||
(documentWithExtensions.webkitFullscreenElement &&
documentWithExtensions.webkitFullscreenElement !== null) ||
(documentWithExtensions.mozFullScreenElement &&
documentWithExtensions.mozFullScreenElement !== null) ||
(documentWithExtensions.msFullscreenElement &&
documentWithExtensions.msFullscreenElement !== null)
if (!isInFullScreen) {
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
}
} else {
if (documentWithExtensions.exitFullscreen) {
documentWithExtensions.exitFullscreen()
} else if (documentWithExtensions.webkitExitFullscreen) {
documentWithExtensions.webkitExitFullscreen()
} else if (documentWithExtensions.mozCancelFullScreen) {
documentWithExtensions.mozCancelFullScreen()
} else if (documentWithExtensions.msExitFullscreen) {
documentWithExtensions.msExitFullscreen()
}
}
}

View File

@@ -7,9 +7,8 @@ function imageStatus(imageStatus: string, sequenceStatus: string): string {
if (sequenceStatus === 'hidden') return sequenceStatus
return imageStatus
}
//TODO REMOVE ANY
function scrollIntoSelected(id: string, userPhotos: any): void {
const itemPosition = userPhotos.map((el: any) => el.id).indexOf(id)
function scrollIntoSelected(id: string, userPhotos: string[]): void {
const itemPosition = userPhotos.map((el: string) => el).indexOf(id)
const elementTarget = document.querySelector(`#el-list${itemPosition}`)
if (elementTarget) elementTarget.scrollIntoView({ behavior: 'smooth' })
}

View File

@@ -11,9 +11,9 @@ function deleteACollection(collectionId: string | string[]): Promise<unknown> {
function patchACollection(
collectionId: string | string[],
visible: string
fieldObject: object
): Promise<unknown> {
return axios.patch(`api/collections/${collectionId}`, { visible: visible })
return axios.patch(`api/collections/${collectionId}`, fieldObject)
}
function deleteACollectionItem(

View File

@@ -19,12 +19,32 @@ interface SequenceCreatedInterface {
}
}
interface PictureCreatedInterface {
data: {
assets: object
bbox: number[]
collection: string
geometry: object
id: string
links: object[]
properties: object
providers: object[]
stac_extensions: string[]
stac_version: string
type: string
}
}
function createASequence(title: string): Promise<SequenceCreatedInterface> {
return axios.post('api/collections', { title: title })
}
async function createAPictureToASequence(id: string, body: any): Promise<any> {
return await axios.post(`api/collections/${id}/items`, body)
async function createAPictureToASequence(
id: string,
body: FormData
): Promise<PictureCreatedInterface> {
const { data } = await axios.post(`api/collections/${id}/items`, body)
return data
}
export { createASequence, createAPictureToASequence }

BIN
static/meta-img.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

1
v-calendar.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'v-calendar'

View File

@@ -1,33 +1,62 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import eslintPlugin from 'vite-plugin-eslint'
import { createHtmlPlugin } from 'vite-plugin-html'
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: true,
port: 5173,
strictPort: true,
hmr: {
clientPort: 5173,
overlay: false
}
},
base: '/',
plugins: [vue(), eslintPlugin()],
css: {
preprocessorOptions: {
scss: {
additionalData:
'@import "@/assets/font-size.scss"; @import "@/assets/rem-calc.scss";'
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
return defineConfig({
server: {
host: true,
port: 5173,
strictPort: true,
hmr: {
clientPort: 5173,
overlay: false
}
},
base: '/',
plugins: [
vue(),
eslintPlugin(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: 'src/main.ts',
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: '/index.html',
/**
* Data that needs to be injected into the index.html ejs template
*/
inject: {
data: {
instanceName: process.env.VITE_INSTANCE_NAME,
frontUrl: process.env.VITE_FRONT_URL
}
}
})
],
css: {
preprocessorOptions: {
scss: {
additionalData:
'@import "@/assets/font-size.scss"; @import "@/assets/rem-calc.scss";'
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
}
})
})
}

1
vue-matomo.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'vue-matomo'

6030
yarn.lock

File diff suppressed because it is too large Load Diff