30 Commits

Author SHA1 Message Date
Andreani Jean
facaa4aa07 Release 2.1.2 2023-09-12 21:37:49 +02:00
Andreani Jean
6830bd5616 Release 2.1.1 2023-09-06 09:47:22 +02:00
Jean Andreani
692408de0f Merge branch 'fix/css-report-button' into 'develop'
fix version

See merge request geovisio/website!88
2023-09-06 07:43:34 +00:00
Jean Andreani
e2dce48730 fix version 2023-09-06 07:43:34 +00:00
Jean Andreani
6e021d1611 Merge branch 'fix/css-report-button' into 'develop'
fix css report button

See merge request geovisio/website!87
2023-09-06 07:12:18 +00:00
Andreani Jean
157bf0f01e fix css report button 2023-09-06 09:09:02 +02:00
Jean Andreani
2bc18bf494 Merge branch 'fix/maxzoom-doc' into 'develop'
Add doc for VITE_MAX_ZOOM env var

See merge request geovisio/website!86
2023-09-05 17:27:30 +00:00
Adrien Pavie
74e82d5352 Add doc for VITE_MAX_ZOOM env var 2023-09-05 17:27:30 +00:00
Jean Andreani
4999142e0d Merge branch 'fix-feat/some-bugs-add-report-button-viewer' into 'develop'
fix-feat/format date - add report button - license from API - maxZoom in ENV var

See merge request geovisio/website!85
2023-09-05 16:18:39 +00:00
Jean Andreani
6955d71752 fix-feat/format date - add report button - license from API - maxZoom in ENV var 2023-09-05 16:18:39 +00:00
Adrien Pavie
d72f01bbe0 Merge branch 'feature/docker2' into 'develop'
Improve Dockerfile

See merge request geovisio/website!83
2023-08-31 08:26:07 +00:00
Adrien Pavie
f9d1d154e0 Improve Dockerfile 2023-08-31 08:26:07 +00:00
Jean Andreani
d3a83f5157 Merge branch 'feature/gvs-213' into 'develop'
Web viewer update to 2.1.3, minor docs updates

See merge request geovisio/website!82
2023-08-30 11:01:52 +00:00
Adrien Pavie
c551ebdb21 Web viewer update to 2.1.3, minor docs updates 2023-08-30 11:01:52 +00:00
Andreani Jean
8f7e35dc43 Release 2.1.0 2023-08-29 14:56:25 +02:00
Jean Andreani
e0b17ae423 Merge branch 'fix-url-upload-responsive-sequences' into 'develop'
fix css my sequences

See merge request geovisio/website!81
2023-08-29 12:13:22 +00:00
Andreani Jean
876d0f1683 fix css my sequences 2023-08-29 14:08:02 +02:00
Jean Andreani
86c0e59c14 Merge branch 'fix-url-upload-responsive-sequences' into 'develop'
fix url upload to /envoyer + fix css responsive

See merge request geovisio/website!80
2023-08-29 10:24:34 +00:00
Andreani Jean
25c5833e26 fix url upload to /envoyer + fix css responsive 2023-08-29 12:21:42 +02:00
Jean Andreani
9e4b1f87f4 Merge branch 'feat-add-sort-file' into 'develop'
Feat add sorting for file upload

See merge request geovisio/website!79
2023-08-29 09:51:05 +00:00
Jean Andreani
35d95a85f8 Feat add sorting for file upload 2023-08-29 09:51:05 +00:00
Andreani Jean
6383fafcbf add fix button 2023-08-28 15:04:32 +02:00
Jean Andreani
c64604e4b7 Merge branch 'fix-authconf' into 'main'
fix-authconf

See merge request geovisio/website!78
2023-08-28 09:37:15 +00:00
Jean Andreani
197cdba0ca fix-authconf 2023-08-28 09:37:14 +00:00
Jean Andreani
456704f449 Merge branch 'feat/upload-interface' into 'main'
Feat/upload interface

See merge request geovisio/website!70
2023-08-28 09:23:20 +00:00
Jean Andreani
7f570e4086 Feat/upload interface 2023-08-28 09:23:19 +00:00
Adrien Pavie
0d39d522a5 Merge branch 'feature/gvs-212' into 'develop'
Update GeoVisio to 2.1.2

See merge request geovisio/website!77
2023-08-20 20:22:57 +00:00
Adrien Pavie
b708543df1 Update GeoVisio to 2.1.2 2023-08-20 22:18:36 +02:00
Adrien Pavie
fe8112356e Merge branch 'feature/docker' into 'main'
Add Dockerfile and CI Docker Hub deployment (fixes #16)

Closes #16

See merge request geovisio/website!74
2023-08-16 08:09:09 +00:00
Adrien Pavie
92fc0302a4 Add Dockerfile and CI Docker Hub deployment (fixes #16) 2023-08-03 09:02:44 +02:00
77 changed files with 4428 additions and 2493 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
docs/
.git/
.idea/
dist/
node_modules/
*.md
LICENSE

5
.gitignore vendored
View File

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

View File

@@ -5,6 +5,12 @@ stages:
variables:
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress'
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
CI_IMAGE_CACHE: $GITLAB_REGISTRY/$IMAGE_NAME:build_cache
DOCKER_TLS_CERTDIR: ""
before_script:
## chmod is unfortunately currently mandatory : https://github.com/nodejs/docker-node/issues/661
@@ -57,3 +63,71 @@ deploy:
script:
- yarn install
- yarn build
deploy:develop:
rules:
# run job only for fork that have the credentials to pull images from the gitlab-registry
# and only for merge on 'develop' branch
- if: $CI_DEPLOY_PASSWORD == null || $CI_DEPLOY_USER == null
when: never
- if: $CI_COMMIT_REF_SLUG == "develop"
stage: Deploy
image: docker:latest
services:
- 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
- docker buildx create --use --name "geovisio-image-builder" --driver=docker-container # use docker-container driver to be able to publish a full cache
# login to dockerhub
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
# build image using repository as cache
- docker buildx build
--cache-from "type=registry,ref=$CI_IMAGE_CACHE"
--cache-to "type=registry,mode=max,ref=$CI_IMAGE_CACHE"
--tag "$CI_REGISTRY_IMAGE:develop"
--label "org.opencontainers.image.title=$CI_PROJECT_TITLE"
--label "org.opencontainers.image.url=$CI_PROJECT_URL"
--label "org.opencontainers.image.created=$CI_JOB_STARTED_AT"
--label "org.opencontainers.image.revision=$CI_COMMIT_SHORT_SHA"
--load
--progress=plain
.
# publish image to dockerhub with the develop tag
- docker push "$CI_REGISTRY_IMAGE:develop"
deploy:latest:
# we consider that tag always land on main
# and they always should publish a tagged image and the `latest` docker image
only:
- tags
stage: Deploy
image: docker:latest
services:
- 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
- docker buildx create --use --name "geovisio-image-builder" --driver=docker-container # use docker-container driver to be able to publish a full cache
# login to dockerhub
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
# build image using repository as cache
- docker buildx build
--cache-from "type=registry,ref=$CI_IMAGE_CACHE"
--cache-to "type=registry,mode=max,ref=$CI_IMAGE_CACHE"
--tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"
--tag "$CI_REGISTRY_IMAGE:latest"
--label "org.opencontainers.image.title=$CI_PROJECT_TITLE"
--label "org.opencontainers.image.url=$CI_PROJECT_URL"
--label "org.opencontainers.image.created=$CI_JOB_STARTED_AT"
--label "org.opencontainers.image.revision=$GIT_DESCRIBE"
--load
--progress=plain
.
# publish image to dockerhub
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker push $CI_REGISTRY_IMAGE:latest

View File

@@ -2,14 +2,52 @@
All notable changes to this project will be documented in this file.
Before _0.1.0_ Changelog didn't exist.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] -
Before _0.1.0_, website development was on rolling release, meaning there are no version tags.
## [2.1.2] - 2023-09-12
### Changed
- add a fix to center the map with ENV variables
## [2.1.1] - 2023-09-06
### Changed
- GeoVisio web viewer upgraded to 2.1.4, [with alls its changes embedded](https://gitlab.com/geovisio/web-viewer/-/blob/develop/CHANGELOG.md?ref_type=heads#213-2023-08-30).
- Dockerfile creates smaller and faster containers, using pre-built website and Nginx for HTTP serving.
- In the upload input you can now choose between gallery and camera on mobile IOS
- Some CSS fix for the responsive of one Sequence
- Insert the report button in the viewer by passing a params
### Added
- Get the License with the API route
- Add ENV var for maxZoom params of the viewer
## [2.1.0] - 2023-08-29
### Added
- A new page `/envoyer` to upload picture with an interface ([#13](https://gitlab.com/geovisio/website/-/issues/13)) :
- the user can upload multiples pictures with the interface
- the pictures are sorted by name
- the user can see all the pictures uploaded and all the errors
### Changed
- Website releases now follow the synced `MAJOR.MINOR` API version rule, meaning that any version >= 2.1 of the website will be compatible with corresponding [GeoVisio API](https://gitlab.com/geovisio/api) version.
### Fixed
- fix a bug in the header hidden sub menu when authentication is not with keycloak
## [0.1.0] - 2023-07-04
### Added
- A new page `/mes-sequences` to access to a list of sequences for a logged user ([#14](https://gitlab.com/geovisio/website/-/issues/14)) :
- the user can see all his sequences
- the user can filter sequences
- the user can enter to a specific sequence
@@ -20,10 +58,13 @@ Before _0.1.0_ Changelog didn't exist.
- the user can see all the sequence's photos
- the user can disable and delete one or many photo(s) of the sequence
### Changed
- 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
### Fixed
[Unreleased]: https://gitlab.com/geovisio/website/-/compare/2.1.0...develop
[2.1.1]: https://gitlab.com/geovisio/website/-/compare/2.1.0...2.1.1
[2.1.0]: https://gitlab.com/geovisio/website/-/compare/0.1.0...2.1.0
[0.1.0]: https://gitlab.com/geovisio/website/-/commits/0.1.0

55
Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
#--------------------------------------------------------------
#- Build image
#-
FROM node:18.16.0-alpine AS build
WORKDIR /opt/geovisio
# Import dependencies files
COPY package.json yarn.lock ./
# Install NodeJS dependencies
RUN yarn install --frozen-lockfile
# Import source code
COPY static ./static
COPY src ./src
COPY *.json *.js *.ts *.html ./
# Replace env variables by placeholder for dynamic change on container start
ENV VITE_INSTANCE_NAME=DOCKER_VITE_INSTANCE_NAME
ENV VITE_API_URL=DOCKER_VITE_API_URL
ENV VITE_TILES=DOCKER_VITE_TILES
ENV VITE_MAX_ZOOM=DOCKER_VITE_MAX_ZOOM
# Build code
ENV PORT=3000
RUN yarn deploy
#--------------------------------------------------------------
#- Final image
#-
FROM nginx:1-alpine
RUN apk add bash
# Retrieve files from build
COPY --from=build /opt/geovisio/dist /usr/share/nginx/html
# Add Docker scripts and Nginx conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/docker-entrypoint.sh /etc/nginx/docker-entrypoint.sh
RUN chmod +x /etc/nginx/docker-entrypoint.sh
# Define env variables defaults
ENV VITE_INSTANCE_NAME="GeoVisio/Docker"
ENV VITE_API_URL="https://panoramax.openstreetmap.fr"
ENV VITE_TILES="https://tile-vect.openstreetmap.fr/styles/basic/style.json"
ENV VITE_MAX_ZOOM=""
# Start Nginx
EXPOSE 3000
ENTRYPOINT ["/etc/nginx/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -12,11 +12,6 @@ export default defineConfig({
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:5173',
supportFile: 'src/tests/cypress/support/e2e.{js,jsx,ts,tsx}',
specPattern: 'src/tests/cypress/**/*.cy.{js,jsx,ts,tsx}',
fixturesFolder: 'src/tests/cypress/fixtures',
videosFolder: 'src/tests/cypress/videos',
screenshotsFolder: 'src/tests/cypress/screenshot'
baseUrl: 'http://localhost:5173'
}
})

View File

@@ -1,5 +1,5 @@
{
"addressToSearch": "97 boulevard Voltaire 75011 paris",
"textAddressToSelect": "Boulevard Voltaire, Quartier de la Folie-Méricourt, Paris 11e Arrondissement, Paris, Île-de-France, France métropolitaine, 75011, France",
"textLinkUpload": "Partager vos photos"
"textLinkUpload": "À propos"
}

View File

@@ -0,0 +1,16 @@
#!/bin/bash
ROOT_DIR=/usr/share/nginx/html
DOCKER_VARS=(VITE_INSTANCE_NAME VITE_API_URL VITE_TILES VITE_MAX_ZOOM)
echo "Setting env variables in web files"
for file in $ROOT_DIR/assets/*.js $ROOT_DIR/index.html; do
echo "Processing $file...";
for i in ${!DOCKER_VARS[@]}; do
sed -i "s|DOCKER_${DOCKER_VARS[i]}|${!DOCKER_VARS[i]}|g" $file
done
done
echo "GeoVisio website is now ready !"
exec "$@"

22
docker/nginx.conf Normal file
View File

@@ -0,0 +1,22 @@
events {
worker_connections 1024;
}
http {
include mime.types;
sendfile on;
server {
listen 3000;
listen [::]:3000;
resolver 127.0.0.11;
autoindex off;
server_name _;
server_tokens off;
root /usr/share/nginx/html;
gzip_static on;
}
}

View File

@@ -1,6 +1,15 @@
# Setup
## System requirements
GeoVisio website can be installed through classic method, or using Docker.
__Contents__
[[_TOC_]]
## Classic install
### System requirements
**You need to have [Nodejs installed](https://nodejs.org/en/download)**
Node version : >=18.13.0
@@ -9,7 +18,7 @@ Node version : >=18.13.0
You can use npm or [yarn](https://yarnpkg.com/) as package manager
## Install
### Install
The website can be installed locally by retrieving this repository and installing dependencies:
@@ -22,7 +31,7 @@ cd website/
npm install
```
## Build for production
### Build for production
Before building, you need to define a bit of settings. At least, you have to create a `.env` file and edit its content.
@@ -41,6 +50,33 @@ PORT=3000 npm run start
The website is now available at [localhost:3000](http://localhost:3000).
## Docker setup
The [Docker](https://docs.docker.com/get-docker/) deployment is a really convenient way to have a Geovisio website running in an easy and fast way. Note that this setup documentation only covers __GeoVisio front-end__ (website), if you also need an API running, please refer to [Docker API deployment](https://gitlab.com/geovisio/api/-/blob/develop/docs/14_Running_Docker.md).
You can use the provided __Docker Hub__ `geovisio/website:latest` image directly:
```bash
docker run \
-e VITE_API_URL="https://your.geovisio.api/" \
-e VITE_INSTANCE_NAME="My Own GeoVisio" \
-e VITE_TILES="https://your.geovisio.api/vector/tiles/style.json" \
-p 3000:3000 \
--name geovisio-website \
-d \
geovisio/website:latest
```
This will run a container bound on [localhost:3000](http://localhost:3000).
You can also build the image from the local source with:
```bash
docker build -t geovisio/website:latest .
```
## Next steps
You can check out [the available settings for your instance](./03_Settings.md).

View File

@@ -11,6 +11,7 @@ Available parameters are:
- `VITE_API_URL`: the URL to the GeoVisio API (with trailing `/`, example: `https://geovisio.fr/`)
- `VITE_INSTANCE_NAME`: the name of the instance (example: `IGN`)
- `VITE_TILES`: the URL of your tiles : default tiles are the Open Street Map Tiles (example: `https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json`)
- `VITE_MAX_ZOOM`: the max zoom to use on the map (defaults to 24).
- Settings for the work environment:
- `NPM_CONFIG_PRODUCTION`: is it production environment (`true`, `false`)
- `YARN_PRODUCTION`: same as below, but if you use Yarn instead of NPM

24
docs/90_Releases.md Normal file
View File

@@ -0,0 +1,24 @@
# Make a release
The web site uses [semantic versioning](https://semver.org/) for its release numbers.
__Note__ : make sure that versions are in-sync with other Website components. Each component can have different `PATCH` versions, but compatibility __must__ be ensured between `MAJOR.MINOR` versions.
Run these commands in order to issue a new release:
```bash
git checkout develop
vim package.json # Change version
npm run doc
vim CHANGELOG.md # Replace unreleased to version number and update versions links (at bottom)
git add *
git commit -m "Release x.x.x"
git tag -a x.x.x -m "Release x.x.x"
git push origin develop
git checkout main
git merge develop
git push origin main --tags
```

View File

@@ -1,6 +1,6 @@
{
"name": "geovisio-website",
"version": "1.0.0",
"version": "2.1.2",
"engines": {
"node": "18.16.0"
},
@@ -25,13 +25,13 @@
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"geovisio": "2.1.1",
"geovisio": "2.1.4",
"moment": "^2.29.4",
"pinia": "^2.1.4",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-eslint-parser": "^9.1.0",
"vue-i18n": "^9.2.2",
"vue-i18n": "9.2.2",
"vue-meta": "^3.0.0-alpha.10",
"vue-router": "^4.1.6",
"vue3-cookies": "^1.0.6",

View File

@@ -3,7 +3,7 @@ import Header from '@/components/Header.vue'
import { RouterView } from 'vue-router'
import { useMeta } from 'vue-meta'
import { useI18n } from 'vue-i18n'
import title from '@/utils/index'
import { title } from '@/utils/index'
import authConfig from './composables/auth'
const { authConf } = authConfig()
const { t } = useI18n()
@@ -26,8 +26,19 @@ useMeta({
<template v-slot:title="{ content }">{{ content }}</template>
</metainfo>
<Header
:auth-enabled="authConf.enabled"
:user-profile-url="authConf.user_profile ? authConf.user_profile.url : ''"
:auth-enabled="
authConf && authConf.auth && authConf.auth.enabled
? authConf.auth.enabled
: true
"
:user-profile-url="
authConf &&
authConf.auth &&
authConf.auth.user_profile &&
authConf.auth.user_profile.url
? authConf.auth.user_profile.url
: ''
"
/>
<RouterView />
</template>

View File

@@ -1,48 +1,48 @@
@mixin text($size) {
@if $size == h1 {
font-weight: normal;
font-size: 4rem;
font-size: toRem(4);
@media (max-width: 500px) {
font-size: 2.6rem;
@media (max-width: toRem(50)) {
font-size: toRem(2.6);
}
}
@if $size == h2 {
font-weight: normal;
font-size: 2rem;
font-size: toRem(2);
@media (max-width: 500px) {
font-size: 1.8rem;
@media (max-width: toRem(50)) {
font-size: toRem(1.8);
}
}
@if $size == h4 {
font-weight: normal;
font-size: 1.6rem;
font-size: toRem(1.6);
}
@if $size == m-regular {
font-size: 1.6rem;
font-size: toRem(1.6);
font-weight: normal;
}
@if $size == m-r-regular {
font-size: 1.6rem;
font-size: toRem(1.6);
font-weight: normal;
@media (max-width: 500px) {
font-size: 1.2rem;
@media (max-width: toRem(50)) {
font-size: toRem(1.2);
}
}
@if $size == s-regular {
font-size: 1.4rem;
font-size: toRem(1.4);
font-weight: normal;
}
@if $size == xs-r-regular {
font-size: 1.2rem;
font-size: toRem(1.2);
font-weight: normal;
@media (max-width: 500px) {
font-size: 1rem;
@media (max-width: toRem(50)) {
font-size: toRem(1);
}
}
@if $size == xss-regular {
font-size: 0.9rem;
font-size: toRem(0.9);
font-weight: normal;
}
}

View File

@@ -1,5 +1,4 @@
html {
font-size: 62.5%; /* 1rem = 10px */
height: -webkit-fill-available;
}
body {
@@ -25,17 +24,18 @@ h5 {
padding: 0;
list-style: none;
}
:root {
--white: #ffffff;
--black: #181818;
--black-pale: #1b1a17;
--red: #f70000;
--red-pale: #ff726f;
--grey: #e6e6e6;
--grey-pale: #cfd2cf;
--grey-semi-dark: #808080;
--grey-dark: #54595e;
--blue: #4945ff;
--blue-dark: #051f61;
--blue-geovisio: #34495e;
--blue-semi: rgba(207, 226, 255, 0.5);
--blue-pale: #f9fafd;
@@ -43,15 +43,16 @@ h5 {
--yellow: #fec868;
--orange: #ff6f00;
--green: #59ce8f;
--green-pale: #f0ffee;
}
@media (min-width: 1024px) {
@media (min-width: toRem(102.4)) {
body {
display: flex;
place-items: center;
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
@supports (-webkit-touch-callout: none) {
/* CSS specific to iOS devices */
body {

4
src/assets/rem-calc.scss Normal file
View File

@@ -0,0 +1,4 @@
@function toRem($value) {
$remValue: calc($value / 1.6) + rem;
@return $remValue;
}

View File

@@ -0,0 +1,3 @@
export function createLink(href: string, text: string): string {
return `<a href='mailto:signalement.ign@panoramax.fr${href}' target='_blank' title='${text}' class='gvs-btn gvs-widget-bg gvs-btn-large' style='font-size: 1.6em;display: block'><i class="bi bi-exclamation-triangle"></i></a>`
}

View File

@@ -6,39 +6,39 @@
</template>
<script lang="ts" setup>
import title from '@/utils/index'
import { title } from '@/utils/index'
</script>
<style lang="scss" scoped>
.beta {
@include text(xs-r-regular);
color: var(--red);
border: 1px solid var(--red);
border-radius: 0.5rem;
padding: 0.2rem 0.3rem;
margin-left: 0.5rem;
border: toRem(0.1) solid var(--red);
border-radius: toRem(0.5);
padding: toRem(0.2) toRem(0.3);
margin-left: toRem(0.5);
position: absolute;
top: -1rem;
right: -9.5rem;
width: 9rem;
top: toRem(-1);
right: toRem(-9.5);
width: toRem(9);
display: flex;
justify-content: center;
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.instance-beta {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--grey);
padding: 1rem;
padding: toRem(1);
margin-bottom: 0;
}
.beta {
position: relative;
top: initial;
right: initial;
margin-left: 1rem;
margin-left: toRem(1);
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<button
:disabled="isLoading || disabled"
type="button"
:class="[look, 'default', { disabled }]"
:type="type"
:class="[look, 'default', { disabled: disabled || isLoading }]"
@click="$emit('trigger')"
>
<i v-if="icon" :class="[icon, 'icon']"></i>
@@ -14,109 +14,120 @@
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
type TypeInterface = 'button' | 'submit' | 'reset'
defineProps({
icon: { type: String, default: null },
disabled: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false },
text: { type: String, default: '' },
tooltip: { type: String, default: '' },
look: { type: String, default: '' }
look: { type: String, default: '' },
type: { type: String as PropType<TypeInterface>, default: 'button' }
})
</script>
<style lang="scss" scoped>
@media (min-width: 768px) {
@media (min-width: toRem(76.8)) {
.default:hover {
opacity: 0.8;
}
}
.default {
height: toRem(3.5);
min-width: toRem(3.5);
@include text(s-regular);
display: flex;
align-items: center;
justify-content: center;
border: none;
background-color: transparent;
padding: 1rem;
position: relative;
z-index: 1;
border-radius: toRem(0.5);
padding: toRem(1.3) toRem(2) toRem(1.3);
.icon {
font-size: 2.5rem;
font-size: toRem(2);
}
&:hover .tooltip-button {
visibility: visible;
}
}
.button--black {
height: 3.5rem;
border-radius: 0.5rem;
padding: 1.3rem 2rem 1.3rem;
color: var(--white);
background-color: var(--black);
}
.button--blue {
color: var(--white);
background-color: var(--blue);
&.disabled {
opacity: 0.6;
color: var(--white);
cursor: not-allowed;
}
}
.button--transparent {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 0.1rem solid var(--white);
border: toRem(0.1) solid var(--white);
background-color: var(--black);
color: var(--white);
}
.button--red {
height: 3.5rem;
min-width: 3.5rem;
border-radius: 0.5rem;
color: var(--red);
background-color: var(--white);
border: 0.1rem solid var(--red);
border: toRem(0.1) solid var(--red);
.icon {
margin-right: 0;
font-size: 1.4rem;
font-size: toRem(1.4);
color: var(--red);
}
.text {
margin-left: 1rem;
margin-left: toRem(1);
}
}
.button--white {
height: 3.5rem;
border-radius: 0.5rem;
color: var(--black);
background-color: var(--white);
border: 0.1rem solid var(--black);
border: toRem(0.1) solid var(--black);
.icon {
font-size: 1.4rem;
font-size: toRem(1.4);
color: var(--black);
margin-right: 0;
}
.text {
margin-left: 1rem;
margin-left: toRem(1);
}
}
.no-text {
height: 3rem;
width: 3rem;
height: toRem(3);
width: toRem(3);
padding: 0;
.icon {
color: var(---black);
font-size: toRem(1.8);
margin-right: 0;
}
}
.link--grey {
color: var(--grey-semi-dark);
.icon {
font-size: 1.4rem;
font-size: toRem(1.4);
color: var(--grey-semi-dark);
}
}
.link--red {
height: 3rem;
color: var(--red);
background-color: var(--white);
.icon {
font-size: 1.4rem;
font-size: toRem(1.4);
color: var(--red);
}
}
.icon {
margin-right: 1rem;
font-size: 2rem;
margin-right: toRem(1);
font-size: toRem(2);
color: var(--white);
}
.button--rounded {
@@ -125,32 +136,28 @@ defineProps({
align-items: center;
border-radius: 50%;
padding: 0;
height: 2.5rem;
width: 2.5rem;
height: toRem(2.5);
width: toRem(2.5);
.icon {
color: var(---black);
font-size: 1.8rem;
font-size: toRem(1.8);
margin-right: 0;
}
}
.default .tooltip-button {
.tooltip-button {
background-color: var(--black);
color: var(--white);
text-align: center;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
border-radius: toRem(0.5);
padding: toRem(0.5) toRem(1);
position: absolute;
bottom: -100%;
visibility: hidden;
width: 18rem;
width: toRem(18);
right: 0;
@include text(xss-regular);
}
.default:hover .tooltip-button {
visibility: visible;
}
.disabled {
color: var(--grey-pale);
border-color: var(--grey-pale);

View File

@@ -6,12 +6,9 @@
<nav class="nav">
<div class="wrapper-logo desktop">
<Link
:image="{
url: 'logo.jpeg',
alt: $t('general.header.alt_logo')
}"
:image="{ url: 'logo.jpeg', alt: $t('general.header.alt_logo') }"
:text="title($t('general.header.title'))"
path="/"
:route="{ name: 'home' }"
/>
</div>
<div class="wrapper-logo responsive">
@@ -20,7 +17,7 @@
url: 'logo.jpeg',
alt: $t('general.header.alt_logo')
}"
path="/"
:route="{ name: 'home' }"
/>
</div>
<div ref="list" class="wrapper-entries">
@@ -32,13 +29,13 @@
<Link
:text="$t('general.header.sequences_text')"
icon="bi bi-images"
path="/mes-sequences"
:route="{ name: 'my-sequences' }"
@click.native="closeModal"
/>
</li>
<li v-if="userProfileUrl.length" class="logged-link">
<Link
path="/mes-informations"
:route="{ name: 'my-information' }"
icon="bi bi-person"
:text="$t('general.header.my_information_text')"
@click.native="closeModal"
@@ -46,7 +43,7 @@
</li>
<li class="logged-link">
<Link
path="/mes-parametres"
:route="{ name: 'my-settings' }"
icon="bi bi-gear"
:text="$t('general.header.my_settings_text')"
@click.native="closeModal"
@@ -57,17 +54,24 @@
type="external"
icon="bi bi-power"
:text="$t('general.header.logout_text')"
:path="getAuthRoute('auth/logout', route.path)"
:path-external="getAuthRoute('auth/logout', route.path)"
@click.native="closeModal"
/>
</li>
</ul>
<div class="wrapper-right-entries">
<div v-if="isLogged && authEnabled">
<Link
:text="$t('general.header.upload_text')"
look="button button--blue"
:route="{ name: 'upload-pictures' }"
@click.native="closeModal"
/>
</div>
<div>
<Link
:text="$t('general.header.contribute_text')"
look="button white"
path="/partager-des-photos"
:route="{ name: 'share-pictures' }"
@click.native="closeModal"
/>
</div>
@@ -89,7 +93,7 @@
<Link
type="external"
icon="bi bi-person-circle"
:path="getAuthRoute('auth/login', route.path)"
:path-external="getAuthRoute('auth/login', route.path)"
/>
</div>
</div>
@@ -105,7 +109,7 @@ import { useCookies } from 'vue3-cookies'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { getAuthRoute } from '@/utils/auth'
import title from '@/utils/index'
import { title } from '@/utils/index'
import Link from '@/components/Link.vue'
import BetaText from '@/components/BetaText.vue'
@@ -147,13 +151,13 @@ const userName = computed((): string =>
.header {
display: flex;
align-items: center;
height: 8rem;
border-bottom: 0.1rem solid var(--black);
height: toRem(8);
background-color: var(--blue-pale);
}
.nav {
width: 100%;
padding-right: 2rem;
padding-left: 2rem;
padding-right: toRem(2);
padding-left: toRem(2);
display: flex;
justify-content: space-between;
}
@@ -173,7 +177,7 @@ const userName = computed((): string =>
.wrapper-logo p {
@include text(m-r-regular);
margin-bottom: 0;
margin-left: 1rem;
margin-left: toRem(1);
position: relative;
}
.item-with-sub {
@@ -186,45 +190,42 @@ const userName = computed((): string =>
}
.sub-nav-block {
display: none;
border-radius: 0.5rem;
border: 1px solid var(--black);
border-radius: toRem(0.5);
border: toRem(0.1) solid var(--black);
background-color: var(--white);
position: absolute;
right: 0;
top: 3.5rem;
top: toRem(3.5);
z-index: 1;
width: 15rem;
width: toRem(15);
}
.logged-link {
display: flex;
padding: 0.5rem 2rem 0.7rem;
padding: toRem(0.5) toRem(2) toRem(0.7);
}
.logged-link:hover {
border-radius: 0.5rem;
border-radius: toRem(0.5);
background-color: var(--grey);
}
.nav-list-item {
margin-right: 1.5rem;
}
.menu-burger {
display: none;
margin-right: toRem(1.5);
}
.wrapper-right-entries {
display: flex;
align-items: center;
div:first-child {
margin-right: 2rem;
div {
margin-right: toRem(2);
}
}
.cross {
font-size: 2rem;
font-size: toRem(2);
}
.item-with-sub {
margin-right: 1.5rem;
margin-right: toRem(1.5);
}
.nav {
align-items: center;
padding: 1.5rem;
padding: toRem(1.5);
}
.nav-list {
display: none;
@@ -232,23 +233,23 @@ const userName = computed((): string =>
justify-content: center;
align-items: initial;
position: absolute;
width: 20rem;
top: 8rem;
width: toRem(20);
top: toRem(8);
right: 0;
z-index: 2;
background-color: var(--white);
box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%);
box-shadow: 0 toRem(0.2) toRem(0.4) rgb(0 0 0 / 10%);
padding-left: 0;
padding-top: 1rem;
padding-bottom: 1rem;
border-radius: 1rem;
padding-top: toRem(1);
padding-bottom: toRem(1);
border-radius: toRem(1);
}
.menu-burger {
display: block;
background-color: transparent;
border: none;
width: 2.5rem;
font-size: 2.5rem;
width: toRem(2.5);
font-size: toRem(2.5);
padding: 0;
.item-with-sub {
@include text(s-regular);
@@ -257,8 +258,8 @@ const userName = computed((): string =>
align-items: center;
background-color: var(--blue);
color: var(--white);
height: 3rem;
width: 3rem;
height: toRem(3);
width: toRem(3);
border-radius: 50%;
margin-right: 0;
}
@@ -266,19 +267,18 @@ const userName = computed((): string =>
.menu-open {
display: flex;
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.header {
flex-direction: column;
height: 11rem;
height: toRem(11);
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 4;
background: var(--white);
}
.nav-list {
top: 11rem;
top: toRem(11);
width: 100%;
}
.desktop {

View File

@@ -8,7 +8,7 @@
@click="$emit('trigger')"
>
<div
v-if="status.length && status !== 'waiting-for-process'"
v-if="status.length && (status === 'ready' || status === 'hidden')"
class="photo-img-wrapper"
>
<i v-if="status === 'hidden'" class="bi bi-eye-slash icon-hidden"></i>
@@ -20,8 +20,18 @@
class="photo-img"
/>
</div>
<div
v-else-if="status.length && status === 'broken'"
class="waiting-wrapper error"
>
<i class="bi bi-exclamation-octagon icon-waiting"></i>
<span class="waiting info">{{ $t('pages.sequence.broken') }}</span>
</div>
<div v-else class="waiting-wrapper">
<i class="bi bi-card-image icon-waiting"></i>
<span class="waiting info">{{
$t('pages.sequence.waiting_process')
}}</span>
</div>
<div
v-if="selectedOnMap && !selected"
@@ -36,22 +46,20 @@
>
<i class="bi bi-check-lg" />
</div>
<div v-if="status === 'waiting-for-process'" class="photo-info">
<span class="waiting info">{{
$t('pages.sequence.waiting_process')
}}</span>
</div>
<div v-else class="photo-info">
<div
v-if="status.length && (status === 'ready' || status === 'hidden')"
class="photo-info"
>
<span v-if="created" class="info"
><i class="bi bi-clock"></i> {{ created }}</span
>
<div class="button-info">
<Link
look="button--blue no-text"
look="button button--white-blue no-text"
icon="bi bi-cloud-download-fill"
type="external"
target="_blank"
:path="hrefHd"
:path-external="hrefHd"
/>
</div>
</div>
@@ -72,7 +80,7 @@ defineProps({
status: {
type: String,
validator: (value: string): boolean =>
['waiting-for-process', 'ready', 'hidden', ''].includes(value),
['waiting-for-process', 'ready', 'hidden', 'broken', ''].includes(value),
default: ''
}
})
@@ -87,8 +95,8 @@ defineProps({
position: relative;
}
.selected {
border: 0.1rem solid var(--blue);
border-radius: 0.5rem;
border: toRem(0.1) solid var(--blue);
border-radius: toRem(0.5);
box-shadow: 0px 4px 4px 0px #00000040;
}
.wrapper-image {
@@ -98,45 +106,64 @@ defineProps({
display: flex;
justify-content: center;
align-items: center;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
height: 16rem;
border-top-right-radius: toRem(0.5);
border-top-left-radius: toRem(0.5);
height: toRem(16);
width: 100%;
object-fit: cover;
}
.photo-img {
height: 100%;
width: 100%;
border-radius: 0.5rem;
border-radius: toRem(0.5);
}
.icon-hidden {
color: var(--grey-dark);
position: absolute;
font-size: 4rem;
font-size: toRem(4);
}
.waiting-wrapper {
height: 16rem;
height: toRem(16);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--blue);
margin-right: toRem(1);
margin-left: toRem(1);
&.error {
color: var(--red);
}
}
.icon-waiting {
height: 4rem;
font-size: 4rem;
font-size: toRem(3);
margin-bottom: toRem(1);
}
.waiting-info {
text-align: center;
width: 100%;
color: var(--black);
width: fit-content;
}
.info {
@include text(xs-r-regular);
padding: toRem(0.5) toRem(0.8);
background-color: var(--white);
border-radius: toRem(0.5);
}
.icon-img {
top: 1rem;
right: 1rem;
top: toRem(1);
right: toRem(1);
background-color: var(--white);
border-radius: 50%;
position: absolute;
height: 2rem;
width: 2rem;
height: toRem(2);
width: toRem(2);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.3rem;
font-size: toRem(1.3);
}
.pointer-map,
.button-check-pointer {
@@ -146,29 +173,16 @@ defineProps({
opacity: 1;
}
.photo-info {
height: 5rem;
height: toRem(5);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
padding: toRem(1);
position: absolute;
bottom: 0;
width: 100%;
}
.waiting {
text-align: center;
width: 100%;
color: var(--black);
}
.info {
padding: 0.5rem 0.8rem;
background-color: var(--white);
border-radius: 0.5rem;
color: var(--blue);
}
.button-image-item:hover {
.photo-img {
opacity: 0.5;

View File

@@ -52,8 +52,8 @@ function updateValue(value: boolean): void {
display: flex;
justify-content: center;
align-items: center;
height: 2rem;
width: 2rem;
height: toRem(2);
width: toRem(2);
}
.input {
-webkit-appearance: none;
@@ -66,7 +66,7 @@ function updateValue(value: boolean): void {
width: 100%;
}
.icon {
font-size: 2rem;
font-size: toRem(2);
position: absolute;
color: var(--grey-semi-dark);
}
@@ -76,7 +76,6 @@ function updateValue(value: boolean): void {
}
.label {
cursor: pointer;
margin-left: 0.5rem;
@include text(s-regular);
margin-left: toRem(0.5);
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<label
@dragover="dragover"
@dragleave="dragleave"
@drop="drop"
:class="['file-upload', { dragging: isDragging }]"
>
<input
ref="upload"
type="file"
multiple
:accept="accept"
class="input-file"
@change="changeFile"
/>
<i class="bi bi-cloud-upload-fill"></i>
<span v-if="text" class="input-text">
{{ text }}
<span v-if="textSecondPart" class="last-word">{{ textSecondPart }}</span>
</span>
<span v-if="textPictureType" class="input-text-type">{{
textPictureType
}}</span>
</label>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const emit = defineEmits<{ (e: 'trigger', value: FileList): void }>()
let isDragging = ref<boolean>(false)
defineProps({
text: { type: String, default: null },
textPictureType: { type: String, default: null },
textSecondPart: { type: String, default: null },
accept: { type: String, default: '' }
})
interface HTMLInputChangeEvent extends Event {
target: HTMLInputElement & EventTarget
}
function changeFile(event: Event): void {
const { target } = event as HTMLInputChangeEvent
if (target && target.files) {
if (!checkPicturesType(target.files)) return
emit('trigger', target.files)
}
}
function dragover(event: DragEvent): void {
event.preventDefault()
isDragging.value = true
}
function dragleave(): void {
isDragging.value = false
}
function drop(event: DragEvent): void | boolean {
event.preventDefault()
const { dataTransfer } = event
if (dataTransfer && dataTransfer.files) {
if (!checkPicturesType(dataTransfer.files))
return (isDragging.value = false)
emit('trigger', dataTransfer.files)
isDragging.value = false
}
}
function checkPicturesType(files: FileList): number {
const picturesToUpload = [...files].filter((p) => p.type == 'image/jpeg')
return picturesToUpload.length
}
</script>
<style scoped lang="scss">
.file-upload {
border: toRem(0.1) dashed var(--blue);
background-color: var(--blue-semi);
border-radius: toRem(0.5);
padding: toRem(0.3) toRem(0.3) toRem(3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.dragging {
border-color: var(--green);
background-color: var(--green-pale);
.last-word,
.bi-cloud-upload-fill {
color: var(--green);
}
}
.file-upload input {
overflow: hidden;
width: 0;
}
.bi-cloud-upload-fill {
color: var(--blue);
font-size: toRem(7);
}
.input-text {
font-size: toRem(2);
@include text(m-regular);
font-weight: bold;
width: toRem(21);
text-align: center;
margin-bottom: toRem(1);
}
.input-text-type {
@include text(xs-r-regular);
}
.last-word {
color: var(--blue);
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<a
v-if="type === 'external'"
:href="path"
:href="pathExternal"
:target="target"
:class="['default', look, { disabled }]"
:title="titleImg"
@@ -14,7 +14,8 @@
</a>
<router-link
v-else
:to="path"
:to="route"
:target="target"
:class="['default', look, { disabled }]"
:title="text"
>
@@ -28,6 +29,7 @@
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import type { PropType } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import { img } from '../utils/image'
const { t } = useI18n()
@@ -38,7 +40,8 @@ interface ImageInterface {
const props = defineProps({
text: { type: String, default: null },
path: { type: String, default: '' },
route: { type: Object as PropType<RouteLocationRaw>, default: {} },
pathExternal: { type: String, default: '' },
look: { type: String, default: '' },
type: { type: String, default: null },
alt: { type: String, default: '' },
@@ -54,11 +57,6 @@ const titleImg = computed<string>(() =>
</script>
<style lang="scss" scoped>
.icon {
color: var(--black);
font-size: 2.4rem;
margin-right: 1rem;
}
.default {
@include text(s-regular);
display: flex;
@@ -66,58 +64,81 @@ const titleImg = computed<string>(() =>
color: var(--black);
text-decoration: none;
.icon {
margin-right: 1rem;
margin-right: toRem(1);
}
&:hover {
opacity: toRem(0.8);
}
}
.icon {
color: var(--black);
font-size: toRem(2.4);
}
.logo {
height: toRem(4);
border-radius: toRem(0.5);
margin-right: toRem(1);
}
.button {
height: toRem(4);
border-radius: toRem(0.5);
padding: toRem(1.3) toRem(2) toRem(1.3);
background-color: var(--black);
color: var(--white);
}
.text {
width: 100%;
text-align: center;
white-space: break-spaces;
}
.link:hover {
background-color: transparent;
text-decoration: underline;
}
.button {
height: 4rem;
border-radius: 0.5rem;
padding: 1.3rem 2rem 1.3rem;
background-color: var(--black);
color: var(--white);
.link--grey {
color: var(--grey-semi-dark);
text-decoration: underline;
}
.white {
.button--white {
background-color: var(--white);
color: var(--black);
border: 0.1rem solid var(--black);
border: toRem(0.1) solid var(--black);
.icon {
font-size: toRem(1.6);
}
&:hover {
background-color: var(--black);
color: var(--white);
.icon {
color: white;
}
}
}
.white .icon {
font-size: 1.6rem;
.button--white-blue {
background-color: var(--white);
border: toRem(0.1) solid var(--blue);
.icon {
font-size: toRem(1.4);
color: var(--blue);
}
}
.button:hover {
opacity: 0.9;
}
.white:hover {
background-color: var(--black);
color: var(--white);
}
.white:hover > .icon {
color: white;
}
.logo {
height: 4rem;
border-radius: 0.5rem;
margin-right: 1rem;
.button--blue {
background-color: var(--blue);
border: toRem(0.1) solid var(--blue);
.icon {
font-size: toRem(1.4);
color: var(--white);
}
}
.disabled {
color: grey;
cursor: not-allowed;
}
.disabled .icon {
color: grey;
cursor: not-allowed;
}
.disabled:hover {
text-decoration: none;
.icon {
color: grey;
cursor: not-allowed;
}
&:hover {
text-decoration: none;
}
}
.button--rounded {
background-color: var(--black);
@@ -126,50 +147,29 @@ const titleImg = computed<string>(() =>
align-items: center;
border-radius: 50%;
padding: 0;
height: 4rem;
width: 4rem;
height: toRem(4);
width: toRem(4);
.icon {
color: var(--white);
font-size: 2.8rem;
font-size: toRem(2.8);
margin-right: 0;
}
}
.button--white {
height: 4rem;
border-radius: 0.5rem;
color: var(--black);
background-color: var(--white);
border: 0.1rem solid var(--black);
.icon {
font-size: 1.4rem;
color: var(--black);
}
}
.button--blue {
height: 4rem;
border-radius: 0.5rem;
background-color: var(--white);
border: 0.1rem solid var(--blue);
.icon {
font-size: 1.4rem;
color: var(--blue);
}
}
.no-text {
height: 3rem;
width: 3rem;
height: toRem(3);
width: toRem(3);
padding: 0;
.icon {
margin: auto;
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.icon {
margin-right: 0.5rem;
margin-right: toRem(0.5);
}
.button {
padding-right: 1rem;
padding-left: 1rem;
padding-right: toRem(1);
padding-left: toRem(1);
}
.disable-mobile {
pointer-events: none;

View File

@@ -1,5 +1,5 @@
<template>
<div class="lds-ring">
<div :class="['lds-ring', look, { loaded: isLoaded }]">
<div></div>
<div></div>
<div></div>
@@ -7,27 +7,66 @@
</div>
</template>
<script setup></script>
<script setup lang="ts">
defineProps({
text: { type: String, default: null },
look: {
type: String,
validator: (value: string): boolean => ['sm', 'md', 'lg'].includes(value),
default: 'sm'
},
isLoaded: { type: Boolean, default: true }
})
</script>
<style scoped scss>
<style scoped lang="scss">
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid var(--blue);
width: 256px;
height: 256px;
margin: 10px;
border: 10px solid var(--blue);
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: var(--blue) transparent transparent transparent;
}
.loaded {
div {
border-color: var(--blue);
}
}
.sm {
width: toRem(8);
height: toRem(8);
div {
width: toRem(8.4);
height: toRem(8.4);
}
}
.md {
width: toRem(16);
height: toRem(16);
div {
width: toRem(16.8);
height: toRem(16.8);
}
}
.lg {
width: toRem(32);
height: toRem(32);
div {
width: toRem(25.6);
height: toRem(25.6);
}
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}

View File

@@ -40,34 +40,34 @@ function triggerPagination(): void {
<style lang="scss" scoped>
.wrapper-pagination {
border: 0.1rem solid var(--black);
border: toRem(0.1) solid var(--black);
border-radius: 50%;
background-color: var(--blue-pale);
height: 3rem;
width: 3rem;
height: toRem(3);
width: toRem(3);
display: flex;
&:first-child {
margin-right: 2rem;
margin-right: toRem(2);
}
&:last-child {
margin-left: 2rem;
margin-left: toRem(2);
}
&:nth-child(2) {
margin-right: 1rem;
margin-right: toRem(1);
}
&:nth-child(3) {
margin-left: 1rem;
margin-left: toRem(1);
}
}
.pagination-button {
background-color: transparent;
border: none;
font-size: 1.7rem;
font-size: toRem(1.7);
width: 100%;
display: flex;
align-items: center;
.chevron {
width: 3.5rem;
width: toRem(3.5);
}
}
.no-border {

View File

@@ -14,7 +14,7 @@
<div class="entry-button-terminal">
<Button
look="button--transparent"
:text="$t('pages.upload.button_copy')"
:text="$t('pages.share_pictures.button_copy')"
:icon="clipboardIcon"
@trigger="copyText(textInstall)"
class="entry-button"
@@ -28,7 +28,7 @@
<div class="entry-button-terminal">
<Button
look="button--transparent"
:text="$t('pages.upload.button_copy')"
:text="$t('pages.share_pictures.button_copy')"
:icon="clipboardIcon"
@trigger="copyText(textUpload)"
class="entry-button"
@@ -65,23 +65,23 @@ async function copyText(text: string): Promise<void> {
.header {
display: flex;
align-items: center;
border-top-right-radius: 1.5rem;
border-top-left-radius: 1.5rem;
border-top-right-radius: toRem(1.5);
border-top-left-radius: toRem(1.5);
background-color: var(--grey);
padding-left: 0.5rem;
height: 3rem;
padding-left: toRem(0.5);
height: toRem(3);
width: 100%;
}
.editor {
border-bottom-right-radius: 1.5rem;
border-bottom-left-radius: 1.5rem;
border-bottom-right-radius: toRem(1.5);
border-bottom-left-radius: toRem(1.5);
background-color: var(--black-pale);
height: 32rem;
height: toRem(32);
width: 100%;
position: relative;
}
.screen {
padding: 3rem 2rem 2rem;
padding: toRem(3) toRem(2) toRem(2);
height: 100%;
}
.screen:nth-child(2n) {
@@ -90,10 +90,10 @@ async function copyText(text: string): Promise<void> {
.upload-command {
display: flex;
align-items: center;
margin-top: 2rem;
margin-top: toRem(2);
}
.entry-button-terminal {
margin-top: 2rem;
margin-top: toRem(2);
margin-left: auto;
}
.code {
@@ -105,13 +105,13 @@ async function copyText(text: string): Promise<void> {
}
.tilde {
color: var(--green);
margin-right: 1rem;
margin-right: toRem(1);
}
.round {
height: 1.5rem;
width: 1.5rem;
height: toRem(1.5);
width: toRem(1.5);
border-radius: 50%;
margin: 0.5rem;
margin: toRem(0.5);
}
.red {
background-color: var(--red);
@@ -123,9 +123,9 @@ async function copyText(text: string): Promise<void> {
background-color: var(--green);
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.editor {
min-height: 27rem;
min-height: toRem(27);
}
.upload-command {
margin-top: 0;

View File

@@ -27,26 +27,26 @@ defineProps({
.toast-wrapper {
position: fixed;
right: 0;
bottom: 2rem;
bottom: toRem(2);
transform: translateX(100%);
display: flex;
justify-content: center;
align-items: center;
color: var(--white);
@include text(s-regular);
border-radius: 0.5rem;
height: 4rem;
min-width: 10rem;
padding-right: 1rem;
padding-left: 1rem;
border-radius: toRem(0.5);
height: toRem(4);
min-width: toRem(10);
padding-right: toRem(1);
padding-left: toRem(1);
}
.button-close {
position: absolute;
top: -0.5rem;
right: -0.5rem;
height: 1.8rem;
width: 1.8rem;
border: 0.1rem solid var(--black);
top: toRem(-0.5);
right: toRem(-0.5);
height: toRem(1.8);
width: toRem(1.8);
border: toRem(0.1) solid var(--black);
background-color: var(--white);
border-radius: 50%;
display: flex;
@@ -55,10 +55,10 @@ defineProps({
}
.toast-text {
margin-bottom: 0;
margin-left: 1rem;
margin-left: toRem(1);
}
.display {
transform: translateX(-3rem);
transform: translateX(toRem(-3));
transition: transform 0.3s ease-in-out;
}
.error {

View File

@@ -0,0 +1,131 @@
<template>
<section :class="['information-section', { 'first-sequence': index === 0 }]">
<div class="uploaded-pictures">
<p v-if="index === 0" class="title-current-upload">
{{ $t('pages.upload.sequence_uploading_title') }}
</p>
<p v-if="sequence.pictures" class="uploaded-title">
<span
>{{ $t('pages.upload.import') }} {{ sequence.title }} -
{{ sequence.pictures.length }}/{{ sequence.pictureCount }}</span
>
</p>
<ul class="uploaded-picture-list">
<PictureItem
v-for="picture in uploadPictures"
:text="$t('pages.upload.uploaded_word')"
:name="picture.name"
look="success"
>
<i class="bi bi-check-circle"></i>
</PictureItem>
</ul>
<div v-if="sequence.id" class="wrapper-button-sequence">
<Link
:text="$t('pages.upload.sequence_link')"
look="button button--white"
target="_blank"
:route="{ name: 'sequence', params: { id: sequence.id } }"
/>
</div>
</div>
<div class="errors-pictures">
<p v-if="uploadErrors.length" class="uploaded-title">
{{ uploadErrors.length }} {{ $t('pages.upload.error_word') }}
</p>
<ul class="uploaded-error-list">
<PictureItem
v-for="error in uploadErrors"
:text="error.message"
:name="error.name"
look="error"
/>
</ul>
</div>
</section>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import Link from '@/components/Link.vue'
import PictureItem from '@/components/upload/PictureItem.vue'
import type { uploadErrorInterface } from '@/views/interfaces/UploadPicturesView'
defineProps({
index: { type: Number, default: 0 },
sequence: { type: Object, default: {} },
picturesCount: { type: Number, default: null },
uploadErrors: {
type: Array as PropType<uploadErrorInterface[]>,
default: []
},
uploadPictures: {
type: Array as PropType<uploadErrorInterface[]>,
default: []
}
})
</script>
<style scoped lang="scss">
.information-section {
display: flex;
margin-bottom: toRem(4);
padding-top: toRem(2);
padding-bottom: toRem(2);
width: 80%;
box-shadow: 0px 6.45694px 8.60925px rgba(0, 0, 0, 0.05);
}
.first-sequence {
background-color: var(--blue-pale);
border-radius: toRem(0.5);
}
.uploaded-pictures,
.errors-pictures {
width: 50%;
}
.title-current-upload {
margin-bottom: toRem(2);
@include text(m-regular);
color: var(--blue-dark);
margin-left: toRem(2);
}
.uploaded-title {
margin-left: toRem(2);
margin-bottom: toRem(2);
width: 100%;
color: var(--grey-semi-dark);
font-weight: bold;
@include text(s-regular);
}
.uploaded-picture-list {
padding-left: 0;
}
.uploaded-picture-list,
.uploaded-error-list {
padding: 0rem toRem(2) toRem(2);
overflow-y: auto;
max-height: toRem(35);
}
.bi-check-circle {
color: var(--green);
font-size: toRem(2);
}
.wrapper-button-sequence {
padding-top: toRem(2);
width: fit-content;
margin: auto;
}
.errors-pictures {
padding-left: toRem(2);
padding-right: toRem(2);
.uploaded-title {
margin-top: toRem(4);
}
}
@media (max-width: toRem(76.8)) {
.uploaded-pictures,
.errors-pictures {
width: 100%;
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<li :class="['uploaded-picture-item', look]">
<div class="uploaded-information">
<span v-if="itemUploadedText">{{ itemUploadedText }}</span>
</div>
<slot></slot>
</li>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
name: { type: String, default: '' },
text: { type: String, default: '' },
look: {
type: String,
validator: (value: string): boolean =>
['error', 'success', 'default'].includes(value),
default: 'default'
}
})
const itemUploadedText = computed<string | null>(
() => `${props.name} - ${props.text}`
)
</script>
<style scoped lang="scss">
.uploaded-picture-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: toRem(1) toRem(2);
margin-top: toRem(1);
border-radius: toRem(0.5);
width: 100%;
@include text(s-regular);
}
.success {
background-color: var(--white);
border: toRem(0.1) solid var(--grey);
color: var(--black);
}
.error {
background-color: var(--white);
border: toRem(0.1) solid var(--red-pale);
color: var(--red);
}
.uploaded-information {
margin-right: toRem(1);
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="wrapper-loader">
<div class="loader">
<span class="loader-percentage">{{ loadPercentage }}</span>
<Loader look="lg" :is-loaded="isLoaded" />
</div>
<div v-if="loadPercentage === '100%'" class="wrapper-button-new-upload">
<Button
:text="$t('pages.upload.button_new_upload')"
look="button button--blue"
@trigger="$emit('triggerNewUpload')"
/>
</div>
<div v-if="uploadedSequences[0]" class="loader-information">
<span class="loader-title">{{ uploadPendingTitle }}</span>
<span v-if="loadPercentage !== '100%'" class="loader-text">{{
$t('pages.upload.upload_pending_pictures', { count: picturesCount })
}}</span>
<span class="loader-text-size"
>{{ uploadedSequences[0].pictureSize }}/{{ loadTextSize }}
</span>
<span class="loader-text-warning">{{
$t('pages.upload.leave_message')
}}</span>
</div>
</div>
</template>
<script setup lang="ts">
import Loader from '@/components/Loader.vue'
import Button from '@/components/Button.vue'
import { computed } from 'vue'
import type { PropType } from 'vue'
import type { sequenceInterface } from '@/views/interfaces/UploadPicturesView'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
loadPercentage: { type: String, default: '0%' },
loadTextSize: { type: String, default: '0 Mo' },
isLoaded: { type: Boolean, default: false },
uploadedSequences: {
type: Array as PropType<sequenceInterface[]>,
default: []
},
picturesCount: { type: Number, default: null }
})
const uploadPendingTitle = computed<string>(() => {
if (props.loadPercentage !== '100%') return t('pages.upload.upload_pending')
return t('pages.upload.upload_done')
})
</script>
<style scoped lang="scss">
.wrapper-loader {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: toRem(4);
}
.loader {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.loader-percentage {
width: toRem(10);
position: absolute;
text-align: center;
@include text(h1);
color: var(--blue);
}
.wrapper-button-new-upload {
display: flex;
justify-content: center;
}
.loader-information {
margin-top: toRem(2);
display: flex;
flex-direction: column;
}
.loader-title,
.loader-text,
.loader-text-size {
text-align: center;
@include text(s-regular);
}
.loader-title {
@include text(h2);
margin-bottom: toRem(0.5);
}
.loader-text {
color: var(--blue);
border-bottom: toRem(0.1) solid var(--grey);
}
.loader-text-size {
color: var(--grey-semi-dark);
}
.loader-text-warning {
text-align: center;
@include text(s-regular);
color: var(--orange);
margin-top: toRem(1);
width: toRem(31);
}
</style>

View File

@@ -3,14 +3,15 @@ import { onMounted, ref } from 'vue'
import type { AuthConfigInterface } from './interfaces/Auth'
export default function authConfig() {
const authConf = ref<AuthConfigInterface>({})
const authConf = ref<AuthConfigInterface>()
async function getConfig(): Promise<object> {
async function getConfig(): Promise<AuthConfigInterface> {
const { data } = await axios.get('api/configuration', {
withCredentials: false
})
return data.auth
return data
}
onMounted(async () => (authConf.value = await getConfig()))
return { authConf }
}

View File

@@ -1,7 +1,7 @@
interface UserProfileInterface {
url?: string
}
export interface AuthConfigInterface {
enabled?: boolean
user_profile?: UserProfileInterface
license?: { id: string; url: string }
auth?: {
enabled?: boolean
user_profile?: { url?: string }
}
}

View File

@@ -6,10 +6,11 @@
"description": "Panoramax, lalternative libre pour photo-cartographier les territoires"
},
"header": {
"contribute_text": "Partager vos photos",
"contribute_text": "À propos",
"upload_text": "Contribuer",
"sequences_text": "Mes photos",
"alt_logo": "Logo de l'instance",
"title": "Instance Panoramax",
"title": "Instance\nPanoramax",
"beta_text": "Version beta",
"logout_text": "Déconnexion",
"my_information_text": "Mes informations",
@@ -23,8 +24,7 @@
},
"pages": {
"home": {
"report_mail_subject": "⚠️ Signalement sur l'image {id}",
"report_mail_body": "Bonjour, %0D%0A%0D%0A Problème sur l'image (garder le type de problème signalé) : %0D%0A%0D%0A contenu inapproprié / absence de floutage sur un élément à anonymiser ou flouter pour des raisons de sécurité /surfloutage (floutage en trop) %0D%0A%0D%0A Lien vers la photo concernée : {link} %0D%0A%0D%0A Précision sur les éléments concernés (en particulier pour les problèmes de floutage - que faut-il flouter ou déflouter?) : ",
"report_mail": "?subject=⚠️ Signalement sur l`image {picId}&body=Bonjour, %0D%0A%0D%0A Problème sur l`image (garder le type de problème signalé) : %0D%0A%0D%0A contenu inapproprié / absence de floutage sur un élément à anonymiser ou flouter pour des raisons de sécurité /surfloutage (floutage en trop) %0D%0A%0D%0A Lien vers la photo concernée : {link} %0D%0A%0D%0A Précision sur les éléments concernés (en particulier pour les problèmes de floutage - que faut-il flouter ou déflouter?) :",
"report_button_text": "Signaler la photo"
},
"settings": {
@@ -32,9 +32,9 @@
"setting_tooltip": "Afficher ou masquer le token"
},
"sequence": {
"sequence_published": "Séquence publiée",
"sequence_published": "Publiée",
"sequence_waiting": "En cours de publication",
"sequence_hidden": "Séquence masquée",
"sequence_hidden": "Masquée",
"hide_sequence_tooltip": "Masque la séquence sur la carte",
"delete_sequence_tooltip": "Supprime définitivement la séquence",
"hide_photo_tooltip": "Masque les photos sur la carte",
@@ -58,6 +58,7 @@
"unselect_text": "Tout désélectionner",
"select_shift_text": "Sélectionnez plusieurs photos avec shift",
"waiting_process": "Photo en cours de traitement",
"broken": "Traitement de la photo en erreur",
"no_image": "Aucune photo dans cette séquence"
},
"sequences": {
@@ -73,25 +74,45 @@
"button_upload": "Partager vos photos",
"sequence_deleted": "La séquence a bien été supprimée"
},
"upload": {
"share_pictures": {
"title": "Partagez vos photos",
"sub_title": "Un compte utilisateur est obligatoire pour partager des photos",
"photo_type1": "Des lieux visibles depuis la voie publique",
"photo_type2": "360° ou non",
"photo_type3": "Vues du sol",
"photo_type4": "Géolocalisées",
"description": "Ici, vos photos sont accessibles à tous :\n\n{check} automatiquement floutées dans le respect de <a href='https://panoramax.fr/foire-aux-questions' target='_blank' style='color:black'>la législation</a>\n{check} libres de droit, <a href='https://www.etalab.gouv.fr/licence-ouverte-open-licence/' target='_blank' style='color:black'>sous licence ouverte</a>\n{check} sous forme «brute» pour des réutilisations variées (ex: préparation des chantiers)\n\n",
"footer_block": "⚠️️️ Aujourd'hui, le versement de grands volumes d'images est possible via une ligne de commande. Bientôt, d'autres moyens de versement seront disponibles, notamment via une interface web.",
"description": "Ici, vos photos sont accessibles à tous :\n\n{check} automatiquement floutées dans le respect de <a href='https://panoramax.fr/foire-aux-questions' target='_blank' style='color:black'>la législation</a>\n{check} libres de droit, <a href={licenseUrl} target='_blank' style='color:black'>sous licence {licenseName}</a>\n{check} sous forme «brute» pour des réutilisations variées (ex: préparation des chantiers)\n\n",
"footer_block": "⚠️️️ Aujourd'hui, le versement de grands volumes d'images est possible via une ligne de commande et via une interface web.",
"button": "Accéder à l'outil",
"alt_img_upload": "Image qui représente plusieurs photos en cours de téléchargement",
"title_terminal": "Utilisez la ligne de commande",
"footer_description_terminal": "Les données déposées seront sous <a href='https://www.etalab.gouv.fr/licence-ouverte-open-licence/' target='_blank' style='color:#808080'>la licence ouverte</a>.",
"footer_description_terminal": "Les données déposées seront sous {word}",
"cli_title": "Vous voulez accéder à plus de documentation ?",
"user_account_button": "Créer un compte",
"description_terminal": "<a href='https://gitlab.com/geovisio/cli' target='_blank' style='color:black'>L'outil en ligne de commande</a> vous permet de partager de grands volumes de photos. La procédure est simple et vous devez disposer <a target='_blank' href='https://www.python.org/downloads/' style='color:black'>de python (au moins la version 3.8)</a>.\n\n1. Installer loutil en ligne de commande geovisio\n2. Lancez la commande de versement dimages sur le dossier choisi. Après '--api-url', renseignez l'url de l'api de l'instance où partager les photos et le chemin vers votre dossier de photos sur votre machine. Loutil demandera vos informations de connexion avant l'import. Une fois les données chargées, un temps de traitement est nécessaire pour les rendre disponibles.",
"terminal_install": "pip install geovisio_cli",
"terminal_text": "geovisio upload --api-url {url} <DOSSIER_PHOTOS>",
"button_copy": "Copier"
},
"upload": {
"title": "Déposez vos photos",
"input_label": "Déposez des photos dans la zone ou ",
"import_word": "importez",
"import_type": "Format JPEG uniquement",
"sequence_title": "Séquence du ",
"button_text": "Envoyer",
"uploaded_files": "{count} fichier| {count} fichiers",
"no_uploaded_files": "Aucun fichier sélectionné",
"uploaded_word": " Image téléchargée",
"import": "Imports",
"error_word": "Images en erreur",
"sequence_uploading_title": "Dernier import",
"upload_pending": "Transfert en cours...",
"upload_done": "Transfert terminé !",
"upload_pending_pictures": "Envoi de {count} photo en cours |Envoi de {count} photos en cours",
"sequence_link": "Voir la sequence",
"button_new_upload": "Nouvel envoi",
"leave_message": "⚠️ Attention, le téléchargement sera interrompu si vous quittez la page avant la fin."
}
}
}

View File

@@ -8,10 +8,11 @@ import { globalCookiesConfig } from 'vue3-cookies'
import { createMetaManager } from 'vue-meta'
import { pinia } from './store'
import fr from './locales/fr.json'
import './assets/main.css'
import './assets/main.scss'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js'
import 'bootstrap-icons/font/bootstrap-icons.css'
import 'geovisio/build/index.css'
axios.defaults.baseURL = import.meta.env.VITE_API_URL
axios.defaults.withCredentials = true

View File

@@ -12,7 +12,8 @@ import MyInformationView from '../views/MyInformationView.vue'
import MySettingsView from '../views/MySettingsView.vue'
import MySequencesView from '../views/MySequencesView.vue'
import MySequenceView from '../views/MySequenceView.vue'
import UploadView from '../views/UploadView.vue'
import SharePicturesView from '../views/SharePicturesView.vue'
import UploadPicturesView from '../views/UploadPicturesView.vue'
const { cookies } = useCookies()
const routes: Array<RouteRecordRaw> = [
{
@@ -38,15 +39,19 @@ const routes: Array<RouteRecordRaw> = [
{ path: '/sequence/:id', name: 'sequence', component: MySequenceView },
{
path: '/partager-des-photos',
name: 'upload',
component: UploadView
name: 'share-pictures',
component: SharePicturesView
},
{
path: '/envoyer',
name: 'upload-pictures',
component: UploadPicturesView
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeResolve(
async (
to: RouteLocationNormalized,
@@ -56,7 +61,8 @@ router.beforeResolve(
const siteLoggedRoutes =
to.name === 'my-settings' ||
to.name === 'my-sequences' ||
to.name === 'sequence'
to.name === 'sequence' ||
to.name === 'upload-pictures'
if (siteLoggedRoutes) {
if (!isSiteLogged()) goToLoginPage(to.path)

View File

@@ -1,22 +0,0 @@
describe('In the upload page', () => {
it('go to the login page', () => {
cy.visit('partager-des-photos')
cy.fixture('upload').then((uploadData) => {
cy.contains(uploadData.textButtonUpload).click()
})
})
it('go to the login page', () => {
cy.visit('partager-des-photos')
cy.fixture('upload').then((uploadData) => {
cy.contains(uploadData.textButtonUpload).click()
})
})
it('go to the cli page', () => {
cy.visit('partager-des-photos')
cy.fixture('upload').then((uploadData) => {
cy.contains(uploadData.textButtonCli).click()
})
})
})
export {}

View File

@@ -1,4 +0,0 @@
{
"textButtonUpload": "Créer un compte",
"textButtonCli": "Accéder à l'outil"
}

View File

@@ -0,0 +1,127 @@
import { test, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Button from '../../../components/Button.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.icon).toBe(null)
expect(wrapper.vm.disabled).toBe(false)
expect(wrapper.vm.isLoading).toBe(false)
expect(wrapper.vm.text).toBe('')
expect(wrapper.vm.tooltip).toBe('')
expect(wrapper.vm.look).toBe('')
expect(wrapper.vm.type).toBe('button')
})
})
describe('When the component is disabled', () => {
test('should render the button with disabled class', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
disabled: true
}
})
expect(wrapper.html()).contains('class="default disabled"')
})
})
describe('When the component is loading', () => {
test('should render the button with disabled class', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
isLoading: true
}
})
expect(wrapper.html()).contains('class="default disabled"')
})
})
describe('When the component have an icon', () => {
test('should render the button the icon displayed', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
icon: 'my-icon'
}
})
expect(wrapper.html()).contains('<i')
expect(wrapper.html()).contains('class="my-icon icon"')
})
})
describe('When the component have a type submit', () => {
test('should render the type to submit', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
type: 'submit'
}
})
expect(wrapper.html()).contains('type="submit"')
})
})
describe('When the component have tooltip', () => {
test('should render the button with a tooltip', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
tooltip: 'my tooltip'
}
})
expect(wrapper.html()).contains(
'class="tooltip-button">my tooltip</span>'
)
})
})
describe('When the component have a text', () => {
test('should render the button with a text', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
text: 'My text'
}
})
expect(wrapper.html()).contains('class="text">My text</span>')
})
})
describe('When the component have specific look', () => {
test('should render the button with a specific class', () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
},
props: {
look: 'my--look'
}
})
expect(wrapper.html()).contains('class="my--look default"')
})
})
describe('When the button is trigger', () => {
test('should emit', async () => {
const wrapper = shallowMount(Button, {
global: {
plugins: [i18n]
}
})
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('trigger')
})
})
})

View File

@@ -5,6 +5,9 @@ import { createRouter, createWebHistory } from 'vue-router'
import { useCookies } from 'vue3-cookies'
import fr from '../../../locales/fr.json'
import Header from '../../../components/Header.vue'
import MyInformation from '../../../views/MyInformationView.vue'
import MySettings from '../../../views/MySettingsView.vue'
import SharePictures from '../../../views/SharePicturesView.vue'
vi.mock('vue-router')
vi.mock('vue3-cookies', () => {
const mockCookies = {
@@ -80,26 +83,6 @@ describe('Template', () => {
expect(wrapper.html()).contains('general.header.sequences_text')
expect(wrapper.html()).contains('general.header.my_settings_text')
})
it('should render the component with all links', async () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue('user_id=id')
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(Header, {
props: {
authEnabled: true,
userProfileUrl: 'profil'
},
global: {
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.html()).contains('auth/logout')
expect(wrapper.html()).contains('path="/mes-informations"')
expect(wrapper.html()).contains('path="/mes-parametres"')
expect(wrapper.html()).contains('path="/mes-sequences"')
})
})
})

View File

@@ -39,7 +39,6 @@ describe('Template', () => {
expect(wrapper.html()).contains('<img')
expect(wrapper.html()).contains('src="my-url"')
expect(wrapper.html()).contains('<link-stub')
expect(wrapper.html()).contains('path="my-url-hd"')
expect(wrapper.html()).contains('10 mars 2023')
})
})

View File

@@ -1,5 +1,5 @@
import { test, describe, vi, expect } from 'vitest'
import { mount, shallowMount } from '@vue/test-utils'
import { shallowMount } from '@vue/test-utils'
import Link from '../../../components/Link.vue'
import { createI18n } from 'vue-i18n'
import { createRouter, createWebHistory } from 'vue-router'
@@ -34,7 +34,8 @@ describe('Template', () => {
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.path).toBe('')
expect(wrapper.vm.route).toStrictEqual({})
expect(wrapper.vm.pathExternal).toBe('')
expect(wrapper.vm.look).toBe('')
expect(wrapper.vm.type).toBe(null)
expect(wrapper.vm.alt).toBe('')

View File

@@ -0,0 +1,45 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Loader from '../../../components/Loader.vue'
import i18n from '../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(Loader, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.look).toBe('sm')
expect(wrapper.vm.isLoaded).toBe(true)
})
})
describe('When the component is loading', () => {
it('should not have the loaded class', () => {
const wrapper = shallowMount(Loader, {
global: {
plugins: [i18n]
},
props: {
isLoaded: false
}
})
expect(wrapper.html()).contains('class="lds-ring sm"')
})
})
describe('When the component have a look', () => {
it('should have a look classe', () => {
const wrapper = shallowMount(Loader, {
global: {
plugins: [i18n]
},
props: {
look: 'md'
}
})
expect(wrapper.html()).contains('class="lds-ring md loaded"')
})
})
})

View File

@@ -26,7 +26,7 @@ describe('Template', () => {
}
}
})
expect(wrapper.html()).contains('pages.upload.button_copy')
expect(wrapper.html()).contains('pages.share_pictures.button_copy')
})
it('should render the view with the button', () => {
const wrapper = shallowMount(Terminal, {

View File

@@ -0,0 +1,111 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import ImportedSection from '../../../../components/upload/ImportedSection.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.index).toBe(0)
expect(wrapper.vm.sequence).toStrictEqual({})
expect(wrapper.vm.picturesCount).toBe(null)
expect(wrapper.vm.uploadErrors).toStrictEqual([])
expect(wrapper.vm.uploadPictures).toStrictEqual([])
})
})
describe('When the index is 0', () => {
it('should have a specific class and wordings', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
},
props: {
index: 0
}
})
expect(wrapper.html()).contains(
'class="information-section first-sequence"'
)
expect(wrapper.html()).contains('Dernier impor')
})
})
describe('When the sequence have pictures', () => {
it('should have a title', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
},
props: {
sequence: {
id: 'id132435',
title: 'my title',
pictureCount: 3,
pictures: [{ id: 'id' }]
}
}
})
expect(wrapper.html()).contains('class="uploaded-title"')
expect(wrapper.html()).contains('Imports my title - 1/3')
})
it('should have a Link', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
},
props: {
sequence: {
id: 'id132435',
title: 'my title',
pictureCount: 3,
pictures: [{ id: 'id' }]
}
}
})
expect(wrapper.html()).contains('<link-stub')
expect(wrapper.html()).contains('text="Voir la sequence"')
})
})
describe('When there are uploaded pictures', () => {
it('should have a list of pictures', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
},
props: {
uploadPictures: [{ name: 'my name1' }, { name: 'my name2' }]
}
})
expect(wrapper.html()).contains('name="my name1"')
expect(wrapper.html()).contains('name="my name2"')
expect(wrapper.html()).contains('text=" Image téléchargée"')
expect(wrapper.html()).contains('look="success"')
expect(wrapper.html()).contains('<picture-item-stub')
})
})
describe('When there are uploaded errors', () => {
it('should have a list of errors', () => {
const wrapper = shallowMount(ImportedSection, {
global: {
plugins: [i18n]
},
props: {
uploadErrors: [
{ name: 'my name1', message: 'my message1' },
{ name: 'my name2', message: 'my message2' }
]
}
})
expect(wrapper.html()).contains('name="my name1"')
expect(wrapper.html()).contains('name="my name2"')
expect(wrapper.html()).contains('text="my message1"')
expect(wrapper.html()).contains('text="my message2"')
expect(wrapper.html()).contains('look="error"')
expect(wrapper.html()).contains('<picture-item-stub')
})
})
})

View File

@@ -0,0 +1,47 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import PictureItem from '../../../../components/upload/PictureItem.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(PictureItem, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.name).toBe('')
expect(wrapper.vm.text).toStrictEqual('')
expect(wrapper.vm.look).toBe('default')
})
})
describe('When the component have a look', () => {
it('should have a specific class', () => {
const wrapper = shallowMount(PictureItem, {
global: {
plugins: [i18n]
},
props: {
look: 'success'
}
})
expect(wrapper.html()).contains('class="uploaded-picture-item success"')
})
})
describe('When the component have a name and a text', () => {
it('should have a specific class', () => {
const wrapper = shallowMount(PictureItem, {
global: {
plugins: [i18n]
},
props: {
name: 'name',
text: 'text'
}
})
expect(wrapper.html()).contains('class="uploaded-information"')
expect(wrapper.html()).contains('name - text')
})
})
})

View File

@@ -0,0 +1,68 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import UploadLoader from '../../../../components/upload/UploadLoader.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(UploadLoader, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.loadPercentage).toBe('0%')
expect(wrapper.vm.loadTextSize).toBe('0 Mo')
expect(wrapper.vm.isLoaded).toBe(false)
expect(wrapper.vm.picturesCount).toBe(null)
expect(wrapper.vm.uploadedSequences).toStrictEqual([])
})
})
describe('When the component have a percentage equal to 100%', () => {
it('should have a button to do a new upload', () => {
const wrapper = shallowMount(UploadLoader, {
global: {
plugins: [i18n]
},
props: {
loadPercentage: '100%'
}
})
expect(wrapper.html()).contains('button-stub')
expect(wrapper.html()).contains('text="Nouvel envoi"')
})
})
describe('When the component have an uploaded sequence', () => {
it('should render the uploaded sequences information', () => {
const wrapper = shallowMount(UploadLoader, {
global: {
plugins: [i18n]
},
props: {
uploadedSequences: [{ pictureSize: '2345 Mo' }],
loadTextSize: '2345 Mo',
loadPercentage: '97%'
}
})
expect(wrapper.html()).contains('Transfert en cours...')
expect(wrapper.html()).contains('2345 Mo/2345 Mo')
})
describe('When the loading is completed', () => {
it('should render the loading ended information', () => {
const wrapper = shallowMount(UploadLoader, {
global: {
plugins: [i18n]
},
props: {
uploadedSequences: [{ pictureSize: '2345 Mo' }],
loadTextSize: '2345 Mo',
loadPercentage: '100%'
}
})
expect(wrapper.html()).contains('text="Nouvel envoi"')
expect(wrapper.html()).contains('Transfert terminé !')
expect(wrapper.html()).contains('2345 Mo/2345 Mo')
})
})
})
})

View File

@@ -6,6 +6,7 @@ const i18n = createI18n({
fallbackLocale: 'fr',
globalInjection: true,
legacy: false,
warnHtmlMessage: false,
messages: {
fr
}

View File

@@ -0,0 +1,204 @@
import { it, describe, expect } from 'vitest'
import {
imageStatus,
photoToDeleteOrPatchSelected,
spliceIntoChunks,
formatPaginationItems
} from '../../views/utils/sequence/index'
import {
formatPictureSize,
formatTextSize,
sortByName
} from '../../views/utils/upload/index'
import { getAuthRoute } from '../../utils/auth'
import { img } from '../../utils/image'
import { title } from '../../utils/index'
describe('imageStatus', () => {
it('should render the "status" value', () => {
const sequenceStatus = 'hidden'
const imgStatus = 'not hidden'
expect(imageStatus(imgStatus, sequenceStatus)).toEqual('hidden')
})
it('should render the "sequenceStatus" value', () => {
const sequenceStatus = 'not hidden'
const status = 'hidden'
expect(imageStatus(status, sequenceStatus)).toEqual('hidden')
})
})
describe('photoToDeleteOrPatchSelected', () => {
it('should render true', () => {
const imagesToDelete = ['1', '2']
const item = {
assets: { thumb: { href: '' }, hd: { href: '' } },
properties: { created: new Date(), 'geovisio:status': '' },
id: '1',
bbox: [1, 3]
}
expect(photoToDeleteOrPatchSelected(item, imagesToDelete)).toEqual(true)
})
it('should render false', () => {
const imagesToDelete = ['1', '2']
const item = {
assets: { thumb: { href: '' }, hd: { href: '' } },
properties: { created: new Date(), 'geovisio:status': '' },
id: '3',
bbox: [1, 3]
}
expect(photoToDeleteOrPatchSelected(item, imagesToDelete)).toEqual(false)
})
})
describe('spliceIntoChunks', () => {
it('should render an chunked array of array with 4 elements max', () => {
const array = ['123', '345', '6777', '0000', '66666', '222222', '9393888']
const chunkSize = 4
expect(spliceIntoChunks(array, chunkSize)).toEqual([
['123', '345', '6777', '0000'],
['66666', '222222', '9393888']
])
})
})
describe('formatPaginationItems', () => {
it('should render the "rel" links formated and without the "left" element', () => {
const links = [
{
href: 'http://localhost:5000/api/',
rel: 'root',
title: 'Instance catalog',
type: 'application/json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff5-4d70-88fa-6b2be3357709',
rel: 'parent',
type: 'application/json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff5-4d70-88fa-6b2be3357709/items?limit=100',
rel: 'self',
type: 'application/geo+json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff5-4d70-88fa-6b2be3357709/items?limit=100',
rel: 'first',
type: 'application/geo+json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff…a-6b2be3357709/items?limit=100&startAfterRank=100',
rel: 'next',
type: 'application/geo+json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff…-6b2be3357709/items?limit=100&startAfterRank=1023',
rel: 'last',
type: 'application/geo+json'
}
]
expect(formatPaginationItems(links)).toEqual([
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff5-4d70-88fa-6b2be3357709/items?limit=100',
rel: 'double-left',
type: 'application/geo+json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff…a-6b2be3357709/items?limit=100&startAfterRank=100',
rel: 'right',
type: 'application/geo+json'
},
{
href: 'http://localhost:5000/api/collections/076e04c2-5ff…-6b2be3357709/items?limit=100&startAfterRank=1023',
rel: 'double-right',
type: 'application/geo+json'
}
])
})
})
describe('formatPictureSize', () => {
it('should render the size number', () => {
const size = 560673
expect(formatPictureSize(size)).toEqual(1)
})
})
describe('formatTextSize', () => {
const size = 2260121
it('should render the size text formated in ko', () => {
expect(formatTextSize(size, 1)).toEqual('2207.15 Ko')
})
it('should render the size text formated in mo', () => {
expect(formatTextSize(size, 2)).toEqual('2.16 Mo')
})
it('should render the size text formated in go', () => {
expect(formatTextSize(size, 3)).toEqual('0 Go')
})
})
describe('getAuthRoute', () => {
it('should render auth route', () => {
import.meta.env.VITE_API_URL = 'my-url/'
const authRoute = 'auth'
const nextRoute = 'mes-sequences'
const returnedRoute = `${
import.meta.env.VITE_API_URL
}api/${authRoute}?next_url=${encodeURIComponent(
`${location.protocol}//${location.host}${nextRoute}`
)}`
expect(getAuthRoute(authRoute, nextRoute)).toEqual(returnedRoute)
})
})
describe('img', () => {
it('should render the formated img path', () => {
const name = 'my-img'
expect(img(name)).contains('src/assets/images')
})
})
describe('title', () => {
it('should return the formated title with instance name', () => {
import.meta.env.VITE_INSTANCE_NAME = 'my instance'
const myTitle = 'my title'
expect(title(myTitle)).toEqual('my title my instance')
})
it('should return the formated title without instance name', () => {
import.meta.env.VITE_INSTANCE_NAME = ''
const myTitle = 'my title'
expect(title(myTitle)).toEqual('my title')
})
})
describe('sortByName', () => {
it('should return the the list sorted by name', () => {
const list1 = [
{ name: 'd_1_ct.jpg' },
{ name: 'd_11_ct.jpg' },
{ name: 'd_2_ct.jpg' }
]
expect(sortByName(list1)).toEqual([
{ name: 'd_1_ct.jpg' },
{ name: 'd_2_ct.jpg' },
{ name: 'd_11_ct.jpg' }
])
const list2 = [{ name: 'A.jpg' }, { name: 'Z.jpg' }, { name: 'B.jpg' }]
expect(sortByName(list2)).toEqual([
{ name: 'A.jpg' },
{ name: 'B.jpg' },
{ name: 'Z.jpg' }
])
const list3 = [
{ name: 'CAM1_001.jpg' },
{ name: 'CAM2_002.jpg' },
{ name: 'CAM1_011.jpg' }
]
expect(sortByName(list3)).toEqual([
{ name: 'CAM1_001.jpg' },
{ name: 'CAM1_011.jpg' },
{ name: 'CAM2_002.jpg' }
])
})
})

View File

@@ -60,7 +60,7 @@ describe('Template', () => {
await flushPromises()
expect(axios.get).toHaveBeenCalledWith('api/users/me/catalog')
expect(wrapper.vm.userSequences).toEqual([])
expect(wrapper.html()).contains('general.header.contribute_text')
expect(wrapper.html()).contains('general.header.upload_text')
})
it('should render the view with a loader loading', async () => {
@@ -75,7 +75,7 @@ describe('Template', () => {
await flushPromises()
expect(axios.get).toHaveBeenCalledWith('api/users/me/catalog')
expect(wrapper.vm.userSequences).toEqual([])
expect(wrapper.html()).contains('general.header.contribute_text')
expect(wrapper.html()).contains('general.header.upload_text')
})
it('should render the view with a sequence in the list', async () => {
@@ -149,7 +149,6 @@ describe('Methods', () => {
'[data-test="button-sort-title"]'
)
await buttonWrapper.vm.$emit('trigger')
expect(spy).toHaveBeenCalledWith('title')
expect(wrapper.vm.userSequences[0]).toEqual(
mockResponseSequencesToSort[1]

View File

@@ -46,14 +46,13 @@ describe('Template', () => {
}
})
await flushPromises()
expect(axios.get).toHaveBeenCalledWith('api/users/me/tokens')
expect(wrapper.vm.userTokens).toEqual(mockResponseTokens)
expect(wrapper.html()).contains('•••••••••••••••••••••••••••••••')
expect(wrapper.html()).contains('icon="bi bi-eye"')
expect(wrapper.html()).contains('look="button--rounded"')
expect(wrapper.html()).contains('look="no-text"')
expect(wrapper.html()).contains(
'icon="bi bi-clipboard-plus" disabled="false" isloading="false" text="pages.upload.button_copy" tooltip="" look="button--white"'
'icon="bi bi-clipboard-plus" disabled="false" isloading="false" text="pages.share_pictures.button_copy" tooltip="" look="button--white"'
)
})
})

View File

@@ -1,26 +1,16 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import UploadView from '../../../views/UploadView.vue'
import { createI18n } from 'vue-i18n'
import fr from '../../../locales/fr.json'
import SharePicturesView from '../../../views/SharePicturesView.vue'
import i18n from '../config'
import { createRouter, createWebHistory } from 'vue-router'
const i18n = createI18n({
locale: 'fr',
fallbackLocale: 'fr',
globalInjection: true,
legacy: false,
messages: {
fr
}
})
const router = createRouter({
history: createWebHistory(),
routes: []
})
describe('Template', () => {
it('should render the view with the button link', async () => {
const wrapper = shallowMount(UploadView, {
const wrapper = shallowMount(SharePicturesView, {
global: {
plugins: [i18n, router],
mocks: {
@@ -32,14 +22,13 @@ describe('Template', () => {
}
})
expect(wrapper.html()).contains('<link')
expect(wrapper.html()).contains('path="')
expect(wrapper.html()).contains('/auth/login')
expect(wrapper.html()).contains('look="button"')
expect(wrapper.html()).contains('pathexternal=')
expect(wrapper.html()).contains('look="button button--blue"')
expect(wrapper.html()).contains('type="external"')
})
it('should render the view without the button link', async () => {
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(UploadView, {
const wrapper = shallowMount(SharePicturesView, {
global: {
plugins: [i18n, router],
mocks: {
@@ -50,6 +39,6 @@ describe('Template', () => {
}
}
})
expect(wrapper.html()).not.toContain('pages.upload.sub_title')
expect(wrapper.html()).not.toContain('pages.share_pictures.sub_title')
})
})

View File

@@ -0,0 +1,132 @@
import { it, describe, expect, vi } from 'vitest'
import { shallowMount, mount, flushPromises } from '@vue/test-utils'
import UploadPicturesView from '../../../views/UploadPicturesView.vue'
import { createRouter, createWebHistory } from 'vue-router'
import i18n from '../config'
import InputUpload from '../../../components/InputUpload.vue'
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'
const router = createRouter({
history: createWebHistory(),
routes: []
})
describe('Template', () => {
it('should render the view with the input upload and the good wordings', () => {
const wrapper = shallowMount(UploadPicturesView, {
global: {
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.html()).contains('pages.upload.title')
expect(wrapper.html()).contains('<input-upload-stub')
expect(wrapper.html()).contains('pages.upload.input_label')
expect(wrapper.html()).contains('<button-stub')
expect(wrapper.html()).contains('text="pages.upload.button_text"')
})
describe('trigger addPictures', () => {
it('should trigger to add pictures', async () => {
const wrapper = shallowMount(UploadPicturesView, {
global: {
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
},
components: {
InputUpload
}
}
})
const spy = vi.spyOn(wrapper.vm, 'addPictures')
const sortByNameMock = vi.spyOn(sortByName, 'sortByName')
sortByNameMock.mockReturnValue([{}, {}])
const wrapperInputUpload = await wrapper.findComponent(InputUpload)
await wrapperInputUpload.trigger('trigger')
await wrapperInputUpload.vm.$emit('trigger', [{}, {}])
expect(spy).toHaveBeenCalledTimes(1)
expect(wrapper.html()).contains('2 fichiers')
})
describe('submit uploadPicture', () => {
it('should trigger to uploadPictures', async () => {
const wrapper = mount(UploadPicturesView, {
global: {
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
},
components: {
InputUpload
}
}
})
const spy = vi.spyOn(wrapper.vm, 'uploadPicture')
const wrapperInputUpload = wrapper.findComponent(InputUpload)
await wrapperInputUpload.trigger('trigger')
await wrapperInputUpload.vm.$emit('trigger', [{}, {}])
const buttonWrapper = await wrapper.find('[data-test="button-upload"]')
await buttonWrapper.trigger('submit.prevent')
expect(spy).toHaveBeenCalledTimes(1)
expect(wrapper.html()).contains('class="loader-percentage"')
expect(wrapper.html()).contains('class="lds-ring lg"')
})
})
describe('one sequence has been imported', () => {
it('should render a sequence with a list of uploaded pictures', async () => {
const wrapper = mount(UploadPicturesView, {
global: {
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
}
}
})
const picture = {
lastModified: 1599133968750,
name: '100MSDCF_DSC02790.JPG',
size: 2345,
type: 'image/jpeg'
}
const spyASequence = vi.spyOn(createASequence, 'createASequence')
const spyPicture = vi.spyOn(
createAPictureToASequence,
'createAPictureToASequence'
)
const sortByNameMock = vi.spyOn(sortByName, 'sortByName')
sortByNameMock.mockReturnValue([picture])
const sequenceId = 'my-id'
spyASequence.mockReturnValue({ data: { id: sequenceId } })
spyPicture.mockReturnValue({ data: {} })
const sequenceTitle = `Séquence du ${formatDate(
new Date(),
'Do MMMM YYYY, hh:mm:ss'
)}`
const body = new FormData()
body.append('position', '1')
body.append('picture', picture)
const wrapperInputUpload = wrapper.findComponent(InputUpload)
await wrapperInputUpload.trigger('trigger')
await wrapperInputUpload.vm.$emit('trigger', [picture])
const buttonWrapper = await wrapper.find('[data-test="button-upload"]')
await buttonWrapper.trigger('submit.prevent')
expect(spyASequence).toHaveBeenCalledWith(sequenceTitle)
expect(spyPicture).toHaveBeenCalledWith(sequenceId, body)
expect(wrapper.html()).contains('class="uploaded-picture-list"')
expect(wrapper.html()).contains('class="uploaded-picture-item success"')
expect(wrapper.html()).contains(
'100MSDCF_DSC02790.JPG - pages.upload.uploaded_word'
)
expect(wrapper.html()).contains('<router-link')
expect(wrapper.html()).contains(
'class="default button button--white" title="pages.upload.sequence_link"'
)
})
})
})
})

View File

@@ -1,4 +1,5 @@
import moment from 'moment'
import 'moment/dist/locale/fr'
function formatDate(date: Date, formatType: string): string {
const formatDate = moment(date)

View File

@@ -1,10 +1,3 @@
export function img(name: string): string {
return new URL(`../assets/images/${name}`, import.meta.url).toString()
}
export function getPicId(): string {
return window.location.href.substring(
window.location.href.indexOf('pic=') + 4,
window.location.href.lastIndexOf('&')
)
}

View File

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

View File

@@ -1,44 +1,4 @@
import axios from 'axios'
import type {
ViewerMapInterface,
OptionalViewerMapInterface
} from '@/views/interfaces/common'
import GeoVisio from 'geovisio'
async function fetchMapAndViewer(params?: OptionalViewerMapInterface) {
const tiles = import.meta.env.VITE_TILES
let paramsGeovisio: ViewerMapInterface = {
map: {
startWide: true,
maxZoom: 19
}
}
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsGeovisio = {
map: {
...paramsGeovisio.map,
style
}
}
}
if (params) {
const { picId } = params
const { fetchOptions } = params
if (picId) paramsGeovisio = { ...paramsGeovisio, picId: picId }
if (fetchOptions) {
paramsGeovisio = {
...paramsGeovisio,
fetchOptions: fetchOptions
}
}
}
return new GeoVisio(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}api/search`,
paramsGeovisio
)
}
async function getIgnTiles(): Promise<object> {
const { data } = await axios.get(
@@ -58,4 +18,4 @@ async function getIgnTiles(): Promise<object> {
return data
}
export { fetchMapAndViewer }
export { getIgnTiles }

View File

@@ -1,78 +1,96 @@
<template>
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/geovisio@2.1.1/build/index.css"
/>
<main class="entry-page">
<section id="viewer" class="entry-viewer">
<div v-if="mapIsLoaded" class="entry-report-button">
<Button
:text="$t('pages.home.report_button_text')"
look="button--black"
@trigger="triggerMailto"
/>
</div>
</section>
<section id="viewer" class="entry-viewer"></section>
</main>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { onMounted, computed, ref } from 'vue'
import Button from '@/components/Button.vue'
import { fetchMapAndViewer } from '@/utils/mapAndViewer'
import { getPicId } from '@/utils/image'
import { onMounted, ref } from 'vue'
import 'geovisio/build/index.css'
import GeoVisio from 'geovisio'
import { createLink } from '@/components-viewer/reportLink'
import { createUrlLink } from '@/utils/index'
import type { ViewerMapInterface } from '@/views/interfaces/common'
import { getIgnTiles } from '@/utils/mapAndViewer'
const { t } = useI18n()
let mapIsLoaded = ref<boolean>(false)
const mailtoPath = computed<string>(() => {
return `mailto:signalement.ign@panoramax.fr?subject=${t(
'pages.home.report_mail_subject',
{
id: getPicId()
}
)}&body=${t('pages.home.report_mail_body', {
id: getPicId(),
link: encodeURIComponent(window.location.href)
})}`
})
function triggerMailto(): void {
window.location.href = mailtoPath.value
}
let viewer = ref()
onMounted(async () => {
const reportLink = document.createElement('div')
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
const tiles = import.meta.env.VITE_TILES
const maxZoom = import.meta.env.VITE_MAX_ZOOM
let paramsGeovisio: ViewerMapInterface = {
map: {
startWide: true
}
}
if (maxZoom && maxZoom !== '') {
paramsGeovisio = {
map: {
...paramsGeovisio.map,
maxZoom: parseInt(maxZoom)
}
}
}
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsGeovisio = {
map: {
...paramsGeovisio.map,
style
}
}
}
try {
await fetchMapAndViewer()
viewer.value = await new GeoVisio(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsGeovisio,
widgets: { customWidget: reportLink }
}
)
if (viewer.value && viewer.value.addEventListener) {
viewer.value.addEventListener(
'picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
const href = t('pages.home.report_mail', {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
reportLink.innerHTML = createLink(
href,
t('pages.home.report_button_text')
)
}
)
}
mapIsLoaded.value = true
} catch (err) {
console.log(err)
}
})
</script>
<style scoped>
<style scoped lang="scss">
.entry-page {
display: flex;
}
.entry-viewer {
font-size: initial;
width: 100vw;
position: relative;
min-height: calc(100vh - 8rem);
}
.gvs-has-map .entry-report-button {
display: block;
position: absolute;
right: 1rem;
top: 2rem;
z-index: 1;
min-height: calc(100vh - #{toRem(8)});
}
.gvs-focus-map .entry-report-button {
display: none;
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
padding-top: 11rem;
padding-top: toRem(11);
overflow: hidden;
}
@supports (-webkit-touch-callout: none) {
@@ -86,7 +104,7 @@ onMounted(async () => {
@supports not (-webkit-touch-callout: none) {
/* CSS for other than iOS devices */
.entry-viewer {
min-height: calc(100vh - 17rem);
min-height: calc(100vh - #{toRem(17)});
}
}
}

View File

@@ -12,30 +12,30 @@ const myAccountUrl = computed<string>(
)
</script>
<style scoped>
<style lang="scss" scoped>
.entry-page {
padding-right: 4rem;
padding-left: 8rem;
padding-right: toRem(4);
padding-left: toRem(8);
}
.iframe {
width: 100%;
min-height: calc(100vh - 9rem);
min-height: calc(100vh - #{toRem(9)});
}
@media (max-width: 848px) {
@media (max-width: toRem(84.8)) {
.entry-page {
padding-top: 4rem;
padding-top: toRem(4);
}
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.entry-page {
padding-right: 2rem;
padding-left: 2rem;
padding-right: toRem(2);
padding-left: toRem(2);
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
padding-top: 14rem;
padding-top: toRem(14);
}
}
</style>

View File

@@ -1,9 +1,4 @@
<template>
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/geovisio@2.1.1/build/index.css"
/>
<main :class="['entry-page', { 'menu-is-open': menuIsOpen }]">
<div class="button-close">
<Button
@@ -22,38 +17,42 @@
class="button-collapse"
@click="toggleMenu"
>
<span :class="[sequence.status, 'sequence-status']">{{
sequenceStatus
}}</span>
<h1 class="title">
{{ sequence.title }}
</h1>
<div class="wrapper-title">
<span :class="[sequence.status, 'sequence-status']">{{
sequenceStatus
}}</span>
<h1 class="title desktop">
{{ sequence.title }}
</h1>
</div>
<i :class="headerPanelIsOpen ? 'bi bi-dash' : 'bi bi-plus'"></i>
</button>
<div class="wrapper-button">
<div class="disable-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
:text="
sequence.status === 'ready'
? $t('pages.sequence.button_disable')
: $t('pages.sequence.button_enable')
"
look="button--white"
:icon="
sequence.status === 'ready' ? 'bi bi-eye-slash' : 'bi bi-eye'
"
@trigger="patchCollection"
/>
</div>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
:text="$t('pages.sequence.button_delete')"
look="button--red"
icon="bi bi-trash"
@trigger="deleteCollection"
/>
</div>
<h1 class="title responsive">
{{ sequence.title }}
</h1>
</div>
<div class="wrapper-button">
<Button
:tooltip="$t('pages.sequence.hide_sequence_tooltip')"
:text="
sequence.status === 'ready'
? $t('pages.sequence.button_disable')
: $t('pages.sequence.button_enable')
"
look="button--white"
:icon="
sequence.status === 'ready' ? 'bi bi-eye-slash' : 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection"
/>
<Button
:tooltip="$t('pages.sequence.delete_sequence_tooltip')"
:text="$t('pages.sequence.button_delete')"
look="button--red"
icon="bi bi-trash"
@trigger="deleteCollection"
/>
</div>
<div
:class="[sequence.status, 'collapse py-2 show']"
@@ -156,7 +155,7 @@
:selected="photoToDeleteOrPatchSelected(item, picturesToDelete)"
:selected-on-map="itemSelected === item.id"
:status="
imageStatus(item.properties['geovisio:status'], sequence)
imageStatus(item.properties['geovisio:status'], sequence.status)
"
@trigger="selectImageAndMove(item)"
/>
@@ -176,7 +175,7 @@
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</div>
<div v-else class="menu-right wrapper-loader">
<Loader />
<Loader look="sm" :is-loaded="false" />
</div>
</main>
</template>
@@ -193,6 +192,7 @@ import Pagination from '@/components/Pagination.vue'
import InputCheckbox from '@/components/InputCheckbox.vue'
import Loader from '@/components/Loader.vue'
import ImageItem from '@/components/ImageItem.vue'
import 'geovisio/build/index.css'
import { durationCalc, formatDate } from '@/utils/dates'
import {
deleteACollectionItem,
@@ -211,7 +211,7 @@ import {
spliceIntoChunks,
formatPaginationItems
} from '@/views/utils/sequence/index'
import { fetchMapAndViewer } from '@/utils/mapAndViewer'
import { getIgnTiles } from '@/utils/mapAndViewer'
import type {
ResponseUserPhotoInterface,
ResponseUserPhotoLinksInterface,
@@ -219,6 +219,10 @@ import type {
UserSequenceInterface,
ResponseUserSequenceInterface
} from './interfaces/MySequenceView'
import type { ViewerMapInterface } from '@/views/interfaces/common'
import GeoVisio from 'geovisio'
import { createUrlLink } from '@/utils'
import { createLink } from '@/components-viewer/reportLink'
const { t } = useI18n()
const route = useRoute()
@@ -258,11 +262,58 @@ onMounted(async () => {
(el) => el.properties['geovisio:status'] === 'ready'
)
pictures.value = collectionItems
viewer.value = await fetchMapAndViewer({
fetchOptions: {
credentials: 'include'
const tiles = import.meta.env.VITE_TILES
const maxZoom = import.meta.env.VITE_MAX_ZOOM
let paramsGeovisio: ViewerMapInterface = {
map: {
startWide: true
}
})
}
if (maxZoom) {
paramsGeovisio = {
map: {
...paramsGeovisio.map,
maxZoom: parseInt(maxZoom)
}
}
}
if (tiles) {
const style = tiles.includes('wxs.ign.fr') ? await getIgnTiles() : tiles
paramsGeovisio = {
map: {
...paramsGeovisio.map,
style
}
}
}
const reportLink = document.createElement('div')
reportLink.className = 'gvs-group gvs-group-large gvs-group-btnpanel'
viewer.value = await new GeoVisio(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsGeovisio,
fetchOptions: {
credentials: 'include'
},
widgets: { customWidget: reportLink }
}
)
if (viewer.value && viewer.value.addEventListener) {
viewer.value.addEventListener(
'picture-loaded',
async (e: { detail: { picId: string } }): Promise<void> => {
const href = t('pages.home.report_mail', {
picId: e.detail.picId,
link: createUrlLink(e.detail.picId)
})
reportLink.innerHTML = createLink(
href,
t('pages.home.report_button_text')
)
}
)
}
setHeightValue()
if (itemSelected.value.length || !collectionItemsReady[0]) return
viewer.value._api.onceReady().then(() => {
@@ -548,17 +599,20 @@ async function patchOrDeleteCollectionItems(
</script>
<style lang="scss" scoped>
.responsive {
display: none;
}
.entry-page {
display: flex;
}
.entry-viewer {
width: 50vw;
position: relative;
height: calc(100vh - 8rem);
height: calc(100vh - #{toRem(8)});
}
.menu-right {
width: 50vw;
height: calc(100vh - 8rem);
height: calc(100vh - #{toRem(8)});
overflow: hidden;
box-shadow: 0px 4px 20px 0px #00000033;
}
@@ -567,18 +621,22 @@ async function patchOrDeleteCollectionItems(
justify-content: center;
align-items: center;
}
.wrapper-title {
display: flex;
align-items: center;
}
.wrapper-button {
display: flex;
align-items: center;
}
.disable-button {
margin-right: 1rem;
margin-right: toRem(1);
}
.collapse {
&:first-child {
@include text(s-regular);
color: var(--grey-dark);
margin-bottom: 1rem;
margin-bottom: toRem(1);
}
&.hidden {
opacity: 0.4;
@@ -594,28 +652,29 @@ async function patchOrDeleteCollectionItems(
.wrapper-info-top {
display: flex;
flex-direction: column;
margin-top: 1rem;
margin-top: toRem(1);
&:first-child {
border-right: 0.1rem solid var(--grey-dark);
padding-right: 2rem;
border-right: toRem(0.1) solid var(--grey-dark);
padding-right: toRem(2);
}
&:nth-child(2) {
padding-left: 2rem;
padding-left: toRem(2);
}
}
.title {
@include text(h2);
@include text(h4);
color: var(--grey-dark);
margin-right: 1rem;
margin-right: toRem(1);
margin-bottom: 0;
}
.button-close {
display: none;
}
.menu-top {
margin: 2rem 2rem 0;
padding: 1rem 2rem;
border: 0.1rem solid var(--grey);
border-radius: 0.5rem;
margin: toRem(2) toRem(2) 0;
padding: toRem(1) toRem(2);
border: toRem(0.1) solid var(--grey);
border-radius: toRem(0.5);
background-color: var(--blue-semi);
}
.header-menu {
@@ -623,21 +682,22 @@ async function patchOrDeleteCollectionItems(
justify-content: space-between;
}
.sequence-status {
border-radius: 3rem;
padding: 0.5rem 1rem;
margin-right: 2rem;
@include text(xs-r-regular);
border-radius: toRem(3);
padding: toRem(0.5) toRem(1);
margin-right: toRem(1);
color: var(--white);
&.ready {
background-color: var(--orange);
border: 0.1rem solid var(--orange);
border: toRem(0.1) solid var(--orange);
}
&.waiting-for-process {
background-color: var(--yellow);
border: 0.1rem solid var(--yellow);
border: toRem(0.1) solid var(--yellow);
}
&.hidden {
background-color: var(--blue-geovisio);
border: 0.1rem solid var(--blue-geovisio);
border: toRem(0.1) solid var(--blue-geovisio);
}
}
.button-collapse {
@@ -646,24 +706,28 @@ async function patchOrDeleteCollectionItems(
padding: 0;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: toRem(1);
}
.bi-plus,
.bi-dash {
color: var(--grey-dark);
font-size: 3rem;
font-size: toRem(3);
}
.photos-wrapper {
padding: 1rem 0rem 2rem 1rem;
padding: toRem(1) 0 toRem(2) toRem(1);
height: calc(100vh - v-bind(menuHeight));
}
.delete-all {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 1rem;
margin-right: 2rem;
margin-bottom: 1rem;
margin-left: toRem(1);
margin-right: toRem(2);
margin-bottom: toRem(1);
@include text(xs-r-regular);
}
.wrapper-select {
display: flex;
@@ -673,15 +737,12 @@ async function patchOrDeleteCollectionItems(
@include text(xs-regular);
}
.photo-selected-separator {
margin-right: 0.5rem;
margin-left: 0.5rem;
margin-right: toRem(0.5);
margin-left: toRem(0.5);
}
.button-hidde {
margin-right: 1rem;
margin-left: 1rem;
}
.delete-all-text {
@include text(xs-r-regular);
margin-right: toRem(1);
margin-left: toRem(1);
}
.action-buttons {
display: flex;
@@ -696,29 +757,28 @@ async function patchOrDeleteCollectionItems(
padding: 0;
}
.photo-item {
width: calc(33% - 2rem);
width: calc(33% - #{toRem(2)});
height: fit-content;
margin: 1rem;
border-radius: 0.5rem;
margin: toRem(1);
border-radius: toRem(0.5);
background-color: var(--grey);
}
.no-photo {
@include text(s-regular);
text-align: center;
margin-top: 10rem;
margin-top: toRem(10);
color: var(--grey-dark);
}
.entry-pagination {
margin-top: 2rem;
margin-top: toRem(2);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: 1024px) {
@media (max-width: toRem(102.4)) {
.header-menu {
flex-direction: column;
align-items: flex-start;
margin-bottom: 1rem;
}
.block-collapse {
flex-direction: column;
@@ -735,10 +795,33 @@ async function patchOrDeleteCollectionItems(
}
}
.photo-item {
width: calc(50% - 2rem);
width: calc(50% - #{toRem(2)});
}
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.desktop {
display: none;
}
.responsive {
display: initial;
}
.title {
margin-right: 0;
margin-bottom: toRem(1);
}
.wrapper-button {
flex-direction: column;
align-items: initial;
}
.disable-button {
margin-right: 0;
margin-bottom: toRem(1);
}
.button-collapse {
width: 100%;
justify-content: space-between;
margin-bottom: 0;
}
.photo-item {
width: 100%;
margin-right: 0;
@@ -748,10 +831,10 @@ async function patchOrDeleteCollectionItems(
margin-right: 0;
}
.wrapper-button {
margin-top: 1rem;
margin-top: toRem(1);
}
.photo-list {
padding-right: 2rem;
padding-right: toRem(2);
}
.delete-all {
text-align: left;
@@ -761,16 +844,16 @@ async function patchOrDeleteCollectionItems(
align-items: initial;
}
.wrapper-photo-selected:nth-child(2) {
margin-top: 0.5rem;
margin-top: toRem(0.5);
}
.photo-selected-separator {
display: none;
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
height: calc(100vh - 11rem);
height: calc(100vh - #{toRem(11)});
overflow: hidden;
}
.entry-viewer {
@@ -779,7 +862,7 @@ async function patchOrDeleteCollectionItems(
z-index: 1;
}
.menu-right {
padding-top: 11rem;
padding-top: toRem(11);
height: 100%;
position: fixed;
top: 0;
@@ -794,15 +877,19 @@ async function patchOrDeleteCollectionItems(
.button-close {
position: absolute;
right: 0;
top: 22rem;
top: toRem(22);
z-index: 3;
background-color: var(--black);
height: 5rem;
height: toRem(5);
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: toRem(0.5);
border-bottom-left-radius: toRem(0.5);
background-color: var(--white);
border: toRem(0.1) solid var(--black);
}
.menu-top {
padding: toRem(1);
}
.menu-is-open {
.menu-right {
@@ -812,19 +899,19 @@ async function patchOrDeleteCollectionItems(
width: auto;
}
.button-close {
left: calc(20vw - 3rem);
left: calc(20vw - #{toRem(3)});
right: initial;
}
}
.entry-pagination {
margin-top: 1rem;
margin-top: toRem(1);
}
}
@media (min-width: 1900px) {
.menu-right {
width: initial;
max-width: 100rem;
max-width: toRem(100);
}
.entry-viewer {
width: 100%;

View File

@@ -102,7 +102,7 @@
{{ $t('pages.sequences.no_sequences_text') }}
</p>
<Link
:text="$t('general.header.contribute_text')"
:text="$t('general.header.upload_text')"
look="button"
:route="{ name: 'share-pictures' }"
/>
@@ -194,19 +194,19 @@ onMounted(async () => {
<style lang="scss" scoped>
.entry-page {
padding-right: 8rem;
padding-left: 8rem;
padding-top: 11rem;
min-height: calc(100vh - 8rem);
padding-right: toRem(8);
padding-left: toRem(8);
padding-top: toRem(11);
min-height: calc(100vh - #{toRem(8)});
}
.sequences-title {
@include text(h1);
margin-bottom: 4rem;
margin-bottom: toRem(4);
}
.sequence-list {
box-shadow: 0px 2px 30px 0px #0000000f;
border-radius: 2rem;
border-radius: toRem(2);
padding: 0;
}
.sequence-item {
@@ -217,18 +217,18 @@ onMounted(async () => {
margin: auto;
background-color: var(--blue-pale);
&:last-child {
border-bottom-right-radius: 2rem;
border-bottom-left-radius: 2rem;
border-bottom-right-radius: toRem(2);
border-bottom-left-radius: toRem(2);
.button-item {
border-bottom-right-radius: 2rem;
border-bottom-left-radius: 2rem;
border-bottom-right-radius: toRem(2);
border-bottom-left-radius: toRem(2);
}
}
&:first-child {
margin-bottom: 1rem;
padding: 1rem 3rem;
border-bottom: 0.1rem solid var(--grey);
border-radius: 2rem 2rem 0rem 0rem;
margin-bottom: toRem(1);
padding: toRem(1) toRem(3);
border-bottom: toRem(0.1) solid var(--grey);
border-radius: toRem(2) toRem(2) 0rem 0rem;
background-color: var(--white);
}
&:nth-child(2n) {
@@ -244,11 +244,11 @@ onMounted(async () => {
}
.wrapper-thumb-hover {
display: none;
border-radius: 0.3rem;
border: 1rem solid var(--grey);
border-radius: toRem(0.3);
border: toRem(1) solid var(--grey);
position: absolute;
bottom: 0;
height: 15rem;
height: toRem(15);
z-index: 1;
}
.thumb-hover {
@@ -263,30 +263,30 @@ onMounted(async () => {
height: 100%;
width: 100%;
object-fit: cover;
border-radius: 0.5rem;
border-radius: toRem(0.5);
position: relative;
}
.button-item {
display: flex;
align-items: center;
width: 100%;
padding: 2rem 3rem;
padding: toRem(2) toRem(3);
background-color: transparent;
border: none;
text-decoration: none;
& > * {
padding: 1rem;
padding: toRem(1);
text-align: initial;
width: 31%;
color: var(--black);
}
> :first-child {
color: var(--blue);
width: 6rem;
width: toRem(6);
}
& > :first-child {
padding: 0;
margin-right: 2rem;
margin-right: toRem(2);
}
& > :nth-child(2) {
color: var(--blue);
@@ -299,48 +299,49 @@ onMounted(async () => {
}
}
.bi-images {
margin-right: 0.5rem;
margin-right: toRem(0.5);
}
.sequence-header-item {
width: 31%;
&:first-child {
margin-right: 2rem;
margin-right: toRem(2);
}
&:first-child {
width: 6rem;
width: toRem(6);
}
}
.no-sequence {
padding-top: 2rem;
padding-bottom: 4rem;
padding-top: toRem(2);
padding-bottom: toRem(4);
margin: auto;
width: fit-content;
text-align: center;
@include text(m-regular);
}
.no-sequence-text {
margin-bottom: 4rem;
margin-bottom: toRem(4);
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
margin-top: 20rem;
margin-top: toRem(20);
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.entry-page {
padding-right: 2rem;
padding-left: 2rem;
padding-top: 14rem;
min-height: calc(100vh - 11rem);
padding-right: toRem(2);
padding-left: toRem(2);
padding-top: toRem(14);
min-height: calc(100vh - #{toRem(11)});
}
.button-item,
.sequence-item:first-child {
padding-right: 1rem;
padding-left: 1rem;
padding-right: toRem(1);
padding-left: toRem(1);
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.button-item {
flex-direction: column;
align-items: center;
@@ -356,8 +357,8 @@ onMounted(async () => {
display: none;
}
.sequence-item {
border-top-right-radius: 1rem;
border-top-left-radius: 1rem;
border-top-right-radius: toRem(1);
border-top-left-radius: toRem(1);
}
}
</style>

View File

@@ -9,7 +9,7 @@
<div class="entry-button">
<Button
:data-test="`button-eye-${i}`"
look="button--rounded"
look="no-text"
:tooltip="$t('pages.settings.setting_tooltip')"
:icon="
!item.token || item.isHidden ? 'bi bi-eye' : 'bi bi-eye-slash'
@@ -26,7 +26,7 @@
<Button
:data-test="`button-copy-${i}`"
look="button--white"
:text="$t('pages.upload.button_copy')"
:text="$t('pages.share_pictures.button_copy')"
:icon="
item.copied
? 'bi bi-clipboard-check-fill'
@@ -86,31 +86,31 @@ onMounted(async () => {
<style lang="scss" scoped>
.entry-page {
padding-right: 8rem;
padding-left: 8rem;
padding-top: 11rem;
min-height: calc(100vh - 8rem);
padding-right: toRem(8);
padding-left: toRem(8);
padding-top: toRem(11);
min-height: calc(100vh - #{toRem(8)});
}
.settings-title {
@include text(h1);
margin-bottom: 4rem;
margin-bottom: toRem(4);
}
.settings-list {
padding-left: 0;
}
.settings-item-title {
font-size: 1.8rem;
font-size: toRem(1.8);
}
.settings-item {
font-size: 1.4rem;
border: 0.1rem solid var(--grey);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
font-size: toRem(1.4);
border: toRem(0.1) solid var(--grey);
border-radius: toRem(0.5);
padding: toRem(1.5);
margin-bottom: toRem(2);
}
.item-information {
height: 5rem;
margin-top: 1rem;
height: toRem(5);
margin-top: toRem(1);
display: flex;
justify-content: space-between;
align-items: center;
@@ -120,17 +120,18 @@ onMounted(async () => {
width: 80%;
}
.token {
margin-top: toRem(0.4);
width: 100%;
word-wrap: break-word;
}
.entry-button {
margin-right: 1rem;
margin-right: toRem(1);
}
@media (max-width: 1024px) {
@media (max-width: toRem(102.4)) {
.settings-item {
position: relative;
height: 14rem;
height: toRem(14);
}
.item-information {
height: initial;
@@ -138,21 +139,21 @@ onMounted(async () => {
align-items: flex-start;
}
.reveal-token {
margin-right: 2rem;
margin-right: toRem(2);
}
.entry-copy-button {
position: absolute;
bottom: 1rem;
right: 1rem;
bottom: toRem(1);
right: toRem(1);
}
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.entry-page {
padding-right: 2rem;
padding-left: 2rem;
padding-top: 14rem;
min-height: calc(100vh - 11rem);
padding-right: toRem(2);
padding-left: toRem(2);
padding-top: toRem(14);
min-height: calc(100vh - #{toRem(11)});
}
.settings-item {
height: initial;
@@ -162,7 +163,7 @@ onMounted(async () => {
}
.token {
max-width: initial;
width: calc(100% - 3.5rem);
width: calc(100% - #{toRem(3.5)});
}
.entry-copy-button {
display: none;

View File

@@ -3,7 +3,7 @@
<section id="sec-1" class="section-upload">
<div class="wrapper-upload">
<div class="wrapper-upload-text">
<h1 class="upload-title">{{ $t('pages.upload.title') }}</h1>
<h1 class="upload-title">{{ $t('pages.share_pictures.title') }}</h1>
<div class="wrapper-check">
<div class="wrapper-img-icon">
<i class="bi bi-images img-icon"></i>
@@ -11,41 +11,53 @@
<div class="element-check">
<span class="block-check"
><span class="check-border"></span
>{{ $t('pages.upload.photo_type1') }}</span
>{{ $t('pages.share_pictures.photo_type1') }}</span
>
<span class="block-check"
><span class="check-border"></span
>{{ $t('pages.upload.photo_type2') }}</span
>{{ $t('pages.share_pictures.photo_type2') }}</span
>
<span class="block-check"
><span class="check-border"></span
>{{ $t('pages.upload.photo_type3') }}</span
>{{ $t('pages.share_pictures.photo_type3') }}</span
>
<span class="block-check"
><span class="check-border"></span
>{{ $t('pages.upload.photo_type4') }}</span
>{{ $t('pages.share_pictures.photo_type4') }}</span
>
</div>
</div>
<p
v-if="authConf && authConf.license && authConf.license.url"
class="upload-text"
v-html="
$t('pages.upload.description', {
check: checkImg
$t('pages.share_pictures.description', {
check: checkImg,
licenseUrl: authConf.license.url,
licenseName: authConf.license.id
})
"
/>
<p class="upload-text">{{ $t('pages.upload.footer_block') }}</p>
<div v-if="!isLogged && authConf.enabled" class="wrapper-account">
<p class="upload-text">
{{ $t('pages.share_pictures.footer_block') }}
</p>
<div
v-if="
!isLogged && authConf && authConf.auth && authConf.auth.enabled
"
class="wrapper-account"
>
<h4 class="account-subtitle">
{{ $t('pages.upload.sub_title') }}
{{ $t('pages.share_pictures.sub_title') }}
</h4>
<div class="entry-link">
<Link
:text="$t('pages.upload.user_account_button')"
:text="$t('pages.share_pictures.user_account_button')"
type="external"
look="button white"
:path="getAuthRoute('auth/login', 'partager-des-photos')"
look="button button--blue"
:path-external="
getAuthRoute('auth/login', 'partager-des-photos')
"
/>
</div>
</div>
@@ -54,7 +66,7 @@
<div class="image">
<img
src="@/assets/images/upload.png"
:alt="$t('pages.upload.alt_img_upload')"
:alt="$t('pages.share_pictures.alt_img_upload')"
/>
</div>
</div>
@@ -80,25 +92,43 @@
</div>
</div>
<div class="wrapper-upload-text">
<h2 class="upload-title">{{ $t('pages.upload.title_terminal') }}</h2>
<h2 class="upload-title">
{{ $t('pages.share_pictures.title_terminal') }}
</h2>
<p
class="upload-text"
v-html="$t('pages.upload.description_terminal')"
v-html="$t('pages.share_pictures.description_terminal')"
></p>
<p
<div
v-if="authConf && authConf.license && authConf.license.url"
class="upload-text grey"
v-html="$t('pages.upload.footer_description_terminal')"
></p>
>
<p>
{{
$t('pages.share_pictures.footer_description_terminal', {
word: 'la license'
})
}}
</p>
<Link
:href="authConf.license.url"
:text="authConf.license.id"
target="_blank"
type="external"
look="link--grey"
@trigger="triggerHref"
/>
</div>
<div class="wrapper-account">
<h4 class="account-subtitle">
{{ $t('pages.upload.cli_title') }}
{{ $t('pages.share_pictures.cli_title') }}
</h4>
<div class="entry-link">
<Link
:text="$t('pages.upload.button')"
:text="$t('pages.share_pictures.button')"
type="external"
look="button"
path="https://gitlab.com/geovisio/cli"
look="button button--blue"
path-external="https://gitlab.com/geovisio/cli"
/>
</div>
</div>
@@ -125,17 +155,17 @@ const route = useRoute()
let hrefSection = ref<string>('#sec-2')
let icon = ref<string>('bi bi-chevron-down')
const checkImg =
"<span style='background:white; padding: 0.5rem 0.525rem;border-radius:50%; font-size:0.8rem;'>✔</span>"
"<span style='background:white; padding: 5px 0.0525px;border-radius:50%; font-size:8px;'>✔</span>"
const isLogged = computed((): boolean => !!cookies.get('user_id'))
const terminalText = computed((): string => {
const url = import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL
: 'https://panoramax.ign.fr/'
return t('pages.upload.terminal_text', { url })
return t('pages.share_pictures.terminal_text', { url })
})
const terminalTextInstall = computed((): string =>
t('pages.upload.terminal_install')
t('pages.share_pictures.terminal_install')
)
function triggerHref(): void {
icon.value =
@@ -152,32 +182,32 @@ function triggerHref(): void {
@include text(h4);
}
.entry-link {
margin-top: 1rem;
margin-top: toRem(1);
}
.upload-text {
@include text(m-regular);
margin-top: 5rem;
margin-top: toRem(5);
}
.wrapper-check {
position: relative;
width: fit-content;
padding: 3rem;
padding: toRem(3);
background-color: var(--white);
border-radius: 1.5rem;
font-size: 1.6rem;
margin-top: 8rem;
border-radius: toRem(1.4);
font-size: toRem(1.6);
margin-top: toRem(8);
}
.wrapper-img-icon {
background-color: var(--black);
border-radius: 50%;
position: absolute;
height: 5.5rem;
width: 5.5rem;
height: toRem(5.5);
width: toRem(5.5);
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
top: -3rem;
font-size: toRem(3);
top: toRem(-3);
left: 50%;
transform: translate(-50%);
}
@@ -187,31 +217,31 @@ function triggerHref(): void {
.element-check {
display: flex;
flex-direction: column;
margin-top: 3rem;
margin-top: toRem(3);
}
.block-check {
margin-bottom: 1rem;
margin-bottom: toRem(1);
display: flex;
}
.check-border {
display: flex;
justify-content: center;
align-items: center;
width: 2.2rem;
height: 2.2rem;
width: toRem(2.2);
height: toRem(2.2);
border-radius: 50%;
font-size: 0.8rem;
font-size: toRem(0.8);
background: var(--white);
border: 0.1rem solid var(--black);
margin-right: 0.5rem;
border: toRem(0.1) solid var(--black);
margin-right: toRem(0.5);
}
.block-check:last-child {
margin-bottom: 0;
}
.section-upload {
height: 100%;
padding-right: 2rem;
padding-left: 2rem;
padding-right: toRem(2);
padding-left: toRem(2);
display: flex;
flex-direction: column;
justify-content: center;
@@ -230,26 +260,33 @@ function triggerHref(): void {
white-space: pre-wrap;
}
.grey {
display: flex;
align-items: center;
flex-wrap: wrap;
color: var(--grey-semi-dark);
p {
margin-bottom: 0;
margin-right: toRem(0.5);
}
}
.wrapper-account {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 3rem;
padding-top: 1rem;
border-top: 0.1rem solid #e6e6e6;
margin-top: toRem(3);
padding-top: toRem(1);
border-top: toRem(0.1) solid #e6e6e6;
}
.upload-button {
display: flex;
margin-top: 3rem;
margin-top: toRem(3);
}
.image {
background-color: var(--white);
border-radius: 1rem;
padding: 1rem;
border: 1px solid black;
border-radius: toRem(1);
padding: toRem(1);
border: 1px solid var(--black);
width: 75%;
height: fit-content;
}
@@ -258,30 +295,30 @@ function triggerHref(): void {
justify-content: center;
width: 35%;
overflow: hidden;
margin-left: 6rem;
margin-left: toRem(6);
}
.entry-terminal {
margin-left: 0;
margin-right: 6rem;
margin-right: toRem(6);
}
.entry-image img {
width: 100%;
border-radius: 1rem;
border-radius: toRem(1);
}
.entry-button-down {
z-index: 1;
position: fixed;
right: 2rem;
bottom: calc(20vh - 10.5rem);
right: toRem(2);
bottom: calc(20vh - #{toRem(10.5)});
}
@media (max-width: 1024px) {
@media (max-width: toRem(102.4)) {
.section-upload {
height: initial;
}
.wrapper-upload {
flex-direction: column-reverse;
padding-top: 6rem;
padding-bottom: 6rem;
padding-top: toRem(6);
padding-bottom: toRem(6);
}
.wrapper-upload-text {
width: 100%;
@@ -295,12 +332,12 @@ function triggerHref(): void {
}
.terminal {
width: 100%;
margin-top: 4rem;
margin-top: toRem(4);
}
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.entry-page {
padding-top: 11rem;
padding-top: toRem(11);
}
.entry-image {
width: 60%;
@@ -315,10 +352,10 @@ function triggerHref(): void {
margin-right: 0;
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
min-height: calc(100vh - 11rem);
padding-top: 11rem;
min-height: calc(100vh - #{toRem(11)});
padding-top: toRem(11);
}
.entry-image {
width: 100%;
@@ -334,14 +371,14 @@ function triggerHref(): void {
padding-left: 0;
}
.upload-text {
margin-bottom: 3rem;
margin-bottom: toRem(3);
}
.wrapper-account {
padding-top: 3rem;
padding-top: toRem(3);
flex-direction: column;
}
.account-subtitle {
margin-bottom: 2rem;
margin-bottom: toRem(2);
}
.entry-button-down {
display: none;

View File

@@ -0,0 +1,266 @@
<template>
<main class="entry-page">
<section class="upload-section">
<h1 class="settings-title">{{ $t('pages.upload.title') }}</h1>
<form v-if="!isLoading" @submit.prevent="uploadPicture">
<InputUpload
:text="$t('pages.upload.input_label')"
:text-second-part="$t('pages.upload.import_word')"
:text-picture-type="$t('pages.upload.import_type')"
accept="image/jpeg"
data-test="input-add-pictures"
@trigger="addPictures"
/>
<div class="footer-form">
<span v-if="fileUploaded" class="number-file-text">{{
fileUploaded
}}</span>
<span v-else class="number-file-text">{{
t('pages.upload.no_uploaded_files')
}}</span>
<Button
:text="$t('pages.upload.button_text')"
:disabled="!pictures || pictures.length < 1"
type="submit"
data-test="button-upload"
look="button button--blue"
/>
</div>
</form>
<UploadLoader
v-else
:load-percentage="loadPercentage"
:load-text-size="loadTextSize"
:is-loaded="isLoaded"
:uploaded-sequences="uploadedSequences"
:pictures-count="picturesCount"
@triggerNewUpload="triggerNewUpload"
/>
</section>
<ImportedSection
v-for="(sequence, i) in uploadedSequences"
:index="i"
:sequence="sequence"
:pictures-count="picturesCount"
:upload-errors="sequence.picturesOnError"
:upload-pictures="sequence.pictures"
/>
</main>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onUnmounted } 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 ImportedSection from '@/components/upload/ImportedSection.vue'
import UploadLoader from '@/components/upload/UploadLoader.vue'
import type { sequenceInterface } from './interfaces/UploadPicturesView'
import { formatDate } from '@/utils/dates'
import {
createAPictureToASequence,
createASequence
} from '@/views/utils/upload/request'
import {
formatPictureSize,
formatTextSize,
sortByName
} from '@/views/utils/upload/index'
const { t } = useI18n()
let pictures = ref<File[] | []>([])
let picturesCount = ref<number>(0)
let isLoading = ref<boolean>(false)
let isLoaded = ref<boolean>(false)
let uploadingPictures = ref<FileList | []>([])
let uploadedSequences = ref<sequenceInterface[] | []>([])
let picturesUploadingSize = ref<number>(0)
let picturesToUploadSize = ref<number>(0)
let loadPercentage = ref<string>('0%')
let loadTextSize = ref<string>('0 Mo')
watch(isLoading, () => {
if (isLoading.value) {
window.onbeforeunload = function (e) {
e = e || window.event
if (e) e.returnValue = 'Sure?'
return 'Sure?'
}
} else {
window.onbeforeunload = null
}
})
onUnmounted(() => {
window.onbeforeunload = null
})
onBeforeRouteLeave((to, from, next) => {
if (isLoading.value) {
const answer = window.confirm(t('pages.upload.leave_message'))
if (answer) return next()
return next(false)
}
next()
})
const fileUploaded = computed<string>(() => {
return t('pages.upload.uploaded_files', { count: picturesCount.value })
})
function picturesToUploadSizeText(): void {
let fullSize = 0
for (let i = 0; i < pictures.value.length; i++) {
fullSize += pictures.value[i].size
}
picturesToUploadSize.value = fullSize
loadTextSize.value = formatTextSize(fullSize, formatPictureSize(fullSize))
}
function calcPercentage(): void {
if (picturesUploadingSize.value && picturesToUploadSize.value) {
loadPercentage.value = `${(
(picturesUploadingSize.value / picturesToUploadSize.value) *
100
).toFixed(0)}%`
}
}
function triggerNewUpload(): void {
isLoading.value = false
picturesCount.value = 0
}
function addPictures(value: FileList): void {
const files = sortByName([].slice.call(value))
pictures.value = files
picturesCount.value = pictures.value.length
picturesUploadingSize.value = 0
picturesToUploadSize.value = 0
loadPercentage.value = '0%'
loadTextSize.value = '0 Mo'
}
function setPictureSizeValue(sequence: sequenceInterface, size: number): void {
picturesUploadingSize.value = picturesUploadingSize.value + size
sequence.pictureSize = formatTextSize(
picturesUploadingSize.value,
formatPictureSize(picturesUploadingSize.value)
)
}
async function uploadPicture(): Promise<void> {
if (!pictures.value || !pictures.value.length) {
return
}
isLoaded.value = false
isLoading.value = true
picturesToUploadSizeText()
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 sequence: sequenceInterface = {
title: sequenceTitle,
id: data.id,
pictures: [],
picturesOnError: [],
pictureCount: pictures.value.length,
pictureSize: ''
}
uploadedSequences.value = [sequence, ...uploadedSequences.value]
let i = 0
for (let el of picturesToUpload) {
const body = new FormData()
i++
body.append('position', i.toString())
body.append('picture', el)
try {
const pictureUploaded = await createAPictureToASequence(data.id, body)
const pictures = { ...pictureUploaded.data, name: el.name }
setPictureSizeValue(sequence, el.size)
sequence.pictures = [...sequence.pictures, pictures]
const index = uploadedSequences.value.findIndex(
(item: sequenceInterface) => item.id === data.id
)
uploadedSequences.value.splice(index, 1)
uploadedSequences.value = [sequence, ...uploadedSequences.value]
calcPercentage()
} catch (err: any) {
setPictureSizeValue(sequence, el.size)
const picturesOnError = {
message: err.response.data.message,
name: el.name
}
sequence.picturesOnError = [...sequence.picturesOnError, picturesOnError]
calcPercentage()
}
}
isLoaded.value = true
uploadingPictures.value = []
pictures.value = []
}
</script>
<style scoped lang="scss">
ul {
padding-left: 0;
margin-bottom: 0;
}
.entry-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.upload-section {
width: 80%;
padding: toRem(4);
margin-top: toRem(6);
margin-bottom: toRem(4);
box-shadow: 0px 6.45694px 8.60925px rgba(0, 0, 0, 0.05);
}
.settings-title {
text-align: center;
@include text(h1);
margin-bottom: toRem(4);
}
.footer-form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: toRem(3);
}
.number-file-text {
margin-bottom: toRem(2);
color: var(--blue-dark);
@include text(s-regular);
}
::-webkit-scrollbar {
width: toRem(2);
}
::-webkit-scrollbar-track {
background: var(--grey-pale);
}
::-webkit-scrollbar-thumb {
background: var(--blue);
border-radius: toRem(5);
}
@media (max-width: toRem(76.8)) {
.entry-page {
overflow-x: hidden;
}
.information-section {
flex-direction: column;
}
}
@media (max-width: toRem(50)) {
.upload-section {
margin-top: toRem(15);
width: 100%;
padding-right: toRem(2);
padding-left: toRem(2);
}
.information-section {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,12 @@
export interface sequenceInterface {
title: string
id: string
pictures: any[]
picturesOnError: uploadErrorInterface[] | []
pictureCount: number
pictureSize: string
}
export interface uploadErrorInterface {
message: string
name: string
}

View File

@@ -3,12 +3,15 @@ export interface OptionalViewerMapInterface {
credentials: string
}
picId?: string
widgets?: {
customWidget: HTMLAnchorElement
}
}
export interface ViewerMapInterface extends OptionalViewerMapInterface {
map: {
startWide: boolean
style?: object | string
maxZoom: number
maxZoom?: number
}
}

View File

@@ -1,14 +1,10 @@
import type {
UserSequenceInterface,
ResponseUserPhotoInterface,
ResponseUserPhotoLinksInterface
} from '@/views/interfaces/MySequenceView'
function imageStatus(
imageStatus: string,
userSequence: UserSequenceInterface
): string {
if (userSequence.status === 'hidden') return userSequence.status
function imageStatus(imageStatus: string, sequenceStatus: string): string {
if (sequenceStatus === 'hidden') return sequenceStatus
return imageStatus
}

View File

@@ -0,0 +1,16 @@
function formatPictureSize(size: number): number {
return Math.floor(Math.log(size) / Math.log(1024))
}
function formatTextSize(size: number, i: number): string {
const sizes = ['0', 'Ko', 'Mo', 'Go', 'To', 'Po', 'Eo', 'Zo', 'o']
return `${parseFloat((size / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`
}
function sortByName(fileList: File[]): File[] {
return fileList.sort((a: File, b: File) =>
a.name.localeCompare(b.name, navigator.languages[0] || navigator.language, {
numeric: true,
ignorePunctuation: true
})
)
}
export { formatPictureSize, formatTextSize, sortByName }

View File

@@ -0,0 +1,11 @@
import axios from 'axios'
function createASequence(title: string): Promise<any> {
return axios.post('api/collections', { title: title })
}
async function createAPictureToASequence(id: string, body: any): Promise<any> {
return await axios.post(`api/collections/${id}/items`, body)
}
export { createASequence, createAPictureToASequence }

View File

@@ -19,7 +19,8 @@ export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/font-size.scss";'
additionalData:
'@import "@/assets/font-size.scss"; @import "@/assets/rem-calc.scss";'
}
}
},

View File

@@ -19,5 +19,10 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
},
test: {
deps: {
inline: ['moment']
}
}
})

3533
yarn.lock

File diff suppressed because it is too large Load Diff