forked from Ivasoft/geovisio-website
Compare commits
1 Commits
0.1.0
...
feat/add-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7fb6c217 |
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_API_URL=https://geovisio-backend-dev.osc-fr1.scalingo.io/
|
||||
VITE_ENV=dev
|
||||
@@ -29,8 +29,7 @@ module.exports = {
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
'@typescript-eslint/no-namespace': 'off'
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ test:e2e:
|
||||
script:
|
||||
- yarn install
|
||||
- ./node_modules/.bin/cypress install
|
||||
- echo "VITE_API_URL=https://geovisio-proxy-dev.osc-fr1.scalingo.io/" > .env
|
||||
- echo "VITE_ENV=dev" >> .env
|
||||
- PORT=5173 yarn start &
|
||||
- yarn test:e2e
|
||||
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,29 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
Before _0.1.0_ Changelog didn't exist.
|
||||
|
||||
## [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
|
||||
|
||||
- A new page `/sequence/:id` to access to a sequence of photos for a logged user ([#14](https://gitlab.com/geovisio/website/-/issues/14)) :
|
||||
- the user can see the sequence on the map and move on the map from photos to photos
|
||||
- the user can see information about the sequence
|
||||
- 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
|
||||
119
README.md
119
README.md
@@ -1,60 +1,109 @@
|
||||
# 
|
||||
# panoramax-website
|
||||
|
||||
__GeoVisio__ is a complete solution for storing and __serving your own 📍📷 geolocated pictures__ (like [StreetView](https://www.google.com/streetview/) / [Mapillary](https://mapillary.com/)).
|
||||
Welcome to the Panoramax website documentation !
|
||||
[Panoramax](http://panoramax.ign.fr/) is a website where you can upload a lots of photos to see them in map web viewer based on [Geovisio](https://gitlab.com/geovisio).
|
||||
|
||||
➡️ __Give it a try__ at [panoramax.ign.fr](https://panoramax.ign.fr/) or [geovisio.fr](https://geovisio.fr/viewer) !
|
||||
## Technologies
|
||||
|
||||
## 📦 Components
|
||||
- Frontend website made in [Vue 3](https://vuejs.org/guide/introduction.html)
|
||||
- Project use [Vite](https://vitejs.dev/guide/) who offer a fast development server and an optimized compilation for production (like webpack)
|
||||
- The style is made with CSS/SASS and the [bootstrap library](https://getbootstrap.com/)
|
||||
- [Typescript](https://www.typescriptlang.org/) used to type
|
||||
- [Jest](https://jestjs.io/fr/) used for unit testing
|
||||
|
||||
GeoVisio is __modular__ and made of several components, each of them standardized and ♻️ replaceable.
|
||||
## Configuration
|
||||
|
||||

|
||||
All the commands and packages used are available in the `package.json` file.
|
||||
|
||||
All of them are 📖 __open-source__ and available online:
|
||||
You can change the vite server configuration in the `vite.config.ts` file. See [Vite Configuration Reference](https://vitejs.dev/config/) if you need.
|
||||
|
||||
| 🌐 Server | 💻 Client |
|
||||
|:-----------------------------------------------------------------------:|:----------------------------------------------------:|
|
||||
| [API](https://gitlab.com/geovisio/api) | [Website](https://gitlab.com/geovisio/website) |
|
||||
| [Blur API](https://gitlab.com/geovisio/blurring) | [Web viewer](https://gitlab.com/geovisio/web-viewer) |
|
||||
| [GeoPic Tag Reader](https://gitlab.com/geovisio/geo-picture-tag-reader) | [Command line](https://gitlab.com/geovisio/cli) |
|
||||
## Project Setup
|
||||
|
||||
**You need to have [Nodejs installed](https://nodejs.org/en/download)**
|
||||
Node version : >=18.13.0
|
||||
|
||||
# 💻 GeoVisio Website
|
||||
**You need to have [Npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)**
|
||||
|
||||
This repository only contains __the web front-end of GeoVisio__.
|
||||
You can use npm or [yarn](https://yarnpkg.com/) as package manager
|
||||
|
||||
Note that the 📷 __web viewer__ (component showing pictures and their location on a map) is in [a separate, dedicated repository](https://gitlab.com/geovisio/web-viewer).
|
||||
Install all dependencies :
|
||||
|
||||
## ⚙️ Features
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
The website offers these functionalities:
|
||||
or
|
||||
|
||||
- Display of pictures and their location (using the embed [web viewer](https://gitlab.com/geovisio/web-viewer))
|
||||
- Handle user authentication and account management
|
||||
- Show simple to read documentation
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
## 🕮 Documentation
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
[A full documentation](./docs/) is available to help you through the install, setup and usage of the GeoVisio website.
|
||||
Launch your dev server :
|
||||
|
||||
## 💁 Contributing
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Pull requests are welcome. For major changes, please open an [issue](https://gitlab.com/geovisio/website/-/issues) first to discuss what you would like to change.
|
||||
or
|
||||
|
||||
## 🤗 Special thanks
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||

|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
GeoVisio was made possible thanks to a group of ✨ __amazing__ people ✨ :
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
- __[GéoVélo](https://geovelo.fr/)__ team, for 💶 funding initial development and for 🔍 testing/improving software
|
||||
- __[Carto Cité](https://cartocite.fr/)__ team (in particular Antoine Riche), for 💶 funding improvements on viewer (map browser, flat pictures support)
|
||||
- __[La Fabrique des Géocommuns (IGN)](https://www.ign.fr/institut/la-fabrique-des-geocommuns-incubateur-de-communs-lign)__ for offering long-term support and funding the [Panoramax](https://panoramax.fr/) initiative and core team (Camille Salou, Mathilde Ferrey, Christian Quest, Antoine Desbordes, Jean Andreani, Adrien Pavie)
|
||||
- Many _many_ __wonderful people__ who worked on various parts of GeoVisio or core dependencies we use : 🧙 Stéphane Péneau, 🎚 Albin Calais & Cyrille Giquello, 📷 [Damien Sorel](https://www.strangeplanet.fr/), Pascal Rhod, Nick Whitelegg...
|
||||
- __[Adrien Pavie](https://pavie.info/)__, for ⚙️ initial development of GeoVisio
|
||||
- And you all ✨ __GeoVisio users__ for making this project useful !
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn test:unit
|
||||
```
|
||||
|
||||
## ⚖️ License
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
Copyright (c) GeoVisio team 2022-2023, [released under MIT license](./LICENSE).
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Build in Production
|
||||
|
||||
In your production app, you must set some env variables [like the .env.example file here](https://gitlab.com/geovisio/website/-/blob/main/.env.example)
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Instance customization
|
||||
|
||||
### Wordings
|
||||
|
||||
- All the wordings of the website are on this [locale file](https://gitlab.com/geovisio/website/-/blob/main/src/locales/fr.json)
|
||||
- You can change the title `"title": "Instance Panoramax IGN"` of your instance on the [locale file](https://gitlab.com/geovisio/website/-/blob/main/src/locales/fr.json)
|
||||
- In the same [locale file](https://gitlab.com/geovisio/website/-/blob/main/src/locales/fr.json) you can change the meta data wordings inside the `"meta": {}` object
|
||||
- You can change the instance name inside the documentation of the page /partager-des-photos for the keys `"terminal_text_logged"` and `"terminal_text_not_logged"` on the [locale file](https://gitlab.com/geovisio/website/-/blob/main/src/locales/fr.json)
|
||||
|
||||
### Images
|
||||
|
||||
- If you want to change the logo in the header you can replace the logo.jpeg in the [assets/images folder](https://gitlab.com/geovisio/geovisio_website/-/tree/main/src/assets/images) file by your own jpeg logo with the same file name
|
||||
- You can change the favicon [inside the static folder](https://gitlab.com/geovisio/website/-/tree/main/static)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# GeoVisio Website hands-on guide
|
||||
|
||||

|
||||
|
||||
Welcome to GeoVisio __Website__ documentation ! It will help you through all phases of setup, run and develop on GeoVisio Website.
|
||||
|
||||
__Note that__ this only covers the Website / front-end component, if you're looking for docs on another component, you may go to [this page](https://gitlab.com/geovisio) instead.
|
||||
|
||||
Also, if at some point you're lost or need help, you can contact us through [issues](https://gitlab.com/geovisio/website/-/issues) or by [email](mailto:panieravide@riseup.net).
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
The website relies on the following technologies and components:
|
||||
|
||||
- Frontend website made in [Vue 3](https://vuejs.org/guide/introduction.html)
|
||||
- Project use [Vite](https://vitejs.dev/guide/) who offer a fast development server and an optimized compilation for production (like webpack)
|
||||
- The style is made with CSS/SASS and the [bootstrap library](https://getbootstrap.com/)
|
||||
- [Typescript](https://www.typescriptlang.org/) used to type
|
||||
- [Jest](https://jestjs.io/fr/) used for unit testing
|
||||
|
||||
|
||||
## All the docs
|
||||
|
||||
You might want to dive into docs :
|
||||
|
||||
- [Install and setup](./02_Setup.md)
|
||||
- [Change the settings](./03_Settings.md)
|
||||
- [Work on the code](./09_Develop.md)
|
||||
@@ -1,46 +0,0 @@
|
||||
# Setup
|
||||
|
||||
## System requirements
|
||||
|
||||
**You need to have [Nodejs installed](https://nodejs.org/en/download)**
|
||||
Node version : >=18.13.0
|
||||
|
||||
**You need to have [Npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)**
|
||||
|
||||
You can use npm or [yarn](https://yarnpkg.com/) as package manager
|
||||
|
||||
## Install
|
||||
|
||||
The website can be installed locally by retrieving this repository and installing dependencies:
|
||||
|
||||
```sh
|
||||
# Retrieve source code
|
||||
git clone https://gitlab.com/geovisio/website.git
|
||||
cd website/
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```sh
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
More details about settings [can be found in docs here](./03_Settings.md).
|
||||
|
||||
Then, building for production can be done with these commands:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
PORT=3000 npm run start
|
||||
```
|
||||
|
||||
The website is now available at [localhost:3000](http://localhost:3000).
|
||||
|
||||
## Next steps
|
||||
|
||||
You can check out [the available settings for your instance](./03_Settings.md).
|
||||
@@ -1,43 +0,0 @@
|
||||
# Settings
|
||||
|
||||
Many things can be customized in your GeoVisio Website.
|
||||
|
||||
## Basic settings
|
||||
|
||||
Low-level settings can be changed through the `.env` file. An example is given in `env.example` file.
|
||||
|
||||
Available parameters are:
|
||||
|
||||
- `VITE_API_URL`: the URL to the GeoVisio API (example: `https://geovisio.fr`)
|
||||
- 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
|
||||
- `VITE_ENV`: `dev`
|
||||
|
||||
More settings are available [in official Vite documentation](https://vitejs.dev/guide/env-and-mode.html#env-files)
|
||||
|
||||
Note that you can also change the _Vite_ server configuration in the `vite.config.ts` file. See [Vite Configuration Reference](https://vitejs.dev/config/) if you need.
|
||||
|
||||
## Wording customization
|
||||
|
||||
GeoVisio website can be customized to have wording reflecting your brand, licence and other elements.
|
||||
|
||||
All the wordings of the website are on this [locale file](./src/locales/fr.json). In there, you might want to change:
|
||||
|
||||
- The website title (properties `title` and `meta.title`)
|
||||
- The description (property `meta.description`)
|
||||
- The used API URL (property `pages.upload.terminal_text`)
|
||||
- Links to help pages:
|
||||
- `upload.description`
|
||||
- `upload.footer_description_terminal`
|
||||
|
||||
## Visuals
|
||||
|
||||
The following images can be changed to make the website more personal:
|
||||
|
||||
- Logo: [`src/assets/images/logo.jpeg`](../src/assets/images/logo.jpeg)
|
||||
- Favicon: [`static/favicon.ico`](../static/favicon.ico)
|
||||
|
||||
## Next steps
|
||||
|
||||
You may be interested [in developing on the website](./09_Develop.md).
|
||||
@@ -1,43 +0,0 @@
|
||||
# Work on the code
|
||||
|
||||
## Available commands
|
||||
|
||||
Note that all the commands and packages used are available in the `package.json` file.
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
Launch your dev server :
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn test:unit
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn lint
|
||||
```
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "geovisio-website",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.0",
|
||||
"engines": {
|
||||
"node": "18.16.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"scalingo-prebuild": "yarn upgrade geovisio@develop",
|
||||
"start": "vite --port $PORT",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
@@ -21,13 +22,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@vueuse/components": "^10.2.1",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"axios": "^1.2.3",
|
||||
"bootstrap": "^5.2.3",
|
||||
"bootstrap-icons": "^1.10.3",
|
||||
"geovisio": "2.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"geovisio": "^2.0.2-develop-33c2d8bd",
|
||||
"vue": "^3.2.45",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-eslint-parser": "^9.1.0",
|
||||
|
||||
BIN
src/assets/.DS_Store
vendored
Normal file
BIN
src/assets/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -41,8 +41,4 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@if $size == xss-regular {
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/assets/images/.DS_Store
vendored
Normal file
BIN
src/assets/images/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -32,15 +32,10 @@ h5 {
|
||||
--black-pale: #1b1a17;
|
||||
--red: #f70000;
|
||||
--grey: #e6e6e6;
|
||||
--grey-pale: #CFD2CF;
|
||||
--grey-semi-dark: #808080;
|
||||
--grey-dark: #54595e;
|
||||
--grey-dark: #808080;
|
||||
--blue: #4945ff;
|
||||
--blue-semi: rgba(207, 226, 255, 0.5);
|
||||
--blue-pale: #f9fafd;
|
||||
--beige: #f5f3ec;
|
||||
--yellow: #fec868;
|
||||
--orange: #ff6f00;
|
||||
--green: #59ce8f;
|
||||
}
|
||||
|
||||
|
||||
BIN
src/components/.DS_Store
vendored
Normal file
BIN
src/components/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<p class="instance-beta">
|
||||
{{ $t('general.header.title') }}
|
||||
<span class="beta">{{ $t('general.header.beta_text') }}</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
<button
|
||||
:disabled="isLoading || disabled"
|
||||
type="button"
|
||||
:class="[look, 'default', { disabled }]"
|
||||
@click="$emit('trigger')"
|
||||
:class="['default', look]"
|
||||
>
|
||||
<i v-if="icon" :class="[icon, 'icon']"></i>
|
||||
<span v-if="text.length" class="text">{{ text }}</span>
|
||||
<span v-if="tooltip && tooltip.length" class="tooltip-button">{{
|
||||
tooltip
|
||||
}}</span>
|
||||
{{ text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -19,101 +16,38 @@ defineProps({
|
||||
disabled: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
text: { type: String, default: '' },
|
||||
tooltip: { type: String, default: '' },
|
||||
look: { type: String, default: '' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 764px) {
|
||||
.default:hover {
|
||||
opacity: 0.8;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.default {
|
||||
@include text(s-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
.button--black {
|
||||
height: 3.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.3rem 2rem 1.3rem;
|
||||
color: var(--white);
|
||||
background-color: var(--black);
|
||||
color: var(--white);
|
||||
}
|
||||
.button--transparent {
|
||||
.button--white {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 0.1rem 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);
|
||||
.icon {
|
||||
margin-right: 0;
|
||||
font-size: 1.4rem;
|
||||
color: var(--red);
|
||||
}
|
||||
.text {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.button--white {
|
||||
height: 3.5rem;
|
||||
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);
|
||||
margin-right: 0;
|
||||
}
|
||||
.text {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.no-text {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
padding: 0;
|
||||
.icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.link--grey {
|
||||
color: var(--grey-semi-dark);
|
||||
.icon {
|
||||
font-size: 1.4rem;
|
||||
color: var(--grey-semi-dark);
|
||||
}
|
||||
}
|
||||
.link--red {
|
||||
height: 3rem;
|
||||
color: var(--red);
|
||||
background-color: var(--white);
|
||||
.icon {
|
||||
font-size: 1.4rem;
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 1rem;
|
||||
font-size: 2rem;
|
||||
@@ -133,30 +67,4 @@ defineProps({
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.default .tooltip-button {
|
||||
background-color: var(--black);
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
position: absolute;
|
||||
bottom: -100%;
|
||||
visibility: hidden;
|
||||
width: 18rem;
|
||||
right: 0;
|
||||
@include text(xss-regular);
|
||||
}
|
||||
|
||||
.default:hover .tooltip-button {
|
||||
visibility: visible;
|
||||
}
|
||||
.disabled {
|
||||
color: var(--grey-pale);
|
||||
border-color: var(--grey-pale);
|
||||
cursor: not-allowed;
|
||||
.icon {
|
||||
color: var(--grey-pale);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="responsive beta">
|
||||
<div class="responsive">
|
||||
<beta-text />
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<div class="wrapper-logo desktop">
|
||||
<Link
|
||||
:image="{
|
||||
url: 'logo.jpeg',
|
||||
alt: $t('general.header.alt_logo')
|
||||
}"
|
||||
:text="$t('general.header.title')"
|
||||
path="/"
|
||||
/>
|
||||
</div>
|
||||
<div class="wrapper-logo responsive">
|
||||
<div class="wrapper-logo">
|
||||
<Link
|
||||
:image="{
|
||||
url: 'logo.jpeg',
|
||||
@@ -22,74 +12,69 @@
|
||||
}"
|
||||
path="/"
|
||||
/>
|
||||
<div class="desktop">
|
||||
<beta-text />
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-entries">
|
||||
<ul :class="['nav-list', { 'menu-open': !menuIsClosed }]">
|
||||
<li v-if="isLogged" class="logged-link">
|
||||
<ul :class="['nav-list', { 'mobile-menu-open': !menuIsClosed }]">
|
||||
<li class="nav-list-item desktop">
|
||||
<Link
|
||||
:text="$t('general.header.sequences_text')"
|
||||
icon="bi bi-images"
|
||||
path="/mes-sequences"
|
||||
@click.native="closeModal"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="userProfileUrl && isLogged" class="logged-link">
|
||||
<Link
|
||||
path="/mes-informations"
|
||||
icon="bi bi-person"
|
||||
:text="$t('general.header.my_information_text')"
|
||||
@click.native="closeModal"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="userProfileUrl && isLogged" class="logged-link">
|
||||
<Link
|
||||
path="/mes-parametres"
|
||||
icon="bi bi-gear"
|
||||
:text="$t('general.header.my_settings_text')"
|
||||
@click.native="closeModal"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="userProfileUrl && isLogged" class="logged-link">
|
||||
<Link
|
||||
type="external"
|
||||
icon="bi bi-power"
|
||||
:text="$t('general.header.logout_text')"
|
||||
:path="getAuthRoute('auth/logout', route.path)"
|
||||
@click.native="closeModal"
|
||||
:text="$t('general.header.contribute_text')"
|
||||
icon="bi bi-upload"
|
||||
look="button white"
|
||||
path="/partager-des-photos"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="wrapper-right-entries">
|
||||
<div>
|
||||
<div class="responsive">
|
||||
<Link
|
||||
:text="$t('general.header.contribute_text')"
|
||||
:text="$t('general.header.contribute_text_responsive')"
|
||||
icon="bi bi-upload"
|
||||
look="button white"
|
||||
path="/partager-des-photos"
|
||||
@click.native="closeModal"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="isLogged"
|
||||
class="menu-burger"
|
||||
:aria-label="ariaLabel"
|
||||
v-on-click-outside="closeModal"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<div v-if="isLogged" class="item-with-sub">
|
||||
<span>{{ userName }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<i v-if="!menuIsClosed" class="cross bi bi-x-lg"></i>
|
||||
<i v-else class="bi bi-list"></i>
|
||||
</div>
|
||||
</button>
|
||||
<div v-else>
|
||||
<div v-if="authEnabled" class="item-with-sub">
|
||||
<Link
|
||||
type="external"
|
||||
icon="bi bi-person-circle"
|
||||
:path="getAuthRoute('auth/login', route.path)"
|
||||
:look="isLogged ? 'disable-mobile' : ''"
|
||||
:path="userUrl"
|
||||
:text="userName"
|
||||
/>
|
||||
<i v-if="isLogged" class="chevron bi bi-chevron-up"></i>
|
||||
<div v-if="isLogged" class="sub-nav-block">
|
||||
<div v-if="userProfileUrl" class="logged-link">
|
||||
<Link
|
||||
path="mes-informations"
|
||||
:text="$t('general.header.my_information_text')"
|
||||
/>
|
||||
</div>
|
||||
<div class="logged-link">
|
||||
<Link
|
||||
path="mes-parametres"
|
||||
:text="$t('general.header.my_settings_text')"
|
||||
/>
|
||||
</div>
|
||||
<div class="logged-link">
|
||||
<Link
|
||||
type="external"
|
||||
:path="logoutUrl"
|
||||
:text="$t('general.header.logout_text')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="menu-burger"
|
||||
:aria-label="ariaLabel"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<i v-if="!menuIsClosed" class="cross bi bi-x-lg"></i>
|
||||
<i v-else class="bi bi-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -98,11 +83,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import type { vOnClickOutside } from '@vueuse/components'
|
||||
import { useCookies } from 'vue3-cookies'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getAuthRoute } from '@/utils/auth'
|
||||
import Link from '@/components/Link.vue'
|
||||
import BetaText from '@/components/BetaText.vue'
|
||||
|
||||
@@ -116,10 +99,6 @@ defineProps({
|
||||
|
||||
let menuIsClosed = ref<boolean>(true)
|
||||
|
||||
function closeModal() {
|
||||
menuIsClosed.value = true
|
||||
}
|
||||
|
||||
function toggleMenu(): void {
|
||||
menuIsClosed.value = !menuIsClosed.value
|
||||
}
|
||||
@@ -129,12 +108,17 @@ const ariaLabel = computed((): string =>
|
||||
? t('general.header.burger_menu_aria_label_open')
|
||||
: t('general.header.burger_menu_aria_label_closed')
|
||||
)
|
||||
const logoutUrl = computed(
|
||||
(): string =>
|
||||
`${import.meta.env.VITE_API_URL}api/auth/logout?next_url=${route.path}`
|
||||
)
|
||||
const userUrl = computed((): string =>
|
||||
isLogged.value
|
||||
? ''
|
||||
: `${import.meta.env.VITE_API_URL}api/auth/login?next_url=${route.path}`
|
||||
)
|
||||
const userName = computed((): string =>
|
||||
cookies!
|
||||
.get('user_name')
|
||||
.match(/\b(\w)/g)!
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
isLogged.value ? cookies.get('user_name') : t('general.header.login_text')
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -171,14 +155,26 @@ const userName = computed((): string =>
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.nav-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.item-with-sub {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.item-with-sub:hover .sub-nav-block {
|
||||
display: block;
|
||||
}
|
||||
.item-with-sub:hover .chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.chevron {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.sub-nav-block {
|
||||
display: none;
|
||||
border-radius: 0.5rem;
|
||||
@@ -192,7 +188,14 @@ const userName = computed((): string =>
|
||||
}
|
||||
.logged-link {
|
||||
display: flex;
|
||||
padding: 0.5rem 2rem 0.7rem;
|
||||
justify-content: center;
|
||||
padding: 1rem 1rem 1.5rem;
|
||||
}
|
||||
.logged-link:first-child {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
.logged-link:nth-child(2) {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
.logged-link:hover {
|
||||
border-radius: 0.5rem;
|
||||
@@ -207,61 +210,8 @@ const userName = computed((): string =>
|
||||
.wrapper-right-entries {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
div:first-child {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
.cross {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.item-with-sub {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
.nav {
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.nav-list {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: initial;
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
top: 8rem;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--white);
|
||||
box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%);
|
||||
padding-left: 0;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.menu-burger {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 2.5rem;
|
||||
font-size: 2.5rem;
|
||||
padding: 0;
|
||||
.item-with-sub {
|
||||
@include text(s-regular);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 50%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.menu-open {
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
height: 11rem;
|
||||
@@ -269,24 +219,57 @@ const userName = computed((): string =>
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
z-index: 2;
|
||||
background: var(--white);
|
||||
}
|
||||
.nav-list {
|
||||
top: 11rem;
|
||||
width: 100%;
|
||||
.cross {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.item-with-sub {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
.responsive {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.nav {
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.nav-list {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: initial;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 20rem;
|
||||
top: 11rem;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--white);
|
||||
box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%);
|
||||
}
|
||||
.menu-burger {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 2.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
.mobile-menu-open {
|
||||
display: flex;
|
||||
}
|
||||
.beta {
|
||||
width: 100%;
|
||||
.instance-beta {
|
||||
width: 100%;
|
||||
}
|
||||
.nav-list-item {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.item-with-sub {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<template>
|
||||
<div :class="status">
|
||||
<div class="wrapper-image">
|
||||
<button
|
||||
:class="[{ selected }, 'button-image-item']"
|
||||
:disabled="status === 'waiting-for-process'"
|
||||
type="button"
|
||||
@click="$emit('trigger')"
|
||||
>
|
||||
<div
|
||||
v-if="href && status !== 'waiting-for-process'"
|
||||
class="photo-img-wrapper"
|
||||
>
|
||||
<i v-if="status === 'hidden'" class="bi bi-eye-slash icon-hidden"></i>
|
||||
<img :src="href" alt="" loading="lazy" class="photo-img" />
|
||||
</div>
|
||||
<div v-else class="waiting-wrapper">
|
||||
<i class="bi bi-card-image icon-waiting"></i>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedOnMap && !selected"
|
||||
class="icon-img pointer-map"
|
||||
></div>
|
||||
<div v-if="selected && !selectedOnMap" class="icon-img button-check">
|
||||
<i class="bi bi-check-lg" />
|
||||
</div>
|
||||
<div
|
||||
v-if="selected && selectedOnMap"
|
||||
class="icon-img button-check-pointer"
|
||||
>
|
||||
<i class="bi bi-check-lg" />
|
||||
</div>
|
||||
<div v-if="status === 'waiting-for-process'" class="photo-info">
|
||||
<span class="waiting">{{
|
||||
$t('pages.sequence.waiting_process')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-else class="photo-info">
|
||||
<span v-if="created"><i class="bi bi-clock"></i> {{ created }}</span>
|
||||
<div class="button-info">
|
||||
<Link
|
||||
look="button--blue no-text"
|
||||
icon="bi bi-cloud-download-fill"
|
||||
type="external"
|
||||
target="_blank"
|
||||
:path="hrefHd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Link from '@/components/Link.vue'
|
||||
|
||||
defineProps({
|
||||
created: { type: String, default: null },
|
||||
href: { type: String, default: null },
|
||||
hrefHd: { type: String, default: null },
|
||||
selected: { type: Boolean, default: false },
|
||||
selectedOnMap: { type: Boolean, default: false },
|
||||
status: { type: String, default: null }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-image-item {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.selected {
|
||||
border: 0.1rem solid var(--blue);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0px 4px 4px 0px #00000040;
|
||||
}
|
||||
.wrapper-image {
|
||||
position: relative;
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
.photo-img-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
height: 12rem;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.photo-img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
.icon-hidden {
|
||||
color: var(--grey-dark);
|
||||
position: absolute;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.waiting-wrapper {
|
||||
height: 12rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--blue);
|
||||
}
|
||||
.icon-waiting {
|
||||
height: 4rem;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.icon-img {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.pointer-map,
|
||||
.button-check-pointer {
|
||||
background-color: var(--orange);
|
||||
}
|
||||
.delete-checked {
|
||||
opacity: 1;
|
||||
}
|
||||
.photo-info {
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.photo-img,
|
||||
.photo-info {
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
.photo-img,
|
||||
.photo-info {
|
||||
opacity: 0.3;
|
||||
}
|
||||
&:hover {
|
||||
.photo-img,
|
||||
.photo-info {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<div class="wrapper-checkbox">
|
||||
<div class="input-checkbox">
|
||||
<i v-if="isChecked && !isIndeterminate" class="icon bi bi-check-square" />
|
||||
<i v-if="!isChecked && !isIndeterminate" class="icon bi bi-square" />
|
||||
<i v-if="isIndeterminate && !isChecked" class="icon bi bi-dash-square" />
|
||||
<input
|
||||
id="checkbox"
|
||||
v-model="inputValue"
|
||||
type="checkbox"
|
||||
@input="updateValue(!inputValue)"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<label v-if="label && label.length" for="checkbox" class="label">{{
|
||||
label
|
||||
}}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import type { CheckboxInterface } from '@/views/interfaces/MySequenceView'
|
||||
const emit = defineEmits<{ (e: 'trigger', value: CheckboxInterface): void }>()
|
||||
const props = defineProps({
|
||||
name: { type: String, default: null },
|
||||
label: { type: String, label: '' },
|
||||
isChecked: { type: Boolean, default: false },
|
||||
isIndeterminate: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const htmlCheckbox = <HTMLInputElement>document.getElementById('checkbox')
|
||||
|
||||
watchEffect(async () => {
|
||||
if (htmlCheckbox) {
|
||||
htmlCheckbox.indeterminate = props.isIndeterminate
|
||||
}
|
||||
})
|
||||
|
||||
let inputValue = ref<boolean>(props.isChecked)
|
||||
|
||||
function updateValue(value: boolean): void {
|
||||
if (htmlCheckbox) {
|
||||
htmlCheckbox.indeterminate = false
|
||||
}
|
||||
inputValue.value = value
|
||||
emit('trigger', { isChecked: value, isIndeterminate: false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
.input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-ms-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.icon {
|
||||
font-size: 2rem;
|
||||
position: absolute;
|
||||
color: var(--grey-semi-dark);
|
||||
}
|
||||
.wrapper-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
@include text(s-regular);
|
||||
}
|
||||
</style>
|
||||
@@ -65,17 +65,13 @@ const titleImg = computed<string>(() =>
|
||||
align-items: center;
|
||||
color: var(--black);
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
.icon {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
.link:hover {
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.button {
|
||||
height: 4rem;
|
||||
height: 4.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.3rem 2rem 1.3rem;
|
||||
background-color: var(--black);
|
||||
@@ -103,7 +99,6 @@ const titleImg = computed<string>(() =>
|
||||
.logo {
|
||||
height: 4rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.disabled {
|
||||
color: grey;
|
||||
@@ -123,43 +118,14 @@ const titleImg = computed<string>(() =>
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
height: 4.5rem;
|
||||
width: 4.5rem;
|
||||
.icon {
|
||||
color: var(--white);
|
||||
font-size: 2.8rem;
|
||||
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;
|
||||
padding: 0;
|
||||
.icon {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div class="lds-ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped scss>
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border: 8px 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;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="entry-button-terminal">
|
||||
<Button
|
||||
look="button--transparent"
|
||||
look="button--white"
|
||||
:text="$t('pages.upload.button_copy')"
|
||||
:icon="clipboardIcon"
|
||||
@trigger="copyText(textInstall)"
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="entry-button-terminal">
|
||||
<Button
|
||||
look="button--transparent"
|
||||
look="button--white"
|
||||
:text="$t('pages.upload.button_copy')"
|
||||
:icon="clipboardIcon"
|
||||
@trigger="copyText(textUpload)"
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div :class="['toast-wrapper', look, { display: text.length }]">
|
||||
<button class="button-close" @click="$emit('trigger')">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
<i v-if="look === 'error'" class="bi bi-exclamation-triangle"></i>
|
||||
<i v-else class="bi bi-check-circle"></i>
|
||||
<p v-if="text.length" class="toast-text">
|
||||
{{ text }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
text: { type: String, default: '' },
|
||||
look: { type: String, default: '' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toast-wrapper {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 2rem;
|
||||
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;
|
||||
}
|
||||
.button-close {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: -0.5rem;
|
||||
height: 1.8rem;
|
||||
width: 1.8rem;
|
||||
border: 0.1rem solid var(--black);
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.toast-text {
|
||||
margin-bottom: 0;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.display {
|
||||
transform: translateX(-3rem);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
.error {
|
||||
background-color: var(--red);
|
||||
}
|
||||
.success {
|
||||
background-color: var(--green);
|
||||
}
|
||||
</style>
|
||||
@@ -6,9 +6,9 @@ export default function authConfig() {
|
||||
const authConf = ref<AuthConfigInterface>({})
|
||||
|
||||
async function getConfig(): Promise<object> {
|
||||
const { data } = await axios.get('api/configuration', {
|
||||
withCredentials: false
|
||||
})
|
||||
const { data } = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}api/configuration`
|
||||
)
|
||||
return data.auth
|
||||
}
|
||||
onMounted(async () => (authConf.value = await getConfig()))
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
},
|
||||
"header": {
|
||||
"contribute_text": "Partager vos photos",
|
||||
"sequences_text": "Mes photos",
|
||||
"contribute_text_responsive": "Verser",
|
||||
"alt_logo": "Logo de l'instance",
|
||||
"title": "Instance Panoramax IGN",
|
||||
"beta_text": "Version beta",
|
||||
"logout_text": "Déconnexion",
|
||||
"my_information_text": "Mes informations",
|
||||
"my_settings_text": "Mes paramètres",
|
||||
@@ -18,8 +17,7 @@
|
||||
"burger_menu_aria_label_open": "Afficher le menu",
|
||||
"burger_menu_aria_label_closed": "Masquer le menu"
|
||||
},
|
||||
"error_text": "Une erreur est survenue",
|
||||
"success_text": "Mise à jour réussie"
|
||||
"feature_not_available": "Fonctionnalité en cours de développement"
|
||||
},
|
||||
"pages": {
|
||||
"home": {
|
||||
@@ -28,43 +26,7 @@
|
||||
"report_button_text": "Signaler la photo"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Mes Tokens",
|
||||
"setting_tooltip": "Afficher ou masquer le token"
|
||||
},
|
||||
"sequence": {
|
||||
"title": "Séquence :",
|
||||
"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",
|
||||
"delete_photo_tooltip": "Supprime définitivement les photo",
|
||||
"confirm_dialog": "Les photos sélectionnées vont être définitivement supprimées",
|
||||
"created": "Versement :",
|
||||
"taken": "Prise de vue :",
|
||||
"duration": "Durée :",
|
||||
"camera": "Matériel :",
|
||||
"button_delete": "Supprimer",
|
||||
"button_disable": "Masquer",
|
||||
"button_enable": "Afficher",
|
||||
"hours": "{count} heure| {count} heures",
|
||||
"minutes": "{count} minute| {count} minutes",
|
||||
"seconds": "{count} seconde| {count} secondes",
|
||||
"select_text": "Tout sélectionner",
|
||||
"unselect_text": "Tout désélectionner",
|
||||
"select_shift_text": "Sélectionnez plusieurs photos avec shift",
|
||||
"waiting_process": "Photo en cours de traitement",
|
||||
"no_image": "Aucune photo dans cette séquence"
|
||||
},
|
||||
"sequences": {
|
||||
"title": "Mes séquences de photos",
|
||||
"sequence_name": "Nom",
|
||||
"sequence_photos": "Photos",
|
||||
"sequence_date": "Prise de vue",
|
||||
"sequence_status": "Statut",
|
||||
"sequence_published": "✅ Publiée",
|
||||
"sequence_waiting": "⌛ En cours de publication",
|
||||
"sequence_hidden": "❌ Masquée",
|
||||
"no_sequences_text": "Vous n'avez pas encore de photos publiées \uD83D\uDE22",
|
||||
"button_upload": "Partager vos photos"
|
||||
"title": "Mes Tokens"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Partagez vos photos",
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'bootstrap/dist/js/bootstrap.js'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
|
||||
axios.defaults.baseURL = import.meta.env.VITE_API_URL
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useCookies } from 'vue3-cookies'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { getAuthRoute } from '@/utils/auth'
|
||||
import axios from 'axios'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
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'
|
||||
const { cookies } = useCookies()
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
@@ -25,33 +23,29 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'my-settings',
|
||||
component: MySettingsView
|
||||
},
|
||||
{
|
||||
path: '/mes-sequences',
|
||||
name: 'my-sequences',
|
||||
component: MySequencesView
|
||||
},
|
||||
{ path: '/sequence/:id', name: 'sequence', component: MySequenceView },
|
||||
{
|
||||
path: '/partager-des-photos',
|
||||
name: 'upload',
|
||||
component: UploadView
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const loggedRoutes =
|
||||
to.name === 'my-information' ||
|
||||
to.name === 'my-settings' ||
|
||||
to.name === 'my-sequences' ||
|
||||
to.name === 'sequence'
|
||||
if (loggedRoutes) {
|
||||
const isSiteLogin = !!cookies.get('user_id')
|
||||
if (!isSiteLogin) {
|
||||
next((window.location.href = getAuthRoute('auth/login', to.path)))
|
||||
} else {
|
||||
if (to.name === 'my-information' || to.name === 'my-settings') {
|
||||
try {
|
||||
const loginUrl = `${import.meta.env.VITE_API_URL}api/auth/login`
|
||||
const isKeycloakLogout = await axios.get(loginUrl)
|
||||
const isSiteLogin = !!cookies.get('user_id')
|
||||
if (isKeycloakLogout || !isSiteLogin) {
|
||||
const win: Window = window
|
||||
win.location = `${loginUrl}?next_url=${from.path}`
|
||||
} else next()
|
||||
} catch (e) {
|
||||
next()
|
||||
}
|
||||
} else next()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('In the login page', () => {
|
||||
it('type in the form to login', () => {
|
||||
cy.visit('https://geovisio-proxy-dev.osc-fr1.scalingo.io/api/auth/login')
|
||||
cy.visit('https://geovisio-backend-dev.osc-fr1.scalingo.io/api/auth/login')
|
||||
cy.get('#password').type('coucouc')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { vi, it, beforeEach, describe, expect } from 'vitest'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useCookies } from 'vue3-cookies'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import Header from '../../../components/Header.vue'
|
||||
vi.mock('vue-router')
|
||||
vi.mock('vue3-cookies', () => {
|
||||
const mockCookies = {
|
||||
get: vi.fn()
|
||||
}
|
||||
return {
|
||||
useCookies: () => ({
|
||||
cookies: mockCookies
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
@@ -26,79 +15,86 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
})
|
||||
|
||||
describe('Template', () => {
|
||||
beforeEach(async () => {
|
||||
const VueRouter = await import('vue-router')
|
||||
VueRouter.useRoute.mockReturnValueOnce({
|
||||
path: 'my-path'
|
||||
})
|
||||
})
|
||||
describe('When the user is not logged', () => {
|
||||
it('should render the component with good wording keys', async () => {
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
const wrapper = shallowMount(Header, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.html()).contains('general.header.contribute_text')
|
||||
expect(wrapper.html()).contains(
|
||||
'general.header.contribute_text_responsive'
|
||||
)
|
||||
})
|
||||
it('should render the component login link', async () => {
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
const wrapper = shallowMount(Header, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.html()).contains('api/auth/login')
|
||||
expect(wrapper.html()).contains('path="api-url/api/auth/login')
|
||||
})
|
||||
})
|
||||
describe('When the user is logged', () => {
|
||||
it('should render the component with good wording keys', async () => {
|
||||
vi.spyOn(useCookies().cookies, 'get').mockReturnValue('user_id=id')
|
||||
|
||||
beforeEach(async () => {
|
||||
Object.defineProperty(document, 'cookie', {
|
||||
value: 'user_id=abc123'
|
||||
})
|
||||
vi.resetModules()
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
})
|
||||
it('should render the component with good wording keys', async () => {
|
||||
const wrapper = shallowMount(Header, {
|
||||
props: {
|
||||
authEnabled: true,
|
||||
userProfileUrl: 'profil'
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.html()).contains('general.header.contribute_text')
|
||||
expect(wrapper.html()).contains(
|
||||
'general.header.contribute_text_responsive'
|
||||
)
|
||||
expect(wrapper.html()).contains('general.header.my_information_text')
|
||||
expect(wrapper.html()).contains('general.header.logout_text')
|
||||
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/'
|
||||
it('should render the component logout link', async () => {
|
||||
const wrapper = shallowMount(Header, {
|
||||
props: {
|
||||
authEnabled: true,
|
||||
userProfileUrl: 'profil'
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.html()).contains('chevron bi bi-chevron-up')
|
||||
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"')
|
||||
expect(wrapper.html()).contains('path="mes-informations"')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -115,7 +111,7 @@ describe('Methods', () => {
|
||||
it('Menu should be closed by default', () => {
|
||||
wrapper = shallowMount(Header, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
@@ -124,5 +120,20 @@ describe('Methods', () => {
|
||||
|
||||
expect(wrapper.vm.menuIsClosed).toBe(true)
|
||||
})
|
||||
it('Should be open and close by clicking', async () => {
|
||||
wrapper = shallowMount(Header, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
await wrapper.find('.menu-burger').trigger('click')
|
||||
expect(wrapper.vm.menuIsClosed).toBe(false)
|
||||
|
||||
await wrapper.find('.menu-burger').trigger('click')
|
||||
expect(wrapper.vm.menuIsClosed).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import { test, describe, vi, expect } from 'vitest'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import Link from '../../../components/Link.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
vi.mock('vue-i18n')
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
fr
|
||||
}
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
useI18n.mockReturnValue({
|
||||
t: (tKey) => tKey
|
||||
})
|
||||
const stubs = {
|
||||
'router-link': {
|
||||
@@ -29,8 +17,7 @@ describe('Template', () => {
|
||||
test('Should match snapshot with external link', () => {
|
||||
const wrapper = mount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external',
|
||||
@@ -43,9 +30,6 @@ describe('Template', () => {
|
||||
})
|
||||
test('Should match snapshot with internal link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n, router]
|
||||
},
|
||||
props: {
|
||||
text: 'My-text',
|
||||
path: 'my-path'
|
||||
@@ -56,11 +40,7 @@ describe('Template', () => {
|
||||
})
|
||||
describe('Props', () => {
|
||||
test('should have default props', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n, router]
|
||||
}
|
||||
})
|
||||
const wrapper = shallowMount(Link)
|
||||
|
||||
expect(wrapper.vm.text).toBe(null)
|
||||
expect(wrapper.vm.path).toBe('')
|
||||
@@ -77,8 +57,7 @@ describe('Template', () => {
|
||||
test('should render the component as a <a>', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external'
|
||||
@@ -89,8 +68,7 @@ describe('Template', () => {
|
||||
test('should render an icon inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external',
|
||||
@@ -102,8 +80,7 @@ describe('Template', () => {
|
||||
test('should render the text inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external',
|
||||
@@ -115,8 +92,7 @@ describe('Template', () => {
|
||||
test('should render an image inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external',
|
||||
@@ -133,8 +109,7 @@ describe('Template', () => {
|
||||
test('should render the text inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
stubs,
|
||||
plugins: [i18n]
|
||||
stubs
|
||||
},
|
||||
props: {
|
||||
type: 'external',
|
||||
@@ -147,18 +122,11 @@ describe('Template', () => {
|
||||
|
||||
describe('When the component is an internal link', () => {
|
||||
test('should render the component as an internal link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n, router]
|
||||
}
|
||||
})
|
||||
const wrapper = shallowMount(Link)
|
||||
expect(wrapper.html()).contains('<router-link')
|
||||
})
|
||||
test('should render an icon inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
},
|
||||
props: {
|
||||
type: 'internal',
|
||||
icon: 'my-icon'
|
||||
@@ -168,20 +136,14 @@ describe('Template', () => {
|
||||
})
|
||||
test('should render the text inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n, router]
|
||||
},
|
||||
props: {
|
||||
text: 'my-text'
|
||||
}
|
||||
})
|
||||
expect(wrapper.html()).contains('my-text')
|
||||
})
|
||||
test('should render the disabled attribute', () => {
|
||||
test('should render the text inside the link', () => {
|
||||
const wrapper = shallowMount(Link, {
|
||||
global: {
|
||||
plugins: [i18n, router]
|
||||
},
|
||||
props: {
|
||||
disabled: true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import Terminal from '../../../components/Terminal.vue'
|
||||
import Button from '../../../components/Button.vue'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import * as copyToClipboardModule from '../../../utils/copyToClipboard'
|
||||
import { updateClipboard } from '../../../utils/copyToClipboard'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
@@ -39,7 +39,7 @@ describe('Template', () => {
|
||||
})
|
||||
expect(wrapper.html()).contains('<button')
|
||||
expect(wrapper.html()).contains('icon="bi bi-clipboard-plus"')
|
||||
expect(wrapper.html()).contains('look="button--transparent"')
|
||||
expect(wrapper.html()).contains('look="button--white"')
|
||||
})
|
||||
})
|
||||
describe('Props', () => {
|
||||
@@ -89,7 +89,7 @@ describe('Methods', () => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
const wrapper = mount(Terminal, {
|
||||
global: {
|
||||
@@ -101,20 +101,15 @@ describe('Methods', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const updateClipboardMock = vi.spyOn(
|
||||
copyToClipboardModule,
|
||||
'updateClipboard'
|
||||
)
|
||||
updateClipboardMock.mockReturnValue(true)
|
||||
|
||||
vi.mock('../../../utils/copyToClipboard')
|
||||
updateClipboard.mockReturnValue(true)
|
||||
const spy = vi.spyOn(wrapper.vm, 'copyText')
|
||||
|
||||
const buttonWrapper = wrapper.findComponent(Button)
|
||||
await buttonWrapper.vm.$emit('trigger')
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(updateClipboardMock).toHaveBeenCalled()
|
||||
expect(updateClipboard).toHaveBeenCalled()
|
||||
expect(wrapper.vm.uploadIsCopied).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,13 +20,19 @@ exports[`Template > Snapshot > Should match snapshot with external link 1`] = `
|
||||
`;
|
||||
|
||||
exports[`Template > Snapshot > Should match snapshot with internal link 1`] = `
|
||||
<router-link-stub
|
||||
ariacurrentvalue="page"
|
||||
<router-link
|
||||
class="default"
|
||||
custom="false"
|
||||
data-v-409c8661=""
|
||||
replace="false"
|
||||
title="My-text"
|
||||
to="my-path"
|
||||
/>
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<span
|
||||
class="text"
|
||||
data-v-409c8661=""
|
||||
>
|
||||
My-text
|
||||
</span>
|
||||
</router-link>
|
||||
`;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { it, describe, expect, vi, beforeEach } from 'vitest'
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import MySequenceView from '../../../views/MySequenceView.vue'
|
||||
import axios from 'axios'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import fr from '../../../locales/fr.json'
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
fr
|
||||
}
|
||||
})
|
||||
|
||||
describe('Template', () => {
|
||||
it('should render the view without sequences', async () => {
|
||||
await axios.get.mockReturnValue({ data: { links: [] } })
|
||||
const wrapper = shallowMount(MySequenceView, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('api/collections/1234567')
|
||||
})
|
||||
})
|
||||
@@ -1,198 +0,0 @@
|
||||
import { it, describe, expect, vi, beforeEach } from 'vitest'
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import MySequencesView from '../../../views/MySequencesView.vue'
|
||||
import axios from 'axios'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import Button from '../../../components/Button.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
vi.mock('../../../utils/dates', () => ({
|
||||
formatDate: vi.fn()
|
||||
}))
|
||||
vi.mock('axios')
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
fallbackLocale: 'fr',
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
fr
|
||||
}
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
})
|
||||
|
||||
const mockResponseSequences = [
|
||||
{
|
||||
href: 'https://my-link',
|
||||
rel: 'self',
|
||||
type: 'application/json'
|
||||
},
|
||||
{
|
||||
href: 'https://my-link',
|
||||
id: 'my-id',
|
||||
rel: 'child',
|
||||
'stats:items': { count: 16 },
|
||||
title: 'ma sequence 1',
|
||||
extent: {
|
||||
temporal: {
|
||||
interval: [['2022-09-22T08:03:08+00:00', '2022-09-22T08:03:08+00:00']]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
describe('Template', () => {
|
||||
it('should render the view without sequences', async () => {
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({ data: { links: [] } })
|
||||
const wrapper = shallowMount(MySequencesView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
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('path="/partager-des-photos"')
|
||||
})
|
||||
|
||||
it('should render the view with a sequence in the list', async () => {
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: { links: mockResponseSequences }
|
||||
})
|
||||
const wrapper = shallowMount(MySequencesView, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
}
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(axios.get).toHaveBeenCalledWith('api/users/me/catalog')
|
||||
expect(wrapper.vm.userSequences).toEqual([mockResponseSequences[1]])
|
||||
expect(wrapper.html()).contains('src="https://my-link/thumb.jpg"')
|
||||
expect(wrapper.html()).contains('ma sequence 1')
|
||||
expect(wrapper.html()).contains('16')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Methods', () => {
|
||||
describe('sortElements', () => {
|
||||
const mockResponseSequencesToSort = [
|
||||
{
|
||||
href: 'https://my-link',
|
||||
id: 'my-id',
|
||||
rel: 'child',
|
||||
'stats:items': { count: 16 },
|
||||
title: 'za sequence 1',
|
||||
extent: {
|
||||
temporal: {
|
||||
interval: [
|
||||
['2030-09-22T08:03:08+00:00', '2022-09-22T08:03:08+00:00']
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
href: 'https://my-link',
|
||||
id: 'my-id',
|
||||
rel: 'child',
|
||||
'stats:items': { count: 2 },
|
||||
title: 'ma sequence 1',
|
||||
extent: {
|
||||
temporal: {
|
||||
interval: [
|
||||
['2022-09-22T08:03:08+00:00', '2022-09-22T08:03:08+00:00']
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
beforeEach(async () => {
|
||||
await axios.get.mockReturnValue({
|
||||
data: { links: mockResponseSequencesToSort }
|
||||
})
|
||||
await flushPromises()
|
||||
})
|
||||
it('should should sort sequences by title', async () => {
|
||||
const wrapper = shallowMount(MySequencesView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
},
|
||||
components: {
|
||||
Button
|
||||
}
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
const spy = vi.spyOn(wrapper.vm, 'sortElements')
|
||||
const buttonWrapper = wrapper.findComponent(
|
||||
'[data-test="button-sort-title"]'
|
||||
)
|
||||
await buttonWrapper.vm.$emit('trigger')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('alpha')
|
||||
expect(wrapper.vm.userSequences[0]).toEqual(
|
||||
mockResponseSequencesToSort[1]
|
||||
)
|
||||
})
|
||||
it('should should sort sequences by number of pictures', async () => {
|
||||
const wrapper = shallowMount(MySequencesView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
},
|
||||
components: {
|
||||
Button
|
||||
}
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
const spy = vi.spyOn(wrapper.vm, 'sortElements')
|
||||
const buttonWrapper = wrapper.findComponent(
|
||||
'[data-test="button-sort-number"]'
|
||||
)
|
||||
await buttonWrapper.vm.$emit('trigger')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('num')
|
||||
expect(wrapper.vm.userSequences[0]).toEqual(
|
||||
mockResponseSequencesToSort[1]
|
||||
)
|
||||
})
|
||||
it('should should sort sequences by date', async () => {
|
||||
const wrapper = shallowMount(MySequencesView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
mocks: {
|
||||
$t: (msg) => msg
|
||||
},
|
||||
components: {
|
||||
Button
|
||||
}
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
const spy = vi.spyOn(wrapper.vm, 'sortElements')
|
||||
const buttonWrapper = wrapper.findComponent(
|
||||
'[data-test="button-sort-date"]'
|
||||
)
|
||||
await buttonWrapper.vm.$emit('trigger')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('date')
|
||||
expect(wrapper.vm.userSequences[0]).toEqual(
|
||||
mockResponseSequencesToSort[1]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,8 @@ import axios from 'axios'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import Button from '../../../components/Button.vue'
|
||||
import { updateClipboard } from '../../../utils/copyToClipboard'
|
||||
import * as copyToClipboardModule from '../../../utils/copyToClipboard'
|
||||
|
||||
vi.mock('axios')
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
@@ -36,7 +37,8 @@ const mockResponseTokens = [
|
||||
describe('Template', () => {
|
||||
describe('When all the tokens list are fetched', () => {
|
||||
it('should render all the tokens hidden', async () => {
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({ data: mockResponseTokens })
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
axios.get.mockResolvedValue({ data: mockResponseTokens })
|
||||
const wrapper = shallowMount(MySettingsView, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
@@ -47,13 +49,15 @@ describe('Template', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('api/users/me/tokens')
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${import.meta.env.VITE_API_URL}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(
|
||||
'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.upload.button_copy" look="button--white"'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -125,7 +129,7 @@ describe('Methods', () => {
|
||||
})
|
||||
describe('copyText', () => {
|
||||
it('calls copyText function on button click', async () => {
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({ data: mockResponseTokens })
|
||||
axios.get.mockResolvedValue({ data: mockResponseTokens })
|
||||
vi.mock('../../../utils/copyToClipboard')
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
@@ -146,11 +150,7 @@ describe('Methods', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const updateClipboardMock = vi.spyOn(
|
||||
copyToClipboardModule,
|
||||
'updateClipboard'
|
||||
)
|
||||
updateClipboardMock.mockReturnValue(true)
|
||||
updateClipboard.mockReturnValue(true)
|
||||
const spy = vi.spyOn(wrapper.vm, 'copyText')
|
||||
const buttonWrapper = wrapper.findComponent('[data-test="button-copy-0"]')
|
||||
const tokenToCopy = 'token to copy'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils'
|
||||
import UploadView from '../../../views/UploadView.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import fr from '../../../locales/fr.json'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'fr',
|
||||
@@ -14,15 +13,13 @@ const i18n = createI18n({
|
||||
fr
|
||||
}
|
||||
})
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
})
|
||||
|
||||
describe('Template', () => {
|
||||
it('should render the view with the button link', async () => {
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
const wrapper = shallowMount(UploadView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg,
|
||||
authConf: {
|
||||
@@ -31,9 +28,10 @@ describe('Template', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).contains('<link')
|
||||
expect(wrapper.html()).contains('path="')
|
||||
expect(wrapper.html()).contains('/auth/login')
|
||||
expect(wrapper.html()).contains('/auth/login?next_url=/partager-des-photos')
|
||||
expect(wrapper.html()).contains('look="button"')
|
||||
expect(wrapper.html()).contains('type="external"')
|
||||
})
|
||||
@@ -41,7 +39,7 @@ describe('Template', () => {
|
||||
import.meta.env.VITE_API_URL = 'api-url/'
|
||||
const wrapper = shallowMount(UploadView, {
|
||||
global: {
|
||||
plugins: [i18n, router],
|
||||
plugins: [i18n],
|
||||
mocks: {
|
||||
$t: (msg) => msg,
|
||||
authConf: {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
function getAuthRoute(authRoute: string, nextRoute: string): string {
|
||||
const next = `${location.protocol}//${location.host}${nextRoute}`
|
||||
return `/api/${authRoute}?next_url=${encodeURIComponent(`${next}`)}`
|
||||
}
|
||||
|
||||
export { getAuthRoute }
|
||||
@@ -1,15 +0,0 @@
|
||||
import moment from 'moment'
|
||||
|
||||
function formatDate(date: Date, formatType: string): string {
|
||||
const formatDate = moment(date)
|
||||
return formatDate.locale('fr').format(formatType)
|
||||
}
|
||||
|
||||
function durationCalc(duration1: Date, duration2: Date, type: string): number {
|
||||
const duration = moment.duration(moment(duration1).diff(moment(duration2)))
|
||||
if (type == 'hours') return duration.hours()
|
||||
if (type == 'minutes') return duration.minutes()
|
||||
return duration.seconds()
|
||||
}
|
||||
|
||||
export { formatDate, durationCalc }
|
||||
@@ -1,10 +1,3 @@
|
||||
export function img(name: string): string {
|
||||
export function img(name: 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('&')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type { ViewerMapInterface } from '@/views/interfaces/common'
|
||||
import GeoVisio from 'geovisio'
|
||||
|
||||
async function fetchMapAndViewer(picId?: string) {
|
||||
let params: ViewerMapInterface = {
|
||||
map: {
|
||||
startWide: true,
|
||||
style: await getIgnTiles(),
|
||||
maxZoom: 19
|
||||
}
|
||||
}
|
||||
if (picId) params = { ...params, picId: picId }
|
||||
return new GeoVisio(
|
||||
'viewer', // Div ID
|
||||
`${import.meta.env.VITE_API_URL}api/search`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
async function getIgnTiles(): Promise<object> {
|
||||
const { data } = await axios.get(
|
||||
'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json'
|
||||
)
|
||||
data.sources.plan_ign.scheme = 'xyz'
|
||||
data.sources.plan_ign.attribution = 'Données cartographiques : © IGN'
|
||||
// Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS
|
||||
return data
|
||||
}
|
||||
|
||||
export { fetchMapAndViewer }
|
||||
@@ -1,41 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import type {
|
||||
ResponseUserPhotoInterface,
|
||||
ResponseUserSequenceInterface
|
||||
} from '../views/interfaces/MySequenceView'
|
||||
|
||||
function deleteACollectionItem(
|
||||
collectionId: string | string[],
|
||||
itemId: string
|
||||
): Promise<unknown> {
|
||||
return axios.delete(`api/collections/${collectionId}/items/${itemId}`)
|
||||
}
|
||||
|
||||
function patchACollectionItem(
|
||||
isVisible: string,
|
||||
collectionId: string | string[],
|
||||
itemId: string
|
||||
): Promise<unknown> {
|
||||
return axios.patch(`api/collections/${collectionId}/items/${itemId}`, {
|
||||
visible: isVisible
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchCollectionItems(collectionId: string | string[]): Promise<{
|
||||
data: { features: [ResponseUserPhotoInterface] }
|
||||
}> {
|
||||
return await axios.get(`api/collections/${collectionId}/items`)
|
||||
}
|
||||
|
||||
async function fetchCollection(collectionId: string | string[]): Promise<{
|
||||
data: ResponseUserSequenceInterface
|
||||
}> {
|
||||
return await axios.get(`api/collections/${collectionId}`)
|
||||
}
|
||||
|
||||
export {
|
||||
deleteACollectionItem,
|
||||
patchACollectionItem,
|
||||
fetchCollectionItems,
|
||||
fetchCollection
|
||||
}
|
||||
BIN
src/views/.DS_Store
vendored
Normal file
BIN
src/views/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -18,11 +18,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from 'axios'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onMounted, computed, ref } from 'vue'
|
||||
import GeoVisio from 'geovisio'
|
||||
import Button from '@/components/Button.vue'
|
||||
import { fetchMapAndViewer } from '@/utils/mapAndViewer'
|
||||
import { getPicId } from '@/utils/image'
|
||||
|
||||
const { t } = useI18n()
|
||||
let mapIsLoaded = ref<boolean>(false)
|
||||
@@ -30,10 +30,10 @@ const mailtoPath = computed<string>(() => {
|
||||
return `mailto:signalement.ign@panoramax.fr?subject=${t(
|
||||
'pages.home.report_mail_subject',
|
||||
{
|
||||
id: getPicId()
|
||||
id: getPitId()
|
||||
}
|
||||
)}&body=${t('pages.home.report_mail_body', {
|
||||
id: getPicId(),
|
||||
id: getPitId(),
|
||||
link: encodeURIComponent(window.location.href)
|
||||
})}`
|
||||
})
|
||||
@@ -42,9 +42,35 @@ function triggerMailto(): void {
|
||||
window.location.href = mailtoPath.value
|
||||
}
|
||||
|
||||
function getPitId(): string {
|
||||
return window.location.href.substring(
|
||||
window.location.href.indexOf('pic=') + 4,
|
||||
window.location.href.lastIndexOf('&')
|
||||
)
|
||||
}
|
||||
async function getIgnTiles(): Promise<object> {
|
||||
const { data } = await axios.get(
|
||||
'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json'
|
||||
)
|
||||
data.sources.plan_ign.scheme = 'xyz'
|
||||
data.sources.plan_ign.attribution = 'Données cartographiques : © IGN'
|
||||
// Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS
|
||||
return data
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchMapAndViewer()
|
||||
await new GeoVisio(
|
||||
'viewer', // Div ID
|
||||
`${import.meta.env.VITE_API_URL}api/search`, // STAC API search endpoint
|
||||
{
|
||||
map: {
|
||||
startWide: true,
|
||||
style: await getIgnTiles(),
|
||||
maxZoom: 19
|
||||
}
|
||||
} // Viewer options
|
||||
)
|
||||
mapIsLoaded.value = true
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
||||
@@ -1,657 +0,0 @@
|
||||
<template>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cdn.jsdelivr.net/npm/geovisio@develop/build/index.css"
|
||||
/>
|
||||
<main :class="['entry-page', { 'menu-is-open': menuIsOpen }]">
|
||||
<div class="button-close">
|
||||
<Button
|
||||
look="no-text"
|
||||
:icon="menuIsOpen ? 'bi bi-chevron-right' : 'bi bi-chevron-left'"
|
||||
@trigger="menuIsOpen = !menuIsOpen"
|
||||
/>
|
||||
</div>
|
||||
<section id="viewer" class="entry-viewer"></section>
|
||||
<div v-if="userSequence && !patchOrDeleteIsLoading" class="menu-right">
|
||||
<div class="menu-top">
|
||||
<div class="header-menu">
|
||||
<button
|
||||
data-bs-target="#collapseTarget"
|
||||
data-bs-toggle="collapse"
|
||||
class="button-collapse"
|
||||
@click="onToggleHeader"
|
||||
>
|
||||
<h1 class="title">
|
||||
{{ $t('pages.sequence.title') }} {{ userSequence.title }}
|
||||
</h1>
|
||||
<i :class="headerPanelIsOpen ? 'bi bi-dash' : 'bi bi-plus'"></i>
|
||||
</button>
|
||||
<div v-if="false" class="wrapper-button">
|
||||
<div class="disable-button">
|
||||
<Button
|
||||
:text="$t('pages.sequence.button_disable')"
|
||||
look="button--white"
|
||||
icon="bi bi-eye"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
:text="$t('pages.sequence.button_delete')"
|
||||
look="button--red"
|
||||
icon="bi bi-trash"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse py-2 show" id="collapseTarget">
|
||||
<span class="description">{{ userSequence.description }}</span>
|
||||
<div class="block-collapse">
|
||||
<div class="wrapper-info-top">
|
||||
<span
|
||||
>{{ $t('pages.sequence.created') }}
|
||||
{{
|
||||
formatDate(new Date(userSequence.created), 'Do MMMM YYYY')
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
>{{ $t('pages.sequence.taken') }}
|
||||
{{ formatDate(userSequence.taken, 'Do MMMM YYYY') }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="wrapper-info-top">
|
||||
<span
|
||||
>{{ $t('pages.sequence.duration') }}
|
||||
{{ userSequence.duration }}</span
|
||||
>
|
||||
<span
|
||||
>{{ $t('pages.sequence.camera') }} {{ userSequence.camera }} -
|
||||
{{ userSequence.cameraModel }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userPhotos && userPhotos.length"
|
||||
:class="['photos-wrapper', { 'header-open': headerPanelIsOpen }]"
|
||||
>
|
||||
<div class="delete-all">
|
||||
<div class="wrapper-select">
|
||||
<InputCheckbox
|
||||
:is-checked="userPhotos.length === imagesToDelete.length"
|
||||
:is-indeterminate="isIndeterminate"
|
||||
:label="selectedText"
|
||||
@trigger="triggerCheck"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<Button
|
||||
look="button--white"
|
||||
:icon="
|
||||
imagesToDeleteStatus === 'hidden' ||
|
||||
imagesSelectedHaveDifferentStatus
|
||||
? 'bi bi-eye'
|
||||
: 'bi bi-eye-slash'
|
||||
"
|
||||
:tooltip="$t('pages.sequence.hide_photo_tooltip')"
|
||||
:disabled="!imagesToDelete.length"
|
||||
@trigger="patchOrDeleteCollectionItems('PATCH')"
|
||||
/>
|
||||
<div class="button-hidde">
|
||||
<Button
|
||||
look="button--red"
|
||||
icon="bi bi-trash"
|
||||
:tooltip="$t('pages.sequence.delete_photo_tooltip')"
|
||||
:disabled="!imagesToDelete.length"
|
||||
@trigger="patchOrDeleteCollectionItems('DELETE')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="photo-list">
|
||||
<li
|
||||
v-for="(item, i) in userPhotos"
|
||||
:id="`photo${i}`"
|
||||
class="photo-item"
|
||||
>
|
||||
<ImageItem
|
||||
:href="item.assets.thumb.href"
|
||||
:href-hd="item.assets.hd.href"
|
||||
:created="formatDate(item.properties.created, 'HH:mm')"
|
||||
:selected="photoToDeleteOrPatchSelected(item)"
|
||||
:selected-on-map="itemSelected === item.id"
|
||||
:status="item.properties['geovisio:status']"
|
||||
@trigger="selectImageAndMove(item)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else class="no-photo">{{ $t('pages.sequence.no_image') }}</p>
|
||||
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
|
||||
</div>
|
||||
<div v-else class="menu-right wrapper-loader">
|
||||
<Loader />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watchEffect, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/Button.vue'
|
||||
import Toast from '@/components/Toast.vue'
|
||||
import InputCheckbox from '@/components/InputCheckbox.vue'
|
||||
import Loader from '@/components/Loader.vue'
|
||||
import ImageItem from '@/components/ImageItem.vue'
|
||||
import { formatDate, durationCalc } from '@/utils/dates'
|
||||
import {
|
||||
deleteACollectionItem,
|
||||
patchACollectionItem,
|
||||
fetchCollectionItems,
|
||||
fetchCollection
|
||||
} from '@/utils/sequence'
|
||||
import { fetchMapAndViewer } from '@/utils/mapAndViewer'
|
||||
import type {
|
||||
ResponseUserPhotoInterface,
|
||||
CheckboxInterface,
|
||||
UserSequenceInterface
|
||||
} from './interfaces/MySequenceView'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
let userSequence = ref<UserSequenceInterface>()
|
||||
let userPhotos = ref<ResponseUserPhotoInterface[] | []>([])
|
||||
let imagesToDelete = ref<string[]>([])
|
||||
let menuIsOpen = ref<boolean>(true)
|
||||
let headerPanelIsOpen = ref<boolean>(true)
|
||||
let isShiftPressed = ref<boolean>(false)
|
||||
let itemSelected = ref<string>('')
|
||||
let toastText = ref<string>('')
|
||||
let toastLook = ref<string>('')
|
||||
let patchOrDeleteIsLoading = ref<boolean>(false)
|
||||
let viewer = ref()
|
||||
|
||||
interface EventInterface {
|
||||
detail: {
|
||||
picId: string
|
||||
}
|
||||
}
|
||||
watchEffect(async () => {
|
||||
const viewerMap = await viewer.value
|
||||
if (viewerMap && viewerMap.addEventListener) {
|
||||
viewerMap.addEventListener('picture-loaded', (e: EventInterface): void => {
|
||||
itemSelected.value = e.detail.picId
|
||||
scrollIntoSelected(e.detail.picId)
|
||||
})
|
||||
}
|
||||
})
|
||||
const imagesToDeleteStatus = computed((): string => {
|
||||
if (fullImagesToDelete().length) {
|
||||
return fullImagesToDelete()[0].properties['geovisio:status']
|
||||
}
|
||||
return 'hidden'
|
||||
})
|
||||
const imagesSelectedHaveDifferentStatus = computed((): boolean => {
|
||||
function filterByStatus(status: string): ResponseUserPhotoInterface[] {
|
||||
return fullImagesToDelete().filter((el) => {
|
||||
return el.properties['geovisio:status'] === status
|
||||
})
|
||||
}
|
||||
return (
|
||||
filterByStatus('hidden').length > 0 && filterByStatus('ready').length > 0
|
||||
)
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(
|
||||
(): boolean =>
|
||||
!!imagesToDelete.value.length &&
|
||||
!!userSequence.value &&
|
||||
userPhotos.value.length !== imagesToDelete.value.length
|
||||
)
|
||||
|
||||
const selectedText = computed((): string =>
|
||||
imagesToDelete.value.length === userPhotos.value.length
|
||||
? t('pages.sequence.unselect_text')
|
||||
: t('pages.sequence.select_text')
|
||||
)
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const fetchAllCollectionInfo = await Promise.all([
|
||||
fetchCollection(route.params.id),
|
||||
fetchCollectionItems(route.params.id)
|
||||
])
|
||||
const collection = fetchAllCollectionInfo[0].data
|
||||
userSequence.value = {
|
||||
title: collection.title,
|
||||
description: collection.description,
|
||||
license: collection.license,
|
||||
taken: collection.extent.temporal.interval[0][0],
|
||||
created: collection.created,
|
||||
location: collection.extent.spatial.bbox[0],
|
||||
imageCount: collection['stats:items'].count,
|
||||
duration: formatSequenceDuration(collection.extent.temporal.interval[0]),
|
||||
camera: collection.summaries['pers:interior_orientation'][0].make,
|
||||
cameraModel: collection.summaries['pers:interior_orientation'][0].model
|
||||
}
|
||||
const collectionItems = fetchAllCollectionInfo[1].data.features
|
||||
const collectionItemsReady = collectionItems.filter(
|
||||
(el) => el.properties['geovisio:status'] === 'ready'
|
||||
)
|
||||
userPhotos.value = collectionItems
|
||||
if (collectionItemsReady[0]) {
|
||||
viewer.value = await fetchMapAndViewer(collectionItemsReady[0].id)
|
||||
return scrollIntoSelected(collectionItemsReady[0].id)
|
||||
}
|
||||
viewer.value = await fetchMapAndViewer()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
function fullImagesToDelete(): ResponseUserPhotoInterface[] {
|
||||
return userPhotos.value.filter((el) => imagesToDelete.value.includes(el.id))
|
||||
}
|
||||
function scrollIntoSelected(id: string): void {
|
||||
const itemPosition = userPhotos.value.map((el) => el.id).indexOf(id)
|
||||
const elementTarget = document.querySelector(`#photo${itemPosition - 2}`)
|
||||
if (elementTarget) elementTarget.scrollIntoView()
|
||||
}
|
||||
function triggerCheck(value: CheckboxInterface): void {
|
||||
value.isChecked
|
||||
? (imagesToDelete.value = userPhotos.value
|
||||
.filter(
|
||||
(el) => el.properties['geovisio:status'] !== 'waiting-for-process'
|
||||
)
|
||||
.map((el) => el.id))
|
||||
: (imagesToDelete.value = [])
|
||||
}
|
||||
function onToggleHeader(): void {
|
||||
headerPanelIsOpen.value = !headerPanelIsOpen.value
|
||||
}
|
||||
function photoToDeleteOrPatchSelected(
|
||||
item: ResponseUserPhotoInterface
|
||||
): boolean {
|
||||
return imagesToDelete.value.includes(item.id)
|
||||
}
|
||||
|
||||
function selectPhotoToDeleteOrPatch(
|
||||
item: ResponseUserPhotoInterface
|
||||
): string[] {
|
||||
document.addEventListener('keydown', function (evt) {
|
||||
if (evt.key === 'Shift') {
|
||||
isShiftPressed.value = true
|
||||
}
|
||||
})
|
||||
document.addEventListener('keyup', function (evt) {
|
||||
if (evt.key === 'Shift') {
|
||||
isShiftPressed.value = false
|
||||
}
|
||||
})
|
||||
if (isShiftPressed.value) {
|
||||
const userPhotosIndex = userPhotos.value.findIndex(
|
||||
(el) => el.id === item.id
|
||||
)
|
||||
const userPhotosLastIndex = userPhotos.value.findIndex(
|
||||
(el) => el.id === imagesToDelete.value[0]
|
||||
)
|
||||
const slicedUserPhotos = userPhotos.value.slice(
|
||||
userPhotosLastIndex,
|
||||
userPhotosIndex + 1
|
||||
)
|
||||
return (imagesToDelete.value = slicedUserPhotos.map((el) => el.id))
|
||||
}
|
||||
if (imagesToDelete.value.includes(item.id)) {
|
||||
return (imagesToDelete.value = imagesToDelete.value.filter(
|
||||
(el) => el !== item.id
|
||||
))
|
||||
}
|
||||
return (imagesToDelete.value = [...imagesToDelete.value, item.id])
|
||||
}
|
||||
async function selectImageAndMove(
|
||||
item: ResponseUserPhotoInterface
|
||||
): Promise<void> {
|
||||
selectPhotoToDeleteOrPatch(item)
|
||||
if (
|
||||
imagesToDelete.value.length < 2 &&
|
||||
item.properties['geovisio:status'] === 'ready'
|
||||
) {
|
||||
const viewerMap = await viewer.value
|
||||
viewerMap.goToPicture(item.id)
|
||||
itemSelected.value = item.id
|
||||
scrollIntoSelected(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
function spliceIntoChunks(arr: string[], chunkSize: number) {
|
||||
const res = []
|
||||
arr = ([] as string[]).concat(...arr)
|
||||
while (arr.length) {
|
||||
res.push(arr.splice(0, chunkSize))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function patchOrDeleteCollectionItems(
|
||||
requestType: string
|
||||
): Promise<void> {
|
||||
patchOrDeleteIsLoading.value = true
|
||||
toastText.value = ''
|
||||
const chunksItems = spliceIntoChunks(imagesToDelete.value, 4)
|
||||
try {
|
||||
let items: unknown[] = []
|
||||
if (imagesSelectedHaveDifferentStatus.value) {
|
||||
for (let el of chunksItems) {
|
||||
items = [
|
||||
...items,
|
||||
...(await Promise.all(
|
||||
el.map((ele) => {
|
||||
if (requestType === 'PATCH') {
|
||||
return patchACollectionItem('true', route.params.id, ele)
|
||||
}
|
||||
if (confirm(t('pages.sequence.confirm_dialog'))) {
|
||||
return deleteACollectionItem(route.params.id, ele)
|
||||
}
|
||||
})
|
||||
))
|
||||
]
|
||||
}
|
||||
} else {
|
||||
for (let el of chunksItems) {
|
||||
items = [
|
||||
...items,
|
||||
...(await Promise.all(
|
||||
el.map((ele) => {
|
||||
if (requestType === 'PATCH') {
|
||||
const imageToDelete = userPhotos.value.find(
|
||||
(elem) => elem.id === ele
|
||||
)
|
||||
const isVisible =
|
||||
imageToDelete?.properties['geovisio:status'] === 'ready'
|
||||
? 'false'
|
||||
: 'true'
|
||||
return patchACollectionItem(isVisible, route.params.id, ele)
|
||||
}
|
||||
if (confirm(t('pages.sequence.confirm_dialog'))) {
|
||||
return deleteACollectionItem(route.params.id, ele)
|
||||
}
|
||||
})
|
||||
))
|
||||
]
|
||||
}
|
||||
}
|
||||
const { data } = await fetchCollectionItems(route.params.id)
|
||||
userPhotos.value = data.features
|
||||
toastText.value = t('general.success_text')
|
||||
toastLook.value = 'success'
|
||||
patchOrDeleteIsLoading.value = false
|
||||
scrollIntoSelected(imagesToDelete.value[0])
|
||||
imagesToDelete.value = []
|
||||
} catch (e) {
|
||||
toastText.value = t('general.error_text')
|
||||
toastLook.value = 'error'
|
||||
patchOrDeleteIsLoading.value = false
|
||||
imagesToDelete.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function formatSequenceDuration(temporal: Date[]): string {
|
||||
let timer = ''
|
||||
if (durationCalc(temporal[1], temporal[0], 'hours') > 0) {
|
||||
timer += ` ${t(
|
||||
'pages.sequence.hours',
|
||||
durationCalc(temporal[1], temporal[0], 'hours')
|
||||
)}`
|
||||
}
|
||||
if (durationCalc(temporal[1], temporal[0], 'minutes')) {
|
||||
timer += ` ${t(
|
||||
'pages.sequence.minutes',
|
||||
durationCalc(temporal[1], temporal[0], 'minutes')
|
||||
)}`
|
||||
}
|
||||
if (durationCalc(temporal[1], temporal[0], 'seconds') > 0)
|
||||
timer += ` ${t(
|
||||
'pages.sequence.seconds',
|
||||
durationCalc(temporal[1], temporal[0], 'seconds')
|
||||
)}`
|
||||
return timer
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry-page {
|
||||
display: flex;
|
||||
}
|
||||
.entry-viewer {
|
||||
width: 50vw;
|
||||
position: relative;
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
.menu-right {
|
||||
width: 50vw;
|
||||
height: calc(100vh - 8rem);
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 4px 20px 0px #00000033;
|
||||
}
|
||||
.wrapper-loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.wrapper-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.disable-button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.collapse {
|
||||
&:first-child {
|
||||
@include text(s-regular);
|
||||
color: var(--grey-dark);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.block-collapse {
|
||||
display: flex;
|
||||
}
|
||||
.description,
|
||||
.wrapper-info-top {
|
||||
@include text(s-regular);
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
.wrapper-info-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
&:first-child {
|
||||
border-right: 0.1rem solid var(--grey-dark);
|
||||
padding-right: 2rem;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
@include text(h2);
|
||||
color: var(--grey-dark);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.button-close {
|
||||
display: none;
|
||||
}
|
||||
.menu-top {
|
||||
margin: 2rem 2rem 0;
|
||||
padding: 1rem 2rem;
|
||||
border: 0.1rem solid var(--grey);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--blue-semi);
|
||||
}
|
||||
.header-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.button-collapse {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.bi-plus,
|
||||
.bi-dash {
|
||||
color: var(--grey-dark);
|
||||
font-size: 3rem;
|
||||
}
|
||||
.photos-wrapper {
|
||||
padding: 1rem 0rem 2rem 1rem;
|
||||
height: calc(100vh - 21rem);
|
||||
}
|
||||
.header-open {
|
||||
height: calc(100vh - 31rem);
|
||||
}
|
||||
.delete-all {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-left: 1rem;
|
||||
margin-right: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.wrapper-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.button-hidde {
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.delete-all-text {
|
||||
@include text(xs-r-regular);
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.photo-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.photo-item {
|
||||
width: calc(33% - 2rem);
|
||||
height: fit-content;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--grey);
|
||||
}
|
||||
.no-photo {
|
||||
@include text(s-regular);
|
||||
text-align: center;
|
||||
margin-top: 10rem;
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.block-collapse {
|
||||
flex-direction: column;
|
||||
}
|
||||
.wrapper-info-top {
|
||||
&:first-child,
|
||||
&:nth-child(2) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
border-right: none;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.photo-item {
|
||||
width: calc(50% - 2rem);
|
||||
}
|
||||
.header-open {
|
||||
height: calc(100vh - 35rem);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header-menu {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.photos-wrapper {
|
||||
height: calc(100vh - 29rem);
|
||||
}
|
||||
.header-open {
|
||||
height: calc(100vh - 41rem);
|
||||
}
|
||||
.photo-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
.wrapper-info-top {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
.wrapper-button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.photo-list {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
.delete-all {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.entry-page {
|
||||
height: calc(100vh - 11rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
.entry-viewer {
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
}
|
||||
.menu-right {
|
||||
padding-top: 11rem;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
width: 80vw;
|
||||
background-color: var(--white);
|
||||
}
|
||||
.menu-top {
|
||||
width: 0vw;
|
||||
}
|
||||
.button-close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 22rem;
|
||||
z-index: 3;
|
||||
background-color: var(--black);
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
.menu-is-open {
|
||||
.menu-right {
|
||||
z-index: 3;
|
||||
}
|
||||
.menu-top {
|
||||
width: auto;
|
||||
}
|
||||
.button-close {
|
||||
left: calc(20vw - 3rem);
|
||||
right: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,324 +0,0 @@
|
||||
<template>
|
||||
<main class="entry-page">
|
||||
<h1 class="sequences-title">{{ $t('pages.sequences.title') }}</h1>
|
||||
<ul class="sequence-list">
|
||||
<li class="sequence-item">
|
||||
<div class="sequence-header-item"></div>
|
||||
<div class="sequence-header-item">
|
||||
<Button
|
||||
:text="$t('pages.sequences.sequence_name')"
|
||||
look="link--grey"
|
||||
icon="bi bi-arrow-down-up"
|
||||
data-test="button-sort-title"
|
||||
@trigger="sortElements('alpha')"
|
||||
/>
|
||||
</div>
|
||||
<div class="sequence-header-item">
|
||||
<Button
|
||||
:text="$t('pages.sequences.sequence_photos')"
|
||||
look="link--grey"
|
||||
icon="bi bi-arrow-down-up"
|
||||
data-test="button-sort-number"
|
||||
@trigger="sortElements('num')"
|
||||
/>
|
||||
</div>
|
||||
<div class="sequence-header-item">
|
||||
<Button
|
||||
:text="$t('pages.sequences.sequence_date')"
|
||||
look="link--grey"
|
||||
icon="bi bi-arrow-down-up"
|
||||
data-test="button-sort-date"
|
||||
@trigger="sortElements('date')"
|
||||
/>
|
||||
</div>
|
||||
<div class="sequence-header-item">
|
||||
<Button
|
||||
:text="$t('pages.sequences.sequence_status')"
|
||||
look="link--grey"
|
||||
icon="bi bi-arrow-down-up"
|
||||
data-test="button-sort-date"
|
||||
@trigger="sortElements('alpha')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="userSequences && userSequences.length"
|
||||
v-for="item in userSequences"
|
||||
class="sequence-item"
|
||||
>
|
||||
<router-link
|
||||
class="button-item"
|
||||
:to="{
|
||||
name: 'sequence',
|
||||
params: { id: item.id }
|
||||
}"
|
||||
>
|
||||
<div class="wrapper-thumb">
|
||||
<img
|
||||
:src="`${item.href}/thumb.jpg`"
|
||||
lazy="loading"
|
||||
alt=""
|
||||
class="thumb"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-images"></i>
|
||||
<span>
|
||||
{{ item['stats:items'].count }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
{{
|
||||
formatDate(
|
||||
item.extent.temporal.interval[0][0],
|
||||
'Do MMMM YYYY HH:mm:ss'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ sequenceStatus(item['geovisio:status']) }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
<div v-else class="no-sequence">
|
||||
<p class="no-sequence-text">
|
||||
{{ $t('pages.sequences.no_sequences_text') }}
|
||||
</p>
|
||||
<Link
|
||||
:text="$t('general.header.contribute_text')"
|
||||
look="button"
|
||||
path="/partager-des-photos"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import axios from 'axios'
|
||||
import Button from '@/components/Button.vue'
|
||||
import Link from '@/components/Link.vue'
|
||||
import type {
|
||||
LinkInterface,
|
||||
ExtentInterface
|
||||
} from './interfaces/MySequencesView'
|
||||
import { formatDate } from '@/utils/dates'
|
||||
const { t } = useI18n()
|
||||
let userSequences = ref<LinkInterface[]>([])
|
||||
let isSorted = ref<boolean>(false)
|
||||
|
||||
function sequenceStatus(status: string): string {
|
||||
if (status === 'ready') {
|
||||
return t('pages.sequences.sequence_published')
|
||||
}
|
||||
if (status === 'waiting-for-process') {
|
||||
return t('pages.sequences.sequence_waiting')
|
||||
}
|
||||
return t('pages.sequences.sequence_hidden')
|
||||
}
|
||||
function sortElements(type: string): void {
|
||||
let aa: string | number
|
||||
let bb: string | number
|
||||
if (!isSorted.value) {
|
||||
const sorted = userSequences.value.sort(
|
||||
(a: ExtentInterface, b: ExtentInterface): number => {
|
||||
aa = new Date(a.extent.temporal.interval[0][0]).getTime()
|
||||
bb = new Date(b.extent.temporal.interval[0][0]).getTime()
|
||||
if (type === 'alpha') {
|
||||
aa = a.title
|
||||
bb = b.title
|
||||
}
|
||||
if (type === 'num') {
|
||||
aa = Number(a['stats:items'].count)
|
||||
bb = Number(b['stats:items'].count)
|
||||
}
|
||||
isSorted.value = true
|
||||
if (aa < bb) return -1
|
||||
return 0
|
||||
}
|
||||
)
|
||||
userSequences.value = sorted
|
||||
} else {
|
||||
const sorted = userSequences.value.sort(
|
||||
(a: ExtentInterface, b: ExtentInterface): number => {
|
||||
aa = new Date(a.extent.temporal.interval[0][0]).getTime()
|
||||
bb = new Date(b.extent.temporal.interval[0][0]).getTime()
|
||||
if (type === 'alpha') {
|
||||
aa = a.title
|
||||
bb = b.title
|
||||
}
|
||||
if (type === 'num') {
|
||||
aa = Number(a['stats:items'].count)
|
||||
bb = Number(b['stats:items'].count)
|
||||
}
|
||||
isSorted.value = false
|
||||
if (aa > bb) return -1
|
||||
return 0
|
||||
}
|
||||
)
|
||||
userSequences.value = sorted
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await axios.get('api/users/me/catalog')
|
||||
const relChild = data.links.filter(
|
||||
(el: LinkInterface) => el.rel === 'child'
|
||||
)
|
||||
userSequences.value = relChild
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry-page {
|
||||
padding-right: 8rem;
|
||||
padding-left: 8rem;
|
||||
padding-top: 11rem;
|
||||
min-height: calc(100vh - 8rem);
|
||||
}
|
||||
|
||||
.sequences-title {
|
||||
@include text(h1);
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.sequence-list {
|
||||
box-shadow: 0px 2px 30px 0px #0000000f;
|
||||
border-radius: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
.sequence-item {
|
||||
@include text(s-regular);
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
background-color: var(--blue-pale);
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 2rem;
|
||||
border-bottom-left-radius: 2rem;
|
||||
.button-item {
|
||||
border-bottom-right-radius: 2rem;
|
||||
border-bottom-left-radius: 2rem;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 3rem;
|
||||
border-bottom: 0.1rem solid var(--grey);
|
||||
border-radius: 2rem 2rem 0rem 0rem;
|
||||
background-color: var(--white);
|
||||
}
|
||||
&:nth-child(2n) {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
.wrapper-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.thumb {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.button-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 2rem 3rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
& > * {
|
||||
padding: 1rem;
|
||||
text-align: initial;
|
||||
width: 31%;
|
||||
color: var(--black);
|
||||
}
|
||||
> :first-child {
|
||||
color: var(--blue);
|
||||
width: 6rem;
|
||||
}
|
||||
& > :first-child {
|
||||
padding: 0;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
& > :nth-child(2) {
|
||||
color: var(--blue);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--blue);
|
||||
& > * {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
.bi-images {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.sequence-header-item {
|
||||
width: 31%;
|
||||
&:first-child {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
&:first-child {
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
||||
.no-sequence {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 4rem;
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
@include text(m-regular);
|
||||
}
|
||||
.no-sequence-text {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.entry-page {
|
||||
padding-right: 2rem;
|
||||
padding-left: 2rem;
|
||||
padding-top: 14rem;
|
||||
min-height: calc(100vh - 11rem);
|
||||
}
|
||||
.button-item,
|
||||
.sequence-item:first-child {
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.button-item {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
& > * {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.wrapper-thumb {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.sequence-item:first-child {
|
||||
display: none;
|
||||
}
|
||||
.sequence-item {
|
||||
border-top-right-radius: 1rem;
|
||||
border-top-left-radius: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,6 @@
|
||||
<Button
|
||||
:data-test="`button-eye-${i}`"
|
||||
look="button--rounded"
|
||||
:tooltip="$t('pages.settings.setting_tooltip')"
|
||||
:icon="
|
||||
!item.token || item.isHidden ? 'bi bi-eye' : 'bi bi-eye-slash'
|
||||
"
|
||||
@@ -76,7 +75,9 @@ async function copyText(i: number): Promise<void> {
|
||||
}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await axios.get('api/users/me/tokens')
|
||||
const data = await axios.get(
|
||||
`${import.meta.env.VITE_API_URL}api/users/me/tokens`
|
||||
)
|
||||
userTokens.value = data.data
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
:text="$t('pages.upload.user_account_button')"
|
||||
type="external"
|
||||
look="button white"
|
||||
:path="getAuthRoute('auth/login', 'partager-des-photos')"
|
||||
:path="loginUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +115,6 @@ import { useCookies } from 'vue3-cookies'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getAuthRoute } from '@/utils/auth'
|
||||
import authConfig from '../composables/auth'
|
||||
const { cookies } = useCookies()
|
||||
const { t } = useI18n()
|
||||
@@ -127,6 +126,12 @@ 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>"
|
||||
|
||||
const loginUrl = computed<string>(
|
||||
() =>
|
||||
`${
|
||||
import.meta.env.VITE_API_URL
|
||||
}api/auth/login?next_url=/partager-des-photos`
|
||||
)
|
||||
const isLogged = computed((): boolean => !!cookies.get('user_id'))
|
||||
const terminalTextInstall = computed((): string =>
|
||||
t('pages.upload.terminal_install')
|
||||
@@ -224,7 +229,7 @@ function triggerHref(): void {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.grey {
|
||||
color: var(--grey-semi-dark);
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
.wrapper-account {
|
||||
display: flex;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
export interface ResponseUserPhotoInterface {
|
||||
assets: { thumb: { href: string }; hd: { href: string } }
|
||||
properties: { created: Date; 'geovisio:status': string }
|
||||
id: string
|
||||
bbox: number[]
|
||||
}
|
||||
|
||||
export interface UserSequenceInterface {
|
||||
title: string
|
||||
description: string
|
||||
license: string
|
||||
created: string
|
||||
taken: Date
|
||||
location: string
|
||||
imageCount: number
|
||||
duration: string
|
||||
camera: string
|
||||
cameraModel: string
|
||||
}
|
||||
|
||||
export interface ResponseUserSequenceInterface extends UserSequenceInterface {
|
||||
extent: { temporal: { interval: [Date[]] }; spatial: { bbox: string[] } }
|
||||
['stats:items']: { count: number }
|
||||
summaries: {
|
||||
['pers:interior_orientation']: [{ make: string; model: string }]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CheckboxInterface {
|
||||
isChecked: boolean
|
||||
isIndeterminate: boolean
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export interface LinkInterface {
|
||||
id: string
|
||||
href: string
|
||||
rel: string
|
||||
title: string
|
||||
type: string
|
||||
extent: { temporal: { interval: [Date[]] } }
|
||||
['stats:items']: { count: number }
|
||||
['geovisio:status']: string
|
||||
}
|
||||
|
||||
export interface ExtentInterface {
|
||||
extent: { temporal: { interval: [Date[]] } }
|
||||
['stats:items']: { count: number }
|
||||
title: string
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface ViewerMapInterface {
|
||||
map: {
|
||||
startWide: boolean
|
||||
style: object
|
||||
maxZoom: number
|
||||
}
|
||||
picId?: string
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
hmr: {
|
||||
port: 9000
|
||||
}
|
||||
7229
yarn-error.log
Normal file
7229
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user