19 Commits

Author SHA1 Message Date
Andreani Jean
8a7dc6d07d add new deisgn 2023-08-09 12:54:47 +02:00
Andreani Jean
fd8d3168a8 add nex version 2023-08-08 18:44:38 +02:00
Andreani Jean
db29dfdc4c refacto css to convert rem to px with sass function 2023-08-07 15:33:25 +02:00
Andreani Jean
e27c68c46c fix add other type file drag drop 2023-08-07 10:37:20 +02:00
Andreani Jean
403188eea1 fix sequences 2023-08-07 09:53:58 +02:00
Andreani Jean
b48acd231d wip test + add alerte is leave page 2023-08-07 09:53:58 +02:00
Andreani Jean
86d2ad76cb wip 2023-08-07 09:53:58 +02:00
Andreani Jean
a68a08f909 fix pipeline test e2e 2023-08-07 09:53:58 +02:00
Andreani Jean
cbe3f8e7be fix test:e2e 2023-08-07 09:53:58 +02:00
Andreani Jean
da67295896 add and fix test 2023-08-07 09:53:58 +02:00
Andreani Jean
62acc345c2 update geovisio + add tests to utils 2023-08-07 09:53:58 +02:00
Andreani Jean
9162a3a600 add fr moment locale 2023-08-07 09:53:58 +02:00
Andreani Jean
dcb9b1eb8e fix sort function 2023-08-07 09:53:58 +02:00
Andreani Jean
3e2ea47f74 fix TS 2023-08-07 09:53:58 +02:00
Andreani Jean
4bb802acbc fix interface + button & link css + font css viewer 2023-08-07 09:53:58 +02:00
Andreani Jean
12ef180e90 add V1 upload pictures 2023-08-07 09:53:58 +02:00
Andreani Jean
7ef22ea09d wip upload interface 2023-08-07 09:53:57 +02:00
Andreani Jean
0e9ec753da fix type check 2023-08-07 09:53:57 +02:00
Andreani Jean
ad60792020 test in dev 2023-08-07 09:53:57 +02:00
47 changed files with 2139 additions and 2149 deletions

View File

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

4
.gitignore vendored
View File

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

View File

@@ -5,12 +5,6 @@ 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
@@ -63,71 +57,3 @@ 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

@@ -4,24 +4,12 @@ All notable changes to this project will be documented in this file.
Before _0.1.0_ Changelog didn't exist.
## [2.1.0] - 2023-08-29
### Added
- A new page `/telecharger` 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
### Fixed
- fix a bug in the header hidden sub menu when authentication is not with keycloak
## [0.1.0] -
### 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
@@ -32,6 +20,7 @@ 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

View File

@@ -1,26 +0,0 @@
FROM node:18.16.0-alpine
WORKDIR /opt/geovisio
# Import dependencies files
COPY package.json yarn.lock ./
# Install NodeJS dependencies
RUN yarn install
# Import source code
COPY static ./static
COPY src ./src
COPY *.json *.js *.ts *.html ./
# Build code
RUN yarn build
# Environment variables
ENV PORT=3000
ENV VITE_INSTANCE_NAME="GeoVisio/Docker"
ENV VITE_API_URL="https://panoramax.ign.fr/"
# Expose
EXPOSE 3000
CMD ["yarn", "start"]

View File

@@ -12,6 +12,11 @@ export default defineConfig({
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:5173'
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'
}
})

View File

@@ -1,15 +1,6 @@
# Setup
GeoVisio website can be installed through classic method, or using Docker.
__Contents__
[[_TOC_]]
## Classic install
### System requirements
## System requirements
**You need to have [Nodejs installed](https://nodejs.org/en/download)**
Node version : >=18.13.0
@@ -18,7 +9,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:
@@ -31,7 +22,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.
@@ -50,31 +41,6 @@ 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/" \
-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

@@ -1,24 +0,0 @@
# 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
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": "2.1.0",
"version": "1.0.0",
"engines": {
"node": "18.16.0"
},
@@ -25,7 +25,7 @@
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"geovisio": "2.1.2",
"geovisio": "2.1.1",
"moment": "^2.29.4",
"pinia": "^2.1.4",
"vue": "^3.2.45",

View File

@@ -27,11 +27,7 @@ useMeta({
</metainfo>
<Header
:auth-enabled="authConf.enabled"
:user-profile-url="
authConf.user_profile && authConf.user_profile.url
? authConf.user_profile.url
: ''
"
:user-profile-url="authConf.user_profile ? authConf.user_profile.url : ''"
/>
<RouterView />
</template>

View File

@@ -1,7 +1,7 @@
@mixin text($size) {
@if $size == h1 {
font-weight: normal;
font-size: toRem(4);
font-size: toRem(3);
@media (max-width: toRem(50)) {
font-size: toRem(2.6);
@@ -15,6 +15,14 @@
font-size: toRem(1.8);
}
}
@if $size == h3 {
font-weight: normal;
font-size: toRem(1.8);
@media (max-width: toRem(50)) {
font-size: toRem(1.6);
}
}
@if $size == h4 {
font-weight: normal;
font-size: toRem(1.6);

BIN
src/assets/images/360.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -40,6 +40,7 @@ h5 {
--blue-semi: rgba(207, 226, 255, 0.5);
--blue-pale: #f9fafd;
--beige: #f5f3ec;
--beige-pale: #ececec80;
--yellow: #fec868;
--orange: #ff6f00;
--green: #59ce8f;

View File

@@ -69,14 +69,14 @@ defineProps({
}
.button--transparent {
border: toRem(1) solid var(--white);
border: toRem(0.1) solid var(--white);
background-color: var(--black);
color: var(--white);
}
.button--red {
color: var(--red);
background-color: var(--white);
border: toRem(1) solid var(--red);
border: toRem(0.1) solid var(--red);
.icon {
margin-right: 0;
font-size: toRem(1.4);
@@ -89,7 +89,7 @@ defineProps({
.button--white {
color: var(--black);
background-color: var(--white);
border: toRem(1) solid var(--black);
border: toRem(0.1) solid var(--black);
.icon {
font-size: toRem(1.4);
color: var(--black);

View File

@@ -7,9 +7,14 @@
<div class="wrapper-logo desktop">
<Link
:image="{ url: 'logo.jpeg', alt: $t('general.header.alt_logo') }"
:text="title($t('general.header.title'))"
:route="{ name: 'home' }"
/>
<span class="title-text"
>{{ $t('general.header.title') }}
<span v-if="instanceName" class="instance-text">{{
instanceName
}}</span></span
>
</div>
<div class="wrapper-logo responsive">
<Link
@@ -109,7 +114,6 @@ 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 Link from '@/components/Link.vue'
import BetaText from '@/components/BetaText.vue'
@@ -133,6 +137,7 @@ function toggleMenu(): void {
menuIsClosed.value = !menuIsClosed.value
}
const isLogged = computed((): boolean => !!cookies.get('user_id'))
const instanceName = computed((): string => import.meta.env.VITE_INSTANCE_NAME)
const ariaLabel = computed((): string =>
menuIsClosed.value
? t('general.header.burger_menu_aria_label_open')
@@ -164,6 +169,16 @@ const userName = computed((): string =>
.wrapper-logo {
display: flex;
align-items: center;
color: var(--blue-dark);
}
.title-text {
font-weight: bolder;
}
.instance-text {
font-weight: normal;
border-left: toRem(0.1) solid var(--blue-dark);
padding-left: toRem(0.7);
margin-left: toRem(0.5);
}
.wrapper-entries {
display: flex;
@@ -172,7 +187,7 @@ const userName = computed((): string =>
display: none;
}
.desktop {
display: block;
display: flex;
}
.wrapper-logo p {
@include text(m-r-regular);

View File

@@ -68,7 +68,9 @@ function drop(event: DragEvent): void | boolean {
}
function checkPicturesType(files: FileList): number {
const picturesToUpload = [...files].filter((p) => p.type == 'image/jpeg')
const picturesToUpload = [...files]
.filter((p) => p.type == 'image/jpeg')
.sort((a, b) => a.name.localeCompare(b.name))
return picturesToUpload.length
}
</script>

View File

@@ -61,7 +61,7 @@ const titleImg = computed<string>(() =>
@include text(s-regular);
display: flex;
align-items: center;
color: var(--black);
color: var(--blue-dark);
text-decoration: none;
.icon {
margin-right: toRem(1);

View File

@@ -0,0 +1,38 @@
<template>
<div class="share-card">
<img v-if="image" :src="img(image.url)" :alt="image.alt" class="image" />
<h2 class="title">{{ title }}</h2>
<p class="text">{{ text }}</p>
</div>
</template>
<script lang="ts" setup>
import { img } from '../../utils/image'
import type { PropType } from 'vue'
interface ImageInterface {
url: string
alt: string
}
defineProps({
image: { type: Object as PropType<ImageInterface>, default: null },
title: { type: String, default: '' },
text: { type: String, default: '' }
})
</script>
<style scoped lang="scss">
.share-card {
color: var(--blue-dark);
}
.title {
@include text(h2);
}
.text {
@include text(s-regular);
color: var(--grey-semi-dark);
}
.image {
height: toRem(8);
margin-bottom: toRem(1);
}
</style>

View File

@@ -1,9 +1,6 @@
<template>
<section :class="['information-section', { 'first-sequence': index === 0 }]">
<div :class="['imported', { '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 }} -
@@ -42,7 +39,7 @@
/>
</ul>
</div>
</section>
</div>
</template>
<script setup lang="ts">
@@ -66,16 +63,11 @@ defineProps({
</script>
<style scoped lang="scss">
.information-section {
.imported {
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,
@@ -89,7 +81,6 @@ defineProps({
margin-left: toRem(2);
}
.uploaded-title {
margin-left: toRem(2);
margin-bottom: toRem(2);
width: 100%;
color: var(--grey-semi-dark);
@@ -101,9 +92,11 @@ defineProps({
}
.uploaded-picture-list,
.uploaded-error-list {
padding: 0rem toRem(2) toRem(2);
padding-top: 0rem;
padding-right: toRem(2);
padding-bottom: toRem(2);
overflow-y: auto;
max-height: toRem(35);
max-height: toRem(50);
}
.bi-check-circle {
color: var(--green);
@@ -128,4 +121,13 @@ defineProps({
padding: 0;
}
}
@media (max-width: toRem(50)) {
.imported {
flex-direction: column;
}
.uploaded-error-list {
padding-left: 0;
}
}
</style>

View File

@@ -34,8 +34,9 @@ const itemUploadedText = computed<string | null>(
margin-top: toRem(1);
border-radius: toRem(0.5);
width: 100%;
@include text(s-regular);
@include text(xs-r-regular);
}
.success {
background-color: var(--white);
border: toRem(0.1) solid var(--grey);
@@ -43,7 +44,7 @@ const itemUploadedText = computed<string | null>(
}
.error {
background-color: var(--white);
border: toRem(1) solid var(--red-pale);
border: toRem(0.1) solid var(--red-pale);
color: var(--red);
}
.uploaded-information {

View File

@@ -0,0 +1,46 @@
<template>
<div v-if="isDisplayed" class="tooltip-banner">
<div class="top-banner">
<span
><i class="bi bi-info-circle"></i>
{{ $t('pages.upload.tooltip_banner_title') }}</span
>
<Button look="no-text" icon="bi bi-x-lg" @trigger="isDisplayed = false" />
</div>
<p class="text-banner">{{ $t('pages.upload.tooltip_banner_text') }}</p>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Button from '@/components/Button.vue'
let isDisplayed = ref<boolean>(true)
</script>
<style lang="scss" scoped>
.tooltip-banner {
border-left: toRem(1) solid var(--blue);
border-radius: toRem(1);
padding: toRem(2);
background-color: var(--white);
}
.top-banner {
@include text(h2);
display: flex;
justify-content: space-between;
margin-bottom: toRem(0.5);
color: var(--blue-dark);
}
.text-banner {
@include text(s-regular);
margin-bottom: 0;
margin-right: 3rem;
color: var(--grey-semi-dark);
}
@media (max-width: toRem(76.8)) {
.text-banner {
margin-right: 0;
}
}
</style>

View File

@@ -57,7 +57,6 @@ const uploadPendingTitle = computed<string>(() => {
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: toRem(4);
}
.loader {
position: relative;

View File

@@ -6,11 +6,11 @@
"description": "Panoramax, lalternative libre pour photo-cartographier les territoires"
},
"header": {
"contribute_text": "À propos",
"upload_text": "Contribuer",
"contribute_text": "Comment contribuer ?",
"upload_text": "Partager des photos",
"sequences_text": "Mes photos",
"alt_logo": "Logo de l'instance",
"title": "Instance\nPanoramax",
"title": "Panoramax",
"beta_text": "Version beta",
"logout_text": "Déconnexion",
"my_information_text": "Mes informations",
@@ -75,13 +75,20 @@
"sequence_deleted": "La séquence a bien été supprimée"
},
"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",
"title": "Pourquoi partager ses photos sur Panoramax ?",
"description": "Avec Panoramax, vos photos sont accessibles à tous. Elles seront automatiquement floutées grâce à notre algorithme de floutage dans le respect de <a href='https://panoramax.fr/foire-aux-questions' target='_blank' style='color:black'>la législation</a> et libres de droit.\nLa mise en ligne se fera <a href='https://www.etalab.gouv.fr/licence-ouverte-open-licence/' target='_blank' style='color:black'>sous licence ouverte</a> sous forme «brute» pour des réutilisations variées (ex: préparation des chantiers, gestion de la voirie etc).",
"arg_title1": "Des lieux visibles depuis la voie publique",
"arg_title2": "Une gestion des photos au format 360° ou non",
"arg_title3": "Une fonctionnalité de visualisation de vue du sol",
"arg_title4": "Des photos automatiquement géolocalisées",
"arg_text1": "Le lorem ipsum est, en imprimerie, une suite de mots sans signification utilisée à titre provisoire pour calibrer une mise en page, le texte définitif venant remplacer le faux-texte dès qu'il est prêt ou que la",
"arg_text2": "Le lorem ipsum est, en imprimerie, une suite de mots sans signification utilisée à titre provisoire pour calibrer une mise en page, le texte définitif venant remplacer le faux-texte dès qu'il est prêt ou que la",
"arg_text3": "Le lorem ipsum est, en imprimerie, une suite de mots sans signification utilisée à titre provisoire pour calibrer une mise en page, le texte définitif venant remplacer le faux-texte dès qu'il est prêt ou que la",
"arg_text4": "Le lorem ipsum est, en imprimerie, une suite de mots sans signification utilisée à titre provisoire pour calibrer une mise en page, le texte définitif venant remplacer le faux-texte dès qu'il est prêt ou que la",
"arg_alt1": "Image d'un immeuble",
"arg_alt2": "Image d'un signe 360",
"arg_alt3": "Image d'un pointer de localisation sur une carte",
"arg_alt4": "Image d'un pointer de localisation",
"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.",
"button": "Accéder à l'outil",
"alt_img_upload": "Image qui représente plusieurs photos en cours de téléchargement",
@@ -96,11 +103,16 @@
},
"upload": {
"title": "Déposez vos photos",
"input_label": "Déposez des photos dans la zone ou ",
"import_word": "importez",
"title_uploading": "Traitement des images",
"text": "Déposez vos fichiers dans cet espace. Chaque photo ou groupe de photos envoyé constituera une 'séquence'. Vous pourrez retrouver ensuite toutes vos séquences dans la section 'Mes images'. Déposez vos fichiers dans cet espace. Chaque photo ou groupe de photos envoyé",
"no_picture_uploading_text": "Aucune photo en cours de téléchargement actuellement",
"tooltip_banner_title": "Contribuez à la base de photos de Panoramax",
"tooltip_banner_text": "Déposez vos fichiers dans cet espace. Chaque photo ou groupe de photos envoyé constituera une 'séquence'. Vous pourrez retrouver ensuite toutes vos séquences dans la section 'Mes images'.",
"input_label": "Glissez vos images ici ou cliquez sur ",
"import_word": "importer",
"import_type": "Format JPEG uniquement",
"sequence_title": "Séquence du ",
"button_text": "Envoyer",
"button_text": "Télécharger",
"uploaded_files": "{count} fichier| {count} fichiers",
"no_uploaded_files": "Aucun fichier sélectionné",
"uploaded_word": " Image téléchargée",
@@ -111,7 +123,7 @@
"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",
"button_new_upload": "Nouveau téléchargement",
"leave_message": "⚠️ Attention, le téléchargement sera interrompu si vous quittez la page avant la fin."
}
}

View File

@@ -1,5 +1,5 @@
describe('In the home page', () => {
it('click on the link in the header to go to the upload page', () => {
it('click on the link in the header to go to the utils page', () => {
cy.visit('/')
cy.fixture('home').then((homeData) => {
cy.contains(homeData.textLinkUpload).click()

View File

@@ -1,4 +1,4 @@
describe('In the login page', () => {
describe('In the utils page', () => {
it('go to the login page', () => {
cy.visit('partager-des-photos')
cy.fixture('upload').then((uploadData) => {

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": "À propos"
"textLinkUpload": "Aide"
}

View File

@@ -29,7 +29,7 @@ describe('Template', () => {
}
})
expect(wrapper.html()).contains('button-stub')
expect(wrapper.html()).contains('text="Nouvel envoi"')
expect(wrapper.html()).contains('text="Nouveau téléchargement"')
})
})
describe('When the component have an uploaded sequence', () => {
@@ -59,7 +59,7 @@ describe('Template', () => {
loadPercentage: '100%'
}
})
expect(wrapper.html()).contains('text="Nouvel envoi"')
expect(wrapper.html()).contains('text="Nouveau téléchargement"')
expect(wrapper.html()).contains('Transfert terminé !')
expect(wrapper.html()).contains('2345 Mo/2345 Mo')
})

View File

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

View File

@@ -7,8 +7,7 @@ import {
} from '../../views/utils/sequence/index'
import {
formatPictureSize,
formatTextSize,
sortByName
formatTextSize
} from '../../views/utils/upload/index'
import { getAuthRoute } from '../../utils/auth'
import { img, getPicId } from '../../utils/image'
@@ -182,36 +181,3 @@ describe('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

@@ -1,9 +1,19 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import SharePicturesView from '../../../views/SharePicturesView.vue'
import i18n from '../config'
import { createI18n } from 'vue-i18n'
import fr from '../../../locales/fr.json'
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: []

View File

@@ -1,13 +1,10 @@
import { it, describe, expect, vi } from 'vitest'
import { shallowMount, mount, flushPromises } from '@vue/test-utils'
import { shallowMount } 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'
import Button from '../../../components/Button.vue'
const router = createRouter({
history: createWebHistory(),
routes: []
@@ -42,91 +39,12 @@ describe('Template', () => {
}
})
const spy = vi.spyOn(wrapper.vm, 'addPictures')
const sortByNameMock = vi.spyOn(sortByName, 'sortByName')
sortByNameMock.mockReturnValue([{}, {}])
const wrapperInputUpload = await wrapper.findComponent(InputUpload)
const wrapperInputUpload = 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 YY, 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

@@ -35,7 +35,7 @@ async function fetchMapAndViewer(params?: OptionalViewerMapInterface) {
}
return new GeoVisio(
'viewer', // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
`${import.meta.env.VITE_API_URL}api/search`,
paramsGeovisio
)
}

View File

@@ -18,7 +18,6 @@ import { onMounted, computed, ref } from 'vue'
import Button from '@/components/Button.vue'
import { fetchMapAndViewer } from '@/utils/mapAndViewer'
import { getPicId } from '@/utils/image'
import 'geovisio/build/index.css'
const { t } = useI18n()
let mapIsLoaded = ref<boolean>(false)
@@ -60,7 +59,7 @@ onMounted(async () => {
.gvs-has-map .entry-report-button {
display: block;
position: absolute;
right: toRem(12);
right: toRem(1);
top: toRem(2);
z-index: 1;
}

View File

@@ -188,7 +188,6 @@ 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,

View File

@@ -1,5 +1,63 @@
<template>
<main class="entry-page">
<section>
<div class="information-wrapper">
<div class="information">
<h1 class="page-title">{{ $t('pages.share_pictures.title') }}</h1>
<p
class="page-description"
v-html="$t('pages.share_pictures.description')"
></p>
</div>
<img
src="@/assets/images/person-map.png"
alt=""
class="information-image"
/>
</div>
<ul class="card-list">
<li class="card-list-item">
<ShareCard
:image="{
url: 'building.png',
alt: $t('pages.share_pictures.arg_alt1')
}"
:title="$t('pages.share_pictures.arg_title1')"
:text="$t('pages.share_pictures.arg_text1')"
/>
</li>
<li class="card-list-item">
<ShareCard
:image="{
url: '360.png',
alt: $t('pages.share_pictures.arg_alt2')
}"
:title="$t('pages.share_pictures.arg_title2')"
:text="$t('pages.share_pictures.arg_text2')"
/>
</li>
<li class="card-list-item">
<ShareCard
:image="{
url: 'pointer-map.png',
alt: $t('pages.share_pictures.arg_alt3')
}"
:title="$t('pages.share_pictures.arg_title3')"
:text="$t('pages.share_pictures.arg_text3')"
/>
</li>
<li class="card-list-item">
<ShareCard
:image="{
url: 'pointer.png',
alt: $t('pages.share_pictures.arg_alt4')
}"
:title="$t('pages.share_pictures.arg_title4')"
:text="$t('pages.share_pictures.arg_text4')"
/>
</li>
</ul>
</section>
<section id="sec-1" class="section-upload">
<div class="wrapper-upload">
<div class="wrapper-upload-text">
@@ -117,6 +175,7 @@
<script lang="ts" setup>
import Link from '@/components/Link.vue'
import Terminal from '@/components/Terminal.vue'
import ShareCard from '@/components/share-pictures/ShareCard.vue'
import { useCookies } from 'vue3-cookies'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -151,6 +210,46 @@ function triggerHref(): void {
</script>
<style lang="scss" scoped>
.entry-page {
margin: auto;
background-color: var(--beige-pale);
padding: toRem(2) 5%;
}
.information-wrapper {
display: flex;
align-items: center;
padding: toRem(4);
background-color: var(--white);
border-radius: toRem(1);
}
.information {
width: 50%;
}
.information-image {
margin: auto;
}
.page-title {
@include text(h1);
color: var(--blue-dark);
}
.page-description {
color: var(--grey-semi-dark);
white-space: pre-wrap;
}
.card-list {
display: flex;
padding: toRem(4);
background-color: var(--white);
border-radius: toRem(1);
margin-top: toRem(2);
}
.card-list-item {
margin-right: toRem(3);
&:last-child {
margin-right: toRem(0);
}
}
.upload-title {
@include text(h1);
}
@@ -224,7 +323,7 @@ function triggerHref(): void {
min-height: 80vh;
}
.section-upload:first-child {
background-color: rgba(236, 236, 236, 0.5);
background-color: var(--beige-pale);
}
.wrapper-upload {
display: flex;
@@ -281,6 +380,23 @@ function triggerHref(): void {
bottom: calc(20vh - #{toRem(10.5)});
}
@media (max-width: toRem(102.4)) {
.information-wrapper {
flex-direction: column;
}
.information {
width: 100%;
}
.card-list {
flex-wrap: wrap;
}
.card-list-item {
width: calc(50% - #{toRem(2)});
margin-right: 0;
&:nth-child(1),
&:nth-child(3) {
margin-right: toRem(2);
}
}
.section-upload {
height: initial;
}
@@ -305,6 +421,17 @@ function triggerHref(): void {
}
}
@media (max-width: toRem(76.8)) {
.card-list-item {
width: 100%;
margin-bottom: toRem(2);
&:nth-child(1),
&:nth-child(3) {
margin-right: 0;
}
&:last-child {
margin-bottom: 0;
}
}
.entry-page {
padding-top: toRem(11);
}

View File

@@ -1,50 +1,77 @@
<template>
<main class="entry-page">
<div class="entry-banner">
<TooltipBanner />
</div>
<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 class="form-upload">
<h1 class="settings-title">{{ $t('pages.upload.title') }}</h1>
<p class="settings-text">{{ $t('pages.upload.text') }}</p>
<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"
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"
/>
</div>
<div class="form-upload">
<h1 class="settings-title">{{ $t('pages.upload.title_uploading') }}</h1>
<div v-if="!uploadedSequences.length" class="uploading-information">
<img src="@/assets/images/uploading.png" class="uploading-img" />
<p class="settings-text">
{{ $t('pages.upload.no_picture_uploading_text') }}
</p>
</div>
</form>
<UploadLoader
v-else
:load-percentage="loadPercentage"
:load-text-size="loadTextSize"
:is-loaded="isLoaded"
:uploaded-sequences="uploadedSequences"
:pictures-count="picturesCount"
@triggerNewUpload="triggerNewUpload"
/>
<template v-for="(sequence, i) in uploadedSequences">
<ImportedSection
v-if="i === 0"
:index="i"
:sequence="sequence"
:pictures-count="picturesCount"
:upload-errors="sequence.picturesOnError"
:upload-pictures="sequence.pictures"
/>
</template>
</div>
</section>
<ImportedSection
v-for="(sequence, i) in uploadedSequences"
:index="i"
:sequence="sequence"
:pictures-count="picturesCount"
:upload-errors="sequence.picturesOnError"
:upload-pictures="sequence.pictures"
/>
<template v-for="(sequence, i) in uploadedSequences">
<section v-if="i !== 0" class="information-section">
<ImportedSection
:index="i"
:sequence="sequence"
:pictures-count="picturesCount"
:upload-errors="sequence.picturesOnError"
:upload-pictures="sequence.pictures"
/>
</section>
</template>
</main>
</template>
@@ -55,6 +82,7 @@ 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 TooltipBanner from '@/components/upload/TooltipBanner.vue'
import UploadLoader from '@/components/upload/UploadLoader.vue'
import type { sequenceInterface } from './interfaces/UploadPicturesView'
import { formatDate } from '@/utils/dates'
@@ -62,13 +90,9 @@ import {
createAPictureToASequence,
createASequence
} from '@/views/utils/upload/request'
import {
formatPictureSize,
formatTextSize,
sortByName
} from '@/views/utils/upload/index'
import { formatPictureSize, formatTextSize } from '@/views/utils/upload/index'
const { t } = useI18n()
let pictures = ref<File[] | []>([])
let pictures = ref<FileList | []>([])
let picturesCount = ref<number>(0)
let isLoading = ref<boolean>(false)
let isLoaded = ref<boolean>(false)
@@ -128,8 +152,7 @@ function triggerNewUpload(): void {
picturesCount.value = 0
}
function addPictures(value: FileList): void {
const files = sortByName([].slice.call(value))
pictures.value = files
pictures.value = value
picturesCount.value = pictures.value.length
picturesUploadingSize.value = 0
picturesToUploadSize.value = 0
@@ -207,20 +230,61 @@ ul {
.entry-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--beige-pale);
min-height: calc(100vh - #{toRem(8)});
padding-top: toRem(2);
padding-bottom: toRem(2);
overflow: hidden;
}
.entry-banner {
width: 90%;
margin-bottom: toRem(2);
}
.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);
display: flex;
width: 90%;
}
.information-section {
width: 90%;
background-color: var(--white);
border-radius: toRem(1);
margin-top: toRem(2);
padding: toRem(2);
}
.uploading-information {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.uploading-img {
height: toRem(20);
margin-bottom: toRem(2);
}
.form-upload {
height: toRem(70);
padding: toRem(2);
width: 50%;
background-color: var(--white);
border-radius: toRem(1);
&:first-child {
margin-right: toRem(1);
}
&:last-child {
margin-left: toRem(1);
}
}
.settings-title {
text-align: center;
@include text(h1);
margin-bottom: toRem(4);
color: var(--blue-dark);
@include text(h3);
margin-bottom: toRem(2);
}
.settings-text {
color: var(--grey-semi-dark);
margin-bottom: toRem(2);
@include text(s-regular);
}
.footer-form {
display: flex;
@@ -234,9 +298,6 @@ ul {
color: var(--blue-dark);
@include text(s-regular);
}
::-webkit-scrollbar {
width: toRem(2);
}
::-webkit-scrollbar-track {
background: var(--grey-pale);
}
@@ -245,22 +306,36 @@ ul {
border-radius: toRem(5);
}
@media (max-width: toRem(76.8)) {
.entry-page {
overflow-x: hidden;
.upload-section {
flex-direction: column;
}
.form-upload {
width: 100%;
&:first-child,
&:last-child {
margin: toRem(0);
}
&:first-child {
margin-bottom: toRem(2);
}
}
.information-section {
flex-direction: column;
}
}
@media (max-width: toRem(50)) {
.upload-section {
margin-top: toRem(15);
.entry-page {
margin-top: toRem(11);
overflow-y: hidden;
}
.upload-section,
.information-section {
width: 100%;
padding-right: toRem(2);
padding-left: toRem(2);
}
.information-section {
width: 100%;
width: 90%;
}
}
</style>

View File

@@ -5,12 +5,4 @@ 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 }
export { formatPictureSize, formatTextSize }

3376
yarn.lock

File diff suppressed because it is too large Load Diff