61 Commits

Author SHA1 Message Date
Andreani Jean
d0a9d0eb27 dit cypress focker compose 2023-12-12 16:13:29 +01:00
Andreani Jean
5488ac8521 wip 2023-12-12 15:54:19 +01:00
Andreani Jean
80089126c4 fix imgs size 2023-12-12 15:46:51 +01:00
Andreani Jean
4fed5f7938 wip 2023-12-12 15:30:56 +01:00
Andreani Jean
5a6d144196 fix 2023-12-12 15:23:24 +01:00
Andreani Jean
9c2ab2453c fix test 2023-12-12 15:05:25 +01:00
Andreani Jean
be2cdbb62b fix test:e2e 2023-12-12 10:47:01 +01:00
Andreani Jean
2e2f23de69 wi^^ 2023-12-12 10:00:18 +01:00
Andreani Jean
669f0528e5 wip 2023-12-12 09:58:54 +01:00
Andreani Jean
6e0f19a5f1 wip docker to CI 2023-12-12 09:58:54 +01:00
Andreani Jean
661ba984fd add some test e2e 2023-12-12 09:58:54 +01:00
Andreani Jean
aa3dbdaf03 fix e2e test 2023-12-12 09:56:27 +01:00
antoine-de
aa257adbcf update fullcourse test 2023-12-12 09:55:57 +01:00
antoine-de
d1779da92d Increase healthcheck timeout on CI 2023-12-12 09:55:57 +01:00
Andreani Jean
379c44c5ce edit wait docker 2023-12-12 09:55:57 +01:00
Andreani Jean
8452b50ea0 wip 2023-12-12 09:55:57 +01:00
antoine-de
30b49a8c7d It wooooorks 2023-12-12 09:55:57 +01:00
antoine-de
482372fccb Fix local cypress running 2023-12-12 09:55:57 +01:00
antoine-de
f37dc950cb Change docker conf for cypress 2023-12-12 09:55:57 +01:00
Andreani Jean
02ba0efe8d fix course 2023-12-12 09:55:57 +01:00
Andreani Jean
a3bec701ea full course wip e2e 2023-12-12 09:55:57 +01:00
antoine-de
87da6714a2 use node image 2023-12-12 09:55:57 +01:00
antoine-de
1bf633b284 Add docker host and update image name
Update image name not to mess with build cache
2023-12-12 09:55:57 +01:00
antoine-de
7b56fb5c33 Update node base image 2023-12-12 09:55:57 +01:00
antoine-de
7eb090492b Attempt on e2e on gitlab-ci 2023-12-12 09:55:57 +01:00
Andreani Jean
e580887969 wip docker to CI 2023-12-12 09:55:57 +01:00
Andreani Jean
56e2908f70 wip 2023-12-12 09:55:57 +01:00
Andreani Jean
2afabb82f4 wip 2023-12-12 09:55:57 +01:00
Andreani Jean
8d4a0968ea fix env var 2023-12-12 09:55:57 +01:00
Andreani Jean
a006811b44 edit wordings 2023-12-12 09:55:57 +01:00
Andreani Jean
387f84b1a7 fix env var 2023-12-12 09:55:55 +01:00
Andreani Jean
86a54fed85 add some test e2e 2023-12-12 09:55:21 +01:00
Andreani Jean
6f6e16c2c7 add some tests 2023-12-12 09:53:32 +01: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
Andreani Jean
93c286ce20 Release 2.2.2 2023-10-16 18:15:17 +02:00
Jean Andreani
cb60e404a7 Merge branch 'fix/some-perf-fix' into 'develop'
fix lazy loading + css fix

See merge request geovisio/website!97
2023-10-16 16:11:18 +00:00
Andreani Jean
1e54768e9d fix lazy loading + css fix 2023-10-16 18:08:36 +02:00
Andreani Jean
0260037912 Release 2.2.1 2023-10-16 16:10:38 +02:00
Jean Andreani
3dad314e68 Merge branch 'fix/change-icon-about-page' into 'develop'
Fix/some fix before release

See merge request geovisio/website!96
2023-10-16 14:06:50 +00:00
Jean Andreani
534d0ab44e Fix/some fix before release 2023-10-16 14:06:50 +00:00
73 changed files with 6024 additions and 1066 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,62 @@ 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
- fix lazy loading on imgs
- fix css to the license link
## [2.2.1] - 2023-10-16
### Changed
- fix some wordings
- licence text in bold
- remove footer when logged
- add tiles custom to the sequence page list
## [2.2.0] - 2023-10-13
### Added
@@ -16,7 +72,7 @@ Before _0.1.0_, website development was on rolling release, meaning there are no
- new header
- add a map to the sequence list page + the uploaded date
- new wordings
### Changed
- Hide buttons delete and hide sequences images when you are not the owner
@@ -91,7 +147,11 @@ 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.0...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
[2.1.3]: https://gitlab.com/geovisio/website/-/compare/2.1.2...2.1.3
[2.1.2]: https://gitlab.com/geovisio/website/-/compare/2.1.1...2.1.2

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,25 @@
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
textButtonCli: 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"]
}

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

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.0",
"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

@@ -1,16 +1,15 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import { RouterView } from 'vue-router'
import { useMeta } from 'vue-meta'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { hasASessionCookieDecoded } from '@/utils/auth'
import { title } from '@/utils/index'
import authConfig from './composables/auth'
const { authConf } = authConfig()
const { t } = useI18n()
const route = useRoute()
let focusMap = ref<string>('focus-map')
@@ -29,6 +28,10 @@ useMeta({
function setFocusMap(value: string) {
focusMap.value = value
}
const isLogged = computed((): boolean => {
const cookie = hasASessionCookieDecoded()
return !!(cookie && cookie.account)
})
</script>
<template>
@@ -50,13 +53,7 @@ function setFocusMap(value: string) {
: ''
"
/>
<RouterView @trigger="setFocusMap" />
<Footer
v-if="
route.name !== 'sequence' &&
route.name !== 'my-sequences' &&
focusMap === 'focus-map'
"
/>
<RouterView @trigger="setFocusMap" :class="{ logged: isLogged }" />
<Footer v-if="!isLogged" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,6 @@
<svg width="65" height="66" viewBox="0 0 65 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32.5" cy="33" r="32.5" fill="#F2F5FF"/>
<path d="M47.5742 20.5197C47.4489 11.3719 39.9685 3.96826 30.7914 3.96826C26.7092 3.96826 22.7763 5.45326 19.7175 8.14974C16.9875 10.5562 15.12 13.772 14.3804 17.3028C14.3264 17.3022 14.2728 17.3017 14.2194 17.3017C6.37897 17.3017 0 23.6806 0 31.5211C0 39.3616 6.37897 45.7405 14.2195 45.7405H24.1503C24.8568 45.7405 25.4299 45.1677 25.4299 44.4609C25.4299 43.7542 24.8568 43.1814 24.1503 43.1814H14.2195C7.78991 43.1814 2.55914 37.9506 2.55914 31.5211C2.55914 25.0916 7.78991 19.8608 14.2195 19.8608C14.562 19.8608 14.9242 19.8792 15.3267 19.9171C16.009 19.981 16.6215 19.4957 16.7144 18.816C17.1752 15.4387 18.8427 12.3324 21.4098 10.0694C24.0009 7.78517 27.3326 6.52728 30.7913 6.52728C38.6352 6.52728 45.0166 12.9087 45.0166 20.7525C45.0166 21.0349 44.9956 21.326 44.9734 21.6342L44.9639 21.7653C44.9373 22.1417 45.0782 22.5107 45.3491 22.7735C45.6197 23.0364 45.9923 23.1662 46.3683 23.1286C46.7086 23.0943 47.052 23.0771 47.3888 23.0771C52.9314 23.0771 57.4409 27.5864 57.4409 33.1291C57.4409 38.6718 52.9315 43.1811 47.3888 43.1811H36.9461C36.2396 43.1811 35.6665 43.7539 35.6665 44.4607C35.6665 45.1674 36.2396 45.7403 36.9461 45.7403H47.3888C54.3426 45.7403 60 40.083 60 33.1291C59.9999 26.2377 54.4429 20.6192 47.5742 20.5197Z" fill="#0A1F69"/>
<path d="M30.7916 9.98218C25.4411 9.98218 20.8609 13.9807 20.1376 19.283C20.0421 19.9832 20.5324 20.6283 21.2325 20.7238C21.2912 20.7318 21.3495 20.7358 21.407 20.7358C22.0369 20.7358 22.5858 20.2705 22.6731 19.629C23.2242 15.5884 26.7144 12.5413 30.7915 12.5413C31.4982 12.5413 32.071 11.9685 32.071 11.2617C32.0712 10.5551 31.4982 9.98218 30.7916 9.98218Z" fill="#0A1F69"/>
<path d="M36.6105 35.161L31.9561 31.0032C31.1537 30.286 29.9432 30.2861 29.1404 31.0031L24.486 35.1611C23.9589 35.6319 23.9134 36.4408 24.3842 36.9679C24.8549 37.4948 25.6637 37.5406 26.1909 37.0697L29.2687 34.3203V50.0622C29.2687 50.7689 29.8416 51.3417 30.5483 51.3417C31.255 51.3417 31.8279 50.7689 31.8279 50.0622V34.3201L34.9058 37.0696C35.1498 37.2877 35.4544 37.3949 35.7578 37.3949C36.1096 37.3949 36.4597 37.2509 36.7126 36.9678C37.1831 36.4407 37.1375 35.6319 36.6105 35.161Z" fill="#0A1F69"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,6 +0,0 @@
<svg width="79" height="71" viewBox="0 0 79 71" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="46.5" cy="38" r="32.5" fill="#F2F5FF"/>
<path d="M66.6685 59.2848L63.3349 23.738C63.2996 23.3631 63.0772 23.0311 62.7434 22.8568L49.9643 16.1787C49.9406 16.1663 49.9132 16.1689 49.8888 16.1583C49.816 16.1268 49.7387 16.112 49.6586 16.0965C49.5815 16.0814 49.5086 16.0638 49.4304 16.0651C49.4129 16.0654 49.3978 16.0572 49.3799 16.0582C49.3169 16.0621 49.264 16.0913 49.2045 16.1052C49.1441 16.1191 49.0811 16.1132 49.023 16.1374L45.067 17.7819C44.5 18.0174 44.232 18.668 44.4674 19.2344C44.7034 19.8019 45.3529 20.0701 45.9199 19.834L48.4374 18.7875L50.5118 51.9502L34.4479 57.8008V34.7293C34.4479 34.1155 33.9503 33.6181 33.3367 33.6181C32.723 33.6181 32.2255 34.1157 32.2255 34.7293V57.8008L16.1616 51.9502L18.2361 18.7875L20.7535 19.834C21.321 20.0699 21.9711 19.8019 22.206 19.2344C22.4415 18.668 22.1735 18.0174 21.6065 17.7819L17.6505 16.1374C17.5926 16.1134 17.5299 16.1193 17.4697 16.1055C17.4099 16.0915 17.3567 16.0622 17.2934 16.0582C17.2757 16.0573 17.2606 16.0655 17.243 16.0654C17.1623 16.0639 17.087 16.0825 17.0074 16.0987C16.9329 16.1139 16.8607 16.1272 16.7924 16.1564C16.7655 16.1678 16.7353 16.1651 16.7091 16.1787L3.93003 22.8568C3.59641 23.0311 3.37386 23.3631 3.33857 23.738L0.0048901 59.2848C-0.0326138 59.6837 0.147613 60.0716 0.476423 60.3005C0.804191 60.5289 1.23275 60.5637 1.59255 60.3901L15.0613 53.9148L32.9565 60.4324C33.2017 60.5225 33.4719 60.5225 33.7172 60.4324L51.6124 53.9148L65.0811 60.3901C65.2336 60.4633 65.3986 60.4998 65.5624 60.4998C65.7854 60.4998 66.0079 60.4324 66.1973 60.3007C66.5258 60.0715 66.7059 59.6835 66.6685 59.2848ZM13.9321 51.9912L2.40071 57.5354L5.49452 24.547L15.9918 19.061L13.9321 51.9912ZM52.7409 51.9912L50.6812 19.061L61.1785 24.547L64.2723 57.5354L52.7409 51.9912Z" fill="#0A1F69"/>
<path d="M23.5455 16.259L32.3821 31.0685C32.5829 31.4048 32.9453 31.6104 33.3364 31.6104C33.7276 31.6104 34.09 31.4048 34.2908 31.0685L43.1514 16.2167C43.5779 15.4245 43.8633 14.7534 44.0489 14.1033C44.3141 13.1749 44.4488 12.2108 44.4488 11.2384C44.4488 9.7837 44.1531 8.37249 43.5693 7.04254C43.0071 5.76259 42.2025 4.61338 41.1792 3.62799C40.1592 2.64534 38.9709 1.87377 37.6485 1.33505C34.9138 0.221651 31.7592 0.221651 29.0245 1.33505C27.7023 1.87377 26.514 2.64547 25.4945 3.62799C24.4706 4.61338 23.6659 5.76259 23.1039 7.04306C22.5201 8.37236 22.2244 9.7837 22.2244 11.2384C22.2244 12.2108 22.3589 13.1749 22.6243 14.1038C22.8097 14.7532 23.0952 15.4244 23.5455 16.259ZM25.1386 7.93664C25.5836 6.92312 26.2222 6.01209 27.0361 5.22855C27.8522 4.4424 28.8033 3.82489 29.8624 3.39346C30.9628 2.94524 32.1315 2.71852 33.3366 2.71852C34.5417 2.71852 35.7104 2.94537 36.8108 3.39346C37.8699 3.82476 38.8211 4.44227 39.6376 5.22855C40.451 6.01209 41.0896 6.92299 41.5346 7.93612C41.9936 8.98219 42.2264 10.093 42.2264 11.2383C42.2264 12.0044 42.1207 12.7629 41.9123 13.4928C41.7718 13.9844 41.5439 14.5151 41.2189 15.1206L33.3366 28.3305L25.4782 15.1629C25.1294 14.5151 24.9015 13.9844 24.761 13.4933C24.5526 12.763 24.4469 12.0045 24.4469 11.2383C24.4467 10.093 24.6796 8.98219 25.1386 7.93664Z" fill="#0A1F69"/>
<path d="M33.3367 17.1678C36.4002 17.1678 38.8929 14.6751 38.8929 11.6116C38.8929 8.54812 36.4002 6.05542 33.3367 6.05542C30.2732 6.05542 27.7805 8.54812 27.7805 11.6116C27.7805 14.6751 30.2732 17.1678 33.3367 17.1678ZM33.3367 8.27791C35.175 8.27791 36.6704 9.77325 36.6704 11.6116C36.6704 13.4499 35.175 14.9453 33.3367 14.9453C31.4984 14.9453 30.003 13.4499 30.003 11.6116C30.003 9.77325 31.4984 8.27791 33.3367 8.27791Z" fill="#0A1F69"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,6 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32.75" cy="33" r="32.5" fill="#F2F5FF"/>
<path d="M55.0182 15.3341H54.5036C54.2024 12.7533 52.1255 10.7071 49.53 10.4567C49.232 7.67438 46.8718 5.5 44.0121 5.5H8.59589C3.8562 5.5 0 9.3562 0 14.0954V38.9277C0 41.8075 2.20367 44.181 5.01251 44.4525C5.30731 47.205 7.62131 49.3611 10.4398 49.4064C10.4471 49.8948 10.5235 50.3663 10.6622 50.8113C11.2935 52.8378 13.1877 54.3132 15.4193 54.3132H55.0182C57.7652 54.3132 60 52.0784 60 49.3318V20.316C60 17.5689 57.7652 15.3341 55.0182 15.3341ZM57.6562 20.316V39.5041L52.5586 35.6533C51.1656 34.6014 49.2819 34.5849 47.8711 35.6126L41.4226 40.3111L29.1252 27.6329C27.6604 26.1223 25.2768 26.0293 23.6989 27.4218L12.7817 37.0536V20.316C12.7817 18.8612 13.965 17.6779 15.4193 17.6779H55.0182C56.4729 17.6779 57.6562 18.8612 57.6562 20.316ZM57.6562 49.3313C57.6562 50.7861 56.4729 51.9694 55.0186 51.9694H15.4193C14.2378 51.9694 13.2349 51.188 12.9007 50.115C12.8233 49.8674 12.7817 49.6042 12.7817 49.3318V40.1788L25.2498 29.1792C25.8875 28.6161 26.8506 28.6537 27.4429 29.2643L32.9929 34.9864C32.9929 34.9868 32.9929 34.9868 32.9933 34.9868L44.0396 46.3755C44.2694 46.6127 44.5747 46.7317 44.881 46.7317C45.1749 46.7317 45.4692 46.6218 45.6967 46.4012C46.1609 45.9503 46.1723 45.2087 45.7219 44.7441L43.0705 42.0103L49.2513 37.5073C49.8216 37.0916 50.5829 37.0981 51.1459 37.5233L57.6562 42.4411V49.3313ZM7.32422 43.8578V16.1888C7.32422 16.0711 7.33017 15.9544 7.34207 15.84C7.51694 14.1201 8.974 12.7739 10.7391 12.7739H25.0735C25.7208 12.7739 26.2454 12.2493 26.2454 11.602C26.2454 10.9547 25.7208 10.4301 25.0735 10.4301H10.7391C7.56363 10.4301 4.98047 13.0133 4.98047 16.1888V42.0845C3.48312 41.8148 2.34375 40.502 2.34375 38.9277V14.0954C2.34375 10.6485 5.14847 7.84375 8.59589 7.84375H44.0121C45.5676 7.84375 46.8681 8.95749 47.1579 10.4301H34.4485C33.8013 10.4301 33.2767 10.9547 33.2767 11.602C33.2767 12.2493 33.8013 12.7739 34.4485 12.7739H48.3902H48.3916H48.9926C50.5389 12.7739 51.8335 13.8748 52.1333 15.3341H15.4193C12.6727 15.3341 10.4379 17.5689 10.4379 20.316V39.6492V39.6501V47.0626C8.71262 47.0137 7.32422 45.5951 7.32422 43.8578Z" fill="#0A1F69"/>
<path d="M45.5292 22.4473C42.7634 22.4473 40.5139 24.6972 40.5139 27.4621C40.5139 30.2274 42.7638 32.4773 45.5292 32.4773C48.2941 32.4773 50.544 30.2274 50.544 27.4621C50.544 24.6972 48.2941 22.4473 45.5292 22.4473ZM45.5292 30.1336C44.0561 30.1336 42.8577 28.9351 42.8577 27.4625C42.8577 25.9894 44.0561 24.791 45.5292 24.791C47.0018 24.791 48.2002 25.9894 48.2002 27.4625C48.2002 28.9351 47.0018 30.1336 45.5292 30.1336Z" fill="#0A1F69"/>
<path d="M28.6793 12.0506C28.7086 12.1211 28.7448 12.1888 28.7869 12.2524C28.8294 12.3165 28.8784 12.3765 28.9324 12.4305C28.9865 12.4841 29.046 12.5335 29.1105 12.577C29.1737 12.6191 29.2428 12.6553 29.3133 12.6846C29.3833 12.7139 29.4575 12.7363 29.5326 12.7514C29.6072 12.7665 29.6845 12.7738 29.761 12.7738C29.8383 12.7738 29.9143 12.7665 29.9908 12.7514C30.0654 12.7363 30.1382 12.7139 30.2096 12.6846C30.2801 12.6553 30.3478 12.6191 30.4115 12.577C30.4756 12.5335 30.5355 12.4841 30.5895 12.4305C30.6445 12.3765 30.6925 12.3165 30.736 12.2524C30.7781 12.1888 30.8143 12.1211 30.8436 12.0506C30.8729 11.9791 30.8953 11.9054 30.9104 11.8317C30.9255 11.7553 30.9329 11.6779 30.9329 11.602C30.9329 11.5255 30.9255 11.4481 30.9104 11.3735C30.8953 11.2985 30.8729 11.2243 30.8436 11.1543C30.8143 11.0828 30.7781 11.0146 30.736 10.9515C30.6925 10.8869 30.6445 10.8274 30.5895 10.7734C30.5355 10.7194 30.4756 10.6704 30.4115 10.6278C30.3478 10.5857 30.2801 10.5496 30.2096 10.5203C30.1382 10.491 30.0654 10.4685 29.9908 10.4534C29.8393 10.4228 29.6836 10.4228 29.5326 10.4534C29.4575 10.4685 29.3833 10.491 29.3133 10.5203C29.2428 10.5496 29.1737 10.5857 29.1105 10.6278C29.046 10.6704 28.9865 10.7194 28.9324 10.7734C28.8784 10.8274 28.8294 10.8869 28.7869 10.9515C28.7448 11.0146 28.7086 11.0828 28.6793 11.1543C28.65 11.2243 28.6276 11.2985 28.6125 11.3735C28.5974 11.4481 28.5891 11.5255 28.5891 11.602C28.5891 11.6779 28.5974 11.7553 28.6125 11.8317C28.6276 11.9054 28.65 11.9791 28.6793 12.0506Z" fill="#0A1F69"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,5 +0,0 @@
<svg width="65" height="71" viewBox="0 0 65 71" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32.5" cy="38" r="32.5" fill="#F2F5FF"/>
<path d="M34.8327 45.2796C39.8898 36.8918 46.2816 25.1 46.2816 19.0388C46.2816 8.81429 37.9673 0.5 27.7429 0.5C17.5184 0.5 9.20408 8.81429 9.20408 19.0388C9.20408 25.1 15.5959 36.8918 20.6531 45.2796C14.6531 45.7327 1 47.3612 1 52.7612C1 58.0755 14.8612 60.5 27.7551 60.5C40.6367 60.5 54.5102 58.0755 54.5102 52.7612C54.498 47.3612 40.8327 45.7327 34.8327 45.2796ZM11.7633 19.0388C11.7633 10.2224 18.9388 3.04694 27.7429 3.04694C36.5592 3.04694 43.7347 10.2224 43.7347 19.0388C43.7347 26.7408 31.4653 46.0633 27.7429 51.7449C24.0204 46.0633 11.7633 26.7408 11.7633 19.0388ZM27.7429 57.9408C12.0816 57.9408 3.54694 54.5122 3.54694 52.749C3.54694 51.3898 9.17959 48.4633 22.1592 47.7163C24.5959 51.6469 26.4449 54.4265 26.6898 54.7571C27.0204 55.198 27.9878 55.7245 28.8082 54.7571C29.0653 54.451 30.902 51.6469 33.3388 47.7163C46.3184 48.4633 51.951 51.3776 51.951 52.749C51.9388 54.5245 43.4041 57.9408 27.7429 57.9408Z" fill="#0A1F69"/>
<path d="M35.8121 19.0387C35.8121 14.5816 32.1999 10.9694 27.7427 10.9694C23.2856 10.9694 19.6733 14.5816 19.6733 19.0387C19.6733 23.4959 23.2856 27.1081 27.7427 27.1081C32.1999 27.1081 35.8121 23.4959 35.8121 19.0387ZM22.2203 19.0387C22.2203 15.9898 24.6937 13.5163 27.7427 13.5163C30.7917 13.5163 33.2652 15.9898 33.2652 19.0387C33.2652 22.0877 30.7917 24.5612 27.7427 24.5612C24.6937 24.5612 22.2203 22.0877 22.2203 19.0387Z" fill="#0A1F69"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

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

@@ -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);
}

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

@@ -0,0 +1,136 @@
<template>
<div class="entry-edit">
<form
v-if="isEditTitle && !isDisabled"
@submit.prevent="isEditTitle = false"
class="edit-form"
>
<div class="wrapper-input">
<Input
:text="text || ''"
:placeholder="$t('pages.upload.edit_placeholder_input')"
@input="changeTextValue"
/>
<div class="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,32 @@ 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

@@ -1,20 +1,22 @@
<template>
<div class="icon-block">
<slot></slot>
<p class="license-text">
{{
$t('pages.share_pictures.information_text2', {
word: 'la license'
})
}}
</p>
<Link
:href="url"
:text="text"
target="_blank"
type="external"
look="link--grey"
/>
<div class="text-block">
<p class="license-text">
{{
$t('pages.share_pictures.information_text2', {
word: 'la licence'
})
}}
</p>
<Link
:href="url"
:text="text"
target="_blank"
type="external"
look="link--grey"
/>
</div>
</div>
</template>
@@ -29,8 +31,15 @@ defineProps({
<style scoped lang="scss">
.icon-block {
display: flex;
font-weight: 600;
}
.text-block {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.license-text {
font-weight: bold;
margin-right: toRem(0.5);
margin-bottom: 0;
}

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;
@@ -116,6 +116,7 @@ function triggerButton() {
.link--grey {
color: var(--grey-semi-dark);
text-decoration: underline;
font-weight: inherit;
}
.link--blue-dark {
color: var(--blue-dark);
@@ -199,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

@@ -4,7 +4,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import type { ViewerInterface } from '@/views/interfaces/common'
import type { ViewerInterface, MapInterface } from '@/views/interfaces/common'
import { getIgnTiles } from '@/utils/mapAndViewer'
import { Viewer, StandaloneMap } from 'geovisio'
import { createUrlLink } from '@/utils'
@@ -17,7 +17,8 @@ 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: '' }
})
defineExpose({
viewer
@@ -28,6 +29,7 @@ onMounted(async () => {
const zoom = import.meta.env.VITE_ZOOM
const center = import.meta.env.VITE_CENTER
let paramsViewer: ViewerInterface
let paramsMap: MapInterface
try {
if (props.geovisioViewer) {
@@ -74,7 +76,7 @@ onMounted(async () => {
}
const reportLink = document.createElement('div')
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
viewer.value = await new Viewer(
viewer.value = new Viewer(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
@@ -98,10 +100,32 @@ onMounted(async () => {
)
}
} else {
viewer.value = await new StandaloneMap(
paramsMap = { minZoom: 7 }
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsMap = {
...paramsMap,
style
}
}
const bbox = [props.bbox[0], props.bbox[1], props.bbox[2], props.bbox[3]]
viewer.value = new StandaloneMap(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`
`${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
} catch (err) {

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;

184
src/locales/en.json Normal file
View File

@@ -0,0 +1,184 @@
{
"general": {
"title": "Panoramax instance",
"meta": {
"title": "Panoramax instance",
"description": "Panoramax, the free alternative to photo-mapping territories"
},
"header": {
"login_text": "Connect",
"register_text": "Register",
"contribute_text": "Why contribute ?",
"my_account": "My account",
"upload_text": "+ Share pictures",
"sequences_text": "My pictures",
"alt_logo": "Instance logo",
"alt_photos": "Pictures icon",
"alt_information": "User icon",
"alt_settings": "Parameters icon",
"alt_logout": "Logout icon",
"title": "Panoramax",
"beta_text": "Beta version",
"logout_text": "Logout",
"my_information_text": "My details",
"my_settings_text": "My parameters",
"burger_menu_aria_label_open": "Show menu",
"burger_menu_aria_label_closed": "Hide menu"
},
"footer": {
"panoramax_site": "Discover Panoramax",
"information_gitlab": "Show source code",
"gitlab_logo": "Gitlab logo",
"ay11_text": "Accessibility: not compliant"
},
"error_text": "An error occured",
"success_text": "Update done"
},
"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"
},
"settings": {
"title": "My tokens",
"setting_tooltip": "Show or hide token"
},
"sequence": {
"sequence_published": "Published",
"sequence_waiting": "Still processing",
"sequence_hidden": "Hidden",
"hide_sequence_tooltip": "Hide this sequences",
"delete_sequence_tooltip": "Permanently delete this sequence",
"hide_photo_tooltip": "Hide selected pictures",
"delete_photo_tooltip": "Permanently delete selected pictures",
"confirm_pictures_dialog": "⚠️ Selected photos will be permanently deleted",
"confirm_sequence_dialog": "⚠️ This sequence will be permanently deleted",
"created": "Uploaded :",
"taken": "Shot on :",
"duration": "Duration :",
"duration_begin": "Start :",
"duration_end": "End :",
"camera": "Camera :",
"button_delete": "Delete",
"button_disable": "Hide",
"button_enable": "Show",
"picture_selected": "{count} picture selected| {count} pictures selected",
"hours": "{count} hour| {count} hours",
"minutes": "{count} minute| {count} minutes",
"seconds": "{count} second| {count} seconds",
"select_text": "Select all",
"unselect_text": "Deselect all",
"select_shift_text": "Select multiple photos with shift",
"waiting_process": "Photo in process",
"broken": "Photo error processing",
"no_image": "No picture in this sequence"
},
"sequences": {
"title": "My sequences",
"sequence_name": "Name",
"sequence_photos": "Photos",
"sequence_date": "Shot on",
"sequence_creation": "Upload",
"sequence_status": "Status",
"sequence_published": "Published",
"sequence_waiting": "Still processing",
"sequence_hidden": "Hidden",
"no_sequences_text": "You have no photos published yet \uD83D\uDE22",
"button_upload": "Upload pictures",
"sequence_deleted": "The sequence has been deleted"
},
"share_pictures": {
"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",
"card_photo2": "Photos published in 360° format or not",
"card_photo3": "Easily reusable photos",
"card_photo4": "A quick and easy image contribution",
"card_alt_photo1": "Image of a building",
"card_alt_photo2": "Image showing 360-degree ",
"card_alt_photo3": "Image showing a map with a pointer",
"card_alt_photo4": "Image representing a pointer",
"card_description1": "All photos taken from the public highway are accepted, as long as they are geolocated and viewed from the ground.",
"card_description2": "360° pictures are not mandatory: photos taken with a smartphone are all that's needed. Dates, locations and jpg format are the only prerequisites.",
"card_description3": "All photos easily accessible and reusable without an account: via the website or a standard API (STAC standard).",
"card_description4": "Several tools are available to facilitate contributions, including a command line and a web interface.",
"upload_subtitle": "Simply upload your images online",
"upload_illustration_alt": "Illustration showing online photo uploading",
"upload_description": "Panoramax's web application lets you upload all your field photos in JPEG format at the click of a button. No programming skills are required. For larger numbers, however, we recommend using the command-line tool",
"upload_button": "+ Upload pictures",
"command_line_subtitle": "Command line tool",
"comment_install": "Install the geovisio command-line tool",
"comment_upload": "LanceStart the image upload command on the chosen folder",
"description_terminal": "<a href='https://gitlab.com/geovisio/cli' target='_blank' style='color:black'>The CLI</a> lets you share large volumes of photos. The procedure is simple and requires <a target='_blank' href='https://www.python.org/downloads/' style='color:black'>python (version 3.8 or above)</a>.\n\nThe tool will ask for your login details before importing. Once the pictures have been uploaded, a processing time is required before publication.",
"terminal_install": "pip install geovisio_cli",
"terminal_text": "geovisio upload --api-url {url} <DOSSIER_PHOTOS>",
"button_copy": "Copy",
"information_subtitle": "Here, your photos are accessible to all : ",
"information_text1": "Automatically blurred in compliance with legislation.",
"information_text2": "The uploaded pictures will be published under {word}",
"information_text3": "In its original format and resolution for various reuse.",
"information_about_title": "Need to access pictures ?",
"information_about_description": "An API is available to retrieve all metadata and pictures. <a href='{docLink}' target='_blank' style='color:#0a1f69'>\nFind out more here</a>\nData is also displayed in the form <a href='{docTiles}' target='_blank' style='color:#0a1f69'>of vector tiles</a>",
"doc_subtitle": "Need help contributing to Panoramax?",
"doc_description": "Panoramax documentation is available from us, and you can access tutorials on the geo-commons forum.",
"doc_button": "See the documentation",
"doc_illustration_alt": "Illustration of a character with a sheet of documents"
},
"upload": {
"title": "Contribute to the Panoramax project",
"description": "For large volumes of pictures, the command line tool is more suitable.",
"know_more_button": "Read more",
"input_label": "Drag your pictures here or click on ",
"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...",
"sequence_title": "Sequence ",
"import": "Uploads",
"upload_pending": "Upload in progress...",
"images_count_text": "Pictures uploaded",
"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.",
"leave_message": "⚠️ WARNING, the download will be interrupted if you leave the page before the end.",
"error_button": "Show errors",
"modal_error_title": "Pictures in error"
},
"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

@@ -26,7 +26,7 @@
"burger_menu_aria_label_closed": "Masquer le menu"
},
"footer": {
"panoramax_site": "Découvrir Panoramax.fr",
"panoramax_site": "Découvrir Panoramax",
"information_gitlab": "Voir le code",
"gitlab_logo": "Logo Gitlab",
"ay11_text": "Accessibilité : non conforme"
@@ -89,7 +89,7 @@
},
"share_pictures": {
"title": "Pourquoi contribuer à la base de photographies de Panoramax ?",
"description": "Contribuer à Panoramax, c'est participer au développement d'un géo-commun, une ressource numérique souveraine, libre et réutilisable. Chaque photo géolocalisée publiée sur Panoramax est utilisable par tous et pour des usages variés, par exemple par une collectivité territoriale qui a besoin d'observer létat de la voirie ou par un opérateur de télécommunications pour préparer une intervention.\n\nChaque contributeur peut envoyer ses séquences, les modifier et les consulter tous comme l'ensemble des vues - 360° ou non - versées par la communauté. Le floutage obligatoire des visages et plaques d'immatriculation est automatisé sur la plateforme.",
"description": "Contribuer à Panoramax, c'est participer au développement d'un géo-commun, une ressource numérique souveraine, libre et réutilisable. Chaque photo géolocalisée publiée sur Panoramax est utilisable par tous et pour des usages variés, par exemple par une collectivité territoriale qui a besoin d'observer létat de la voirie ou par un opérateur de télécommunications pour préparer une intervention.\n\nChaque contributeur peut envoyer ses séquences d'images, les modifier et les consulter tout comme l'ensemble des vues - 360° ou non - versées par la communauté. Le floutage obligatoire des visages et plaques d'immatriculation est automatisé sur la plateforme.",
"alt_img_map": "Illustration d'une femme qui regarde une carte avec son smartphone geolocalisé",
"card_photo1": "Des lieux visibles depuis la voie publique",
"card_photo2": "Des photos publiées au format 360° ou non",
@@ -116,7 +116,7 @@
"button_copy": "Copier",
"information_subtitle": "Ici, vos photos sont accessibles à tous : ",
"information_text1": "Automatiquement floutées dans le respect de la législation.",
"information_text2": "Les données déposées seront sous {word}.",
"information_text2": "Les données déposées seront publiées sous {word}",
"information_text3": "Sous forme «brute» pour des réutilisations variées (ex: préparation des chantiers).",
"information_about_title": "Besoin de récupérer les données ?",
"information_about_description": "Une API est mise à disposition pour récupérer toutes les données. <a href='{docLink}' target='_blank' style='color:#0a1f69'>\nRetrouvez sa documentation ici</a>\nLes données sont aussi exposées sous forme <a href='{docTiles}' target='_blank' style='color:#0a1f69'>de tuiles vectorielles</a>",
@@ -133,9 +133,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 +145,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.",

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

@@ -0,0 +1,184 @@
{
"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"
},
"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

@@ -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

@@ -1,30 +1,16 @@
<template>
<main class="entry-page">
<section :class="['entry-section', focus]">
<section class="entry-section">
<Viewer ref="viewerRef" />
</section>
</main>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
import { ref } from 'vue'
import Viewer from '@/components/Viewer.vue'
const emit = defineEmits<{ (e: 'trigger', value: string): void }>()
const viewerRef = ref<any>(null)
let focus = ref<string>('focus-map')
watchEffect(() => {
if (viewerRef.value && viewerRef.value.viewer) {
viewerRef.value.viewer.addEventListener(
'focus-changed',
(e: { detail: { focus: string } }) => {
if (e.detail.focus === 'pic') focus.value = 'focus-pic'
else focus.value = 'focus-map'
emit('trigger', focus.value)
}
)
}
})
const viewerRef = ref<unknown>(null)
</script>
<style scoped lang="scss">
.entry-page {
@@ -32,23 +18,22 @@ watchEffect(() => {
}
.entry-section {
width: 100%;
}
.focus-pic {
height: calc(100vh - #{toRem(8)});
}
.focus-map {
height: calc(100vh - #{toRem(13.2)});
}
.focus-pic,
.logged .entry-section {
height: calc(100vh - #{toRem(8)});
}
.gvs-focus-map .entry-report-button {
display: none;
}
@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()
@@ -430,7 +429,7 @@ 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')

View File

@@ -1,98 +1,104 @@
<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}`"
class="sequence-item"
v-for="item in userSequences"
:class="[
'sequence-item',
item.id === seqId ? 'button-item-hover' : ''
]"
@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"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
lazy="loading"
alt=""
class="thumb"
/>
<div class="wrapper-thumb-hover">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
lazy="loading"
alt=""
class="thumb-hover"
/>
@@ -130,6 +136,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 +170,211 @@
<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 } 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>('')
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<any>(null)
const headerList = ref<any>(null)
const list = ref<any>(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()
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()
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
}
)
isSorted.value = !isSorted.value
userSequences.value = sorted
function onResizeMap(width: any): void {
if (width) {
width.value = width
mapWidth.value = width.value.width
listWidth.value = window.innerWidth - width.value.width
}
}
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
}
)
isSorted.value = !isSorted.value
userSequences.value = sorted
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()
}
}
function goToSequence(sequence: SequenceLinkInterface) {
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)}`
)
selfLink.value = data.links.filter(
(el: SequenceLinkInterface) => el.rel === 'self'
)
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getRelChild(data.links)
isSorted.value = !isSorted.value
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 {
seqId.value = sequence.id
viewerRef.value.viewer.select(seqId.value)
const currentBbox = [
viewerRef.value.viewer._map.getBounds()._ne.lng,
viewerRef.value.viewer._map.getBounds()._ne.lat,
viewerRef.value.viewer._map.getBounds()._sw.lng,
viewerRef.value.viewer._map.getBounds()._sw.lat
]
if (bboxIsInsideOther(currentBbox, sequence.extent.spatial.bbox[0])) return
viewerRef.value.viewer._map.flyTo({
center: [
sequence.extent.spatial.bbox[0][0],
sequence.extent.spatial.bbox[0][1]
],
zoom: 10,
duration: 0
})
}
function getRelChild(sequences: SequenceLinkInterface[]) {
function 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
console.log(userSequences.value)
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
@@ -256,26 +388,47 @@ watchEffect(() => {
(e: { detail: { seqId: string } }) => {
seqId.value = e.detail.seqId
scrollIntoSelected(e.detail.seqId, userSequences.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,22 +443,60 @@ 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);
}
}
.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);
}
}
.button-item-hover {
background-color: var(--blue);
&:nth-child(2n) {
background-color: var(--blue);
}
.button-item {
& > *,
& > :nth-child(2) {
color: var(--white);
}
}
}
.item-head-fixed {
position: fixed;
top: toRem(8);
width: 100%;
z-index: 2;
}
.sequence-item-head {
margin-bottom: toRem(1);
padding: toRem(1) toRem(2);
border-bottom: toRem(0.1) solid var(--grey);
border-radius: toRem(2) toRem(2) 0rem 0rem;
background-color: var(--white);
}
.sequence-item-head:hover {
background-color: var(--white);
color: initial;
}
.wrapper-title {
display: flex;
align-items: center;
@@ -350,7 +541,6 @@ watchEffect(() => {
display: flex;
align-items: center;
width: 100%;
padding: toRem(2);
background-color: transparent;
border: none;
text-decoration: none;
@@ -374,26 +564,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 +599,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 +631,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 +649,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

@@ -87,7 +87,7 @@ onMounted(async () => {
<style lang="scss" scoped>
.entry-page {
padding: toRem(5);
min-height: calc(100vh - #{toRem(19.82)});
min-height: calc(100vh - #{toRem(8)});
background-color: var(--grey);
}
.settings-title {

View File

@@ -37,7 +37,7 @@
<Card
:title="$t('pages.share_pictures.card_photo3')"
:description="$t('pages.share_pictures.card_description3')"
img-src="map-pointer.svg"
img-src="photos.svg"
:img-alt="$t('pages.share_pictures.card_alt_photo3')"
/>
</li>
@@ -45,7 +45,7 @@
<Card
:title="$t('pages.share_pictures.card_photo4')"
:description="$t('pages.share_pictures.card_description4')"
img-src="pointer.svg"
img-src="cloud.svg"
:img-alt="$t('pages.share_pictures.card_alt_photo4')"
/>
</li>
@@ -282,7 +282,12 @@ ul {
.entry-information-card {
margin-top: toRem(2);
}
.icon-block {
display: flex;
align-items: center;
}
.icon-block-img {
height: toRem(2);
margin-right: toRem(0.5);
}
.about-subtitle {

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: [],
@@ -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

@@ -1,6 +1,25 @@
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
}
}
function createASequence(title: string): Promise<SequenceCreatedInterface> {
return axios.post('api/collections', { title: title })
}

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