32 Commits

Author SHA1 Message Date
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
Andreani Jean
40f3560d94 Release 2.3.0 2023-12-06 12:05:18 +01:00
Jean Andreani
3d65fd9b42 Merge branch 'fix/some-fixs' into 'develop'
feat: add flyTo user sequence list

See merge request geovisio/website!108
2023-12-06 10:32:30 +00:00
Jean Andreani
476699adab feat: add flyTo user sequence list 2023-12-06 10:32:30 +00:00
Jean Andreani
7a49628e0d Merge branch 'fix/docker-nginx-conf' into 'develop'
Fix unrecognized URLs in Nginx in Docker

See merge request geovisio/website!109
2023-12-06 08:02:30 +00:00
Adrien Pavie
52d3f27b6d Fix unrecognized URLs in Nginx in Docker 2023-12-05 16:10:25 +01:00
Jean Andreani
70b252bf8b Merge branch 'feat/pagination-sequence-list' into 'develop'
Feat/pagination sequence list

Closes #45

See merge request geovisio/website!107
2023-11-27 20:32:55 +00:00
Jean Andreani
bc7bd9719d Feat/pagination sequence list 2023-11-27 20:32:54 +00:00
Jean Andreani
ee6736ec5d Merge branch 'feature/gvs-230' into 'develop'
Update GeoVisio to 2.3.0

See merge request geovisio/website!106
2023-11-27 16:52:29 +00:00
Adrien Pavie
36bf95db1e Update GeoVisio to 2.3.0 2023-11-27 16:52:29 +00:00
Jean Andreani
bda308d686 Merge branch 'feat/hu-translation' into 'develop'
feat: add hungarian translation

See merge request geovisio/website!105
2023-11-21 15:40:35 +00:00
Jean Andreani
babee7cb57 feat: add hungarian translation 2023-11-21 15:40:35 +00:00
Jean Andreani
aed3e689f9 Merge branch 'fix/pr-cookie' into 'develop'
fix: remove log + add comments

See merge request geovisio/website!104
2023-11-21 15:11:51 +00:00
Jean Andreani
5e7154c889 fix: remove log + add comments 2023-11-21 15:11:51 +00:00
Jean Andreani
9af517f407 Merge branch 'fix/cookie-bug' into 'develop'
Fix/cookie bug

See merge request geovisio/website!102
2023-11-21 14:55:30 +00:00
Jean Andreani
00fe5433c2 Fix/cookie bug 2023-11-21 14:55:29 +00:00
Jean Andreani
08eec76104 Merge branch 'feat/my-sequences-resize' into 'develop'
Feat/my sequences resize

Closes #51, #47, and #46

See merge request geovisio/website!101
2023-11-15 15:15:31 +00:00
Jean Andreani
316e1880b0 Feat/my sequences resize 2023-11-15 15:15:31 +00:00
Andreani Jean
542eefac03 Release 2.2.3 2023-11-03 16:20:25 +01:00
Jean Andreani
edd5d207a1 Merge branch 'feat/improve-sequence-select' into 'develop'
feat : improve-sequence-select

See merge request geovisio/website!100
2023-11-03 10:47:53 +00:00
Jean Andreani
f99d19a13a feat : improve-sequence-select 2023-11-03 10:47:53 +00:00
Jean Andreani
448cd1af41 Merge branch 'feat/add-i18n-from-browser' into 'develop'
feat : add browser translation in en

See merge request geovisio/website!98
2023-10-25 13:54:17 +00:00
Jean Andreani
81cbe90d01 feat : add browser translation in en 2023-10-25 13:54:17 +00:00
72 changed files with 6041 additions and 1137 deletions

1
.gitignore vendored
View File

@@ -90,6 +90,7 @@ sw.*
*.swp
# Cypress generated screen and videos files
cypress/downloads/*
cypress/screenshot/
cypress/videos/
*.cy.ts.mp4

View File

@@ -8,9 +8,10 @@ variables:
DOCKER_BUILDKIT: 1 # use buildkit for better performance
DOCKER_DRIVER: overlay2 # better docker driver to avoid copying too many files on each run
GITLAB_REGISTRY: registry.gitlab.com # We use docker.io for official images and gitlab's registry to store temporary images
IMAGE_NAME: geovisio/api
IMAGE_NAME: geovisio/website
CI_IMAGE_CACHE: $GITLAB_REGISTRY/$IMAGE_NAME:build_cache
DOCKER_TLS_CERTDIR: ""
DOCKER_TLS_CERTDIR: ''
DOCKER_HOST: tcp://docker:2375
before_script:
## chmod is unfortunately currently mandatory : https://github.com/nodejs/docker-node/issues/661
@@ -22,28 +23,29 @@ cache:
install:
stage: Install
image: node:18.16.0
image: node:18.16.1
script:
- yarn install
- ls node_modules/.bin/cypress
test:unit:
stage: Test
image: node:18.16.0
image: node:18.16.1
script:
- yarn test:unit
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
test:e2e:
stage: Test
image: cypress/browsers:node-18.16.0-chrome-113.0.5672.92-1-ff-113.0-edge-113.0.1774.35-1
image: node:18.16.1-alpine
services:
- docker:dind
script:
- yarn install
- ./node_modules/.bin/cypress install
- echo "VITE_API_URL=https://geovisio-proxy-dev.osc-fr1.scalingo.io/" > .env
- echo "VITE_ENV=dev" >> .env
- PORT=5173 yarn start &
- yarn test:e2e
- apk add --update --no-cache docker-cli docker-cli-compose git
- PROJECT_DIR=$PWD docker compose -f cypress/docker-compose-geovisio.yml -f cypress/docker-compose-gitlab-override.yml run --rm e2e
after_script:
- PROJECT_DIR=$PWD docker compose -f cypress/docker-compose-geovisio.yml -f cypress/docker-compose-gitlab-override.yml logs web || true
- PROJECT_DIR=$PWD docker compose -f cypress/docker-compose-geovisio.yml -f cypress/docker-compose-gitlab-override.yml down || true
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
when: always
@@ -54,7 +56,7 @@ test:e2e:
deploy:
stage: Deploy
image: node:18.16.0
image: node:18.16.1
cache:
paths:
- node_modules
@@ -74,7 +76,7 @@ deploy:develop:
stage: Deploy
image: docker:latest
services:
- docker:dind
- docker:dind
before_script:
# login to the gitlab docker registry to use the cache and to publish
- echo $CI_DEPLOY_PASSWORD | docker login -u $CI_DEPLOY_USER --password-stdin $GITLAB_REGISTRY
@@ -106,7 +108,7 @@ deploy:latest:
stage: Deploy
image: docker:latest
services:
- docker:dind
- docker:dind
before_script:
# login to the gitlab docker registry to use the cache and to publish
- echo $CI_DEPLOY_PASSWORD | docker login -u $CI_DEPLOY_USER --password-stdin $GITLAB_REGISTRY

View File

@@ -7,6 +7,46 @@ 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.3.0] - 2023-12-06
### Added
- Add the possibility to an user to select a sequence in the list using the map : https://gitlab.com/geovisio/website/-/merge_requests/100
- For a selected sequence in the list, if the sequence is not displayed in the map, fly to the sequence on the map : https://gitlab.com/geovisio/website/-/merge_requests/108
- Add the pagination to the sequence with sort with API routes : https://gitlab.com/geovisio/website/-/merge_requests/107
- Add Hungarian translation : https://gitlab.com/geovisio/website/-/merge_requests/105
- Add the possibility to hide/delete a sequence in the sequence list : https://gitlab.com/geovisio/website/-/merge_requests/101
- Add the possibility for the user to change the title of the sequence before upload the pictures : https://gitlab.com/geovisio/website/-/merge_requests/101
- Add the possibility to resize the sequence page by dragging the blocs : https://gitlab.com/geovisio/website/-/merge_requests/101
### Changed
- GeoVisio web viewer updated to [2.3.0](https://gitlab.com/geovisio/web-viewer/-/compare/2.2.1...2.3.0)
### Fixed
- Nginx server in Docker container was not recognizing routes other than `/` on first loading.
- Fix the cookie bug by decoding flask cookie : https://gitlab.com/geovisio/website/-/merge_requests/102
## [2.2.3] - 2023-11-03
### Added
- Add translation based on the browser language (only trad for FR and EN for now)
### Changed
- Page My Sequences, add the possibility to select a sequence in the list with the map :
- the user can only see his sequences on the list
- display the selected sequence in blue in the map
- display a thumbnail the hovered sequence
- hover the sequence selected in the map on the list
- add some test e2e
- maj viewer Geovisio version to 2.2.1
- fix some CSS
## [2.2.2] - 2023-10-16
### Changed
@@ -107,7 +147,9 @@ 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.2.2...develop
[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
[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

View File

@@ -2,7 +2,7 @@
#- Build image
#-
FROM node:18.16.0-alpine AS build
FROM node:18.16.1-alpine AS build
WORKDIR /opt/geovisio

View File

@@ -12,6 +12,6 @@ export default defineConfig({
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:5173'
baseUrl: 'http://localhost:5173/'
}
})

3
cypress.env.json Normal file
View File

@@ -0,0 +1,3 @@
{
"api_url": "http://localhost:5000/"
}

View File

@@ -0,0 +1,79 @@
services:
api:
image: geovisio/api:develop
command: api
restart: always
ports:
- 5000:5000
depends_on:
db:
condition: service_healthy
auth:
condition: service_healthy
environment:
DB_URL: postgres://gvs:gvspwd@db/geovisio
PICTURE_PROCESS_THREADS_LIMIT: 2
PICTURE_PROCESS_DERIVATES_STRATEGY: ON_DEMAND
API_FORCE_AUTH_ON_UPLOAD: 'true'
OAUTH_CLIENT_ID: geovisio
OAUTH_CLIENT_SECRET: what_a_secret
OAUTH_OIDC_URL: http://localhost:8183/realms/geovisio
OAUTH_PROVIDER: oidc
FLASK_SECRET_KEY: a_very_secret_key_never_to_be_used_in_production
healthcheck:
test: python -c "import requests; requests.get('http://localhost:5000/api').raise_for_status()"
interval: 5s
timeout: 5s
retries: 10
extra_hosts:
- 'localhost:host-gateway'
networks:
db: {}
geovisio:
aliases:
- api.localtest.me
db:
image: postgis/postgis:13-3.2
environment:
- POSTGRES_USER=gvs
- POSTGRES_PASSWORD=gvspwd
- POSTGRES_DB=geovisio
healthcheck:
test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
interval: 5s
timeout: 5s
retries: 5
networks:
db: {}
auth:
command: start-dev --import-realm
environment:
GEOVISIO_BASE_URL: http://localhost:5000
GEOVISIO_CLIENT_SECRET: what_a_secret
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: password
KEYCLOAK_FRONTEND_URL: http://localhost:5000/api/auth/login
KC_HTTP_PORT: 8183
ports:
- '8183:8183'
healthcheck:
test: curl --fail http://localhost:8183/realms/geovisio
timeout: 5s
interval: 2s
retries: 20
start_period: 15s
image: quay.io/keycloak/keycloak:20.0.1
volumes:
- ./keycloak-realm.json:/opt/keycloak/data/import/geovisio_realm.json
networks:
geovisio:
aliases:
- keycloak.localtest.me
networks:
db: {}
geovisio: {}

View File

@@ -0,0 +1,55 @@
# Docker-compose used in gitlab-ci to run a container having access to all the other containers
services:
web:
image: node:18.16.1-alpine
volumes:
- $PROJECT_DIR:/src
working_dir: /src
command: >
sh -c "apk add --update --no-cache curl && yarn install && yarn start"
environment:
PORT: 5173
VITE_API_URL: http://api.localtest.me:5000
VITE_ENV: dev
depends_on:
api:
condition: service_healthy
auth:
condition: service_healthy
ports:
- 5173:5173
healthcheck:
test: curl --fail http://0.0.0.0:5173
timeout: 10s
interval: 3s
retries: 20
start_period: 15s
networks:
geovisio:
aliases:
- front.localtest.me
e2e:
image: cypress/included:cypress-12.17.3-node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
volumes:
- $PROJECT_DIR:/src
working_dir: /src
environment:
- CYPRESS_baseUrl=http://front.localtest.me:5173/
- CYPRESS_api_url=http://api.localtest.me:5000/
- LANG=fr
command: sleep 2 && yarn add --dev cypress && ./node_modules/.bin/cypress install && yarn test:e2e
depends_on:
web:
condition: service_healthy
networks:
geovisio: {}
api:
environment:
OAUTH_OIDC_URL: http://keycloak.localtest.me:8183/realms/geovisio
FLASK_SESSION_COOKIE_DOMAIN: localtest.me
auth:
environment:
GEOVISIO_BASE_URL: http://api.localtest.me:5000
KEYCLOAK_FRONTEND_URL: http://api.localtest.me:5000/api/auth/login

View File

@@ -0,0 +1,29 @@
describe('In the contribute page', () => {
it('go to the login page', () => {
cy.visit('/pourquoi-contribuer')
cy.get('.upload-text').scrollIntoView()
cy.fixture('contribute').then((contributeData: contributeInterface) => {
cy.contains(contributeData.textButtonContribute).click()
})
})
it('go to the doc pages', () => {
cy.visit('pourquoi-contribuer')
cy.fixture('contribute').then((contributeData: contributeInterface) => {
cy.get('.upload-text').scrollIntoView()
cy.contains(contributeData.textButtonDocPython).click()
cy.contains(contributeData.textButtonCli).click()
cy.contains(contributeData.textButtonDocCli).click()
cy.contains(contributeData.textButtonTiles).click()
cy.contains(contributeData.textButtonDoc).click()
})
})
})
interface contributeInterface {
textButtonContribute: string
textButtonDocPython: string
textButtonCli: string
textButtonDocCli: string
textButtonTiles: string
textButtonDoc: string
}
export {}

16
cypress/e2e/home.cy.ts Normal file
View File

@@ -0,0 +1,16 @@
describe('In the home page', () => {
it('click on the link in the footer to go to Panoramax.fr', () => {
cy.visit('/')
cy.fixture('home').then((homeData) => {
cy.contains(homeData.textLinkPanoramax).click()
})
})
it('click on the link in the footer to go to Gitlab', () => {
cy.visit('/')
cy.fixture('home').then((homeData) => {
cy.contains(homeData.textLinkGitlab).click()
})
})
})
export {}

View File

@@ -1,7 +1,24 @@
describe('In the login page', () => {
it('type in the form to login', () => {
cy.visit('https://geovisio-proxy-dev.osc-fr1.scalingo.io/api/auth/login')
cy.get('#password').type('coucouc')
cy.visit(`${Cypress.env('api_url')}api/auth/login`)
cy.get('#username').type('Elysee')
cy.get('#password').type('my password')
cy.contains('Sign In').click()
cy.visit('/')
})
it('go to the register form and create an account', () => {
cy.visit(`${Cypress.env('api_url')}api/auth/login`)
cy.fixture('login').then((loginData) => {
cy.contains(loginData.textLinkRegister).click()
cy.get('#firstName').type('Tom')
cy.get('#lastName').type('Tom')
cy.get('#email').type('test@test123.com')
cy.get('#username').type('Elysee12445')
cy.get('#password').type('my password1')
cy.get('#password-confirm').type('my password1')
cy.get('form').submit()
cy.visit('/')
})
})
})

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"types": ["reflect-metadata", "jest", "cypress"],
"module": "commonjs",
"target": "es5",
"sourceMap": true
},
"exclude": ["node_modules"]
}

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,8 @@
{
"textButtonContribute": "Partager des images",
"textButtonDocCli": "Voir la documentation",
"textButtonDoc": "Retrouvez sa documentation ici",
"textButtonDocPython": "de python (au moins la version 3.8)",
"textButtonCli": "L'outil en ligne de commande",
"textButtonTiles": "de tuiles vectorielles"
}

View File

@@ -0,0 +1,5 @@
{
"textLinkContribute": "Pourquoi contribuer ?",
"textLinkPanoramax": "Découvrir Panoramax",
"textLinkGitlab": "Voir le code"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -0,0 +1,4 @@
{
"textLinkRegister": "Register",
"textLinkLogin": "Sign In"
}

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"
}

1987
cypress/keycloak-realm.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,5 +18,10 @@ http {
root /usr/share/nginx/html;
gzip_static on;
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
}
}

View File

@@ -1,8 +1,8 @@
{
"name": "geovisio-website",
"version": "2.2.2",
"version": "2.3.0",
"engines": {
"node": "18.16.0"
"node": "18.16.1"
},
"private": true,
"scripts": {
@@ -25,11 +25,13 @@
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"geovisio": "2.2.0",
"geovisio": "2.3.0",
"moment": "^2.29.4",
"pako": "^2.1.0",
"pinia": "^2.1.4",
"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",
@@ -40,6 +42,7 @@
"devDependencies": {
"@pinia/testing": "^0.1.2",
"@rushstack/eslint-patch": "^1.1.4",
"@types/jest": "^29.5.4",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.46.0",
@@ -50,13 +53,16 @@
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.4",
"@vue/tsconfig": "^0.1.3",
"cypress": "^12.12.0",
"cypress": "^13.1.0",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-vue": "^9.8.0",
"jest": "^29.6.4",
"jsdom": "^20.0.3",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"npm-run-all": "^4.1.5",
"prettier": "2.8.1",
"sass": "^1.62.0",

1
pako.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'pako'

View File

@@ -5,14 +5,11 @@ import Footer from '@/components/Footer.vue'
import { RouterView } from 'vue-router'
import { useMeta } from 'vue-meta'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { useCookies } from 'vue3-cookies'
import { hasASessionCookieDecoded } from '@/utils/auth'
import { title } from '@/utils/index'
import authConfig from './composables/auth'
const { authConf } = authConfig()
const { t } = useI18n()
const { cookies } = useCookies()
const route = useRoute()
let focusMap = ref<string>('focus-map')
@@ -31,7 +28,10 @@ useMeta({
function setFocusMap(value: string) {
focusMap.value = value
}
const getUserId = computed<string>(() => cookies.get('user_id'))
const isLogged = computed((): boolean => {
const cookie = hasASessionCookieDecoded()
return !!(cookie && cookie.account)
})
</script>
<template>
@@ -53,7 +53,7 @@ const getUserId = computed<string>(() => cookies.get('user_id'))
: ''
"
/>
<RouterView @trigger="setFocusMap" :class="{ logged: getUserId }" />
<Footer v-if="!getUserId" />
<RouterView @trigger="setFocusMap" :class="{ logged: isLogged }" />
<Footer v-if="!isLogged" />
</template>
<style scoped></style>

View File

@@ -36,9 +36,9 @@ h5 {
--grey-dark: #3e3e3e;
--blue: #2954e9;
--blue-dark: #0a1f69;
--blue-geovisio: #34495e;
--blue-semi: #d7dffc;
--blue-pale: #f2f5ff;
--blue-geovisio: #34495e;
--beige: #f5f3ec;
--yellow: #fec868;
--orange: #ff6f00;

View File

@@ -1,3 +1,8 @@
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>`
}
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'>
<i class="bi bi-images"></i>
</a>`
}

View File

@@ -35,7 +35,7 @@ defineProps({
}
}
.default {
height: toRem(3.5);
height: toRem(3);
min-width: toRem(3.5);
@include text(s-regular);
display: flex;
@@ -46,7 +46,8 @@ defineProps({
position: relative;
z-index: 1;
border-radius: toRem(1);
padding: toRem(1.3) toRem(2) toRem(1.3);
padding: toRem(1.3);
.icon {
font-size: toRem(2);
}
@@ -103,11 +104,15 @@ defineProps({
width: toRem(3);
padding: 0;
.icon {
color: var(---black);
color: var(--black);
font-size: toRem(1.8);
margin-right: 0;
}
}
.no-text-white .icon {
color: var(--white);
margin-right: 0;
}
.background-white {
background-color: var(--white);
}
@@ -155,7 +160,7 @@ defineProps({
position: absolute;
bottom: -100%;
visibility: hidden;
width: toRem(18);
width: toRem(20);
right: 0;
@include text(xss-regular);
}

137
src/components/EditText.vue Normal file
View File

@@ -0,0 +1,137 @@
<template>
<div class="entry-edit">
<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 class="close-button">
<Button
id="close-button"
look="no-text-white"
icon="bi bi-x"
@trigger="closeEdition"
/>
</div>
</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>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import Button from '@/components/Button.vue'
import Input from '@/components/Input.vue'
const emit = defineEmits<{
(e: 'triggerNewText', value: string | null): void
}>()
const props = defineProps({
defaultText: { type: String, default: null },
isLoading: { type: Boolean, default: false },
isLoaded: { type: Boolean, default: false }
})
let titleToEdit = ref<string | null>(null)
let isEditTitle = ref<boolean>(false)
function changeTextValue(value: string): void {
titleToEdit.value = value
}
function closeEdition(): void {
isEditTitle.value = false
titleToEdit.value = null
}
function validNewName(): void {
emit('triggerNewText', titleToEdit.value)
isEditTitle.value = false
}
function goToEditMode(): void {
isEditTitle.value = true
titleToEdit.value = props.defaultText
}
const text = computed<string | null>(() => {
if (isEditTitle.value) return titleToEdit.value
if (props.defaultText) return props.defaultText
return null
})
const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
</script>
<style scoped lang="scss">
.title {
color: var(--blue-dark);
}
.entry-edit {
display: flex;
align-items: center;
margin-bottom: toRem(2);
width: 100%;
height: toRem(4.7);
}
.wrapper-edit {
display: flex;
align-items: center;
margin-bottom: toRem(1);
width: 100%;
}
.edit-button {
background-color: var(--grey);
border-radius: 50%;
height: toRem(3.5);
width: toRem(3.5);
padding: toRem(1);
display: flex;
justify-content: center;
align-items: center;
margin-left: toRem(1.5);
}
.wrapper-input {
position: relative;
margin-right: toRem(1.5);
width: 100%;
}
.edit-form {
width: 100%;
display: flex;
align-items: center;
}
.close-button {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: toRem(2);
width: toRem(2);
top: toRem(-1);
right: toRem(-1);
background-color: var(--blue-dark);
color: var(--white);
border-radius: 50%;
}
</style>

View File

@@ -89,12 +89,31 @@ ul {
margin-right: toRem(2);
height: toRem(2);
}
@media (max-width: toRem(76.8)) {
.footer {
padding: toRem(1);
height: toRem(6.5);
}
.link-list {
flex-wrap: wrap;
align-items: center;
height: 100%;
}
.logo {
margin-right: toRem(0.5);
height: toRem(1.5);
}
.link-item {
margin-right: toRem(1);
margin-bottom: toRem(1);
}
.link {
height: toRem(1.5);
}
}
@media (max-width: toRem(50)) {
.title {
margin-bottom: 0;
}
.footer {
padding: toRem(2);
}
}
</style>

View File

@@ -107,16 +107,13 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onClickOutside } from '@vueuse/core'
import { useCookies } from 'vue3-cookies'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { getAuthRoute } from '@/utils/auth'
import { getAuthRoute, hasASessionCookieDecoded } from '@/utils/auth'
import Link from '@/components/Link.vue'
import InstanceName from '@/components/InstanceName.vue'
import HeaderOpen from '@/components/header/HeaderOpen.vue'
import AccountButton from '@/components/header/AccountButton.vue'
const { cookies } = useCookies()
const { t } = useI18n()
const route = useRoute()
defineProps({
@@ -128,26 +125,31 @@ let menuIsClosed = ref<boolean>(true)
onClickOutside(list, () => closeModal())
function closeModal() {
function closeModal(): void {
menuIsClosed.value = true
}
function toggleMenu(): void {
menuIsClosed.value = !menuIsClosed.value
}
const isLogged = computed((): boolean => !!cookies.get('user_id'))
const isLogged = computed((): boolean => {
const cookie = hasASessionCookieDecoded()
return !!(cookie && cookie.account)
})
const ariaLabel = computed((): string =>
menuIsClosed.value
? t('general.header.burger_menu_aria_label_open')
: t('general.header.burger_menu_aria_label_closed')
)
const userName = computed((): string =>
cookies!
.get('user_name')
.match(/\b(\w)/g)!
.join('')
.toUpperCase()
)
const userName = computed((): string => {
const cookie = hasASessionCookieDecoded()
if (cookie && cookie.account) {
return cookie.account.name
.match(/\b(\w)/g)!
.join('')
.toUpperCase()
}
return ''
})
</script>
<style lang="scss" scoped>

View File

@@ -14,9 +14,9 @@
<i v-if="status === 'hidden'" class="bi bi-eye-slash icon-hidden"></i>
<img
v-if="href"
loading="lazy"
:src="href"
alt=""
loading="lazy"
class="photo-img"
/>
</div>
@@ -141,7 +141,6 @@ defineProps({
}
.waiting-info {
text-align: center;
width: 100%;
color: var(--black);
width: fit-content;
}

33
src/components/Input.vue Normal file
View File

@@ -0,0 +1,33 @@
<template>
<input
:value="text"
:required="true"
:placeholder="placeholder"
class="input"
type="text"
@input="emitValue"
/>
</template>
<script lang="ts" setup>
const emit = defineEmits<{ (e: 'input', value: string): void }>()
defineProps({
text: { type: String, default: null },
placeholder: { type: String, default: '' }
})
function emitValue(event: Event): void {
const inputValue = (event.target as HTMLInputElement).value
emit('input', inputValue)
}
</script>
<style scoped lang="scss">
.input {
padding: toRem(1);
border-radius: toRem(0.5);
border: toRem(0.1) solid var(--blue-dark);
color: var(--blue-dark);
width: 100%;
}
</style>

View File

@@ -10,8 +10,8 @@
type="file"
multiple
:accept="accept"
class="input-file"
@change="changeFile"
class="input-file"
/>
<i class="bi bi-cloud-upload-fill"></i>
<span v-if="text" class="input-text">

View File

@@ -55,7 +55,7 @@ const props = defineProps({
})
const titleImg = computed<string>(() =>
props.disabled ? t('general.header.contribute_text') : ''
props.disabled ? t('general.header.about_text') : ''
)
function triggerButton() {
if (props.disabled) return
@@ -80,16 +80,6 @@ function triggerButton() {
opacity: 0.8;
}
}
.icon {
color: var(--black);
font-size: toRem(2.4);
margin-right: toRem(1);
}
.logo {
height: inherit;
border-radius: toRem(0.5);
margin-right: toRem(1);
}
.button {
height: toRem(4);
border-radius: toRem(1);
@@ -105,6 +95,16 @@ function triggerButton() {
opacity: 0.5;
}
}
.icon {
color: var(--black);
font-size: toRem(2.4);
margin-right: toRem(1);
}
.logo {
height: inherit;
border-radius: toRem(0.5);
margin-right: toRem(1);
}
.text {
width: 100%;
white-space: break-spaces;
@@ -200,10 +200,6 @@ function triggerButton() {
}
}
@media (max-width: toRem(50)) {
.default {
min-height: toRem(4);
min-width: toRem(4);
}
.icon {
margin-right: toRem(0.5);
}

View File

@@ -19,7 +19,7 @@
<ul>
<li v-for="item in uploadErrors" class="error-item">
<span>{{ item.name }} - </span>
<span>{{ item.message }}</span>
<span>{{ item.details.error }}</span>
</li>
</ul>
</div>

View File

@@ -3,13 +3,18 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import type { ViewerInterface } 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 } 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()
@@ -17,109 +22,155 @@ let viewer = ref()
const props = defineProps({
fetchOptions: { type: Object, default: {} },
geovisioViewer: { type: Boolean, default: true },
bbox: { type: Array, default: null }
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 {
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 = createSequenceLink(
href,
t('pages.home.sequence_title')
)
sequenceStore.addSequence(e.detail.picId)
} else {
href = t('pages.home.report_mail', {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
link.innerHTML = createLink(href, t('pages.home.report_button_text'))
}
}
)
}
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
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
}
}
let paramsViewer: ViewerInterface = { map: { startWide: true } }
if (center && center !== '') {
const centerMap = center.split(',').map((el: string) => parseInt(el))
paramsViewer = {
map: {
...paramsViewer.map,
center: centerMap
}
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 {
let paramsMap = {}
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsMap = {
...paramsMap,
style
}
}
viewer.value = new StandaloneMap(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsMap
}
)
}
mapIsLoaded.value = true
}
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 = { 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.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
}
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

@@ -110,7 +110,7 @@ ul {
width: toRem(25);
top: toRem(8);
right: 0;
z-index: 2;
z-index: 3;
background-color: var(--white);
box-shadow: 0 toRem(0.2) toRem(0.4) rgb(0 0 0 / 10%);
border-radius: toRem(1);
@@ -157,6 +157,7 @@ ul {
}
.logged-link {
padding: toRem(0);
margin-bottom: toRem(2);
}
}
</style>

View File

@@ -121,7 +121,7 @@ defineProps({
margin-bottom: toRem(1);
}
.sequence-button {
width: fit-content;
width: toRem(22);
}
.text-information {
@include text(s-r-regular);
@@ -167,7 +167,6 @@ defineProps({
margin-bottom: toRem(2);
}
}
@media (max-width: toRem(102.4)) {
.loader-title {
flex-direction: column;

View File

@@ -8,7 +8,7 @@
"header": {
"login_text": "Connect",
"register_text": "Register",
"contribute_text": "PouWhyrquoi contribute ?",
"contribute_text": "Why contribute ?",
"my_account": "My account",
"upload_text": "+ Share pictures",
"sequences_text": "My pictures",
@@ -26,7 +26,7 @@
"burger_menu_aria_label_closed": "Hide menu"
},
"footer": {
"panoramax_site": "Dicover Panoramax",
"panoramax_site": "Discover Panoramax",
"information_gitlab": "Show source code",
"gitlab_logo": "Gitlab logo",
"ay11_text": "Accessibility: not compliant"
@@ -37,7 +37,8 @@
"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"
},
"settings": {
"title": "My tokens",
@@ -88,7 +89,7 @@
"sequence_deleted": "The sequence has been deleted"
},
"share_pictures": {
"title": "Why contribute to the Panoramax project ?",
"title": "Why contribute to Panoramax?",
"description": "Contributing to Panoramax means participating in the development of a geo-common, a sovereign, free and reusable digital resource. Each geolocalized photo published on Panoramax can be used by anyone for a variety of purposes, for example by a local authority needing to observe the status of its roads, or by a telecoms operator to prepare an intervention.\n\nEach contributor can send his or her image sequences, modify them and consult them, as well as all the views - 360° or not - contributed by the community. The compulsory blurring of faces and license plates is automated on the platform.",
"alt_img_map": "Illustration of a woman looking at a map with her geolocated smartphone",
"card_photo1": "Places visible from the public highway",
@@ -133,6 +134,8 @@
"import_word": "upload",
"import_type": "JPEG format only",
"subtitle_import": "Picture upload",
"title_sequence": "Séquence title",
"description_title_sequence": "By default, the sequence title will be the date of the day. You can, if you want, edit the title here.",
"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...",
@@ -143,6 +146,9 @@
"no_img_text": "no picture upload so far",
"upload_done": "Sequence upload done",
"sequence_link": "Show this sequence",
"edit_title_tooltip": "Edit the sequence's title",
"edit_placeholder_input": "Edit the sequence's title",
"ok_button": "OK",
"pictures_error": "{count} picture could not be uploaded| {count} pictures could not be uploaded",
"sequence_loading_information": "Once uploaded, the sequence will be processed then published on Panoramax (usually within a couple of minutes).",
"sequence_loaded_information": "The sequences has been uploaded and is under processing. It should be publicly available on Panoramax within a couple of minutes.",

View File

@@ -37,7 +37,8 @@
"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"
},
"settings": {
"title": "Mes Tokens",
@@ -133,9 +134,11 @@
"import_word": "importer",
"import_type": "Format JPEG uniquement",
"subtitle_import": "Dépôt des images",
"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.",
"title_sequence": "Titre de ma séquence",
"description_title_sequence": "Le titre d'une séquence est par défaut la date du jour. Vous pouvez, si vous le souhaitez le modifier ci-dessous.",
"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": "Téléchargement en cours...",
"uploading_process": "Envoi en cours...",
"sequence_title": "Séquence du ",
"import": "Imports",
"upload_pending": "Transfert en cours...",
@@ -143,7 +146,10 @@
"no_img_text": "aucune image chargée actuellement",
"upload_done": "Le chargement de la séquence est terminé",
"sequence_link": "Accéder à cette séquence",
"pictures_error": "{count} image n'a pas pu être chargée| {count} image n'ont pas pu être chargées",
"edit_title_tooltip": "Modifier le titre de la séquence",
"edit_placeholder_input": "Modifier le titre de la séquence",
"ok_button": "Valider",
"pictures_error": "{count} image n'a pas pu être chargée| {count} images n'ont pas pu être chargées",
"sequence_loading_information": "Une fois chargée, la séquence sera en traitement et accessible sur Panoramax dans les prochaines minutes.",
"sequence_loaded_information": "La séquence est chargée et est en cours de traitement. Elle sera accessible sur Panoramax dans quelques minutes.",
"leave_message": "⚠️ Attention, le téléchargement sera interrompu si vous quittez la page avant la fin.",

185
src/locales/hu.json Normal file
View File

@@ -0,0 +1,185 @@
{
"general": {
"title": "Panoramax-példány",
"meta": {
"title": "Panoramax-példány",
"description": "Panoramax, szabad alternatíva a világ fotós feltérképezéséhez"
},
"header": {
"login_text": "Kapcsolódás",
"register_text": "Regisztráció",
"contribute_text": "Miért működjön közre?",
"my_account": "Saját fiók",
"upload_text": "+ Fényképek megosztása",
"sequences_text": "Saját fényképek",
"alt_logo": "A példány logója",
"alt_photos": "Képek ikon",
"alt_information": "Felhasználó ikon",
"alt_settings": "Paraméterek ikon",
"alt_logout": "Kijelentkezés ikon",
"title": "Panoramax",
"beta_text": "Béta verzió",
"logout_text": "Kijelentkezés",
"my_information_text": "Saját adatok",
"my_settings_text": "Saját beállítások",
"burger_menu_aria_label_open": "A menü megjelenítése",
"burger_menu_aria_label_closed": "A menü bezárása"
},
"footer": {
"panoramax_site": "A Panoramax felfedezése",
"information_gitlab": "Forráskód megjelenítése",
"gitlab_logo": "Gitlab logó",
"ay11_text": "Akadálymentesítés: nem felel meg"
},
"error_text": "Hiba történt",
"success_text": "Sikeres frissítés"
},
"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",
"sequence_title": "lásd a sorrendet"
},
"settings": {
"title": "Saját tokenek",
"setting_tooltip": "A token megjelenítése vagy elrejtése"
},
"sequence": {
"sequence_published": "Közzétéve",
"sequence_waiting": "Feldolgozás alatt",
"sequence_hidden": "Rejtett",
"hide_sequence_tooltip": "A sorozat elrejtése",
"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",
"confirm_pictures_dialog": "⚠️ A kiválasztott fényképek véglegesen elvesznek",
"confirm_sequence_dialog": "⚠️ A kiválasztott sorozat véglegesen elvész",
"created": "Feltöltés ideje:",
"taken": "Elkészítés ideje:",
"duration": "Hossz:",
"duration_begin": "Kezdet:",
"duration_end": "Vég:",
"camera": "Kamera:",
"button_delete": "Törlés",
"button_disable": "Elrejtés",
"button_enable": "Megjelenítés",
"picture_selected": "{count} fénykép kiválasztva| {count} fénykép kiválasztva",
"hours": "{count} óra| {count} óra",
"minutes": "{count} perc| {count} perc",
"seconds": "{count} másodperc| {count} másodperc",
"select_text": "Kiválasztás",
"unselect_text": "Kiválasztás törlése",
"select_shift_text": "Több elemet a Shift segítségével választhat ki",
"waiting_process": "A fénykép feldolgozás alatt áll",
"broken": "Fénykép-feldolgozási hiba",
"no_image": "Nincsenek fényképek ebben a sorozatban"
},
"sequences": {
"title": "Saját fényképsorozatok",
"sequence_name": "Név",
"sequence_photos": "Fényképek",
"sequence_date": "Elkészítés ideje",
"sequence_creation": "Feltöltés ideje",
"sequence_status": "Állapot",
"sequence_published": "Közzétéve",
"sequence_waiting": "Feldolgozás alatt",
"sequence_hidden": "Rejtett",
"no_sequences_text": "Még nincsenek közzétett fényképei \uD83D\uDE22",
"button_upload": "Fényképek feltöltése",
"sequence_deleted": "A sorozat törlésre került"
},
"share_pictures": {
"title": "Miért működjön közre a Panoramaxban?",
"description": "A Panoramaxban való közreműködés azt jelenti, hogy részt vesz egy közös, független, szabad és újrafelhasználható digitális erőforrás fejlesztésében. A Panoramaxon található minden georeferált fénykép bárki által használható, számos célra, például egy önkormányzat megtekintheti az utai állapotát, vagy egy távközlési vállalat felkészülhet a beavatkozásaira.\n\nMinden közreműködő beküldheti a saját képsorozatait, módosíthatja és megnézheti azokat, ahogyan ezt teheti a közösség más tagjai által feltöltött képekkel is. Az arcok és rendszámtáblák kötelező kitakarását maga a platform automatizálja.",
"alt_img_map": "Illusztráció egy nőről, aki egy térképet néz a földrajzi helymeghatározással rendelkező okostelefonján",
"card_photo1": "Közútról látható helyek",
"card_photo2": "Közzétett fényképek 360°-os vagy más formátumban",
"card_photo3": "Könnyen újrafelhasználható fényképek",
"card_photo4": "Képek gyors és egyszerű megosztása",
"card_alt_photo1": "Egy épületet tartalmazó kép",
"card_alt_photo2": "Egy 360°-ot megjelenítő kép",
"card_alt_photo3": "Egy kép egy térképről mutatóval",
"card_alt_photo4": "Egy mutatót ábrázoló kép",
"card_description1": "Minden közútról készült kép elfogadható, ha az georeferált és a földről készült.",
"card_description2": "A 360°-os képek nem kötelezők: az okostelefonnal készült képek is megfelelők. Az előfeltételek a dátum, a helyszín és a jpg formátum használata.",
"card_description3": "Minden fénykép fiók nélkül is könnyedén elérhető és felhasználható: a weboldalon vagy egy szabványos API-n keresztül (STAC szabvány).",
"card_description4": "Számos eszköz elérhető a közreműködések lehetővé tételéhez, köztük egy parancssoros és egy webes felület is.",
"upload_subtitle": "Töltse fel egyszerűen a fényképeit",
"upload_illustration_alt": "Online képfeltöltést ábrázoló illusztráció",
"upload_description": "A Panoramax webalkalmazása lehetővé teszi, hogy egy gombnyomással feltöltse az összes fényképét JPG formátumban. Programozói készségek nem szükségesek. Sok kép esetén viszont a parancssoros eszköz használatát javasoljuk.",
"upload_button": "+ Képek feltöltése",
"command_line_subtitle": "Parancssoros eszköz",
"comment_install": "A geovisio parancssoros eszköz telepítése",
"comment_upload": "A képfeltöltési parancs indítása a kiválasztott mappán",
"description_terminal": "<a href='https://gitlab.com/geovisio/cli' target='_blank' style='color:black'>A parancssor</a> lehetővé teszi, hogy nagy számú képet osszon meg. A folyamat egyszerű, és <a target='_blank' href='https://www.python.org/downloads/' style='color:black'>Pythont (3.8-as vagy újabb verzió)</a> igényli.\n\nAz importálás előtt az eszköz bekéri a bejelentkezési adatait. Amint a képek felöltésre kerültek, azok közzététele előtt feldolgozási idő szükséges.",
"terminal_install": "pip install geovisio_cli",
"terminal_text": "geovisio upload --api-url {webcím} <FÉNYKÉPMAPPA>",
"button_copy": "Másolás",
"information_subtitle": "Itt a fényképek mindenki számára elérhetők: ",
"information_text1": "Automatikusan kitakarva, a törvényi előírásoknak megfelelően.",
"information_text2": "A feltöltött képek a következő alatt lesznek közzétéve: {word}",
"information_text3": "Az eredeti formátumban és felbontásban, számos újrafeldolgozáshoz.",
"information_about_title": "Hozzá kell férnie a fényképeihez?",
"information_about_description": "Az API elérhető az összes metaadat és kép letöltéséhez. <a href='{docLink}' target='_blank' style='color:#0a1f69'>\nTudjon meg többet itt.</a>\nAz adatok <a href='{docTiles}' target='_blank' style='color:#0a1f69'>vektorcsempék</a> formájában is megjelennek.",
"doc_subtitle": "Segítségre van szüksége a Panoramaxban történő közreműködéshez?",
"doc_description": "A Panoramax dokumentációja elérhető tőlünk, oktatóanyagokat pedig a geo-commons fórumán találhat.",
"doc_button": "A dokumentáció megtekintése",
"doc_illustration_alt": "Illusztráció egy karakterről dokumentumokkal"
},
"upload": {
"title": "Közreműködés a Panoramax projektben",
"description": "Nagy számú fényképhez a parancssoros eszköz jobban megfelelő.",
"know_more_button": "További tudnivalók",
"input_label": "Húzza ide a képeket, vagy kattintson a",
"import_word": "feltöltésre",
"import_type": "Csak JPEG formátum",
"subtitle_import": "Képfeltöltés",
"title_sequence": "Sorozat címe",
"description_title_sequence": "Alapértelmezés szerint a sorozat cím a nap dátuma lesz. Ha akarja, itt szerkesztheti a címet.",
"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…",
"sequence_title": "Sorozat ",
"import": "Feltöltések",
"upload_pending": "Feltöltés folyamatban…",
"images_count_text": "Feltöltött képek",
"no_img_text": "még nem volt képfeltöltés",
"upload_done": "A sorozat feltöltése elkészült",
"sequence_link": "A sorozat megjelenítése",
"edit_title_tooltip": "A sorozat címének szerkesztése",
"edit_placeholder_input": "A sorozat címének szerkesztése",
"ok_button": "OK",
"pictures_error": "{count} kép feltöltése nem sikerült| {count} kép feltöltése nem sikerült",
"sequence_loading_information": "Amint feltöltötte, a sorozat feldolgozásra, majd közzétételre került a Panoramaxon (általában néhány perc múlva).",
"sequence_loaded_information": "A sorozatok fel lettek töltve, és feldolgozás alatt vannak. Néhány perc múlva nyilvánosan elérhetőnek kellene lenniük a Panoramaxon.",
"leave_message": "⚠️ FIGYELMEZTETÉS, a feltöltés meg fog szakadni, ha a végezte előtt hagyja el a lapot.",
"error_button": "Hibák megjelenítése",
"modal_error_title": "A hibában érintett képek"
},
"ay11": {
"title": "Déclaration daccessibilité",
"date": "Établie le 18 septembre 2023.",
"introduction": "IGN sengage à rendre son service accessible, conformément à larticle 47 de la loi n° 2005-102 du 11 février 2005.\n Cette déclaration daccessibilité sapplique à Panoramax Instance IGN : https://panoramax.ign.fr",
"subtitle_conformity": "État de conformité",
"conformity_text": "Panoramax Instance IGN est non conforme avec le ",
"conformity_text2": "Le site na encore pas été audité.",
"subtitle_conformity2": "Contenus non accessibles",
"subtitle_increase": "Amélioration et contact",
"increase_text": "Si vous narrivez pas à accéder à un contenu ou à un service, vous pouvez\n contacter le responsable de Panoramax Instance IGN pour être orienté vers une alternative accessible ou obtenir le contenu sous une autre forme.",
"phone": "Téléphone : +33 14 398 84 61",
"email_text": "E-mail :",
"email": "signalement.ign@panoramax.fr",
"address": "Adresse : IGN, Saint-Mandé",
"increase_info": "Nous essayons de répondre dans les 5 jours ouvrés.",
"subtitle_to_do": "Voie de recours",
"to_do_text": "Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet un défaut daccessibilité qui vous\n empêche daccéder à un contenu ou à un des services du portail et vous navez pas obtenu de réponse satisfaisante. \n vous pouvez :",
"write_message": "Écrire un message au",
"defenseur_droits": "Défenseur des droits",
"contact": "Contacter",
"contact_text": "le délégué du Défenseur des droits dans votre région",
"send_letter": "Envoyer un courrier par la poste (gratuit, ne pas mettre de\n timbre):\n Défenseur des droits\n Libre réponse 71120 75342 Paris CEDEX 07",
"end": "Cette déclaration daccessibilité a été créé le\n 18 septembre 2023 grâce au",
"generator_betagouv": "Générateur de Déclaration dAccessibilité de BetaGouv"
}
}
}

View File

@@ -4,10 +4,12 @@ import App from './App.vue'
import router from './router'
import axios from 'axios'
import VueAxios from 'vue-axios'
import { globalCookiesConfig } from 'vue3-cookies'
import { createMetaManager } from 'vue-meta'
import { VueDraggableResizable } from 'vue-draggable-resizable-vue3'
import { pinia } from './store'
import fr from './locales/fr.json'
import en from './locales/en.json'
import hu from './locales/hu.json'
import './assets/main.scss'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
@@ -17,18 +19,17 @@ axios.defaults.baseURL = import.meta.env.VITE_API_URL
axios.defaults.withCredentials = true
const i18n = createI18n({
locale: 'fr',
locale: navigator.language.split('-')[0],
fallbackLocale: 'fr',
warnHtmlMessage: false,
globalInjection: true,
legacy: false,
messages: {
fr
fr,
en,
hu
}
})
globalCookiesConfig({
expireTimes: '7d'
})
const app = createApp(App)
@@ -38,4 +39,5 @@ app.use(router)
app.use(VueAxios, axios)
app.provide('axios', app.config.globalProperties.axios)
app.use(createMetaManager())
app.use(VueDraggableResizable)
app.mount('#app')

View File

@@ -1,12 +1,11 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useCookies } from 'vue3-cookies'
import type {
RouteRecordRaw,
NavigationGuardNext,
RouteLocationNormalized
} from 'vue-router'
import axios from 'axios'
import { getAuthRoute } from '@/utils/auth'
import { getAuthRoute, hasASessionCookieDecoded } from '@/utils/auth'
import HomeView from '../views/HomeView.vue'
import MyInformationView from '../views/MyInformationView.vue'
import MySettingsView from '../views/MySettingsView.vue'
@@ -15,7 +14,6 @@ import MySequenceView from '../views/MySequenceView.vue'
import SharePicturesView from '../views/SharePicturesView.vue'
import UploadPicturesView from '../views/UploadPicturesView.vue'
import Ay11View from '../views/Ay11View.vue'
const { cookies } = useCookies()
let routes: Array<RouteRecordRaw> = [
{
path: '/',
@@ -77,8 +75,9 @@ router.beforeResolve(
to.name === 'upload-pictures'
if (siteLoggedRoutes) {
if (!isSiteLogged()) goToLoginPage(to.path)
else return next()
if (!isSiteLogged()) {
goToLoginPage(to.path)
} else return next()
}
if (to.name === 'my-information') {
try {
@@ -95,7 +94,8 @@ router.beforeResolve(
)
function isSiteLogged(): boolean {
return !!cookies.get('user_id')
const cookie = hasASessionCookieDecoded()
return !!(cookie && cookie.account)
}
async function isKeycloakLogout(): Promise<{ status: number }> {

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

@@ -0,0 +1,88 @@
import { test, describe, expect, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import EditText from '../../../components/EditText.vue'
import Button from '../../../components/Button.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(EditText, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.defaultText).toBe(null)
})
})
describe('When the props are filled', () => {
test('should render the component with the defaultText', () => {
const wrapper = shallowMount(EditText, {
global: {
plugins: [i18n]
},
props: {
defaultText: 'My default text'
}
})
expect(wrapper.html()).contains('class="title">My default text</span>')
expect(wrapper.html()).contains('icon="bi bi-pen"')
})
})
describe('When the component is in editable mode', () => {
test('should render the component with the element to edit the title', async () => {
const wrapper = shallowMount(EditText, {
global: {
plugins: [i18n]
}
})
wrapper.vm.isEditTitle = true
wrapper.vm.titleToEdit = 'title to edit'
await wrapper.vm.$nextTick()
expect(wrapper.html()).contains('text="title to edit"')
expect(wrapper.html()).contains('icon="bi bi-x"')
expect(wrapper.html()).contains('text="Valider"')
})
test('should valid the name and emit', async () => {
const wrapper = shallowMount(EditText, {
global: {
plugins: [i18n],
components: {
Button
}
}
})
wrapper.vm.isEditTitle = true
wrapper.vm.titleToEdit = 'title to edit'
await wrapper.vm.$nextTick()
const validationButton = wrapper.findComponent('#valid-button')
await validationButton.vm.$emit('trigger')
expect(wrapper.emitted()).toHaveProperty('triggerNewText')
expect(wrapper.emitted().triggerNewText[0][0]).toEqual(
wrapper.vm.titleToEdit
)
})
test('should close the edit mode', async () => {
const wrapper = shallowMount(EditText, {
global: {
plugins: [i18n],
components: {
Button
}
}
})
wrapper.vm.isEditTitle = true
wrapper.vm.titleToEdit = 'title to edit'
await wrapper.vm.$nextTick()
const closeButton = wrapper.findComponent('#close-button')
await closeButton.vm.$emit('trigger')
await wrapper.vm.$nextTick()
expect(wrapper.vm.isEditTitle).toEqual(false)
expect(wrapper.vm.titleToEdit).toEqual(null)
})
})
})

View File

@@ -0,0 +1,39 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Footer from '../../../components/Footer.vue'
import Link from '../../../components/Link.vue'
import i18n from '../config'
describe('Template', () => {
test('should have the links without the ay11 link', () => {
const wrapper = shallowMount(Footer, {
global: {
stubs: { Link },
plugins: [i18n]
}
})
expect(wrapper.html()).contains('href="https://panoramax.fr/"')
expect(wrapper.html()).contains('logo.jpeg')
expect(wrapper.html()).contains('Découvrir Panoramax')
expect(wrapper.html()).contains('href="https://gitlab.com/geovisio"')
expect(wrapper.html()).contains('gitlab-logo.svg')
expect(wrapper.html()).contains('Voir le code')
})
test('should have the ay11 link', () => {
Object.create(window)
const url = 'http://test.ign.fr'
Object.defineProperty(window, 'location', {
value: {
href: url
},
writable: true // possibility to override
})
const wrapper = shallowMount(Footer, {
global: {
stubs: { Link },
plugins: [i18n]
}
})
expect(wrapper.html()).contains('Accessibilité : non conforme')
})
})

View File

@@ -28,7 +28,7 @@ const i18n = createI18n({
const router = createRouter({
history: createWebHistory(),
routes: []
routes: [{ path: '/', component: { template: '<div></div>' } }]
})
describe('Template', () => {
@@ -96,7 +96,9 @@ describe('Template', () => {
})
describe('When the user is logged', () => {
it('should render the component with good wording keys', async () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue('user_id=id')
vi.spyOn(useCookies().cookies, 'get').mockReturnValue(
'.eJw9y0EKgzAQQNG7zLoDJpmYxMuUySRDra0pooUi3r3SRZcf_tuBRdo2rzDsMBYYgFxRytljkeyQrK0YVT16m6OhUlIihgvM_Kznfa88n9V4W2_XnzcuiqgmDBQMUtYec00WO3XqAndd7OUvXkt7j6Uup5vqRx6NJziOL8SPLNU.ZVy19Q.4DkVxu-LSF11uREkn6YIwHbn_0U'
)
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(Header, {
@@ -116,7 +118,7 @@ describe('Template', () => {
expect(wrapper.html()).contains('general.header.upload_text')
expect(wrapper.html()).contains('data-test="link-logged-upload"')
expect(wrapper.html()).contains('<account-button')
expect(wrapper.html()).contains('username="UI"')
expect(wrapper.html()).contains('username="J"')
})
})
})

View File

@@ -0,0 +1,33 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import InformationCard from '../../../components/InformationCard.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(InformationCard, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.title).toBe(null)
expect(wrapper.vm.look).toBe('')
})
test('should have all the props filled', () => {
const wrapper = shallowMount(InformationCard, {
global: {
plugins: [i18n]
},
props: {
text: 'my text',
title: 'my title',
look: 'my-look'
}
})
expect(wrapper.html()).contains('my title</h3>')
expect(wrapper.html()).contains('my text</p>')
expect(wrapper.html()).contains('class="information-block my-look"')
})
})
})

View File

@@ -0,0 +1,43 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Input from '../../../components/Input.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(Input, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.placeholder).toBe('')
})
})
describe('When the props are filled', () => {
test('should render the button with the placeholder', () => {
const wrapper = shallowMount(Input, {
global: {
plugins: [i18n]
},
props: {
placeholder: 'My placeholder'
}
})
expect(wrapper.html()).contains('placeholder="My placeholder"')
})
test('should emit an input event with the prop value', async () => {
const wrapper = shallowMount(Input, {
global: {
plugins: [i18n]
},
props: {
text: 'My text'
}
})
await wrapper.trigger('input')
expect(wrapper.emitted()).toHaveProperty('input')
expect(wrapper.emitted().input[0][0]).toEqual(wrapper.vm.text)
})
})
})

View File

@@ -0,0 +1,80 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import InputUpload from '../../../components/InputUpload.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(InputUpload, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.textPictureType).toBe(null)
expect(wrapper.vm.textSecondPart).toBe(null)
expect(wrapper.vm.accept).toBe('')
})
test('should have all the props filled', () => {
const wrapper = shallowMount(InputUpload, {
global: {
plugins: [i18n]
},
props: {
text: 'my text',
textPictureType: 'my textPictureType',
textSecondPart: 'my textSecondPart',
accept: 'accept'
}
})
expect(wrapper.html()).contains('accept="accept"')
expect(wrapper.html()).contains('my text')
expect(wrapper.html()).contains('my textSecondPart')
expect(wrapper.html()).contains('my textPictureType')
})
})
describe('When the input is dragover', () => {
test('should set the component dragging state', async () => {
const wrapper = shallowMount(InputUpload, {
global: {
plugins: [i18n]
}
})
await wrapper.find('label').trigger('dragover')
expect(wrapper.vm.isDragging).toBe(true)
expect(wrapper.html()).contains('class="file-upload dragging"')
})
})
describe('When the input is dragleave', () => {
test('should set the component dragging state', async () => {
const wrapper = shallowMount(InputUpload, {
global: {
plugins: [i18n]
}
})
await wrapper.find('label').trigger('dragover')
await wrapper.find('label').trigger('dragleave')
expect(wrapper.vm.isDragging).toBe(false)
expect(wrapper.html()).contains('class="file-upload"')
})
})
describe('When the input is dropped', () => {
test('should emit with value', async () => {
const wrapper = shallowMount(InputUpload, {
global: {
plugins: [i18n]
}
})
const dataTransfer = {
files: [{ type: 'image/jpeg' }]
}
const label = await wrapper.find('label')
await label.trigger('drop', { dataTransfer })
expect(wrapper.emitted()).toHaveProperty('trigger')
expect(wrapper.emitted().trigger[0][0]).toEqual(dataTransfer.files)
})
})
// TODO TEST -> When the input is changed should emit with value
})

View File

@@ -1,6 +1,6 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import BetaText from '../../../components/InstanceName.vue'
import InstanceName from '../../../components/InstanceName.vue'
import { createI18n } from 'vue-i18n'
import fr from '../../../locales/fr.json'
@@ -16,7 +16,7 @@ const i18n = createI18n({
describe('Template', () => {
it('should render the component with good wording keys', async () => {
const wrapper = shallowMount(BetaText, {
const wrapper = shallowMount(InstanceName, {
global: {
plugins: [i18n],
mocks: {

View File

@@ -17,7 +17,7 @@ const i18n = createI18n({
})
const router = createRouter({
history: createWebHistory(),
routes: []
routes: [{ path: '/', component: { template: '<div></div>' } }]
})
const stubs = {
'router-link': {

View File

@@ -0,0 +1,57 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Modal from '../../../components/Modal.vue'
import { createI18n } from 'vue-i18n'
import fr from '../../../locales/fr.json'
const i18n = createI18n({
locale: 'fr',
fallbackLocale: 'fr',
globalInjection: true,
legacy: false,
messages: {
fr
}
})
describe('Template', () => {
describe('Props', () => {
it('should render the default props', async () => {
document.body.innerHTML = '<div id="bs-modal"></div>'
const wrapper = shallowMount(Modal, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.vm.uploadErrors).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],
mocks: {
$t: (msg) => msg
}
},
props: {
uploadErrors
}
})
expect(wrapper.vm.uploadErrors).toEqual(uploadErrors)
expect(wrapper.html()).contains('my name - ')
expect(wrapper.html()).contains('my error')
})
})
})

View File

@@ -7,7 +7,7 @@ vi.mock('vue-router')
const router = createRouter({
history: createWebHistory(),
routes: []
routes: [{ path: '/', component: { template: '<div></div>' } }]
})
describe('Template', () => {
describe('Props', () => {

View File

@@ -0,0 +1,36 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Card from '../../../../components/share-pictures/Card.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(Card, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.title).toBe(null)
expect(wrapper.vm.description).toBe(null)
expect(wrapper.vm.imgSrc).toBe(null)
expect(wrapper.vm.imgAlt).toBe(null)
})
test('should have all the props filled', () => {
const wrapper = shallowMount(Card, {
global: {
plugins: [i18n]
},
props: {
title: 'my title',
description: 'my description',
imgSrc: 'my-imgSrc.jpg',
imgAlt: 'my imgAlt'
}
})
expect(wrapper.html()).contains('my title</h2>')
expect(wrapper.html()).contains('my description</p>')
expect(wrapper.html()).contains('/my-imgSrc.jpg')
expect(wrapper.html()).contains('alt="my imgAlt"')
})
})
})

View File

@@ -1,4 +1,4 @@
import { it, describe, expect } from 'vitest'
import { it, describe, expect, vi, beforeEach } from 'vitest'
import {
imageStatus,
photoToDeleteOrPatchSelected,
@@ -6,10 +6,26 @@ import {
formatPaginationItems
} from '../../views/utils/sequence/index'
import { sortByName } from '../../views/utils/upload/index'
import { getAuthRoute } from '../../utils/auth'
import {
getAuthRoute,
decodingFlaskCookie,
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 = {
get: vi.fn(),
remove: vi.fn(),
keys: vi.fn()
}
return {
useCookies: () => ({
cookies: mockCookies
})
}
})
describe('imageStatus', () => {
it('should render the "status" value', () => {
const sequenceStatus = 'hidden'
@@ -126,6 +142,42 @@ describe('getAuthRoute', () => {
})
})
describe('decodingFlaskCookie', () => {
it('should return a decoded cookie', () => {
const cookie =
'.eJw9y0EKgzAQQNG7zLoDJpmYxMuUySRDra0pooUi3r3SRZcf_tuBRdo2rzDsMBYYgFxRytljkeyQrK0YVT16m6OhUlIihgvM_Kznfa88n9V4W2_XnzcuiqgmDBQMUtYec00WO3XqAndd7OUvXkt7j6Uup5vqRx6NJziOL8SPLNU.ZVy19Q.4DkVxu-LSF11uREkn6YIwHbn_0U'
expect(decodingFlaskCookie(cookie)).toEqual(
JSON.stringify({
account: {
id: '43df4bb5-dcb3-422e-8ff5-52b814dd994a',
name: 'jean',
oauth_id: '138ccff9-7471-4bf6-be92-0f3f37a0086c',
oauth_provider: 'keycloak'
}
})
)
})
})
describe('hasASessionCookieDecoded', () => {
it('should return a cookie parsed', () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue(
'.eJw9y0EKgzAQQNG7zLoDJpmYxMuUySRDra0pooUi3r3SRZcf_tuBRdo2rzDsMBYYgFxRytljkeyQrK0YVT16m6OhUlIihgvM_Kznfa88n9V4W2_XnzcuiqgmDBQMUtYec00WO3XqAndd7OUvXkt7j6Uup5vqRx6NJziOL8SPLNU.ZVy19Q.4DkVxu-LSF11uREkn6YIwHbn_0U'
)
expect(hasASessionCookieDecoded()).toEqual({
account: {
id: '43df4bb5-dcb3-422e-8ff5-52b814dd994a',
name: 'jean',
oauth_id: '138ccff9-7471-4bf6-be92-0f3f37a0086c',
oauth_provider: 'keycloak'
}
})
})
it('should return null', () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue(null)
expect(hasASessionCookieDecoded()).toBe(null)
})
})
describe('img', () => {
it('should render the formated img path', () => {
const name = 'my-img'

View File

@@ -6,7 +6,7 @@ import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: []
routes: [{ path: '/', component: { template: '<div></div>' } }]
})
describe('Template', () => {
it('should render the view with the button link', async () => {

View File

@@ -7,6 +7,7 @@ import * as createAPictureToASequence from '@/views/utils/upload/request'
import * as createASequence from '@/views/utils/upload/request'
import { formatDate } from '../../../utils/dates'
import * as sortByName from '../../../views/utils/upload/index'
describe('Template', () => {
it('should render the view with the input upload and the good wordings', () => {
const wrapper = shallowMount(UploadPicturesView, {

View File

@@ -1,3 +1,7 @@
import pako from 'pako'
import { useCookies } from 'vue3-cookies'
const { cookies } = useCookies()
function getAuthRoute(authRoute: string, nextRoute: string): string {
const next = `${location.protocol}//${location.host}${nextRoute}`
return `${
@@ -5,4 +9,25 @@ function getAuthRoute(authRoute: string, nextRoute: string): string {
}api/${authRoute}?next_url=${encodeURIComponent(`${next}`)}`
}
export { getAuthRoute }
// This function to decode the flask cookie and have the user information like username
function decodingFlaskCookie(cookie: string): string {
const cookieFormatted = cookie
.split('.')[1]
.replace(/_/g, '/')
.replace(/-/g, '+')
const binaryString = atob(cookieFormatted)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return pako.inflate(bytes.buffer, { to: 'string' })
}
function hasASessionCookieDecoded(): { account: { name: string } } | null {
if (cookies.get('session')) {
return JSON.parse(decodingFlaskCookie(cookies.get('session')))
}
return null
}
export { getAuthRoute, hasASessionCookieDecoded, decodingFlaskCookie }

View File

@@ -7,7 +7,7 @@ async function getIgnTiles(): Promise<object> {
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'
(el: { id: string }) => el.id === 'toponyme - parcellaire - adresse'
)
data.layers[objIndex].layout = {
...data.layers[objIndex].layout,

View File

@@ -9,8 +9,8 @@
<script lang="ts" setup>
import { ref } from 'vue'
import Viewer from '@/components/Viewer.vue'
const emit = defineEmits<{ (e: 'trigger', value: string): void }>()
const viewerRef = ref<any>(null)
const viewerRef = ref<unknown>(null)
</script>
<style scoped lang="scss">
.entry-page {
@@ -29,11 +29,11 @@ const viewerRef = ref<any>(null)
}
@media (max-width: toRem(76.8)) {
.entry-page {
padding-top: toRem(11);
padding-top: toRem(11.5);
overflow: hidden;
}
.entry-section {
height: calc(100vh - #{toRem(11)});
height: calc(100dvh - #{toRem(18)});
}
}
</style>

View File

@@ -227,7 +227,6 @@ import type {
ResponseUserSequenceInterface
} from './interfaces/MySequenceView'
const { cookies } = useCookies()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
@@ -246,7 +245,7 @@ let isLoading = ref<boolean>(false)
const collapseMenu = ref<HTMLDivElement>()
const deleteAll = ref<HTMLDivElement>()
const menuHeight = ref<string>()
const viewerRef = ref()
const viewerRef = ref<InstanceType<typeof Viewer>>()
onMounted(async () => {
try {
@@ -267,37 +266,54 @@ 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)
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 => {
@@ -422,7 +438,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
}
@@ -430,13 +446,16 @@ function fullImagesToDelete(): ResponseUserPhotoInterface[] {
return pictures.value.filter((el) => picturesToDelete.value.includes(el.id))
}
async function goToNextPage(value: string) {
async function goToNextPage(value: string): Promise<void> {
isLoading.value = true
const { data } = await fetchCollectionItemsWithFullUrl(value)
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()
@@ -485,16 +504,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)
)
}
}
@@ -554,9 +583,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) {

View File

@@ -1,98 +1,101 @@
<template>
<main class="entry-page">
<section class="section-viewer">
<Viewer
:fetch-options="{
fetchOptions: {
credentials: 'include'
}
}"
:sequence-id="seqId"
:geovisio-viewer="false"
ref="viewerRef"
/>
<section :style="{ width: `${mapWidth}px` }" class="section-viewer">
<vue-draggable-resizable
:style="{ width: `${mapWidth}px` }"
:w="mapWidth"
:h="windowHeight"
:max-width="windowWidth"
:handles="['mr']"
:draggable="false"
class-name-active="resize-active-map"
class-name-handle="resize-handle-map"
@resizing="onResizeMap"
>
<Viewer
v-if="collectionBbox.length"
:fetch-options="{
fetchOptions: {
credentials: 'include'
}
}"
:geovisio-viewer="false"
:user-id="getUserId"
:bbox="collectionBbox"
ref="viewerRef"
/>
</vue-draggable-resizable>
</section>
<section class="section-sequence">
<h1 class="sequences-title">{{ $t('pages.sequences.title') }}</h1>
<ul v-if="!isLoading" class="sequence-list">
<li class="sequence-item">
<div class="sequence-header-item"></div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_name')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-title"
@trigger="sortAlpha('title')"
/>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_photos')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-number"
@trigger="sortNum('num')"
/>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_date')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-date"
@trigger="sortNum('date')"
/>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_creation')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-date"
@trigger="sortNum('date', 'created')"
/>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_status')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-date"
@trigger="sortAlpha('geovisio:status')"
/>
</div>
</li>
<section
:style="{ width: `${listWidth}px` }"
class="section-sequence"
@scroll="handleScroll"
>
<h1 id="sequenceTitle" class="sequences-title">
{{ $t('pages.sequences.title') }}
</h1>
<div
ref="headerList"
:class="['sequence-item sequence-item-head', headerListClass]"
:style="{ width: `${listWidth}px`, borderRadius: '0px' }"
>
<div class="sequence-header-item"></div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_name') }}</span>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_photos') }}</span>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_date')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-date"
@trigger="sortList('datetime')"
/>
</div>
<div class="sequence-header-item">
<Button
:text="$t('pages.sequences.sequence_creation')"
look="link--grey"
icon="bi bi-arrow-down-up"
data-test="button-sort-date"
@trigger="sortList('created')"
/>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_status') }}</span>
</div>
</div>
<ul v-if="!isLoading" ref="list" class="sequence-list">
<li
v-if="userSequences.length"
v-for="(item, i) in userSequences"
:id="`el-list${i}`"
v-for="item in userSequences"
class="sequence-item"
@mouseover="goToSequence(item)"
>
<router-link
:class="[
'button-item',
item.id === seqId ? 'button-item-hover' : ''
]"
class="button-item"
:to="{
name: 'sequence',
params: { id: item.id }
}"
@mouseover.stop
>
<div class="wrapper-thumb">
<img
v-if="item['stats:items'].count > 0"
:src="`${item.href}/thumb.jpg`"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb"
/>
<div class="wrapper-thumb-hover">
<img
v-if="item['stats:items'].count > 0"
:src="`${item.href}/thumb.jpg`"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb-hover"
/>
@@ -130,6 +133,25 @@
}}</span>
</div>
</router-link>
<div class="wrapper-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
look="button--white background-white"
:icon="
item['geovisio:status'] === 'ready'
? 'bi bi-eye-slash'
: 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection(item)"
/>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
look="button--red background-white"
icon="bi bi-trash"
@trigger="deleteCollection(item)"
/>
</div>
</li>
<div v-else class="no-sequence">
<p class="no-sequence-text">
@@ -145,104 +167,217 @@
<div v-else class="loader">
<Loader look="sm" :is-loaded="false" />
</div>
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="goToNextPage"
/>
</div>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</section>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</main>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect, computed } from 'vue'
import { onMounted, ref, watchEffect, computed, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSequenceStore } from '@/store/sequence'
import { storeToRefs } from 'pinia'
import { scrollIntoSelected } from '@/views/utils/sequence/index'
import {
scrollIntoSelected,
formatPaginationItems
} from '@/views/utils/sequence/index'
import { useCookies } from 'vue3-cookies'
import axios from 'axios'
import Viewer 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 type {
SequenceLinkInterface,
ExtentSequenceLinkInterface
} from './interfaces/MySequencesView'
import Pagination from '@/components/Pagination.vue'
import type { SequenceLinkInterface } from './interfaces/MySequencesView'
import type { ResponseUserPhotoLinksInterface } from './interfaces/MySequenceView'
import { formatDate } from '@/utils/dates'
import {
deleteACollection,
patchACollection
} from '@/views/utils/sequence/request'
const { t } = useI18n()
const { cookies } = useCookies()
const sequenceStore = useSequenceStore()
const { toastText, toastLook } = storeToRefs(sequenceStore)
interface PositionInterface {
bottom: number
top: number
right: number
left: number
y: number
x: number
}
let userSequences = ref<SequenceLinkInterface[]>([])
let paginationLinks = ref<ResponseUserPhotoLinksInterface[] | []>([])
let selfLink = ref<ResponseUserPhotoLinksInterface[] | []>([])
let collectionBbox = ref<number[]>([])
let isSorted = ref<boolean>(false)
let isLoading = ref<boolean>(false)
let isSorted = ref<boolean>(false)
let seqId = ref<string>('')
const viewerRef = ref<any>(null)
let width = ref<number>(0)
let mapWidth = ref<number>(window.innerWidth / 3)
let listWidth = ref<number>(window.innerWidth / 1.5)
const windowWidth = ref<number>(window.innerWidth)
const windowHeight = ref<number>(window.innerHeight - 80)
const viewerRef = ref<InstanceType<typeof Viewer>>()
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 fetchAndFormatSequence(): Promise<void> {
const { data } = await axios.get('api/users/me/collection')
collectionBbox.value = data.extent.spatial.bbox[0]
userSequences.value = getRelChild(data.links)
}
async function patchCollection(sequence: SequenceLinkInterface): Promise<void> {
isLoading.value = true
let visible
if (sequence['geovisio:status'] === 'ready') visible = 'false'
else visible = 'true'
await patchACollection(sequence.id, visible)
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
isLoading.value = false
}
async function deleteCollection(
sequence: SequenceLinkInterface
): Promise<void> {
isLoading.value = true
if (confirm(t('pages.sequence.confirm_sequence_dialog'))) {
await deleteACollection(sequence.id)
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
}
isLoading.value = false
}
function sequenceStatus(status: string): string {
if (status === 'ready') return t('pages.sequences.sequence_published')
if (status === 'hidden') return t('pages.sequences.sequence_hidden')
return t('pages.sequences.sequence_waiting')
}
function sortAlpha<TKey extends keyof SequenceLinkInterface>(key: TKey): void {
const sorted = userSequences.value.sort(
(
a: { [K in TKey]: SequenceLinkInterface[TKey] },
b: { [K in TKey]: SequenceLinkInterface[TKey] }
) => {
if (a[key] < b[key]) return !isSorted.value ? -1 : 1
if (a[key] > b[key]) return !isSorted.value ? 1 : -1
return 0
}
function onResizeMap(width: any): void {
if (width) {
width.value = width
mapWidth.value = width.value.width
listWidth.value = window.innerWidth - width.value.width
}
}
const handleScroll = async () => {
await nextTick()
if (headerList.value && headerList.value.getBoundingClientRect()) {
headerLisPos.value = headerList.value.getBoundingClientRect()
}
if (list.value && list.value.getBoundingClientRect()) {
listPos.value = list.value.getBoundingClientRect()
}
}
async function sortList(dateToSort: string): Promise<void> {
isLoading.value = true
let sortBy = `+${dateToSort}`
if (isSorted.value) sortBy = `-${dateToSort}`
const { data } = await axios.get(
`api/users/me/collection?limit=50&sortby=${encodeURIComponent(sortBy)}`
)
isSorted.value = !isSorted.value
userSequences.value = sorted
}
function sortNum(type: string, dateToSort?: string): void {
let aa, bb: number
const sorted = userSequences.value.sort(
(a: ExtentSequenceLinkInterface, b: ExtentSequenceLinkInterface) => {
aa = new Date(a.extent.temporal.interval[0][0]).getTime()
bb = new Date(b.extent.temporal.interval[0][0]).getTime()
if (dateToSort === 'created') {
aa = new Date(a.created).getTime()
bb = new Date(b.created).getTime()
}
if (type === 'num') {
aa = Number(a['stats:items'].count)
bb = Number(b['stats:items'].count)
}
if (aa < bb) return !isSorted.value ? -1 : 1
if (aa > bb) return !isSorted.value ? 1 : -1
return 0
}
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === 'self'
)
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getRelChild(data.links)
isSorted.value = !isSorted.value
userSequences.value = sorted
}
function goToSequence(sequence: SequenceLinkInterface) {
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
}
function getRelChild(sequences: SequenceLinkInterface[]) {
return sequences.filter((el: SequenceLinkInterface) => el.rel === 'child')
scrollToElement()
isLoading.value = false
}
function bboxIsInsideOther(mainBbox: number[], bboxInside: number[]): boolean {
return (
bboxInside[0] <= mainBbox[0] &&
bboxInside[1] <= mainBbox[1] &&
bboxInside[2] >= mainBbox[2] &&
bboxInside[3] >= mainBbox[3]
)
}
function goToSequence(sequence: SequenceLinkInterface): void {
if (!viewerRef.value) return
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
]
if (
seqId.value === sequence.id &&
bboxIsInsideOther(currentBbox, sequence.extent.spatial.bbox[0])
) {
return
}
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
viewerRef.value.viewer._map.flyTo({
center: [
sequence.extent.spatial.bbox[0][0],
sequence.extent.spatial.bbox[0][1]
],
zoom: 10,
duration: 0
})
}
function getRelChild(
sequences: SequenceLinkInterface[]
): SequenceLinkInterface[] {
return sequences.filter((el: SequenceLinkInterface) => el.rel === 'child')
}
function scrollToElement(): void {
const elementTarget = document.querySelector('#sequenceTitle')
if (elementTarget) elementTarget.scrollIntoView({ behavior: 'smooth' })
}
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
}
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'
: ''
}
return ''
})
onMounted(async () => {
isLoading.value = true
try {
const { data } = await axios.get('api/users/me/catalog')
const collectionData = await axios.get('api/users/me/collection')
collectionBbox.value = collectionData.data.extent.spatial.bbox[0]
viewerRef.value.viewer.fitBounds(collectionBbox.value)
const sequences = getRelChild(data.links)
const sequencesCollection = getRelChild(collectionData.data.links)
sequencesCollection.map((el: SequenceLinkInterface) => {
let index = sequences.findIndex(
(elem: SequenceLinkInterface) => elem.id === el.id
)
sequences[index] = el
})
userSequences.value = sequencesCollection
const { data } = await axios.get('api/users/me/collection?limit=50')
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === '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]
]
isLoading.value = false
} catch (err) {
isLoading.value = false
@@ -254,28 +389,53 @@ watchEffect(() => {
viewerRef.value.viewer.addEventListener(
'hover',
(e: { detail: { seqId: string } }) => {
if (seqId.value === e.detail.seqId) return
seqId.value = e.detail.seqId
scrollIntoSelected(e.detail.seqId, userSequences.value)
scrollIntoSelected(
e.detail.seqId,
userSequences.value.map((e) => e.id)
)
if (viewerRef.value) viewerRef.value.viewer.select(e.detail.seqId)
}
)
}
})
</script>
<style lang="scss">
.resize-handle-map-mr {
top: 0;
right: toRem(0);
cursor: e-resize;
background-color: var(--black);
display: block !important;
}
.resize-handle-map {
z-index: 999999;
box-sizing: border-box;
position: absolute;
height: 100%;
width: toRem(0.5);
&:hover {
cursor: col-resize;
}
}
</style>
<style lang="scss" scoped>
.entry-page {
display: flex;
height: calc(100vh - #{toRem(8)});
overflow: hidden;
position: relative;
}
.section-viewer {
width: 40%;
height: 100%;
position: relative;
}
.section-sequence {
overflow-y: auto;
width: 60%;
overflow-x: hidden;
height: 100%;
position: relative;
}
.sequences-title {
@include text(h1);
@@ -290,21 +450,57 @@ watchEffect(() => {
}
.sequence-item {
@include text(s-regular);
position: relative;
border: none;
display: flex;
justify-content: center;
align-items: center;
margin: auto;
background-color: var(--blue-pale);
&:first-child {
margin-bottom: toRem(1);
padding: toRem(1) toRem(2);
border-bottom: toRem(0.1) solid var(--grey);
border-radius: toRem(2) toRem(2) 0rem 0rem;
background-color: var(--white);
}
padding: toRem(2);
&:nth-child(2n) {
background-color: var(--white);
}
&:hover {
background-color: var(--blue);
color: var(--white);
.button-item {
& > *,
& > :nth-child(2) {
color: var(--white);
}
}
}
}
.wrapper-button {
position: absolute;
right: toRem(1);
bottom: 0;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
height: 100%;
.button--white:first-child {
margin-bottom: toRem(1);
}
}
.item-head-fixed {
position: fixed;
top: toRem(8);
width: 100%;
z-index: 2;
}
.sequence-item-head {
margin-bottom: toRem(1);
padding: toRem(1) toRem(2);
border-bottom: toRem(0.1) solid var(--grey);
border-radius: toRem(2) toRem(2) 0rem 0rem;
background-color: var(--white);
}
.sequence-item-head:hover {
background-color: var(--white);
color: initial;
}
.wrapper-title {
display: flex;
@@ -350,7 +546,6 @@ watchEffect(() => {
display: flex;
align-items: center;
width: 100%;
padding: toRem(2);
background-color: transparent;
border: none;
text-decoration: none;
@@ -374,26 +569,12 @@ watchEffect(() => {
& > :nth-child(2) {
color: var(--blue-dark);
}
&:hover {
background-color: var(--blue);
& > * {
color: var(--white);
}
}
}
.button-item-hover {
background-color: var(--blue);
& > *,
& > :nth-child(2) {
color: var(--white);
}
}
.bi-images {
margin-right: toRem(0.5);
}
.sequence-header-item {
width: calc(31% - #{toRem(4.75)});
margin-left: toRem(-1);
&:first-child {
margin-right: toRem(2);
}
@@ -423,13 +604,19 @@ watchEffect(() => {
justify-content: center;
align-items: center;
height: 100%;
margin-top: toRem(20);
}
.ay11-link {
padding: toRem(2);
margin-left: auto;
width: fit-content;
}
.entry-pagination {
margin-top: toRem(4);
margin-bottom: toRem(4);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: toRem(102.4)) {
.section-viewer {
width: 30%;
@@ -449,18 +636,16 @@ watchEffect(() => {
display: none;
}
.section-sequence {
width: 100%;
width: 100% !important;
}
.button-item,
.sequence-item:first-child {
padding-right: toRem(1);
padding-left: toRem(1);
.sequence-item-head {
display: none;
}
}
@media (max-width: toRem(50)) {
.button-item {
flex-direction: column;
align-items: center;
padding-right: toRem(1);
padding-left: toRem(1);
& > * {
text-align: center;
width: 100%;
@@ -469,12 +654,23 @@ watchEffect(() => {
margin-right: 0;
}
}
.sequence-item:first-child {
display: none;
}
.sequence-item {
border-top-right-radius: toRem(1);
border-top-left-radius: toRem(1);
flex-direction: column;
}
.wrapper-button {
flex-direction: row;
position: relative;
right: 0;
bottom: 0;
justify-content: center;
margin-top: toRem(1);
margin-bottom: 0;
.button--white:first-child {
margin-right: toRem(1);
margin-bottom: 0;
}
}
}
</style>

View File

@@ -35,6 +35,16 @@
:text="authConf.license.id"
class="entry-license"
/>
<h2 class="subtitle">{{ $t('pages.upload.title_sequence') }}</h2>
<p class="import-text">
{{ $t('pages.upload.description_title_sequence') }}
</p>
<EditText
:default-text="newSequenceTitle || sequenceTitle"
:is-loading="isLoading"
:is-loaded="isLoaded"
@triggerNewText="setNewSequenceTitle"
/>
<form>
<div class="wrapper-form">
<InputUpload
@@ -85,11 +95,13 @@
</template>
<script lang="ts" setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import { ref, computed, watch, onUnmounted, onMounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useI18n } from 'vue-i18n'
import InputUpload from '@/components/InputUpload.vue'
import Button from '@/components/Button.vue'
import Link from '@/components/Link.vue'
import EditText from '@/components/EditText.vue'
import Modal from '@/components/Modal.vue'
import InformationCard from '@/components/InformationCard.vue'
import UploadLoader from '@/components/upload/UploadLoader.vue'
@@ -102,7 +114,6 @@ import {
} from '@/views/utils/upload/request'
import { sortByName } from '@/views/utils/upload/index'
import authConfig from '../composables/auth'
import Link from '@/components/Link.vue'
const { authConf } = authConfig()
const { t } = useI18n()
@@ -115,6 +126,8 @@ let uploadedSequence = ref<SequenceInterface | null>(null)
let picturesUploadingSize = ref<number>(0)
let picturesToUploadSize = ref<number>(0)
let loadPercentage = ref<string>('0%')
let sequenceTitle = ref<string>(formatSequenceTitle())
let newSequenceTitle = ref<string | null>(null)
let modal = ref()
watch(isLoading, () => {
@@ -128,11 +141,16 @@ watch(isLoading, () => {
window.onbeforeunload = null
}
})
onMounted(() => {
setInterval(() => {
sequenceTitle.value = formatSequenceTitle()
}, 1000)
})
onUnmounted(() => {
window.onbeforeunload = null
})
onBeforeRouteLeave((to, from, next) => {
if (isLoading.value) {
if (!isLoaded.value && isLoading.value) {
const answer = window.confirm(t('pages.upload.leave_message'))
if (answer) return next()
return next(false)
@@ -145,7 +163,15 @@ const inputIsDisplayed = computed<boolean | null>(
isLoaded.value ||
(uploadedSequence.value && !uploadedSequence.value.pictures)
)
function setNewSequenceTitle(value: string | null): void {
newSequenceTitle.value = value
}
function formatSequenceTitle(): string {
return `${t('pages.upload.sequence_title')}${formatDate(
new Date(),
'Do MMMM YYYY, hh:mm:ss'
)}`
}
function picturesToUploadSizeText(): void {
let fullSize = 0
for (let i = 0; i < pictures.value.length; i++) {
@@ -183,14 +209,12 @@ async function uploadPicture(): Promise<void> {
picturesToUploadSizeText()
uploadedSequence.value = null
const picturesToUpload = [...pictures.value]
const sequenceTitle = `${t('pages.upload.sequence_title')}${formatDate(
new Date(),
'Do MMMM YYYY, hh:mm:ss'
)}`
const { data } = await createASequence(sequenceTitle)
const title = newSequenceTitle.value
? newSequenceTitle.value
: sequenceTitle.value
const { data } = await createASequence(title)
uploadedSequence.value = {
title: sequenceTitle,
title: title,
id: data.id,
pictures: [],
picturesOnError: [],
@@ -205,7 +229,7 @@ async function uploadPicture(): Promise<void> {
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,
@@ -216,6 +240,7 @@ async function uploadPicture(): Promise<void> {
picturesUploadingSize.value = picturesUploadingSize.value + el.size
const picturesOnError = {
message: err.response.data.message,
details: { error: err.response.data.details.error },
name: el.name
}
uploadedSequence.value.picturesOnError = [
@@ -228,6 +253,8 @@ async function uploadPicture(): Promise<void> {
isLoaded.value = true
pictures.value = []
picturesCount.value = 0
newSequenceTitle.value = null
sequenceTitle.value = formatSequenceTitle()
}
</script>
@@ -247,6 +274,9 @@ h3 {
padding: toRem(2) toRem(5) toRem(5);
color: var(--grey-dark);
}
.logged {
min-height: calc(100vh - #{toRem(8)});
}
.section {
width: 100%;
}

View File

@@ -5,14 +5,7 @@ export interface SequenceLinkInterface {
title: string
type: string
created: Date
extent: { temporal: { interval: [Date[]] } }
extent: { temporal: { interval: [Date[]] }; spatial: { bbox: [number[]] } }
['stats:items']: { count: number }
['geovisio:status']: string
}
export interface ExtentSequenceLinkInterface {
extent: { temporal: { interval: [Date[]] } }
['stats:items']: { count: number }
title: string
created: Date
}

View File

@@ -8,5 +8,6 @@ export interface SequenceInterface {
}
export interface uploadErrorInterface {
message: string
details: { error: string }
name: string
}

View File

@@ -1,20 +1,21 @@
export interface OptionalViewerInterface {
export interface MapInterface {
startWide?: boolean
maxZoom?: number
minZoom?: number
style?: object | string
zoom?: number
center?: number[]
bounds?: number[]
}
export interface ViewerInterface {
fetchOptions?: {
credentials: string
}
hash?: boolean
picId?: string
widgets?: {
customWidget: HTMLAnchorElement
}
}
export interface ViewerInterface extends OptionalViewerInterface {
map: {
startWide: boolean
style?: object | string
maxZoom?: number
zoom?: number
center?: number[]
}
bounds?: number[]
map: MapInterface
}

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

@@ -1,11 +1,50 @@
import axios from 'axios'
function createASequence(title: string): Promise<any> {
interface SequenceCreatedInterface {
data: {
created: string
description: string
extent: { spatial: object; temporal: object }
['geovisio:status']: string
id: string
keywords: string[]
license: string
links: object[]
providers: object[]
stac_extensions: string[]
stac_version: string
['stats:items']: { count: number }
title: string
type: string
}
}
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 }

1
vue-draggable-resizable-vue3.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'vue-draggable-resizable-vue3'

2798
yarn.lock

File diff suppressed because it is too large Load Diff