Feat/upload interface

This commit is contained in:
Jean Andreani
2023-08-28 09:23:19 +00:00
parent 0d39d522a5
commit 7f570e4086
58 changed files with 2002 additions and 528 deletions

4
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -47,11 +47,11 @@
>
<div class="button-info">
<Link
look="button--blue no-text"
look="button button--white-blue no-text"
icon="bi bi-cloud-download-fill"
type="external"
target="_blank"
:path="hrefHd"
:path-external="hrefHd"
/>
</div>
</div>
@@ -87,8 +87,8 @@ defineProps({
position: relative;
}
.selected {
border: 0.1rem solid var(--blue);
border-radius: 0.5rem;
border: toRem(0.1) solid var(--blue);
border-radius: toRem(0.5);
box-shadow: 0px 4px 4px 0px #00000040;
}
.wrapper-image {
@@ -98,45 +98,45 @@ defineProps({
display: flex;
justify-content: center;
align-items: center;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
height: 16rem;
border-top-right-radius: toRem(0.5);
border-top-left-radius: toRem(0.5);
height: toRem(16);
width: 100%;
object-fit: cover;
}
.photo-img {
height: 100%;
width: 100%;
border-radius: 0.5rem;
border-radius: toRem(0.5);
}
.icon-hidden {
color: var(--grey-dark);
position: absolute;
font-size: 4rem;
font-size: toRem(4);
}
.waiting-wrapper {
height: 16rem;
height: toRem(16);
display: flex;
justify-content: center;
align-items: center;
color: var(--blue);
}
.icon-waiting {
height: 4rem;
font-size: 4rem;
height: toRem(4);
font-size: toRem(4);
}
.icon-img {
top: 1rem;
right: 1rem;
top: toRem(1);
right: toRem(1);
background-color: var(--white);
border-radius: 50%;
position: absolute;
height: 2rem;
width: 2rem;
height: toRem(2);
width: toRem(2);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.3rem;
font-size: toRem(1.3);
}
.pointer-map,
.button-check-pointer {
@@ -146,11 +146,11 @@ defineProps({
opacity: 1;
}
.photo-info {
height: 5rem;
height: toRem(5);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
padding: toRem(1);
position: absolute;
bottom: 0;
width: 100%;
@@ -163,9 +163,9 @@ defineProps({
}
.info {
padding: 0.5rem 0.8rem;
padding: toRem(0.5) toRem(0.8);
background-color: var(--white);
border-radius: 0.5rem;
border-radius: toRem(0.5);
color: var(--blue);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,11 @@
"description": "Panoramax, lalternative libre pour photo-cartographier les territoires"
},
"header": {
"contribute_text": "Partager vos photos",
"contribute_text": "À propos",
"upload_text": "Contribuer",
"sequences_text": "Mes photos",
"alt_logo": "Logo de l'instance",
"title": "Instance Panoramax",
"title": "Instance\nPanoramax",
"beta_text": "Version beta",
"logout_text": "Déconnexion",
"my_information_text": "Mes informations",
@@ -73,7 +74,7 @@
"button_upload": "Partager vos photos",
"sequence_deleted": "La séquence a bien été supprimée"
},
"upload": {
"share_pictures": {
"title": "Partagez vos photos",
"sub_title": "Un compte utilisateur est obligatoire pour partager des photos",
"photo_type1": "Des lieux visibles depuis la voie publique",
@@ -92,6 +93,26 @@
"terminal_install": "pip install geovisio_cli",
"terminal_text": "geovisio upload --api-url {url} <DOSSIER_PHOTOS>",
"button_copy": "Copier"
},
"upload": {
"title": "Déposez vos photos",
"input_label": "Déposez des photos dans la zone ou ",
"import_word": "importez",
"import_type": "Format JPEG uniquement",
"sequence_title": "Séquence du ",
"button_text": "Envoyer",
"uploaded_files": "{count} fichier| {count} fichiers",
"no_uploaded_files": "Aucun fichier sélectionné",
"uploaded_word": " Image téléchargée",
"import": "Imports",
"error_word": "Images en erreur",
"sequence_uploading_title": "Dernier import",
"upload_pending": "Transfert en cours...",
"upload_done": "Transfert terminé !",
"upload_pending_pictures": "Envoi de {count} photo en cours |Envoi de {count} photos en cours",
"sequence_link": "Voir la sequence",
"button_new_upload": "Nouvel envoi",
"leave_message": "⚠️ Attention, le téléchargement sera interrompu si vous quittez la page avant la fin."
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,8 @@ describe('Template', () => {
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.path).toBe('')
expect(wrapper.vm.route).toStrictEqual({})
expect(wrapper.vm.pathExternal).toBe('')
expect(wrapper.vm.look).toBe('')
expect(wrapper.vm.type).toBe(null)
expect(wrapper.vm.alt).toBe('')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import UploadView from '../../../views/UploadView.vue'
import SharePicturesView from '../../../views/SharePicturesView.vue'
import { createI18n } from 'vue-i18n'
import fr from '../../../locales/fr.json'
import { createRouter, createWebHistory } from 'vue-router'
@@ -20,7 +20,7 @@ const router = createRouter({
})
describe('Template', () => {
it('should render the view with the button link', async () => {
const wrapper = shallowMount(UploadView, {
const wrapper = shallowMount(SharePicturesView, {
global: {
plugins: [i18n, router],
mocks: {
@@ -34,12 +34,12 @@ describe('Template', () => {
expect(wrapper.html()).contains('<link')
expect(wrapper.html()).contains('path="')
expect(wrapper.html()).contains('/auth/login')
expect(wrapper.html()).contains('look="button"')
expect(wrapper.html()).contains('look="button button--blue"')
expect(wrapper.html()).contains('type="external"')
})
it('should render the view without the button link', async () => {
import.meta.env.VITE_API_URL = 'api-url/'
const wrapper = shallowMount(UploadView, {
const wrapper = shallowMount(SharePicturesView, {
global: {
plugins: [i18n, router],
mocks: {
@@ -50,6 +50,6 @@ describe('Template', () => {
}
}
})
expect(wrapper.html()).not.toContain('pages.upload.sub_title')
expect(wrapper.html()).not.toContain('pages.share_pictures.sub_title')
})
})

View File

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

View File

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

View File

@@ -47,29 +47,29 @@ onMounted(async () => {
}
})
</script>
<style scoped>
<style scoped lang="scss">
.entry-page {
display: flex;
}
.entry-viewer {
font-size: initial;
width: 100vw;
position: relative;
min-height: calc(100vh - 8rem);
font-size: 160%; /* Cancels the rule set on HTML 62.5% */
min-height: calc(100vh - #{toRem(8)});
}
.gvs-has-map .entry-report-button {
display: block;
position: absolute;
right: 120px;
top: 10px;
right: toRem(1);
top: toRem(2);
z-index: 1;
}
.gvs-focus-map .entry-report-button {
display: none;
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
padding-top: 11rem;
padding-top: toRem(11);
overflow: hidden;
}
@supports (-webkit-touch-callout: none) {
@@ -83,7 +83,7 @@ onMounted(async () => {
@supports not (-webkit-touch-callout: none) {
/* CSS for other than iOS devices */
.entry-viewer {
min-height: calc(100vh - 17rem);
min-height: calc(100vh - #{toRem(17)});
}
}
}

View File

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

View File

@@ -151,7 +151,7 @@
:selected="photoToDeleteOrPatchSelected(item, picturesToDelete)"
:selected-on-map="itemSelected === item.id"
:status="
imageStatus(item.properties['geovisio:status'], sequence)
imageStatus(item.properties['geovisio:status'], sequence.status)
"
@trigger="selectImageAndMove(item)"
/>
@@ -171,7 +171,7 @@
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</div>
<div v-else class="menu-right wrapper-loader">
<Loader />
<Loader look="sm" :is-loaded="false" />
</div>
</main>
</template>
@@ -550,11 +550,12 @@ async function patchOrDeleteCollectionItems(
.entry-viewer {
width: 50vw;
position: relative;
height: calc(100vh - 8rem);
height: calc(100vh - #{toRem(8)});
font-size: 137.5%;
}
.menu-right {
width: 50vw;
height: calc(100vh - 8rem);
height: calc(100vh - #{toRem(8)});
overflow: hidden;
box-shadow: 0px 4px 20px 0px #00000033;
}
@@ -568,13 +569,13 @@ async function patchOrDeleteCollectionItems(
align-items: center;
}
.disable-button {
margin-right: 1rem;
margin-right: toRem(1);
}
.collapse {
&:first-child {
@include text(s-regular);
color: var(--grey-dark);
margin-bottom: 1rem;
margin-bottom: toRem(1);
}
&.hidden {
opacity: 0.4;
@@ -590,28 +591,28 @@ async function patchOrDeleteCollectionItems(
.wrapper-info-top {
display: flex;
flex-direction: column;
margin-top: 1rem;
margin-top: toRem(1);
&:first-child {
border-right: 0.1rem solid var(--grey-dark);
padding-right: 2rem;
border-right: toRem(0.1) solid var(--grey-dark);
padding-right: toRem(2);
}
&:nth-child(2) {
padding-left: 2rem;
padding-left: toRem(2);
}
}
.title {
@include text(h2);
color: var(--grey-dark);
margin-right: 1rem;
margin-right: toRem(1);
}
.button-close {
display: none;
}
.menu-top {
margin: 2rem 2rem 0;
padding: 1rem 2rem;
border: 0.1rem solid var(--grey);
border-radius: 0.5rem;
margin: toRem(2) toRem(2) 0;
padding: toRem(1) toRem(2);
border: toRem(0.1) solid var(--grey);
border-radius: toRem(0.5);
background-color: var(--blue-semi);
}
.header-menu {
@@ -619,21 +620,21 @@ async function patchOrDeleteCollectionItems(
justify-content: space-between;
}
.sequence-status {
border-radius: 3rem;
padding: 0.5rem 1rem;
margin-right: 2rem;
border-radius: toRem(3);
padding: toRem(0.5) toRem(1);
margin-right: toRem(2);
color: var(--white);
&.ready {
background-color: var(--orange);
border: 0.1rem solid var(--orange);
border: toRem(0.1) solid var(--orange);
}
&.waiting-for-process {
background-color: var(--yellow);
border: 0.1rem solid var(--yellow);
border: toRem(0.1) solid var(--yellow);
}
&.hidden {
background-color: var(--blue-geovisio);
border: 0.1rem solid var(--blue-geovisio);
border: toRem(0.1) solid var(--blue-geovisio);
}
}
.button-collapse {
@@ -646,20 +647,20 @@ async function patchOrDeleteCollectionItems(
.bi-plus,
.bi-dash {
color: var(--grey-dark);
font-size: 3rem;
font-size: toRem(3);
}
.photos-wrapper {
padding: 1rem 0rem 2rem 1rem;
padding: toRem(1) 0rem toRem(2) toRem(1);
height: calc(100vh - v-bind(menuHeight));
}
.delete-all {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 1rem;
margin-right: 2rem;
margin-bottom: 1rem;
margin-left: toRem(1);
margin-right: toRem(2);
margin-bottom: toRem(1);
}
.wrapper-select {
display: flex;
@@ -669,12 +670,12 @@ async function patchOrDeleteCollectionItems(
@include text(xs-regular);
}
.photo-selected-separator {
margin-right: 0.5rem;
margin-left: 0.5rem;
margin-right: toRem(0.5);
margin-left: toRem(0.5);
}
.button-hidde {
margin-right: 1rem;
margin-left: 1rem;
margin-right: toRem(1);
margin-left: toRem(1);
}
.delete-all-text {
@include text(xs-r-regular);
@@ -692,29 +693,29 @@ async function patchOrDeleteCollectionItems(
padding: 0;
}
.photo-item {
width: calc(33% - 2rem);
width: calc(33% - #{toRem(2)});
height: fit-content;
margin: 1rem;
border-radius: 0.5rem;
margin: toRem(1);
border-radius: toRem(0.5);
background-color: var(--grey);
}
.no-photo {
@include text(s-regular);
text-align: center;
margin-top: 10rem;
margin-top: toRem(10);
color: var(--grey-dark);
}
.entry-pagination {
margin-top: 2rem;
margin-top: toRem(2);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: 1024px) {
@media (max-width: toRem(102.4)) {
.header-menu {
flex-direction: column;
align-items: flex-start;
margin-bottom: 1rem;
margin-bottom: toRem(1);
}
.block-collapse {
flex-direction: column;
@@ -731,10 +732,10 @@ async function patchOrDeleteCollectionItems(
}
}
.photo-item {
width: calc(50% - 2rem);
width: calc(50% - #{toRem(2)});
}
}
@media (max-width: 768px) {
@media (max-width: toRem(76.8)) {
.photo-item {
width: 100%;
margin-right: 0;
@@ -744,10 +745,10 @@ async function patchOrDeleteCollectionItems(
margin-right: 0;
}
.wrapper-button {
margin-top: 1rem;
margin-top: toRem(1);
}
.photo-list {
padding-right: 2rem;
padding-right: toRem(2);
}
.delete-all {
text-align: left;
@@ -757,16 +758,16 @@ async function patchOrDeleteCollectionItems(
align-items: initial;
}
.wrapper-photo-selected:nth-child(2) {
margin-top: 0.5rem;
margin-top: toRem(0.5);
}
.photo-selected-separator {
display: none;
}
}
@media (max-width: 500px) {
@media (max-width: toRem(50)) {
.entry-page {
height: calc(100vh - 11rem);
height: calc(100vh - #{toRem(11)});
overflow: hidden;
}
.entry-viewer {
@@ -775,7 +776,7 @@ async function patchOrDeleteCollectionItems(
z-index: 1;
}
.menu-right {
padding-top: 11rem;
padding-top: toRem(11);
height: 100%;
position: fixed;
top: 0;
@@ -790,15 +791,15 @@ async function patchOrDeleteCollectionItems(
.button-close {
position: absolute;
right: 0;
top: 22rem;
top: toRem(22);
z-index: 3;
background-color: var(--black);
height: 5rem;
height: toRem(5);
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: toRem(0.5);
border-bottom-left-radius: toRem(0.5);
}
.menu-is-open {
.menu-right {
@@ -808,19 +809,19 @@ async function patchOrDeleteCollectionItems(
width: auto;
}
.button-close {
left: calc(20vw - 3rem);
left: calc(20vw - #{toRem(3)});
right: initial;
}
}
.entry-pagination {
margin-top: 1rem;
margin-top: toRem(1);
}
}
@media (min-width: 1900px) {
.menu-right {
width: initial;
max-width: 100rem;
max-width: toRem(100);
}
.entry-viewer {
width: 100%;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
function formatPictureSize(size: number): number {
return Math.floor(Math.log(size) / Math.log(1024))
}
function formatTextSize(size: number, i: number): string {
const sizes = ['0', 'Ko', 'Mo', 'Go', 'To', 'Po', 'Eo', 'Zo', 'o']
return `${parseFloat((size / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`
}
export { formatPictureSize, formatTextSize }

View File

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

View File

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

View File

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