102 Commits

Author SHA1 Message Date
Andreani Jean
a9ecd0b2a4 Release 0.1.0 2023-07-04 22:23:15 +02:00
Andreani Jean
9b704e2275 fix tests 2023-07-04 22:16:22 +02:00
Andreani Jean
f9d385f3d9 fix router guards 2023-07-04 21:27:23 +02:00
Andreani Jean
6a75b53245 fix auth guard 2023-07-04 15:26:13 +02:00
Andreani Jean
a2d793e448 fix userPhotos 2023-07-04 12:14:01 +02:00
Andreani Jean
ae80e1b4bd wip 2023-07-04 12:09:49 +02:00
Andreani Jean
764e355295 wip 2023-07-04 11:21:04 +02:00
Andreani Jean
c110d60f46 wip 2023-07-04 11:18:19 +02:00
Andreani Jean
52c2ea5d79 wip 2023-07-04 11:00:18 +02:00
Andreani Jean
d4e61b82fb add click outside menu 2023-07-04 10:02:04 +02:00
Andreani Jean
e2a67eba90 test new guard router 2023-07-04 09:27:32 +02:00
Andreani Jean
a31d076721 test new guard router 2023-07-04 09:02:04 +02:00
Andreani Jean
c5ff54e1cd test 2023-07-03 23:48:26 +02:00
Andreani Jean
d5862cc021 fix header links 2023-07-03 23:02:30 +02:00
Andreani Jean
d71c0ee835 fix ts error 2023-07-03 22:43:58 +02:00
Andreani Jean
972615bd12 fix header + router guard 2023-07-03 17:46:00 +02:00
Andreani Jean
ef1bd0105b fix responsive css 2023-07-03 15:35:17 +02:00
Andreani Jean
ceb0034131 fix css sequence responsive 2023-07-03 15:19:14 +02:00
Andreani Jean
d86426ed33 fix some css 2023-07-03 11:59:21 +02:00
Andreani Jean
9c76f4925d fix some tests 2023-07-03 11:36:36 +02:00
Andreani Jean
74543334c6 fix wording 2023-07-03 11:19:23 +02:00
Andreani Jean
6bddda9e17 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
4ece6669d6 add status sequence 2023-07-03 11:19:23 +02:00
Andreani Jean
94cf617a4f fix sequences 2023-07-03 11:19:23 +02:00
Andreani Jean
d1f240dcce test getItems after call 2023-07-03 11:19:23 +02:00
Andreani Jean
d1207e656b some fix 2023-07-03 11:19:23 +02:00
Andreani Jean
f5a936fad6 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
43838a4331 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
198fe69508 log 2023-07-03 11:19:23 +02:00
Andreani Jean
99709594fe test 2023-07-03 11:19:23 +02:00
Andreani Jean
e82e8b6841 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
671664d84f wip 2023-07-03 11:19:23 +02:00
Andreani Jean
e8eb3a462c wip 2023-07-03 11:19:23 +02:00
Andreani Jean
9cdfd16dc0 check with for of 2023-07-03 11:19:23 +02:00
Andreani Jean
78586932c4 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
33c777bfc6 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
8c265cf403 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
2d8b35fd88 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
5cf899ae4d wip 2023-07-03 11:19:23 +02:00
Andreani Jean
57f01bbafb wip 2023-07-03 11:19:23 +02:00
Andreani Jean
aecf7a41a6 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
2ffa09c622 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
e32a3682b9 wip 2023-07-03 11:19:23 +02:00
Andreani Jean
5778387706 log debug 2023-07-03 11:19:23 +02:00
Andreani Jean
f69aba17ea test log 2023-07-03 11:19:23 +02:00
Andreani Jean
165ddef725 fix ts 2023-07-03 11:19:23 +02:00
Andreani Jean
2b5f515bf5 some fixs after user testing 2023-07-03 11:19:23 +02:00
Andreani Jean
e99bd3f39b wip 2023-07-03 11:19:22 +02:00
Andreani Jean
39d2839bb4 fix tests 2023-07-03 11:19:18 +02:00
Andreani Jean
f72d1f0177 ip 2023-07-03 11:18:35 +02:00
Andreani Jean
2e9936be3e rebase main 2023-07-03 11:18:35 +02:00
Andreani Jean
70987bd363 edit button control 2023-07-03 11:18:35 +02:00
Andreani Jean
423b77f217 fix typescript error getAuthRoute 2023-07-03 11:18:35 +02:00
Andreani Jean
ed1e3afe53 edit changelog + add test sequence list 2023-07-03 11:18:35 +02:00
Andreani Jean
a4e56aa8ae fix shift selection 2023-07-03 11:18:35 +02:00
Adrien Pavie
4b92e8230c Handle next_url callbacks for localhost 2023-07-03 11:18:35 +02:00
Andreani Jean
b601aa7c2a fix type 2023-07-03 11:18:35 +02:00
Andreani Jean
4e600d2907 add loading 2023-07-03 11:18:34 +02:00
Andreani Jean
bbd372b672 fix waiting process img button + add toast success & error 2023-07-03 11:18:34 +02:00
Andreani Jean
6aff4dc3c8 add scroll into viex 2023-07-03 11:18:34 +02:00
Andreani Jean
836c8bf3d3 return value mounted 2023-07-03 11:18:34 +02:00
Andreani Jean
0c5b3a306e fix id error 2023-07-03 11:18:34 +02:00
Andreani Jean
ad5e83a4f4 add filter for only ready element 2023-07-03 11:18:34 +02:00
Andreani Jean
e8b8921cc0 some css button disable 2023-07-03 11:18:34 +02:00
Andreani Jean
aa4b93d3d5 fix border img 2023-07-03 11:18:34 +02:00
Andreani Jean
7d97442de9 fix hidden status"
git push
2023-07-03 11:18:34 +02:00
Andreani Jean
5a8d8d2ab9 fix style disable 2023-07-03 11:18:34 +02:00
Andreani Jean
deaffaeef4 await 2023-07-03 11:18:34 +02:00
Andreani Jean
63c1b869fa wip 2023-07-03 11:18:34 +02:00
Andreani Jean
761aaac1e1 test without await 2023-07-03 11:18:34 +02:00
Andreani Jean
e09db186e7 good api route 2023-07-03 11:18:34 +02:00
Andreani Jean
67e3a646e5 remove id test 2023-07-03 11:18:34 +02:00
Andreani Jean
c4b2fae7c5 factoring 2023-07-03 11:18:33 +02:00
Andreani Jean
d957d2b67f me/catalogue without credentials 2023-07-03 11:17:24 +02:00
antoine-de
a497b81d88 use creds with axios on every calls in mysequence 2023-07-03 11:17:24 +02:00
antoine-de
d83c1f9195 use credentials and user's catalog 2023-07-03 11:17:24 +02:00
Andreani Jean
e011eea200 add no sequence text 2023-07-03 11:17:24 +02:00
Andreani Jean
7937900637 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
7540f10400 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
8a57342d75 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
995fa0be27 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
ffb4d6d0ae wip 2023-07-03 11:17:24 +02:00
Andreani Jean
4a2bc26bda testing hidde 2023-07-03 11:17:24 +02:00
Andreani Jean
4ce0751ce4 add new version 2023-07-03 11:17:24 +02:00
Andreani Jean
d63f367bf4 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
98c471118b wip 2023-07-03 11:17:24 +02:00
Andreani Jean
210f2abd54 fix ts errors 2023-07-03 11:17:24 +02:00
Andreani Jean
8e05e793d6 add patch to test 2023-07-03 11:17:24 +02:00
Andreani Jean
e092e1f064 wip 2023-07-03 11:17:24 +02:00
Andreani Jean
7e233f1cb3 wip 2023-07-03 11:17:22 +02:00
Andreani Jean
c913711c9a test 2023-07-03 11:16:44 +02:00
Adrien Pavie
529bae421b Update yarn.lock 2023-06-30 17:56:33 +02:00
Adrien Pavie
451d7afb49 Merge branch 'feature/maj-geovisio' into 'main'
Update file package.json

See merge request geovisio/website!60
2023-06-30 15:24:55 +00:00
Adrien Pavie
e5cf0197df Update file package.json 2023-06-30 14:55:07 +00:00
Jean Andreani
96971cc4b1 Merge branch 'fix/burger-beta-text' into 'main'
fix: add beta text + remove burger menu

See merge request geovisio/website!59
2023-06-19 15:26:22 +00:00
Jean Andreani
ad039d0480 fix: add beta text + remove burger menu 2023-06-19 15:26:22 +00:00
antoine-de
1c1689268b Merge branch 'update_viewer_2_0_5' into 'main'
upgrade to geovisio viewer 2.0.5

See merge request geovisio/website!58
2023-06-15 14:34:38 +00:00
antoine-de
84a6aa0c77 upgrade to geovisio viewer 2.0.5 2023-06-15 15:08:36 +02:00
Jean Andreani
061b3ab3b3 Merge branch 'feature/docs' into 'main'
Update docs

See merge request geovisio/website!56
2023-06-14 15:21:31 +00:00
Adrien Pavie
c5f9a6da06 Update docs 2023-06-14 15:21:31 +00:00
antoine-de
1e509c48c2 Merge branch 'update_viewer_2_0' into 'main'
update geovisio viewer to latest 2.0.2 version

See merge request geovisio/website!55
2023-06-08 16:24:38 +00:00
antoine-de
6d3112ea0e update geovisio viewer to latest 2.0.2 version 2023-06-08 16:24:38 +00:00
57 changed files with 6127 additions and 9369 deletions

2
.env
View File

@@ -1,2 +0,0 @@
VITE_API_URL=https://geovisio-backend-dev.osc-fr1.scalingo.io/
VITE_ENV=dev

View File

@@ -29,7 +29,8 @@ module.exports = {
rules: {
'vue/require-default-prop': 'off',
'prettier/prettier': 'error',
'@typescript-eslint/no-namespace': 'off'
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}

View File

@@ -34,6 +34,8 @@ 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 Normal file
View File

@@ -0,0 +1,29 @@
# 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
View File

@@ -1,109 +1,60 @@
# panoramax-website
# ![GeoVisio](https://gitlab.com/geovisio/api/-/raw/develop/images/logo_full.png)
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).
__GeoVisio__ is a complete solution for storing and __serving your own 📍📷 geolocated pictures__ (like [StreetView](https://www.google.com/streetview/) / [Mapillary](https://mapillary.com/)).
## Technologies
➡️ __Give it a try__ at [panoramax.ign.fr](https://panoramax.ign.fr/) or [geovisio.fr](https://geovisio.fr/viewer) !
- 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
## 📦 Components
## Configuration
GeoVisio is __modular__ and made of several components, each of them standardized and ♻️ replaceable.
All the commands and packages used are available in the `package.json` file.
![GeoVisio architecture](https://gitlab.com/geovisio/api/-/raw/develop/images/big_picture.png)
You can change the vite server configuration in the `vite.config.ts` file. See [Vite Configuration Reference](https://vitejs.dev/config/) if you need.
All of them are 📖 __open-source__ and available online:
## Project Setup
| 🌐 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) |
**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)**
# 💻 GeoVisio Website
You can use npm or [yarn](https://yarnpkg.com/) as package manager
This repository only contains __the web front-end of GeoVisio__.
Install all dependencies :
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).
```sh
npm install
```
## ⚙️ Features
or
The website offers these functionalities:
```sh
yarn install
```
- 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
### Compile and Hot-Reload for Development
## 🕮 Documentation
Launch your dev server :
[A full documentation](./docs/) is available to help you through the install, setup and usage of the GeoVisio website.
```sh
npm run dev
```
## 💁 Contributing
or
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.
```sh
yarn dev
```
## 🤗 Special thanks
### Run Unit Tests with [Vitest](https://vitest.dev/)
![Sponsors](https://gitlab.com/geovisio/api/-/raw/develop/images/sponsors.png)
```sh
npm run test:unit
```
GeoVisio was made possible thanks to a group of ✨ __amazing__ people ✨ :
or
- __[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 !
```sh
yarn test:unit
```
### Lint with [ESLint](https://eslint.org/)
## ⚖️ 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)
Copyright (c) GeoVisio team 2022-2023, [released under MIT license](./LICENSE).

29
docs/01_Start.md Normal file
View File

@@ -0,0 +1,29 @@
# GeoVisio Website hands-on guide
![GeoVisio logo](https://gitlab.com/geovisio/api/-/raw/develop/images/logo_full.png)
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)

46
docs/02_Setup.md Normal file
View File

@@ -0,0 +1,46 @@
# 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).

43
docs/03_Settings.md Normal file
View File

@@ -0,0 +1,43 @@
# 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).

43
docs/09_Develop.md Normal file
View File

@@ -0,0 +1,43 @@
# 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
```

View File

@@ -1,13 +1,12 @@
{
"name": "geovisio-website",
"version": "0.0.0",
"version": "1.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",
@@ -22,10 +21,13 @@
},
"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.2-develop-33c2d8bd",
"geovisio": "2.0.6",
"moment": "^2.29.4",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-eslint-parser": "^9.1.0",

BIN
src/assets/.DS_Store vendored

Binary file not shown.

View File

@@ -41,4 +41,8 @@
font-size: 1rem;
}
}
@if $size == xss-regular {
font-size: 0.9rem;
font-weight: normal;
}
}

Binary file not shown.

BIN
src/assets/images/thumb.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -32,10 +32,15 @@ h5 {
--black-pale: #1b1a17;
--red: #f70000;
--grey: #e6e6e6;
--grey-dark: #808080;
--grey-pale: #CFD2CF;
--grey-semi-dark: #808080;
--grey-dark: #54595e;
--blue: #4945ff;
--blue-semi: rgba(207, 226, 255, 0.5);
--blue-pale: #f9fafd;
--beige: #f5f3ec;
--yellow: #fec868;
--orange: #ff6f00;
--green: #59ce8f;
}

Binary file not shown.

View File

@@ -1,6 +1,7 @@
<template>
<p class="instance-beta">
{{ $t('general.header.title') }}
<span class="beta">{{ $t('general.header.beta_text') }}</span>
</p>
</template>

View File

@@ -2,11 +2,14 @@
<button
:disabled="isLoading || disabled"
type="button"
:class="[look, 'default', { disabled }]"
@click="$emit('trigger')"
:class="['default', look]"
>
<i v-if="icon" :class="[icon, 'icon']"></i>
{{ text }}
<span v-if="text.length" class="text">{{ text }}</span>
<span v-if="tooltip && tooltip.length" class="tooltip-button">{{
tooltip
}}</span>
</button>
</template>
@@ -16,38 +19,101 @@ 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: 764px) {
@media (min-width: 768px) {
.default:hover {
opacity: 0.7;
opacity: 0.8;
}
}
.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: 4.5rem;
height: 3.5rem;
border-radius: 0.5rem;
padding: 1.3rem 2rem 1.3rem;
background-color: var(--black);
color: var(--white);
background-color: var(--black);
}
.button--white {
.button--transparent {
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;
@@ -67,4 +133,30 @@ 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>

View File

@@ -1,10 +1,20 @@
<template>
<header class="header">
<div class="responsive">
<div class="responsive beta">
<beta-text />
</div>
<nav class="nav">
<div class="wrapper-logo">
<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">
<Link
:image="{
url: 'logo.jpeg',
@@ -12,69 +22,74 @@
}"
path="/"
/>
<div class="desktop">
<beta-text />
</div>
</div>
<div class="wrapper-entries">
<ul :class="['nav-list', { 'mobile-menu-open': !menuIsClosed }]">
<li class="nav-list-item desktop">
<ul :class="['nav-list', { 'menu-open': !menuIsClosed }]">
<li v-if="isLogged" class="logged-link">
<Link
:text="$t('general.header.contribute_text')"
icon="bi bi-upload"
look="button white"
path="/partager-des-photos"
: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"
/>
</li>
</ul>
<div class="wrapper-right-entries">
<div class="responsive">
<div>
<Link
:text="$t('general.header.contribute_text_responsive')"
icon="bi bi-upload"
:text="$t('general.header.contribute_text')"
look="button white"
path="/partager-des-photos"
@click.native="closeModal"
/>
</div>
<div v-if="authEnabled" class="item-with-sub">
<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>
<Link
type="external"
icon="bi bi-person-circle"
:look="isLogged ? 'disable-mobile' : ''"
:path="userUrl"
:text="userName"
:path="getAuthRoute('auth/login', route.path)"
/>
<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>
@@ -83,9 +98,11 @@
<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'
@@ -99,6 +116,10 @@ defineProps({
let menuIsClosed = ref<boolean>(true)
function closeModal() {
menuIsClosed.value = true
}
function toggleMenu(): void {
menuIsClosed.value = !menuIsClosed.value
}
@@ -108,17 +129,12 @@ 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 =>
isLogged.value ? cookies.get('user_name') : t('general.header.login_text')
cookies!
.get('user_name')
.match(/\b(\w)/g)!
.join('')
.toUpperCase()
)
</script>
@@ -155,26 +171,14 @@ 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;
@@ -188,14 +192,7 @@ const userName = computed((): string =>
}
.logged-link {
display: flex;
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;
padding: 0.5rem 2rem 0.7rem;
}
.logged-link:hover {
border-radius: 0.5rem;
@@ -210,8 +207,61 @@ const userName = computed((): string =>
.wrapper-right-entries {
display: flex;
align-items: center;
div:first-child {
margin-right: 2rem;
}
}
@media (max-width: 768px) {
.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) {
.header {
flex-direction: column;
height: 11rem;
@@ -219,57 +269,24 @@ const userName = computed((): string =>
top: 0;
left: 0;
width: 100%;
z-index: 2;
z-index: 4;
background: var(--white);
}
.cross {
font-size: 2rem;
}
.item-with-sub {
margin-right: 1.5rem;
.nav-list {
top: 11rem;
width: 100%;
}
.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;
}
.nav-list-item {
margin-bottom: 2.5rem;
}
}
@media (max-width: 500px) {
.item-with-sub {
margin-right: 1rem;
.beta {
width: 100%;
.instance-beta {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,170 @@
<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>

View File

@@ -0,0 +1,84 @@
<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>

View File

@@ -65,13 +65,17 @@ 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: 4.5rem;
height: 4rem;
border-radius: 0.5rem;
padding: 1.3rem 2rem 1.3rem;
background-color: var(--black);
@@ -99,6 +103,7 @@ const titleImg = computed<string>(() =>
.logo {
height: 4rem;
border-radius: 0.5rem;
margin-right: 1rem;
}
.disabled {
color: grey;
@@ -118,14 +123,43 @@ const titleImg = computed<string>(() =>
align-items: center;
border-radius: 50%;
padding: 0;
height: 4.5rem;
width: 4.5rem;
height: 4rem;
width: 4rem;
.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;

48
src/components/Loader.vue Normal file
View File

@@ -0,0 +1,48 @@
<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>

View File

@@ -13,7 +13,7 @@
</div>
<div class="entry-button-terminal">
<Button
look="button--white"
look="button--transparent"
:text="$t('pages.upload.button_copy')"
:icon="clipboardIcon"
@trigger="copyText(textInstall)"
@@ -27,7 +27,7 @@
</div>
<div class="entry-button-terminal">
<Button
look="button--white"
look="button--transparent"
:text="$t('pages.upload.button_copy')"
:icon="clipboardIcon"
@trigger="copyText(textUpload)"

65
src/components/Toast.vue Normal file
View File

@@ -0,0 +1,65 @@
<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>

View File

@@ -6,9 +6,9 @@ export default function authConfig() {
const authConf = ref<AuthConfigInterface>({})
async function getConfig(): Promise<object> {
const { data } = await axios.get(
`${import.meta.env.VITE_API_URL}api/configuration`
)
const { data } = await axios.get('api/configuration', {
withCredentials: false
})
return data.auth
}
onMounted(async () => (authConf.value = await getConfig()))

View File

@@ -7,9 +7,10 @@
},
"header": {
"contribute_text": "Partager vos photos",
"contribute_text_responsive": "Verser",
"sequences_text": "Mes photos",
"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",
@@ -17,7 +18,8 @@
"burger_menu_aria_label_open": "Afficher le menu",
"burger_menu_aria_label_closed": "Masquer le menu"
},
"feature_not_available": "Fonctionnalité en cours de développement"
"error_text": "Une erreur est survenue",
"success_text": "Mise à jour réussie"
},
"pages": {
"home": {
@@ -26,7 +28,43 @@
"report_button_text": "Signaler la photo"
},
"settings": {
"title": "Mes Tokens"
"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"
},
"upload": {
"title": "Partagez vos photos",

View File

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

View File

@@ -1,10 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useCookies } from 'vue3-cookies'
import type { RouteRecordRaw } from 'vue-router'
import axios from 'axios'
import { getAuthRoute } from '@/utils/auth'
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> = [
@@ -23,29 +25,33 @@ 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) => {
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) {
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 {
next()
}
} else next()

View File

@@ -1,6 +1,6 @@
describe('In the login page', () => {
it('type in the form to login', () => {
cy.visit('https://geovisio-backend-dev.osc-fr1.scalingo.io/api/auth/login')
cy.visit('https://geovisio-proxy-dev.osc-fr1.scalingo.io/api/auth/login')
cy.get('#password').type('coucouc')
})
})

View File

@@ -1,10 +1,21 @@
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',
@@ -15,86 +26,79 @@ 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],
plugins: [i18n, router],
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],
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.html()).contains('path="api-url/api/auth/login')
expect(wrapper.html()).contains('api/auth/login')
})
})
describe('When the user is logged', () => {
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 () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue('user_id=id')
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(Header, {
props: {
authEnabled: true,
userProfileUrl: 'profil'
},
global: {
plugins: [i18n],
plugins: [i18n, router],
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 logout link', async () => {
it('should render the component with all links', async () => {
vi.spyOn(useCookies().cookies, 'get').mockReturnValue('user_id=id')
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(Header, {
props: {
authEnabled: true,
userProfileUrl: 'profil'
},
global: {
plugins: [i18n],
plugins: [i18n, router],
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-informations"')
expect(wrapper.html()).contains('path="/mes-parametres"')
expect(wrapper.html()).contains('path="/mes-sequences"')
})
})
})
@@ -111,7 +115,7 @@ describe('Methods', () => {
it('Menu should be closed by default', () => {
wrapper = shallowMount(Header, {
global: {
plugins: [i18n],
plugins: [i18n, router],
mocks: {
$t: (msg) => msg
}
@@ -120,20 +124,5 @@ 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)
})
})
})

View File

@@ -1,11 +1,23 @@
import { test, describe, vi, expect } from 'vitest'
import { mount, shallowMount } from '@vue/test-utils'
import Link from '../../../components/Link.vue'
import { useI18n } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import { createRouter, createWebHistory } from 'vue-router'
import fr from '../../../locales/fr.json'
vi.mock('vue-i18n')
useI18n.mockReturnValue({
t: (tKey) => tKey
const i18n = createI18n({
locale: 'fr',
fallbackLocale: 'fr',
globalInjection: true,
legacy: false,
messages: {
fr
}
})
const router = createRouter({
history: createWebHistory(),
routes: []
})
const stubs = {
'router-link': {
@@ -17,7 +29,8 @@ describe('Template', () => {
test('Should match snapshot with external link', () => {
const wrapper = mount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external',
@@ -30,6 +43,9 @@ describe('Template', () => {
})
test('Should match snapshot with internal link', () => {
const wrapper = shallowMount(Link, {
global: {
plugins: [i18n, router]
},
props: {
text: 'My-text',
path: 'my-path'
@@ -40,7 +56,11 @@ describe('Template', () => {
})
describe('Props', () => {
test('should have default props', () => {
const wrapper = shallowMount(Link)
const wrapper = shallowMount(Link, {
global: {
plugins: [i18n, router]
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.path).toBe('')
@@ -57,7 +77,8 @@ describe('Template', () => {
test('should render the component as a <a>', () => {
const wrapper = shallowMount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external'
@@ -68,7 +89,8 @@ describe('Template', () => {
test('should render an icon inside the link', () => {
const wrapper = shallowMount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external',
@@ -80,7 +102,8 @@ describe('Template', () => {
test('should render the text inside the link', () => {
const wrapper = shallowMount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external',
@@ -92,7 +115,8 @@ describe('Template', () => {
test('should render an image inside the link', () => {
const wrapper = shallowMount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external',
@@ -109,7 +133,8 @@ describe('Template', () => {
test('should render the text inside the link', () => {
const wrapper = shallowMount(Link, {
global: {
stubs
stubs,
plugins: [i18n]
},
props: {
type: 'external',
@@ -122,11 +147,18 @@ describe('Template', () => {
describe('When the component is an internal link', () => {
test('should render the component as an internal link', () => {
const wrapper = shallowMount(Link)
const wrapper = shallowMount(Link, {
global: {
plugins: [i18n, router]
}
})
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'
@@ -136,14 +168,20 @@ 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 text inside the link', () => {
test('should render the disabled attribute', () => {
const wrapper = shallowMount(Link, {
global: {
plugins: [i18n, router]
},
props: {
disabled: true
}

View File

@@ -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 { updateClipboard } from '../../../utils/copyToClipboard'
import * as copyToClipboardModule 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--white"')
expect(wrapper.html()).contains('look="button--transparent"')
})
})
describe('Props', () => {
@@ -89,7 +89,7 @@ describe('Methods', () => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
const wrapper = mount(Terminal, {
global: {
@@ -101,15 +101,20 @@ describe('Methods', () => {
}
}
})
vi.mock('../../../utils/copyToClipboard')
updateClipboard.mockReturnValue(true)
const updateClipboardMock = vi.spyOn(
copyToClipboardModule,
'updateClipboard'
)
updateClipboardMock.mockReturnValue(true)
const spy = vi.spyOn(wrapper.vm, 'copyText')
const buttonWrapper = wrapper.findComponent(Button)
await buttonWrapper.vm.$emit('trigger')
expect(spy).toHaveBeenCalled()
expect(updateClipboard).toHaveBeenCalled()
expect(updateClipboardMock).toHaveBeenCalled()
expect(wrapper.vm.uploadIsCopied).toBe(true)
})
})

View File

@@ -20,19 +20,13 @@ exports[`Template > Snapshot > Should match snapshot with external link 1`] = `
`;
exports[`Template > Snapshot > Should match snapshot with internal link 1`] = `
<router-link
<router-link-stub
ariacurrentvalue="page"
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>
/>
`;

View File

@@ -0,0 +1,32 @@
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')
})
})

View File

@@ -0,0 +1,198 @@
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]
)
})
})
})

View File

@@ -6,8 +6,7 @@ import axios from 'axios'
import fr from '../../../locales/fr.json'
import Button from '../../../components/Button.vue'
import { updateClipboard } from '../../../utils/copyToClipboard'
vi.mock('axios')
import * as copyToClipboardModule from '../../../utils/copyToClipboard'
const i18n = createI18n({
locale: 'fr',
@@ -37,8 +36,7 @@ const mockResponseTokens = [
describe('Template', () => {
describe('When all the tokens list are fetched', () => {
it('should render all the tokens hidden', async () => {
import.meta.env.VITE_API_URL = 'api-url/'
axios.get.mockResolvedValue({ data: mockResponseTokens })
vi.spyOn(axios, 'get').mockResolvedValue({ data: mockResponseTokens })
const wrapper = shallowMount(MySettingsView, {
global: {
plugins: [i18n],
@@ -49,15 +47,13 @@ describe('Template', () => {
})
await flushPromises()
expect(axios.get).toHaveBeenCalledWith(
`${import.meta.env.VITE_API_URL}api/users/me/tokens`
)
expect(axios.get).toHaveBeenCalledWith('api/users/me/tokens')
expect(wrapper.vm.userTokens).toEqual(mockResponseTokens)
expect(wrapper.html()).contains('•••••••••••••••••••••••••••••••')
expect(wrapper.html()).contains('icon="bi bi-eye"')
expect(wrapper.html()).contains('look="button--rounded"')
expect(wrapper.html()).contains(
'icon="bi bi-clipboard-plus" disabled="false" isloading="false" text="pages.upload.button_copy" look="button--white"'
'icon="bi bi-clipboard-plus" disabled="false" isloading="false" text="pages.upload.button_copy" tooltip="" look="button--white"'
)
})
})
@@ -129,7 +125,7 @@ describe('Methods', () => {
})
describe('copyText', () => {
it('calls copyText function on button click', async () => {
axios.get.mockResolvedValue({ data: mockResponseTokens })
vi.spyOn(axios, 'get').mockResolvedValue({ data: mockResponseTokens })
vi.mock('../../../utils/copyToClipboard')
beforeEach(() => {
vi.useFakeTimers()
@@ -150,7 +146,11 @@ describe('Methods', () => {
})
await flushPromises()
updateClipboard.mockReturnValue(true)
const updateClipboardMock = vi.spyOn(
copyToClipboardModule,
'updateClipboard'
)
updateClipboardMock.mockReturnValue(true)
const spy = vi.spyOn(wrapper.vm, 'copyText')
const buttonWrapper = wrapper.findComponent('[data-test="button-copy-0"]')
const tokenToCopy = 'token to copy'

View File

@@ -3,6 +3,7 @@ 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',
@@ -13,13 +14,15 @@ 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],
plugins: [i18n, router],
mocks: {
$t: (msg) => msg,
authConf: {
@@ -28,10 +31,9 @@ describe('Template', () => {
}
}
})
expect(wrapper.html()).contains('<link')
expect(wrapper.html()).contains('path="')
expect(wrapper.html()).contains('/auth/login?next_url=/partager-des-photos')
expect(wrapper.html()).contains('/auth/login')
expect(wrapper.html()).contains('look="button"')
expect(wrapper.html()).contains('type="external"')
})
@@ -39,7 +41,7 @@ describe('Template', () => {
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(UploadView, {
global: {
plugins: [i18n],
plugins: [i18n, router],
mocks: {
$t: (msg) => msg,
authConf: {

6
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,6 @@
function getAuthRoute(authRoute: string, nextRoute: string): string {
const next = `${location.protocol}//${location.host}${nextRoute}`
return `/api/${authRoute}?next_url=${encodeURIComponent(`${next}`)}`
}
export { getAuthRoute }

15
src/utils/dates.ts Normal file
View File

@@ -0,0 +1,15 @@
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 }

View File

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

31
src/utils/mapAndViewer.ts Normal file
View File

@@ -0,0 +1,31 @@
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 }

41
src/utils/sequence.ts Normal file
View File

@@ -0,0 +1,41 @@
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

Binary file not shown.

View File

@@ -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: getPitId()
id: getPicId()
}
)}&body=${t('pages.home.report_mail_body', {
id: getPitId(),
id: getPicId(),
link: encodeURIComponent(window.location.href)
})}`
})
@@ -42,35 +42,9 @@ 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 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
)
await fetchMapAndViewer()
mapIsLoaded.value = true
} catch (err) {
console.log(err)

View File

@@ -0,0 +1,657 @@
<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>

View File

@@ -0,0 +1,324 @@
<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>

View File

@@ -10,6 +10,7 @@
<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'
"
@@ -75,9 +76,7 @@ async function copyText(i: number): Promise<void> {
}
onMounted(async () => {
try {
const data = await axios.get(
`${import.meta.env.VITE_API_URL}api/users/me/tokens`
)
const data = await axios.get('api/users/me/tokens')
userTokens.value = data.data
} catch (err) {
console.log(err)

View File

@@ -45,7 +45,7 @@
:text="$t('pages.upload.user_account_button')"
type="external"
look="button white"
:path="loginUrl"
:path="getAuthRoute('auth/login', 'partager-des-photos')"
/>
</div>
</div>
@@ -115,6 +115,7 @@ 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()
@@ -126,12 +127,6 @@ 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')
@@ -229,7 +224,7 @@ function triggerHref(): void {
white-space: pre-wrap;
}
.grey {
color: var(--grey-dark);
color: var(--grey-semi-dark);
}
.wrapper-account {
display: flex;

View File

@@ -0,0 +1,32 @@
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
}

View File

@@ -0,0 +1,16 @@
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
}

View File

@@ -0,0 +1,8 @@
export interface ViewerMapInterface {
map: {
startWide: boolean
style: object
maxZoom: number
}
picId?: string
}

View File

@@ -8,7 +8,6 @@ export default defineConfig({
server: {
host: true,
port: 5173,
strictPort: true,
hmr: {
port: 9000
}

File diff suppressed because it is too large Load Diff

5440
yarn.lock

File diff suppressed because it is too large Load Diff