Feat/edit a sequence

This commit is contained in:
Jean Andreani
2024-03-19 13:03:46 +00:00
parent e56c159146
commit 3ced85eaf5
41 changed files with 2274 additions and 585 deletions

View File

@@ -25,7 +25,7 @@
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"geovisio": "2.5.0",
"geovisio": "2.5.1",
"moment": "^2.29.4",
"pako": "^2.1.0",
"pinia": "^2.1.4",

View File

@@ -3,11 +3,9 @@ import { ref, computed } from 'vue'
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import { RouterView } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { hasASessionCookieDecoded } from '@/utils/auth'
import authConfig from './composables/auth'
const { authConf } = authConfig()
const { t } = useI18n()
let focusMap = ref<string>('focus-map')

View File

@@ -0,0 +1,14 @@
@mixin switch-button-view() {
position: fixed;
right: 0;
top: toRem(22);
z-index: 3;
height: toRem(5);
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: toRem(0.5);
border-bottom-left-radius: toRem(0.5);
background-color: var(--white);
border: toRem(0.1) solid var(--grey-pale);
}

View File

@@ -28,7 +28,7 @@
}
@if $size == h4 {
font-weight: normal;
font-size: toRem(1.6);
font-size: toRem(1.8);
}
@if $size == xxl-regular {
font-size: toRem(3.2);

37
src/assets/images/car.svg Normal file
View File

@@ -0,0 +1,37 @@
<svg width="58" height="114" viewBox="0 0 58 114" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M54.5335 97.255C53.702 100.976 51.4048 104.696 50.7988 106.514C50.0096 108.882 39.7427 110.087 29.2996 110.136C29.1939 110.136 29.0953 110.136 28.9896 110.136C28.8839 110.136 28.7852 110.136 28.6795 110.136C18.2365 110.087 7.96957 108.882 7.18035 106.514C6.57434 104.696 4.2701 100.976 3.44565 97.255L2.7269 99.221C3.42451 103.238 6.02471 107.395 6.68004 109.361C7.4904 111.785 17.9898 113.018 28.6725 113.06C28.7782 113.06 28.8839 113.06 28.9896 113.06C29.0953 113.06 29.201 113.06 29.3067 113.06C39.9893 113.011 50.4887 111.778 51.2991 109.354C51.9544 107.381 54.5546 103.23 55.2522 99.2139L54.5335 97.2479V97.255Z" fill="#160302"/>
<path d="M6.30663 32.0951C7.65253 25.2599 10.2386 11.8713 10.2386 11.8713L10.288 11.7304C11.9439 6.12836 27.087 5.0291 28.8276 5.08547C28.8839 5.08547 28.9333 5.08547 28.9896 5.08547C29.046 5.08547 29.0953 5.08547 29.1517 5.08547C30.8852 5.0291 46.0353 6.12836 47.6913 11.7304L47.7406 11.8713C47.7406 11.8713 50.3197 25.2528 51.6726 32.0951C54.5265 27.2752 55.203 21.4759 53.5611 16.1204C52.2786 11.9348 51.0807 8.97519 51.0807 8.97519L51.0173 8.81312C49.0654 2.2175 31.2234 0.920929 29.1799 0.984348C29.1165 0.984348 29.053 0.984348 28.9896 0.984348C28.9262 0.984348 28.8628 0.984348 28.7994 0.984348C26.7559 0.920929 8.91387 2.2175 6.95492 8.81312L6.8915 8.97519C6.8915 8.97519 5.69358 11.9277 4.4111 16.1204C2.76924 21.4759 3.44571 27.2752 6.29958 32.0951H6.30663Z" fill="#160302"/>
<path d="M55.0126 70.3581L53.6879 66.3627L53.0396 48.5559C52.9621 43.6022 53.0466 41.8405 53.2017 37.373C53.2228 36.7881 54.7308 33.7863 54.7519 33.2155C55.1324 23.5969 49.5797 9.87012 49.5797 9.87012L49.5233 9.72214C47.7053 3.57046 31.0683 2.35845 29.1657 2.42187C29.1023 2.42187 29.0459 2.42187 28.9895 2.42187C28.9332 2.42187 28.8698 2.42187 28.8134 2.42187C26.9038 2.35845 10.2738 3.57046 8.44871 9.72214L8.39234 9.87012C8.39234 9.87012 2.83962 23.5969 3.22014 33.2155C3.24128 33.7863 4.74925 36.7881 4.77039 37.373C4.92541 41.8405 5.00997 43.6022 4.93246 48.5559L4.29122 66.257L2.95941 70.2595V93.8162L3.21309 95.9302V95.9795C3.7134 100.095 6.50385 104.485 7.17328 106.507C7.9625 108.875 18.2294 110.08 28.6724 110.129C28.7781 110.129 28.8768 110.129 28.9825 110.129C29.0882 110.129 29.1868 110.129 29.2925 110.129C39.7356 110.08 50.0025 108.875 50.7917 106.507C51.4682 104.478 54.2516 100.095 54.7519 95.9795V95.9302L55.0056 93.9149V70.3581H55.0126Z" fill="#17347A"/>
<path d="M45.8168 47.795L26.0018 27.9799C24.7898 12.8016 31.9843 6.36803 39.1226 3.68328C34.5845 2.66857 30.1029 2.3867 29.1657 2.42194C29.1023 2.42194 29.0459 2.42194 28.9895 2.42194C28.9332 2.42194 28.8698 2.42194 28.8134 2.42194C26.9038 2.35852 10.2738 3.57053 8.44871 9.72221L8.39234 9.87019C8.39234 9.87019 2.83962 23.597 3.22014 33.2156C3.24128 33.7863 4.74925 36.7882 4.77039 37.3731C4.92541 41.8406 5.00997 43.6023 4.93246 48.556L4.29122 66.2571L2.95941 70.2595V93.8163L3.21309 95.9303V95.9796C3.64293 99.5029 5.74282 103.216 6.76458 105.485C9.08291 100.003 12.268 92.1674 12.268 92.1674L12.8881 51.276C20.139 52.9742 28.6302 52.6642 28.6302 52.6642C45.6054 52.8192 45.8239 47.7879 45.8239 47.7879L45.8168 47.795Z" fill="#1E4084"/>
<path d="M43.9213 10.8637L49.0583 18.9673L51.0031 22.0396C49.6572 17.0788 48.0929 13.2173 48.0929 13.2173L48.0436 13.0763C47.4939 11.2231 45.4856 9.85605 42.9771 8.84839C43.2096 9.55305 43.5196 10.2295 43.9142 10.8637H43.9213Z" fill="#C1D1D0"/>
<path d="M14.0508 10.8637C14.4524 10.2366 14.7555 9.55305 14.988 8.84839C12.4724 9.84901 10.4711 11.216 9.92149 13.0763L9.87216 13.2173C9.87216 13.2173 8.30781 17.0788 6.96191 22.0396L14.0508 10.8637Z" fill="#C1D1D0"/>
<path d="M14.4102 10.2225C14.6427 9.77854 14.833 9.32051 14.988 8.84839C12.4724 9.84901 10.4711 11.216 9.92151 13.0763L9.87218 13.2173C9.87218 13.2173 9.40006 14.387 8.75177 16.2544C9.22389 16.1134 9.66078 15.8809 10.0554 15.5286C11.4154 14.3165 13.1488 12.2589 14.4102 10.2225Z" fill="#DFF3F4"/>
<path d="M43.562 10.2225C44.8233 12.266 46.5638 14.3165 47.9167 15.5286C48.3114 15.8809 48.7482 16.1064 49.2204 16.2544C48.5721 14.38 48.1 13.2173 48.1 13.2173L48.0506 13.0763C47.501 11.2231 45.4927 9.85605 42.9841 8.84839C43.1392 9.32051 43.3294 9.78559 43.562 10.2225Z" fill="#DFF3F4"/>
<path d="M45.3729 106.655C39.0521 107.578 33.1189 107.578 28.9826 107.578C24.8462 107.578 18.913 107.578 12.5922 106.655C7.37065 105.894 8.97727 104.097 8.97727 104.097L12.3455 97.6073C18.3633 98.8052 39.5877 98.8052 45.6055 97.6073L48.9738 104.097C48.9738 104.097 50.5874 105.894 45.3589 106.655H45.3729Z" fill="#373637"/>
<path d="M28.9896 107.656C24.9871 107.656 18.9482 107.656 12.5922 106.733C10.3161 106.401 9.01955 105.838 8.73769 105.048C8.55448 104.534 8.87862 104.125 8.92795 104.055L12.3174 97.5227L12.3737 97.5368C18.4268 98.7488 39.5454 98.7488 45.6055 97.5368L45.6619 97.5227L45.69 97.572L49.0583 104.055C49.1147 104.118 49.4388 104.534 49.2486 105.048C48.9667 105.838 47.6701 106.401 45.3941 106.733C39.0381 107.656 32.9921 107.656 28.9967 107.656H28.9896ZM12.4019 97.6989L9.04774 104.154C9.04774 104.154 8.7095 104.541 8.87862 104.999C9.06183 105.499 9.8581 106.183 12.6133 106.585C18.9623 107.508 24.9942 107.508 28.9896 107.508C32.985 107.508 39.024 107.508 45.3659 106.585C48.1211 106.183 48.9174 105.499 49.1006 104.999C49.2697 104.534 48.9385 104.161 48.9315 104.154L48.9174 104.139L45.5773 97.6989C39.4538 98.8968 18.5254 98.8968 12.4019 97.6989Z" fill="#160302"/>
<path d="M44.6753 105.774C38.6223 106.627 32.9427 106.641 28.9896 106.641C25.0365 106.641 19.3499 106.627 13.3039 105.774C8.30079 105.07 9.84399 103.407 9.84399 103.407L12.8177 97.861C19.223 98.9532 38.7562 98.9532 45.1545 97.861L48.1282 103.407C48.1282 103.407 49.6714 105.07 44.6683 105.774H44.6753Z" fill="#DFF3F4"/>
<path d="M48.1282 103.407L47.8674 102.92C47.346 102.765 46.6907 102.624 45.8662 102.512C39.3551 101.596 33.2457 101.582 28.9826 101.582C24.7194 101.582 18.61 101.596 12.0989 102.512C11.2745 102.624 10.6191 102.765 10.0977 102.92L9.83694 103.407C9.83694 103.407 8.29374 105.07 13.2968 105.774C19.3499 106.627 25.0294 106.641 28.9826 106.641C32.9357 106.641 38.6223 106.627 44.6683 105.774C49.6714 105.07 48.1282 103.407 48.1282 103.407Z" fill="#C1D1D0"/>
<path d="M35.8953 109.03L35.8741 108.988L35.8389 108.931L35.7473 108.769C35.5571 108.713 35.3245 108.67 35.0567 108.628C32.6609 108.29 30.406 108.29 28.9192 108.29C27.4323 108.29 25.1774 108.29 22.7816 108.628C22.5138 108.663 22.2883 108.713 22.091 108.769L22.0135 108.917C22.0135 108.917 21.9853 108.952 21.9712 108.981L21.9501 109.023H21.9007C18.2717 108.868 15.0162 108.537 11.9509 108.022C9.66785 107.635 8.33604 106.986 7.98371 106.084C7.71594 105.394 8.13874 104.837 8.15283 104.816L11.9791 97.3396C12.0919 97.1141 12.4301 96.9872 12.7543 97.0577C19.2724 98.3684 38.6787 98.3684 45.1968 97.0577C45.5209 96.9943 45.8592 97.1141 45.9719 97.3396L49.8053 104.816C49.8546 104.879 50.2281 105.422 49.9744 106.077C49.6221 106.979 48.2832 107.628 46.0072 108.015C42.9137 108.537 39.6159 108.868 35.9376 109.023H35.8882L35.8953 109.03ZM35.9658 108.847C35.9658 108.847 35.9728 108.861 35.9869 108.875C39.6441 108.72 42.9137 108.396 45.9931 107.874C48.7624 107.402 49.615 106.613 49.8476 106.035C50.0871 105.429 49.6996 104.922 49.6996 104.915V104.901L45.8521 97.4171C45.7676 97.255 45.4928 97.1634 45.2391 97.2127C38.7069 98.5234 19.2653 98.5234 12.7331 97.2127C12.4794 97.1634 12.2046 97.255 12.1201 97.4171L8.28672 104.901C8.28672 104.901 7.89211 105.429 8.13169 106.035C8.35718 106.613 9.21687 107.409 11.9862 107.874C15.0303 108.389 18.2647 108.713 21.8655 108.868C21.8796 108.847 21.8937 108.833 21.8937 108.833L21.9923 108.642H22.0205C22.2249 108.572 22.4715 108.523 22.7604 108.48C25.1704 108.142 27.4253 108.142 28.9192 108.142C30.413 108.142 32.6679 108.142 35.0779 108.48C35.3668 108.523 35.6134 108.572 35.8178 108.628H35.846L35.9517 108.84L35.9658 108.847Z" fill="#160302"/>
<path d="M45.3166 26.8032C39.0169 24.513 33.1119 24.4778 28.9826 24.4778C24.8533 24.4778 18.9482 24.513 12.6486 26.8032C7.44113 28.6987 9.04775 32.8844 9.04775 32.8844L12.1483 47.7879C13.7478 52.939 44.161 52.8262 45.8099 47.7879L48.9104 32.8844C48.9104 32.8844 50.517 28.6987 45.3095 26.8032H45.3166Z" fill="#160302"/>
<path d="M44.6753 29.0792C38.6223 27.0427 32.9427 27.0145 28.9896 27.0145C25.0364 27.0145 19.3498 27.0427 13.3038 29.0792C8.30076 30.7633 9.84396 34.7376 9.84396 34.7376L12.8176 47.9922C14.3538 52.5725 43.569 52.4668 45.1545 47.9922L48.1281 34.7376C48.1281 34.7376 49.6713 30.7633 44.6683 29.0792H44.6753Z" fill="#DFF3F4"/>
<path d="M44.6753 29.0792C38.6223 27.0427 32.9427 27.0145 28.9896 27.0145C25.0364 27.0145 19.3498 27.0427 13.3038 29.0792C8.30076 30.7633 9.84396 34.7376 9.84396 34.7376L10.3654 37.0489C10.8728 37.4083 11.514 37.7324 12.2962 38.0002C12.3314 38.9233 12.4583 39.783 12.7401 40.5299C12.1693 40.6356 11.6549 40.7554 11.2321 40.8963L11.7606 43.2428C13.2193 43.6234 15.3403 43.87 17.7009 43.87C22.1121 43.87 25.6847 43.0314 25.6847 41.9956C25.6847 41.4037 24.515 40.8752 22.6829 40.5369C22.7463 40.389 22.7956 40.241 22.8449 40.093C25.3394 40.2903 27.5943 40.2974 29.4899 40.2974C33.8094 40.2974 39.9964 40.2692 46.599 38.0425C46.9161 37.9368 47.205 37.817 47.4728 37.6972L48.1352 34.7446C48.1352 34.7446 49.6784 30.7704 44.6753 29.0862V29.0792Z" fill="#C1D1D0"/>
<path d="M6.10921 33.7792C6.1233 33.2789 6.8632 33.2014 6.99708 33.6806C8.54733 39.3037 11.8381 51.29 11.824 51.8185L10.76 92.428C10.7318 93.6823 9.49862 94.2813 8.95604 93.2032C8.95604 93.2032 5.36932 83.4718 5.35522 80.3431L6.10921 33.7792Z" fill="#17347A"/>
<path d="M9.72417 93.8443C9.38594 93.8443 9.08293 93.6259 8.88563 93.2383C8.84335 93.1326 5.29186 83.4506 5.27777 80.3431L6.03176 33.7791C6.0388 33.4761 6.27134 33.3141 6.49683 33.2859C6.71528 33.2577 6.99009 33.3634 7.07465 33.6593C8.88563 40.2127 11.9227 51.297 11.9016 51.8184L10.8375 92.428C10.8164 93.2102 10.3513 93.7175 9.90739 93.8162C9.84397 93.8303 9.78759 93.8373 9.72417 93.8373V93.8443ZM6.18678 33.7862L5.4328 80.3501C5.44689 83.4295 8.99133 93.0833 9.02656 93.182C9.22387 93.5695 9.52687 93.7527 9.86511 93.6752C10.2527 93.5907 10.6614 93.1326 10.6755 92.435L11.7395 51.8255C11.7536 51.4591 9.94967 44.6873 6.91963 33.7087C6.86326 33.4973 6.673 33.4268 6.51093 33.448C6.36295 33.462 6.18678 33.5677 6.17974 33.7932L6.18678 33.7862Z" fill="#160302"/>
<path d="M6.75751 34.822L10.8304 50.1624C10.9855 50.7473 11.0559 51.3462 11.0348 51.9452L9.87915 91.7303C9.86506 92.294 6.79274 84.1129 6.79274 82.3935L6.75751 34.8079V34.822Z" fill="#373637"/>
<path d="M9.87217 91.8431C9.62554 91.8431 8.33601 88.1506 8.18803 87.7278C7.75114 86.4665 6.72234 83.4153 6.72234 82.4006L6.6871 34.223L10.908 50.1413C11.063 50.7332 11.1335 51.3392 11.1194 51.9452L9.96377 91.7303C9.96377 91.8149 9.9074 91.836 9.87217 91.836V91.8431ZM6.83508 35.4139L6.87031 82.4006C6.87031 83.9297 9.26616 90.3421 9.80875 91.5189L10.9573 51.9452C10.9714 51.3463 10.908 50.7543 10.753 50.1836L6.83508 35.4139Z" fill="#160302"/>
<path d="M9.70307 82.5698L10.5205 52.3046C10.5346 51.7409 10.4782 51.1772 10.3443 50.6275L7.25085 37.6759L7.31427 75.4245C7.31427 77.4399 7.84982 79.427 8.86453 81.1675C9.13934 81.6396 9.42121 82.1188 9.70307 82.5768V82.5698Z" fill="#C1D1D0"/>
<path d="M9.70304 82.5698L10.5204 52.3046C10.5345 51.7409 10.4782 51.1772 10.3443 50.6275L9.69599 47.9005C8.25144 54.0592 7.3847 61.9585 7.3847 70.5765C7.3847 73.0921 7.46222 75.5514 7.60315 77.9261C7.85683 79.0606 8.27962 80.1528 8.87154 81.1675C9.14635 81.6396 9.42822 82.1188 9.71008 82.5768L9.70304 82.5698Z" fill="#DFF3F4"/>
<path d="M6.75049 69.3011C6.87028 70.6752 9.42819 75.2554 10.4147 76.0728L10.5275 72.2043C9.7312 70.4638 7.18033 67.2787 6.74344 65.5593L6.75753 69.294L6.75049 69.3011Z" fill="#160302"/>
<path d="M6.7928 34.8502C6.7928 34.8502 0.373348 38.3383 0.373348 39.4587C0.373348 40.5791 -0.00012207 43.2075 -0.00012207 43.2075L5.2355 41.3472L7.66658 38.0705L6.78575 34.8502H6.7928Z" fill="#160302"/>
<path d="M5.62918 66.4665L5.49713 66.5477L10.3844 74.4949L10.5164 74.4137L5.62918 66.4665Z" fill="#160302"/>
<path d="M51.8629 33.8778C51.8488 33.3775 51.1089 33.3 50.975 33.7792C49.4248 39.4024 46.134 51.3886 46.1481 51.9171L47.2122 92.5267C47.2403 93.781 48.4735 94.3799 49.0161 93.3018C49.0161 93.3018 52.6028 83.5705 52.6169 80.4418L51.8629 33.8778Z" fill="#17347A"/>
<path d="M48.2479 93.943C48.1916 93.943 48.1282 93.943 48.0647 93.9219C47.6138 93.8232 47.1487 93.3159 47.1275 92.5337L46.0705 51.9241C46.0494 51.3957 49.0865 40.3184 50.8975 33.7651C50.975 33.4691 51.2568 33.3634 51.4753 33.3916C51.7008 33.4198 51.9333 33.5818 51.9404 33.8849L52.6943 80.4488C52.6803 83.5563 49.1217 93.2383 49.0865 93.337C48.8892 93.7316 48.5862 93.95 48.2479 93.95V93.943ZM51.4119 33.5325C51.2639 33.5325 51.1018 33.61 51.0525 33.8003C48.0225 44.7789 46.2185 51.5507 46.2326 51.9171L47.2896 92.5337C47.3107 93.2313 47.7124 93.6893 48.1 93.7739C48.4382 93.8514 48.7483 93.6682 48.9456 93.2736C48.9808 93.182 52.5252 83.5281 52.5393 80.4488L51.7853 33.8849C51.7783 33.6594 51.6021 33.5607 51.4541 33.5466C51.4401 33.5466 51.4189 33.5466 51.4048 33.5466L51.4119 33.5325Z" fill="#160302"/>
<path d="M51.2145 34.9207L47.1416 50.2612C46.9866 50.846 46.9161 51.445 46.9373 52.0439L48.0929 91.829C48.107 92.3928 51.1793 84.2117 51.1793 82.4923L51.2145 34.9066V34.9207Z" fill="#373637"/>
<path d="M48.1 91.9418C48.1 91.9418 48.0154 91.9207 48.0083 91.8361L46.8598 52.051C46.8457 51.438 46.9091 50.832 47.0712 50.24L51.2921 34.3218V34.9137L51.2568 82.4923C51.2568 83.507 50.228 86.5582 49.7911 87.8195C49.6432 88.2423 48.3536 91.9277 48.107 91.9348L48.1 91.9418ZM51.137 35.5127L47.2191 50.2823C47.0641 50.8601 47.0007 51.4521 47.0148 52.044L48.1634 91.6177C48.713 90.4479 51.1018 84.0285 51.1018 82.4994L51.137 35.5127Z" fill="#160302"/>
<path d="M48.2691 82.6615L47.4517 52.3963C47.4376 51.8326 47.494 51.2689 47.6279 50.7192L50.7213 37.7676L50.6579 75.5162C50.6579 77.5315 50.1224 79.5187 49.1076 81.2592C48.8328 81.7313 48.551 82.2105 48.2691 82.6685V82.6615Z" fill="#C1D1D0"/>
<path d="M48.2691 82.5698L47.4517 52.3046C47.4376 51.7409 47.494 51.1772 47.6279 50.6275L48.2761 47.9005C49.7207 54.0592 50.5874 61.9585 50.5874 70.5765C50.5874 73.0921 50.5099 75.5514 50.369 77.9261C50.1153 79.0606 49.6925 80.1528 49.1006 81.1675C48.8258 81.6396 48.5439 82.1188 48.2621 82.5768L48.2691 82.5698Z" fill="#DFF3F4"/>
<path d="M51.2217 69.3998C51.1019 70.7739 48.544 75.3542 47.5574 76.1716L47.4447 72.303C48.241 70.5625 50.7918 67.3775 51.2287 65.6581L51.2146 69.3928L51.2217 69.3998Z" fill="#160302"/>
<path d="M51.1864 34.9489C51.1864 34.9489 57.6058 38.4299 57.6058 39.5573C57.6058 40.6848 57.9793 43.3061 57.9793 43.3061L52.7437 41.4458L50.3126 38.1691L51.1934 34.9489H51.1864Z" fill="#160302"/>
<path d="M52.342 66.5611L47.4576 74.51L47.5897 74.5912L52.4741 66.6423L52.342 66.5611Z" fill="#160302"/>
<path d="M7.87093 29.7697L7.80047 29.509C7.26492 27.4584 6.55322 24.6821 6.25021 23.0754V23.0472L6.25726 23.0191L13.9803 10.8425C14.6498 9.78555 15.0866 8.62286 15.2699 7.38266L15.5024 5.82536L15.5376 5.80422C15.5729 5.78308 19.3428 3.95097 27.3971 3.38019C27.5944 3.35905 27.7987 3.34496 28.0242 3.33791H28.1158C28.1158 3.33791 28.3906 3.31677 28.7852 3.31677H28.9684H29.1446C29.5533 3.31677 29.8211 3.33087 29.8211 3.33087H29.9127C30.1382 3.35201 30.3425 3.3661 30.5398 3.38019C38.5871 3.95097 42.364 5.78308 42.3993 5.80422L42.4345 5.82536L42.667 7.38266C42.8573 8.62286 43.2871 9.78555 43.9566 10.8425L51.7008 23.0472V23.0754C51.3837 24.6821 50.679 27.4584 50.1435 29.509L50.073 29.7697L49.9955 29.516C49.5093 27.9799 48.2338 26.8454 46.2044 26.1337C39.5243 23.8083 33.1682 23.8083 28.9614 23.8083C24.7546 23.8083 18.3985 23.8083 11.7184 26.1337C9.68895 26.8383 8.41352 27.9799 7.9273 29.516L7.84979 29.7697H7.87093ZM28.9896 23.6603C33.2035 23.6603 39.5806 23.6603 46.2819 25.9927C48.255 26.6762 49.5304 27.7685 50.0871 29.2271C50.6086 27.2259 51.2709 24.6257 51.5669 23.0825L43.8579 10.9271C43.1744 9.84897 42.7305 8.67219 42.5402 7.41085L42.3147 5.93811C41.913 5.74785 38.1502 4.08485 30.5469 3.54226C30.3496 3.52112 30.1452 3.50703 29.9197 3.49998H29.8281C29.8281 3.49998 29.5604 3.47884 29.1658 3.47884H28.9825H28.8134C28.4118 3.47884 28.144 3.49294 28.1369 3.49294H28.0453C27.8199 3.51408 27.6155 3.52817 27.4252 3.54226C19.822 4.08485 16.0591 5.74785 15.6574 5.93811L15.4319 7.41085C15.2417 8.67219 14.7977 9.85602 14.1142 10.9271L6.40524 23.0825C6.7012 24.6186 7.36358 27.2188 7.88502 29.2271C8.44171 27.7614 9.71714 26.6762 11.6902 25.9927C18.3915 23.6603 24.7687 23.6603 28.9825 23.6603H28.9896Z" fill="#160302"/>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,3 @@
<svg width="202" height="135" viewBox="0 0 202 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.18153 39.6234C17.011 27.3107 32.2015 17.7116 48.8458 11.4109C66.2695 4.81515 84.892 1.98332 103.49 3.10152C122.087 4.21972 140.239 9.26262 156.751 17.8989C172.525 26.149 186.463 37.4998 197.732 51.2619L95.8009 131.621L4.18153 39.6234Z" fill="#2954E9" fill-opacity="0.55" stroke="white" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@@ -0,0 +1,18 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_193_56)">
<circle cx="16.8921" cy="14.0595" r="12" transform="rotate(2.04823 16.8921 14.0595)" fill="white" fill-opacity="0.45"/>
<path d="M12.542 9.32783C12.2352 9.04959 11.7609 9.07278 11.4826 9.37962C11.2044 9.68647 11.2276 10.1608 11.5344 10.439L12.542 9.32783ZM22.446 20.0368C22.8597 20.0165 23.1787 19.6648 23.1584 19.251L22.8288 12.5091C22.8086 12.0954 22.4568 11.7764 22.0431 11.7966C21.6294 11.8168 21.3104 12.1686 21.3306 12.5823L21.6236 18.5752L15.6308 18.8682C15.217 18.8884 14.8981 19.2402 14.9183 19.6539C14.9385 20.0676 15.2903 20.3866 15.704 20.3664L22.446 20.0368ZM11.5344 10.439L21.9055 19.8433L22.9131 18.7321L12.542 9.32783L11.5344 10.439Z" fill="#0A1F69"/>
</g>
<defs>
<filter id="filter0_d_193_56" x="0.891907" y="0.0593262" width="32.0003" height="32.0004" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_193_56"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_193_56" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,6 @@
<svg width="173" height="360" viewBox="0 0 173 360" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="173" height="360" fill="#D5DAE8"/>
<line x1="6.5" y1="-1.09278e-07" x2="6.50002" y2="360" stroke="white" stroke-width="5"/>
<line x1="166.5" y1="1.3381e-07" x2="166.5" y2="358.776" stroke="white" stroke-width="5"/>
<line x1="85.0001" y1="5.35242e-08" x2="85" y2="358.776" stroke="white" stroke-width="2" stroke-dasharray="4 4"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -34,10 +34,11 @@ h5 {
--grey-pale: #cfd2cf;
--grey-semi-dark: #808080;
--grey-dark: #3e3e3e;
--blue: #2954e9;
--blue-dark: #0a1f69;
--blue-semi: #d7dffc;
--blue: #2954e9;
--blue-semi-pale: #f2f5ff;
--blue-pale: #f2f5ff;
--blue-very-pale: #f9faff;
--blue-geovisio: #34495e;
--beige: #f5f3ec;
--yellow: #fec868;

View File

@@ -82,6 +82,7 @@ defineProps({
.button--blue {
color: var(--white);
background-color: var(--blue);
height: toRem(4);
&.disabled {
opacity: 0.6;
color: var(--white);
@@ -140,6 +141,7 @@ defineProps({
.no-text-white .icon {
color: var(--white);
margin-right: 0;
font-size: toRem(1.4);
}
.no-text-blue-dark .icon {
color: var(--blue-dark);

View File

@@ -36,7 +36,7 @@
<span v-else class="title">{{ text }}</span>
<div v-if="!isEditTitle" class="edit-button">
<Button
look="no-text-blue-dark"
look="no-text-white"
icon="bi bi-pen"
:tooltip="$t('pages.upload.edit_title_tooltip')"
:disabled="isDisabled"
@@ -110,9 +110,10 @@ const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
align-items: center;
}
.edit-mode {
background-color: var(--blue-pale);
background-color: var(--blue-very-pale);
padding: toRem(1);
border-radius: toRem(0.4);
width: 100%;
}
.wrapper-edit {
display: flex;
@@ -121,7 +122,7 @@ const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
width: 100%;
}
.edit-button {
background-color: var(--grey);
background-color: var(--blue);
border-radius: 50%;
height: toRem(2.5);
width: toRem(2.5);
@@ -150,7 +151,7 @@ const isDisabled = computed<boolean>(() => props.isLoading && !props.isLoaded)
height: toRem(2);
width: toRem(2);
top: toRem(-1);
right: toRem(-1);
left: toRem(-1);
background-color: var(--blue-dark);
color: var(--white);
border-radius: 50%;

View File

@@ -10,9 +10,7 @@
alt=""
class="icon-block-img"
/>
<h3 v-if="title" class="subtitle">
{{ title }}
</h3>
<slot name="title"></slot>
</div>
<p v-if="text" v-html="text" class="information-text"></p>
</div>
@@ -23,7 +21,6 @@
<script setup lang="ts">
defineProps({
text: { type: String, default: null },
title: { type: String, default: null },
look: { type: String, default: '' }
})
</script>
@@ -36,11 +33,11 @@ h3 {
.information-block {
position: relative;
border-left: toRem(1.4) solid var(--blue);
padding: toRem(2) toRem(2) toRem(1.5);
padding: toRem(1.8) toRem(1.8) toRem(1.3);
background-color: var(--white);
border-radius: toRem(1.5);
display: flex;
align-items: flex-end;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
@@ -61,10 +58,6 @@ h3 {
margin-right: toRem(0.5);
height: toRem(2.2);
}
.subtitle {
@include text(h2);
color: var(--blue-dark);
}
.information-text {
margin-top: toRem(1);
@include text(m-r-regular);

View File

@@ -3,16 +3,21 @@
:value="text"
:required="true"
:placeholder="placeholder"
class="input"
type="text"
:type="type"
:min="min"
:max="max"
@input="emitValue"
class="input"
/>
</template>
<script lang="ts" setup>
const emit = defineEmits<{ (e: 'input', value: string): void }>()
defineProps({
text: { type: String, default: null },
type: { type: String, default: 'text' },
min: { type: String, default: '' },
max: { type: String, default: '' },
text: { type: [String, Number], default: null },
placeholder: { type: String, default: '' }
})

View File

@@ -1,18 +1,18 @@
<template>
<div class="wrapper-checkbox">
<div :class="['wrapper-checkbox', { checked: isChecked || isIndeterminate }]">
<div class="input-checkbox">
<i v-if="isChecked && !isIndeterminate" class="icon bi bi-check-square" />
<i v-if="!isChecked && !isIndeterminate" class="icon bi bi-square" />
<i v-if="isIndeterminate && !isChecked" class="icon bi bi-dash-square" />
<input
id="checkbox"
:id="name"
v-model="inputValue"
type="checkbox"
@input="updateValue(!inputValue)"
class="input"
/>
</div>
<label v-if="label && label.length" for="checkbox" class="label">{{
<label v-if="label && label.length" :for="name" class="label">{{
label
}}</label>
</div>
@@ -35,6 +35,7 @@ watchEffect(async () => {
if (htmlCheckbox) {
htmlCheckbox.indeterminate = props.isIndeterminate
}
inputValue.value = props.isChecked
})
function updateValue(value: boolean): void {
@@ -42,7 +43,11 @@ function updateValue(value: boolean): void {
htmlCheckbox.indeterminate = false
}
inputValue.value = value
emit('trigger', { isChecked: value, isIndeterminate: false })
emit('trigger', {
isChecked: value,
isIndeterminate: false,
name: props.name
})
}
</script>
@@ -54,6 +59,8 @@ function updateValue(value: boolean): void {
align-items: center;
height: toRem(2);
width: toRem(2);
background-color: var(--white);
border-radius: toRem(0.5);
}
.input {
-webkit-appearance: none;
@@ -68,7 +75,7 @@ function updateValue(value: boolean): void {
.icon {
font-size: toRem(2);
position: absolute;
color: var(--grey-semi-dark);
color: var(--blue);
}
.wrapper-checkbox {
display: flex;
@@ -77,5 +84,14 @@ function updateValue(value: boolean): void {
.label {
cursor: pointer;
margin-left: toRem(0.5);
@include text(s-r-regular);
}
.checked {
.input-checkbox {
background-color: var(--blue);
}
.icon {
color: var(--white);
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div class="input-radio">
<input
:id="id"
:name="name"
:value="id"
type="radio"
:checked="props.value === id"
@change="updateValue"
class="input"
/>
<label v-if="label && label.length" :for="id" class="label">{{
label
}}</label>
</div>
</template>
<script lang="ts" setup>
const emit = defineEmits<{ (e: 'trigger', value: string): void }>()
const props = defineProps({
name: { type: String, default: null },
id: { type: String, default: '' },
label: { type: String, default: '' },
value: { type: String, default: null }
})
function updateValue(): void {
emit('trigger', props.id)
}
</script>
<style lang="scss" scoped>
.label {
margin-left: toRem(0.5);
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<label class="toggle">
<input
type="checkbox"
v-model="inputValue"
@input="updateValue(inputValue)"
class="input"
/>
<div :class="['labels increase', { increaseChecked: !inputValue }]">
<span>{{ $t('pages.sequence.sort_panel_settings_order_increase') }}</span>
<i class="bi bi-sort-up"></i>
</div>
<div :class="['labels decrease', { decreaseChecked: inputValue }]">
<span>{{ $t('pages.sequence.sort_panel_settings_order_decrease') }}</span>
<i class="bi bi-sort-down"></i>
</div>
</label>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const emit = defineEmits<{
(e: 'trigger', value: { isIncreaseChecked: boolean }): void
}>()
let inputValue = ref<boolean>(false)
const props = defineProps({
increased: { type: Boolean, default: false }
})
watchEffect(() => {
inputValue.value = !props.increased
})
function updateValue(value: boolean): void {
inputValue.value = value
emit('trigger', { isIncreaseChecked: value })
}
</script>
<style scoped lang="scss">
.toggle {
position: relative;
width: 14.5rem;
background-color: var(--blue-very-pale);
padding: toRem(0.7);
border-radius: toRem(0.7);
height: toRem(4.5);
cursor: pointer;
}
.input {
width: 100%;
border: none;
appearance: none;
}
.labels {
position: absolute;
top: toRem(0.7);
@include text(s-r-regular);
padding: toRem(0.5) toRem(1);
color: var(--grey-semi-dark);
span {
margin-right: toRem(0.5);
}
}
.increase {
left: toRem(0.7);
}
.decrease {
right: toRem(0.7);
}
.increaseChecked,
.decreaseChecked {
background-color: var(--blue-dark);
color: var(--white);
border-radius: toRem(0.7);
}
</style>

View File

@@ -124,6 +124,17 @@ function triggerButton() {
margin-right: toRem(0.5);
}
}
.link--grey-dark {
color: var(--grey-dark);
text-decoration: underline;
font-weight: inherit;
font-size: toRem(1.4);
.icon {
color: var(--grey-dark);
font-size: toRem(1.4);
margin-right: toRem(0.5);
}
}
.link--blue-dark {
color: var(--blue-dark);
.icon {
@@ -169,7 +180,7 @@ function triggerButton() {
}
}
.button--blue-bleu {
background-color: var(--blue-semi);
background-color: var(--blue-semi-pale);
color: var(--blue);
}
.disabled {

View File

@@ -0,0 +1,49 @@
<template>
<div class="tab">
<button
:class="['tablinks', { selected: panelSelected === 'photos' }]"
@click="$emit('trigger', 'photos')"
>
{{ $t('pages.sequence.button_panel_photos') }}
</button>
<button
:class="['tablinks', { selected: panelSelected === 'orientation' }]"
@click="$emit('trigger', 'orientation')"
>
{{ $t('pages.sequence.button_panel_orientation') }}
</button>
<button
:class="['tablinks', { selected: panelSelected === 'sort' }]"
@click="$emit('trigger', 'sort')"
>
{{ $t('pages.sequence.button_panel_sort') }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps({
panelSelected: { type: String, default: '' }
})
</script>
<style scoped lang="scss">
.tab {
display: flex;
width: 100%;
@include text(s-regular);
}
.tablinks {
border: none;
border-top-right-radius: toRem(0.5);
border-top-left-radius: toRem(0.5);
margin-right: toRem(0.3);
background-color: var(--blue-very-pale);
padding: toRem(1);
}
.selected {
background-color: var(--blue-semi-pale);
color: var(--blue-dark);
font-weight: 600;
}
</style>

View File

@@ -39,6 +39,7 @@ defineProps({
min-width: toRem(10);
padding-right: toRem(1);
padding-left: toRem(1);
z-index: 1;
}
.button-close {
position: absolute;

View File

@@ -1,5 +1,5 @@
<template>
<div id="viewer" class="entry-viewer"></div>
<div :id="id" class="entry-viewer"></div>
</template>
<script setup lang="ts">
@@ -18,10 +18,12 @@ import { hasASessionCookieDecoded } from '@/utils/auth'
import type { ViewerInterface, MapInterface } from '@/views/interfaces/common'
const sequenceStore = useSequenceStore()
const { t } = useI18n()
const emit = defineEmits<{ (e: 'triggerReady', value: boolean): void }>()
let mapIsLoaded = ref<boolean>(false)
let viewer = ref()
const props = defineProps({
id: { type: String, default: 'viewer' },
fetchOptions: { type: Object, default: {} },
geovisioViewer: { type: Boolean, default: true },
bbox: { type: Array, default: null },
@@ -148,7 +150,6 @@ async function setupViewerMap(tiles: string): Promise<void> {
if (viewer.value && viewer.value.addEventListener) {
createViewerButton(reportLink)
}
mapIsLoaded.value = true
}
async function setupMap(tiles: string): Promise<void> {
let paramsMap: MapInterface
@@ -161,7 +162,7 @@ async function setupMap(tiles: string): Promise<void> {
}
const bbox = [props.bbox[0], props.bbox[1], props.bbox[2], props.bbox[3]]
viewer.value = new StandaloneMap(
'viewer', // Div ID
props.id, // Div ID
`${import.meta.env.VITE_API_URL}/api/search`,
{
...paramsMap,
@@ -176,13 +177,14 @@ async function setupMap(tiles: string): Promise<void> {
speed: 10
})
})
mapIsLoaded.value = true
}
onMounted(async (): Promise<void> => {
const tiles = import.meta.env.VITE_TILES
try {
if (props.geovisioViewer) return await setupViewerMap(tiles)
return await setupMap(tiles)
if (props.geovisioViewer) await setupViewerMap(tiles)
else await setupMap(tiles)
mapIsLoaded.value = true
emit('triggerReady', mapIsLoaded.value)
} catch (err) {
mapIsLoaded.value = true
console.log(err)

View File

@@ -53,7 +53,7 @@ defineProps({
justify-content: center;
align-items: center;
color: var(--blue);
background-color: var(--blue-semi);
background-color: var(--blue-semi-pale);
height: toRem(3);
width: toRem(3);
border-radius: 50%;

View File

@@ -0,0 +1,145 @@
<template>
<div class="wrapper-orientation">
<h2 class="orientation-title">
{{ $t('pages.sequence.orientation_panel_title') }}
</h2>
<div class="entry-information-card">
<InformationCard>
<template v-slot:title>
<h3 class="subtitle">
{{ $t('pages.sequence.orientation_panel_tooltip') }}
</h3>
</template>
</InformationCard>
<div class="wrapper-input-angle">
<label for="inputAngle" name="inputAngle">{{
$t('pages.sequence.orientation_input_label')
}}</label>
<Input
id="inputAngle"
name="inputAngle"
type="number"
min="-180"
max="180"
:text="Number(angleInputValue)"
:placeholder="$t('pages.sequence.orientation_input_placeholder')"
@input="captureAngle"
/>
<span v-if="errorAngleValue" class="errorValue">{{
$t('pages.sequence.orientation_input_error_value')
}}</span>
</div>
</div>
<div class="entry-compass">
<WidgetOrientation
:road-degrees="roadDegrees"
:seq-brute-deg="angleValue"
@triggerAngle="captureAngle"
/>
</div>
<div class="entry-button">
{{ isLoading }}
<Button
look="button--blue"
:text="$t('pages.sequence.orientation_panel_button')"
:disabled="isDisabled(Number(angleInputValue)) || isLoading"
@trigger="triggerAngle"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
import InformationCard from '@/components/InformationCard.vue'
import Button from '@/components/Button.vue'
import Input from '@/components/Input.vue'
import WidgetOrientation from '@/components/sequence/WidgetOrientation.vue'
const emit = defineEmits<{
(e: 'triggerAngle', value: number): void
}>()
let angleValue = ref<number>(0)
let angleInputValue = ref<number>(0)
let errorAngleValue = ref<boolean>(false)
const props = defineProps({
roadDegrees: { type: Number, default: 0 },
seqBruteDeg: { type: Number, default: 0 },
isLoading: { type: Boolean, default: false }
})
watchEffect(() => {
angleValue.value = props.seqBruteDeg
angleInputValue.value = Math.round(props.seqBruteDeg - props.roadDegrees)
})
function isDisabled(value: number): boolean {
return value < -180 || value > 180
}
function captureAngle(value: number | string) {
errorAngleValue.value = false
const valueNum = Number(value)
angleInputValue.value = valueNum
angleValue.value = valueNum + props.roadDegrees
if (isDisabled(valueNum)) return (errorAngleValue.value = true)
}
function triggerAngle() {
const valueToSend = angleValue.value - Number(props.roadDegrees)
if (isDisabled(valueToSend)) return
emit('triggerAngle', valueToSend)
}
</script>
<style scoped lang="scss">
.wrapper-orientation {
background-color: var(--blue-semi-pale);
}
.orientation-title {
@include text(h4);
margin-bottom: toRem(2);
}
.subtitle {
@include text(m-r-regular);
color: var(--blue-dark);
margin-bottom: 0;
}
.entry-compass {
width: 100%;
margin-bottom: toRem(2);
}
.entry-information-card {
margin-top: toRem(2);
margin-bottom: toRem(2.5);
display: flex;
}
.wrapper-input-angle {
margin-left: toRem(1);
width: 45%;
background-color: var(--white);
padding: toRem(1);
border-radius: toRem(1);
}
.entry-button {
width: 100%;
display: flex;
justify-content: center;
}
.errorValue {
color: var(--red);
@include text(xs-r-regular);
text-align: center;
}
@media (max-width: toRem(102.4)) {
.entry-information-card {
flex-direction: column;
}
.wrapper-input-angle {
margin-left: 0;
width: 100%;
margin-top: toRem(1);
}
}
@media (max-width: toRem(50)) {
.orientation-title {
@include text(m-regular);
margin-bottom: toRem(2);
}
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<div v-if="pictures && pictures.length">
<div v-if="isSequenceOwner" class="delete-all">
<div class="wrapper-select">
<InputCheckbox
:is-checked="pictures.length === picturesToDelete.length"
:is-indeterminate="isIndeterminate"
:label="selectedText"
name="picture-selected"
@trigger="triggerInputCheck"
/>
<div v-if="picturesToDelete.length" class="wrapper-photo-selected">
<span class="photo-selected-separator">-</span>
<span>{{
$t('pages.sequence.picture_selected', picturesToDelete.length)
}}</span>
</div>
</div>
<div class="action-buttons">
<Button
look="button--white background-white no-text"
:icon="
picturesToDeleteStatus === 'hidden' ||
imagesSelectedHaveDifferentStatus
? 'bi bi-eye'
: 'bi bi-eye-slash'
"
:tooltip="$t('pages.sequence.hide_photo_tooltip')"
:disabled="!picturesToDelete.length || sequence.status === 'hidden'"
@trigger="triggerPatchOrDeleteCollectionItems('PATCH')"
/>
<div class="button-hidde">
<Button
look="button--red background-white no-text"
icon="bi bi-trash"
:tooltip="$t('pages.sequence.delete_photo_tooltip')"
:disabled="!picturesToDelete.length"
@trigger="triggerPatchOrDeleteCollectionItems('DELETE')"
/>
</div>
</div>
</div>
<ul class="photo-list">
<li v-for="(item, i) in pictures" :id="`el-list${i}`" class="photo-item">
<ImageItem
:href="item.assets.thumb.href"
:href-hd="item.assets.hd.href"
:created="formatDate(item.properties.datetime, 'HH:mm:ss')"
:selected="photoToDeleteOrPatchSelected(item, picturesToDelete)"
:selected-on-map="itemSelected === item.id"
:status="
imageStatus(item.properties['geovisio:status'], sequence.status)
"
@trigger="triggerSelectImageAndMove(item)"
/>
</li>
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="triggerGoToNextPage"
/>
</div>
</ul>
</div>
<p v-else class="no-photo">{{ $t('pages.sequence.no_image') }}</p>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { PropType } from 'vue'
import Pagination from '@/components/Pagination.vue'
import InputCheckbox from '@/components/InputCheckbox.vue'
import ImageItem from '@/components/ImageItem.vue'
import Button from '@/components/Button.vue'
import { formatDate } from '@/utils/dates'
import {
imageStatus,
photoToDeleteOrPatchSelected
} from '@/views/utils/sequence/index'
import type {
ResponseUserPhotoLinksInterface,
ResponseUserPhotoInterface,
UserSequenceInterface,
CheckboxInterface
} from '../../views/interfaces/MySequenceView'
const { t } = useI18n()
const emit = defineEmits<{
(e: 'triggerInputCheck', value: CheckboxInterface): void
(e: 'triggerGoToNextPage', value: string): void
(e: 'triggerSelectImageAndMove', value: ResponseUserPhotoInterface): void
(e: 'triggerPatchOrDeleteCollectionItems', value: string): void
}>()
const props = defineProps({
pictures: {
type: Array as PropType<ResponseUserPhotoInterface[]>,
default: []
},
picturesToDelete: {
type: Array as PropType<string[]>,
default: []
},
sequence: {
type: Object as PropType<UserSequenceInterface>,
default: {}
},
paginationLinks: {
type: Array as PropType<ResponseUserPhotoLinksInterface[]>,
default: []
},
selfLink: {
type: Array as PropType<ResponseUserPhotoLinksInterface[]>,
default: []
},
fullImagesToDelete: {
type: Array as PropType<ResponseUserPhotoInterface[]>,
default: []
},
menuHeight: {
type: String,
default: '0'
},
isSequenceOwner: { type: Boolean, default: false },
imagesSelectedHaveDifferentStatus: { type: Boolean, default: false },
itemSelected: { type: String, default: '' }
})
const isIndeterminate = computed(
(): boolean =>
!!props.picturesToDelete.length &&
!!props.sequence &&
props.pictures.length !== props.picturesToDelete.length
)
const selectedText = computed((): string =>
props.picturesToDelete.length === props.pictures.length
? t('pages.sequence.unselect_text')
: t('pages.sequence.select_text')
)
const picturesToDeleteStatus = computed((): string => {
if (props.fullImagesToDelete.length) {
return props.fullImagesToDelete[0].properties['geovisio:status']
}
return 'hidden'
})
function triggerInputCheck(value: CheckboxInterface): void {
emit('triggerInputCheck', value)
}
function triggerGoToNextPage(value: string): void {
emit('triggerGoToNextPage', value)
}
function triggerSelectImageAndMove(value: ResponseUserPhotoInterface): void {
emit('triggerSelectImageAndMove', value)
}
function triggerPatchOrDeleteCollectionItems(value: string): void {
emit('triggerPatchOrDeleteCollectionItems', value)
}
</script>
<style scoped lang="scss">
.delete-all {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: toRem(1);
margin-right: toRem(2);
margin-bottom: toRem(1);
@include text(xs-r-regular);
}
.wrapper-select {
display: flex;
align-items: center;
}
.wrapper-photo-selected {
@include text(xs-regular);
}
.photo-selected-separator {
margin-right: toRem(0.5);
margin-left: toRem(0.5);
}
.action-buttons {
display: flex;
align-items: center;
}
.button-hidde {
margin-right: toRem(1);
margin-left: toRem(1);
}
.photo-list {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
height: 100%;
width: 100%;
padding: 0;
}
.photo-item {
width: calc(33% - #{toRem(2)});
height: fit-content;
margin: toRem(1);
border-radius: toRem(0.5);
background-color: var(--grey);
}
.no-photo {
@include text(s-regular);
text-align: center;
margin-top: toRem(10);
color: var(--grey-dark);
}
.entry-pagination {
margin-top: toRem(2);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: toRem(102.4)) {
.photo-item {
width: calc(50% - #{toRem(2)});
}
}
@media (max-width: toRem(76.8)) {
.delete-all {
text-align: left;
}
.wrapper-select {
flex-direction: column;
align-items: initial;
}
.wrapper-photo-selected:nth-child(2) {
margin-top: toRem(0.5);
}
.photo-selected-separator {
display: none;
}
.entry-pagination {
margin-top: toRem(0.5);
margin-bottom: toRem(1);
}
.photo-list {
padding-right: toRem(2);
}
.photo-item {
width: 100%;
margin-right: 0;
}
}
@media (max-width: toRem(50)) {
.entry-pagination {
margin-top: toRem(1);
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<form class="wrapper-sort" @submit.prevent="triggerSort">
<h2 class="sort-title">
{{ $t('pages.sequence.sort_panel_title') }}
</h2>
<span class="sort-subtitle">
{{ $t('pages.sequence.sort_panel_settings') }}
</span>
<div class="wrapper-checkboxs">
<div class="checkbox-item">
<InputRadio
:value="sortValue"
:label="$t('pages.sequence.sort_panel_check_gps')"
name="sort"
id="gpsdate"
@trigger="triggerInputCheck"
/>
</div>
<div class="checkbox-item">
<InputRadio
:value="sortValue"
:label="$t('pages.sequence.sort_panel_check_file')"
name="sort"
id="filedate"
@trigger="triggerInputCheck"
/>
</div>
<div class="checkbox-item">
<InputRadio
:value="sortValue"
:label="$t('pages.sequence.sort_panel_check_name')"
name="sort"
id="filename"
@trigger="triggerInputCheck"
/>
</div>
</div>
<span class="sort-subtitle">
{{ $t('pages.sequence.sort_panel_settings_order') }}
</span>
<div class="wrapper-checkboxs">
<InputSwitch @trigger="triggerInputSwitch" :increased="increaseChecked" />
</div>
<Button
look="button--blue"
type="submit"
:text="$t('pages.sequence.orientation_panel_button')"
:disabled="sortValue.length === 0"
/>
</form>
</template>
<script setup lang="ts">
import { watchEffect, ref } from 'vue'
import InputRadio from '@/components/InputRadio.vue'
import InputSwitch from '@/components/InputSwitch.vue'
import Button from '@/components/Button.vue'
const props = defineProps({
sequenceSorted: { type: String, default: null }
})
const emit = defineEmits<{
(e: 'triggerSort', value: string): void
}>()
let sortValue = ref<string>('')
let increaseChecked = ref<boolean>(true)
watchEffect(() => {
if (props.sequenceSorted) {
const filtersType = ['gpsdate', 'filedate', 'filename']
const filtersValue = ['+', '-']
filtersType.map((e: string) => {
if (props.sequenceSorted.includes(e)) sortValue.value = e
})
filtersValue.map((e: string) => {
if (props.sequenceSorted.includes(e) && e === '+') {
increaseChecked.value = true
}
if (props.sequenceSorted.includes(e) && e === '-') {
increaseChecked.value = false
}
})
}
})
function triggerInputSwitch(value: { isIncreaseChecked: boolean }): void {
increaseChecked.value = value.isIncreaseChecked
}
function triggerInputCheck(value: string): void {
sortValue.value = value
}
function triggerSort() {
const value = increaseChecked.value
? `+${sortValue.value}`
: `-${sortValue.value}`
emit('triggerSort', value)
}
</script>
<style scoped lang="scss">
.sort-title {
@include text(h4);
color: var(--blue-dark);
margin-bottom: toRem(2);
}
.sort-subtitle {
color: var(--grey-semi-dark);
}
.wrapper-checkboxs {
display: flex;
flex-wrap: wrap;
margin-top: toRem(1.5);
margin-bottom: toRem(2.5);
}
.checkbox-item {
margin-right: toRem(4);
margin-bottom: toRem(1);
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="wrapper-widget-orientation">
<div
class="wrapper-img-road"
:style="{
transform: `rotate(${roadDegrees}deg)`
}"
></div>
<div class="wrapper-widget">
<div class="wrapper-car">
<div class="wrapper-elements">
<div class="rounded-transparent"></div>
<img
src="@/assets/images/car.svg"
alt=""
:style="{
transform: `rotate(${roadDegrees}deg)`
}"
class="car-img"
/>
<div
:style="{
transform: `rotate(${angle}deg)`
}"
id="rotateWrapper"
class="rotate-wrapper"
>
<div
id="rotate"
@mousedown="mousedown"
@mousemove="handleMouseMove"
@mouseup="mouseup"
class="cursor-img"
>
<button class="arrow-img arrow-img-1" @click="clickAndMove(45)">
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
</button>
<button class="arrow-img arrow-img-2" @click="clickAndMove(-45)">
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect } from 'vue'
let angleValue = ref<number>(0)
let angle = ref<number>(0)
let prevRotation = ref<number>(0)
let active = ref<boolean>(false)
let rotation = ref<number>(0)
let startAngle = ref<number>(0)
let rotateWrapper = ref<HTMLElement | null>(null)
let rotate = ref<HTMLElement | null>(null)
let center: { x: number; y: number } = { x: 0, y: 0 }
const R2D: number = 180 / Math.PI
const emit = defineEmits<{
(e: 'triggerAngle', value: number): void
}>()
const props = defineProps({
roadDegrees: { type: Number, default: 0 },
seqBruteDeg: { type: Number, default: 0 }
})
watchEffect(() => {
angle.value = Math.round(props.seqBruteDeg)
})
onMounted(() => {
const app = document.getElementById('app')
rotateWrapper.value = document.getElementById('rotateWrapper')
rotate.value = document.getElementById('rotate')
if (app) app.addEventListener('mouseup', () => (active.value = false))
})
function mousedown(e: MouseEvent): void {
e.preventDefault()
if (!rotateWrapper.value) return
const bb = rotateWrapper.value.getBoundingClientRect()
const { top: t, left: l, height: h, width: w } = bb
center = { x: l + w / 2, y: t + h / 2 }
let x = e.clientX - center.x
let y = e.clientY - center.y
startAngle.value = R2D * Math.atan2(y, x)
active.value = true
}
function handleMouseMove(e: MouseEvent): void {
e.preventDefault()
if (!active.value || !rotateWrapper.value) return
const x = e.clientX - center.x
const y = e.clientY - center.y
const d = R2D * Math.atan2(y, x)
rotation.value = d - startAngle.value
const calc = angle.value + rotation.value
rotateWrapper.value.style.transform = `rotate(${calc}deg)`
}
function mouseup(): void {
if (!active.value || !rotate.value) return
angle.value += Math.round(rotation.value)
if (rotation.value !== prevRotation.value) {
prevRotation.value = rotation.value
angleValue.value = angle.value
if (angleValue.value !== 0) {
emit('triggerAngle', modulo180(angle.value, props.roadDegrees))
}
}
active.value = false
}
function clickAndMove(value: number): void {
const moduloAngle = modulo180(angle.value, Math.round(props.roadDegrees))
if (moduloAngle % 45 === 0) {
return emit('triggerAngle', moduloAngle + value)
}
let closestMultiple = Math.ceil(moduloAngle / 45) * value
return emit('triggerAngle', closestMultiple)
}
function modulo180(value: number, roadDegrees: number): number {
let moduloAngle = (value - roadDegrees) % 360
if (moduloAngle < -180) moduloAngle += 360
if (moduloAngle > 180) moduloAngle -= 360
return Math.round(moduloAngle)
}
</script>
<style scoped lang="scss">
.wrapper-widget-orientation {
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: toRem(40);
width: 100%;
border-radius: toRem(1);
}
.wrapper-img-road {
background-image: url('@/assets/images/road.svg');
background-repeat: no-repeat;
background-size: contain;
background-position: center;
height: 210%;
width: 250%;
}
.wrapper-widget {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.wrapper-car {
height: toRem(40);
width: toRem(40);
display: flex;
justify-content: center;
position: relative;
}
.wrapper-elements {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.rotate-wrapper {
width: 100%;
height: 100%;
border-radius: 50%;
transform: rotate(0);
background-color: transparent;
display: flex;
justify-content: center;
position: relative;
z-index: 0;
.cursor-img {
background-image: url('@/assets/images/cursor.svg');
content: '';
display: block;
position: absolute;
left: toRem(2);
right: 0;
top: 0;
margin: auto;
height: 50%;
width: 74%;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
cursor: move;
}
}
.arrow-img {
position: absolute;
border: none;
background-color: transparent;
height: toRem(5);
width: toRem(5);
padding: 0;
img {
height: 100%;
&:hover {
opacity: 0.8;
}
}
}
.arrow-img-1 {
right: toRem(4);
top: toRem(6);
}
.arrow-img-2 {
left: toRem(3.2);
bottom: toRem(10.5);
transform: rotate(90deg);
}
.car-img {
position: absolute;
height: 45%;
z-index: 1;
pointer-events: none;
}
.rounded-transparent {
background-color: rgba(white, 0.4);
height: toRem(25);
width: toRem(25);
position: absolute;
border-radius: 50%;
}
</style>

View File

@@ -56,8 +56,25 @@
"delete_sequence_tooltip": "Permanently delete this sequence",
"hide_photo_tooltip": "Hide selected pictures",
"delete_photo_tooltip": "Permanently delete selected pictures",
"confirm_pictures_dialog": "⚠️ Selected photos will be permanently deleted",
"confirm_sequence_dialog": "⚠️ This sequence will be permanently deleted",
"conf_pic_msg": "⚠️ Selected photos will be permanently deleted",
"conf_sequence_msg": "⚠️ This sequence will be permanently deleted",
"button_panel_photos": "Manage pictures",
"button_panel_orientation": "Set orientation",
"button_panel_sort": "Sort sequence",
"orientation_panel_title": "Adjusting the orientation of all photos in the sequence",
"orientation_panel_tooltip": "Drag the blue box in the desired direction\"",
"orientation_input_label": "or change the angle here",
"orientation_input_placeholder": "Value between -180 and 180",
"orientation_input_error_value": "Value must be between -180 and 180",
"orientation_panel_button": "Validate position",
"sort_panel_title": "Sequence sort setting",
"sort_panel_settings": "Sort sequence by:",
"sort_panel_settings_order": "Order :",
"sort_panel_settings_order_increase": "Ascending",
"sort_panel_settings_order_decrease": "Decreasing",
"sort_panel_check_gps": "GPS Date",
"sort_panel_check_file": "File date",
"sort_panel_check_name": "File name",
"created": "Uploaded :",
"taken": "Shot on :",
"duration": "Duration :",

View File

@@ -1,7 +1,6 @@
{
"general": {
"title": "DESC Panoramax {instanceName}: photo-cartographier les territoires",
"description": "DESC L'instance Panoramax {instanceName} permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages.",
"title": "Instance Panoramax",
"meta": {
"title": "Instance Panoramax",
"description": "Panoramax, lalternative libre pour photo-cartographier les territoires"
@@ -57,8 +56,27 @@
"delete_sequence_tooltip": "Supprime définitivement la séquence",
"hide_photo_tooltip": "Masque les photos sur la carte",
"delete_photo_tooltip": "Supprime définitivement les photos",
"confirm_pictures_dialog": "⚠️ Les photos sélectionnées vont être définitivement supprimées",
"confirm_sequence_dialog": "⚠️ La séquence va être définitivement supprimée",
"conf_pic_msg": "⚠️ Les photos sélectionnées vont être définitivement supprimées",
"conf_sequence_msg": "⚠️ La séquence va être définitivement supprimée",
"button_panel_photos": "Gérer les photos",
"button_panel_orientation": "Régler l'orientation",
"button_panel_sort": "Trier la séquence",
"orientation_panel_title": "Définir l'orientation de la caméra sur le véhicule",
"orientation_panel_tooltip": "Faites glisser la zone bleu dans la direction souhaitée",
"orientation_input_label": "ou modifiez l'angle ici",
"orientation_input_placeholder": "Valeur entre -180 et 180",
"orientation_input_error_value": "La valeur doit être entre -180 et 180",
"orientation_panel_button": "Valider la position",
"orientation_updated": "L'orientation a bien été modifiée",
"sort_updated": "La séquence a bien triée",
"sort_panel_title": "Réglage du tri de la séquence",
"sort_panel_settings": "Trier la séquence par :",
"sort_panel_settings_order": "Ordre :",
"sort_panel_settings_order_increase": "Croissant",
"sort_panel_settings_order_decrease": "Décroissant",
"sort_panel_check_gps": "Date du GPS",
"sort_panel_check_file": "Date de la caméra",
"sort_panel_check_name": "Nom du fichier",
"created": "Versement :",
"taken": "Prise de vue :",
"duration": "Durée :",

View File

@@ -56,8 +56,25 @@
"delete_sequence_tooltip": "A sorozat végleges törlése",
"hide_photo_tooltip": "A kiválasztott fényképek elrejtése",
"delete_photo_tooltip": "A kiválasztottt fényképek végleges törlése",
"confirm_pictures_dialog": "⚠️ A kiválasztott fényképek véglegesen elvesznek",
"confirm_sequence_dialog": "⚠️ A kiválasztott sorozat véglegesen elvész",
"conf_pic_msg": "⚠️ A kiválasztott fényképek véglegesen elvesznek",
"conf_sequence_msg": "⚠️ A kiválasztott sorozat véglegesen elvész",
"button_panel_photos": "Fényképek kezelése",
"button_panel_orientation": "Tájolás beállítása",
"button_panel_sort": "Rendezési sorrend",
"orientation_panel_title": "Az összes kép tájolásának beállítása a sorozatban",
"orientation_panel_tooltip": "Húzza a kék dobozt a kívánt irányba",
"orientation_input_label": "vagy módosítsa a szöget itt",
"orientation_input_placeholder": "-180 és 180 közötti érték",
"orientation_input_error_value": "Az értéknek -180 és 180 között kell lennie",
"orientation_panel_button": "Pozíció ellenőrzése",
"sort_panel_title": "Szekvenciás rendezés beállítása",
"sort_panel_settings": "Sorrend rendezése:",
"sort_panel_settings_order": "Rendelés :",
"sort_panel_settings_order_increase": "Növekvő",
"sort_panel_settings_order_decrease": "Csökkenő",
"sort_panel_check_gps": "GPS dátum",
"sort_panel_check_file": "Fájl dátuma",
"sort_panel_check_name": "Fájlnév",
"created": "Feltöltés ideje:",
"taken": "Elkészítés ideje:",
"duration": "Hossz:",

View File

@@ -11,7 +11,6 @@ describe('Template', () => {
}
})
expect(wrapper.vm.text).toBe(null)
expect(wrapper.vm.title).toBe(null)
expect(wrapper.vm.look).toBe('')
})
test('should have all the props filled', () => {
@@ -21,11 +20,9 @@ describe('Template', () => {
},
props: {
text: 'my text',
title: 'my title',
look: 'my-look'
}
})
expect(wrapper.html()).contains('my title</h3>')
expect(wrapper.html()).contains('my text</p>')
expect(wrapper.html()).contains('class="information-block my-look"')
})

View File

@@ -0,0 +1,74 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import PanelOrientationManagement from '../../../../components/sequence/PanelOrientationManagement.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(PanelOrientationManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.vm.roadDegrees).toBe(0)
expect(wrapper.vm.seqBruteDeg).toBe(0)
})
describe('When the component have props filled', () => {
it('should render the component all the element', () => {
const wrapper = shallowMount(PanelOrientationManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
},
props: {
roadDegrees: 45,
seqBruteDeg: 22
}
})
expect(wrapper.html()).contains(
'pages.sequence.orientation_panel_title'
)
expect(wrapper.html()).contains(
'pages.sequence.orientation_input_label'
)
expect(wrapper.html()).contains(
'pages.sequence.orientation_input_placeholder'
)
expect(wrapper.html()).contains('<widget-orientation')
expect(wrapper.html()).contains('<information-card')
expect(wrapper.html()).contains('<input')
expect(wrapper.html()).contains('<button')
expect(wrapper.html()).contains(
'type="number" min="-180" max="180" text="-23"'
)
})
})
describe('Emit functions', () => {
it('should emit triggerAngle event', async () => {
const wrapper = shallowMount(PanelOrientationManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
},
props: {
roadDegrees: 45,
seqBruteDeg: 22
}
})
const valueToEmit = 22 - 45
await wrapper.vm.triggerAngle(valueToEmit)
expect(wrapper.emitted('triggerAngle')).toHaveLength(1)
expect(wrapper.emitted('triggerAngle')[0]).toEqual([valueToEmit])
})
})
})
})

View File

@@ -0,0 +1,347 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import PanelPhotosManagement from '../../../../components/sequence/PanelPhotosManagement.vue'
import i18n from '../../config'
import { createPinia } from 'pinia'
const pinia = createPinia()
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.vm.isSequenceOwner).toBe(false)
expect(wrapper.vm.imagesSelectedHaveDifferentStatus).toBe(false)
expect(wrapper.vm.itemSelected).toBe('')
expect(wrapper.vm.menuHeight).toBe('0')
expect(wrapper.vm.fullImagesToDelete).toStrictEqual([])
expect(wrapper.vm.selfLink).toStrictEqual([])
expect(wrapper.vm.paginationLinks).toStrictEqual([])
expect(wrapper.vm.pictures).toStrictEqual([])
expect(wrapper.vm.picturesToDelete).toStrictEqual([])
expect(wrapper.vm.sequence).toStrictEqual({})
})
describe('When the component have props filled', () => {
it('should render the component with images selected', () => {
const pictures = [
{
assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } },
properties: { datetime: new Date(), 'geovisio:status': 'ready' },
id: 'pictureId',
bbox: [1, 2, 3, 4]
}
]
const sequence = [
{
id: 'seqId',
title: 'title',
description: 'descr',
license: 'license',
created: new Date(),
taken: 'taken date',
location: 'location',
imageCount: 4,
duration: 'duration',
camera: 'camera',
cameraModel: 'camera model',
status: 'ready',
providers: [{ name: 'provider', roles: ['role1'] }],
extent: {
temporal: { interval: ['date'] },
spatial: { bbox: ['1', '2'] }
}
}
]
const paginationLinks = [
{
href: 'href',
rel: 'rel',
title: 'title',
type: 'type'
}
]
const selfLink = [
{
href: 'href',
rel: 'self',
title: 'title',
type: 'type'
}
]
const fullImagesToDelete = [
{
assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } },
properties: { datetime: new Date(), 'geovisio:status': 'ready' },
id: 'pictureId',
bbox: [1, 2, 3, 4]
}
]
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
},
props: {
isSequenceOwner: true,
imagesSelectedHaveDifferentStatus: true,
itemSelected: 'pictureId',
menuHeight: '10px',
pictures,
picturesToDelete: ['1', '2'],
sequence,
paginationLinks,
selfLink,
fullImagesToDelete
}
})
expect(wrapper.html()).contains('<image-item')
expect(wrapper.html()).contains('selectedonmap="true"')
expect(wrapper.html()).contains('href="thumbHref"')
expect(wrapper.html()).contains('hrefhd="hdHref"')
expect(wrapper.html()).contains('ischecked="false"')
expect(wrapper.html()).contains('isindeterminate="true"')
expect(wrapper.html()).contains('pages.sequence.picture_selected')
expect(wrapper.html()).contains(
'icon="bi bi-eye" disabled="false" isloading="false"'
)
expect(wrapper.html()).contains(
'icon="bi bi-trash" disabled="false" isloading="false"'
)
expect(wrapper.html()).contains('pages.sequence.hide_photo_tooltip')
expect(wrapper.html()).contains('pages.sequence.delete_photo_tooltip')
expect(wrapper.html()).contains('<pagination')
expect(wrapper.html()).contains('selflink="')
})
it('should render the component with no images', () => {
const pictures = []
const sequence = [
{
id: 'seqId',
title: 'title',
description: 'descr',
license: 'license',
created: new Date(),
taken: 'taken date',
location: 'location',
imageCount: 4,
duration: 'duration',
camera: 'camera',
cameraModel: 'camera model',
status: 'ready',
providers: [{ name: 'provider', roles: ['role1'] }],
extent: {
temporal: { interval: ['date'] },
spatial: { bbox: ['1', '2'] }
}
}
]
const paginationLinks = []
const selfLink = [
{
href: 'href',
rel: 'self',
title: 'title',
type: 'type'
}
]
const fullImagesToDelete = []
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
},
props: {
isSequenceOwner: true,
imagesSelectedHaveDifferentStatus: false,
itemSelected: '',
menuHeight: '10px',
pictures,
picturesToDelete: [],
sequence,
paginationLinks,
selfLink,
fullImagesToDelete
}
})
expect(wrapper.html()).contains('class="no-photo"')
expect(wrapper.html()).contains('pages.sequence.no_image')
})
it('should render the component with all the images selected', () => {
const pictures = [
{
assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } },
properties: { datetime: new Date(), 'geovisio:status': 'ready' },
id: 'pictureId',
bbox: [1, 2, 3, 4]
}
]
const sequence = [
{
id: 'seqId',
title: 'title',
description: 'descr',
license: 'license',
created: new Date(),
taken: 'taken date',
location: 'location',
imageCount: 4,
duration: 'duration',
camera: 'camera',
cameraModel: 'camera model',
status: 'ready',
providers: [{ name: 'provider', roles: ['role1'] }],
extent: {
temporal: { interval: ['date'] },
spatial: { bbox: ['1', '2'] }
}
}
]
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
},
props: {
isSequenceOwner: true,
pictures,
picturesToDelete: [1],
sequence
}
})
expect(wrapper.html()).contains(
'ischecked="true" isindeterminate="false"></input'
)
})
it('should render the component with images hidden', () => {
const pictures = [
{
assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } },
properties: { datetime: new Date(), 'geovisio:status': 'hidden' },
id: 'pictureId',
bbox: [1, 2, 3, 4]
}
]
const sequence = [
{
id: 'seqId',
title: 'title',
description: 'descr',
license: 'license',
created: new Date(),
taken: 'taken date',
location: 'location',
imageCount: 4,
duration: 'duration',
camera: 'camera',
cameraModel: 'camera model',
status: 'ready',
providers: [{ name: 'provider', roles: ['role1'] }],
extent: {
temporal: { interval: ['date'] },
spatial: { bbox: ['1', '2'] }
}
}
]
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
},
props: {
isSequenceOwner: true,
pictures,
picturesToDelete: [],
sequence
}
})
expect(wrapper.html()).contains('status="hidden"></image-item')
})
})
})
describe('Emit functions', () => {
it('should emit triggerInputCheck event', async () => {
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
}
})
await wrapper.vm.triggerInputCheck({
isChecked: true,
isIndeterminate: false
})
expect(wrapper.emitted('triggerInputCheck')).toHaveLength(1)
expect(wrapper.emitted('triggerInputCheck')[0]).toEqual([
{ isChecked: true, isIndeterminate: false }
])
})
it('should emit triggerGoToNextPage event', async () => {
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
}
})
await wrapper.vm.triggerGoToNextPage('nextPage')
expect(wrapper.emitted('triggerGoToNextPage')).toHaveLength(1)
expect(wrapper.emitted('triggerGoToNextPage')[0]).toEqual(['nextPage'])
})
it('should emit triggerSelectImageAndMove event', async () => {
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
}
})
const picture = {
assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } },
properties: { datetime: new Date(), 'geovisio:status': 'ready' },
id: 'pictureId',
bbox: [1, 2, 3, 4]
}
await wrapper.vm.triggerSelectImageAndMove(picture)
expect(wrapper.emitted('triggerSelectImageAndMove')).toHaveLength(1)
expect(wrapper.emitted('triggerSelectImageAndMove')[0]).toEqual([picture])
})
it('should emit triggerPatchOrDeleteCollectionItems event', async () => {
const wrapper = shallowMount(PanelPhotosManagement, {
global: {
plugins: [i18n, pinia],
mocks: {
$t: (msg) => msg
}
}
})
await wrapper.vm.triggerPatchOrDeleteCollectionItems('itemToDelete')
expect(
wrapper.emitted('triggerPatchOrDeleteCollectionItems')
).toHaveLength(1)
expect(wrapper.emitted('triggerPatchOrDeleteCollectionItems')[0]).toEqual(
['itemToDelete']
)
})
})
})

View File

@@ -0,0 +1,83 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import PanelSortManagement from '../../../../components/sequence/PanelSortManagement.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(PanelSortManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.vm.sequenceSorted).toBe(null)
})
describe('When the component have props filled', () => {
it('should render the component all the element', () => {
const wrapper = shallowMount(PanelSortManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.html()).contains('pages.sequence.sort_panel_title')
expect(wrapper.html()).contains('pages.sequence.sort_panel_settings')
expect(wrapper.html()).contains(
'name="sort" id="gpsdate" label="pages.sequence.sort_panel_check_gps" value=""'
)
expect(wrapper.html()).contains(
'name="sort" id="filedate" label="pages.sequence.sort_panel_check_file" value=""'
)
expect(wrapper.html()).contains(
'name="sort" id="filename" label="pages.sequence.sort_panel_check_name" value=""'
)
expect(wrapper.html()).contains(
'pages.sequence.sort_panel_settings_order'
)
expect(wrapper.html()).contains('increased="true"></input-switch')
})
it('should render the component with gpsdate selected', () => {
const wrapper = shallowMount(PanelSortManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
},
props: {
sequenceSorted: '-gpsdate'
}
})
expect(wrapper.html()).contains(
'name="sort" id="gpsdate" label="pages.sequence.sort_panel_check_gps" value="gpsdate"'
)
expect(wrapper.html()).contains('increased="false"></input-switch')
})
})
})
describe('Emit functions', () => {
it('should emit triggerSort event', async () => {
const wrapper = shallowMount(PanelSortManagement, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
},
props: {
sequenceSorted: '-gpsdate'
}
})
await wrapper.vm.triggerSort('-gpsdate')
expect(wrapper.emitted('triggerSort')).toHaveLength(1)
expect(wrapper.emitted('triggerSort')[0]).toEqual(['-gpsdate'])
})
})
})

View File

@@ -0,0 +1,53 @@
import { it, describe, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import WidgetOrientation from '../../../../components/sequence/WidgetOrientation.vue'
import i18n from '../../config'
describe('Template', () => {
describe('Props', () => {
it('should have default props', () => {
const wrapper = shallowMount(WidgetOrientation, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
}
})
expect(wrapper.vm.roadDegrees).toBe(0)
expect(wrapper.vm.seqBruteDeg).toBe(0)
})
describe('When the component have props filled', () => {
it('should render the component all the element', () => {
const wrapper = shallowMount(WidgetOrientation, {
global: {
plugins: [i18n],
mocks: {
$t: (msg) => msg
}
},
props: {
roadDegrees: 45,
seqBruteDeg: 22
}
})
expect(wrapper.html()).contains(
'class="wrapper-img-road" style="transform: rotate(45deg);"'
)
expect(wrapper.html()).contains(
'src="/assets/images/car.svg" alt="" style="transform: rotate(45deg);" class="car-img"'
)
expect(wrapper.html()).contains(
'style="transform: rotate(22deg);" id="rotateWrapper" class="rotate-wrapper"'
)
expect(wrapper.html()).contains(
'src="/assets/images/icon/cursor-arrow.svg"'
)
expect(wrapper.html()).contains('class="arrow-img arrow-img-1"')
expect(wrapper.html()).contains('class="arrow-img arrow-img-2"')
})
})
})
// TODO TEST -> All Events
})

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

@@ -0,0 +1,29 @@
import axios from 'axios'
async function getIgnTiles(): Promise<object | string> {
try {
const { data } = await axios.get(
'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json'
)
data.sources.plan_ign.scheme = 'xyz'
data.sources.plan_ign.attribution = 'Données cartographiques : © IGN'
const objIndex = data.layers.findIndex(
(el: { id: string }) => el.id === 'toponyme - parcellaire - adresse'
)
data.layers[objIndex].layout = {
...data.layers[objIndex].layout,
'text-field': [
'concat',
['get', 'numero'],
['get', 'indice_de_repetition']
]
}
// Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS
// Patch num_repetition
return data
} catch (error) {
return 'https://tile-vect.openstreetmap.fr/styles/basic/style.json'
}
}
export { getIgnTiles }

View File

@@ -20,16 +20,25 @@
icon="bi bi-arrow-left"
:text="$t('pages.sequence.back_button')"
:route="{ name: 'my-sequences' }"
look="link--grey"
look="link--grey-dark"
/>
</div>
<div class="entry-tab-panel">
<TabPanel :panel-selected="panelView" @trigger="setPanelView" />
</div>
</div>
<div
v-if="panelView === 'photos'"
ref="deleteAll"
class="entry-panel-photo"
>
<div class="menu-top">
<div class="header-menu">
<div class="wrapper-title">
<span :class="[sequence.status, 'sequence-status']">{{
sequenceStatus
}}</span>
<div @click.stop class="entry-edit-text">
<span :class="[sequence.status, 'sequence-status']">{{
sequenceStatus
}}</span>
<EditText
:default-text="sequence.title"
:is-loading="isLoadingTitle"
@@ -118,84 +127,45 @@
</div>
</div>
</div>
</div>
<div v-if="pictures && pictures.length" class="photos-wrapper">
<div v-if="isSequenceOwner" class="delete-all" ref="deleteAll">
<div class="wrapper-select">
<InputCheckbox
:is-checked="pictures.length === picturesToDelete.length"
:is-indeterminate="isIndeterminate"
:label="selectedText"
@trigger="triggerCheck"
/>
<div v-if="picturesToDelete.length" class="wrapper-photo-selected">
<span class="photo-selected-separator">-</span>
<span>{{
$t('pages.sequence.picture_selected', picturesToDelete.length)
}}</span>
</div>
</div>
<div class="action-buttons">
<Button
look="button--white background-white no-text"
:icon="
picturesToDeleteStatus === 'hidden' ||
imagesSelectedHaveDifferentStatus
? 'bi bi-eye'
: 'bi bi-eye-slash'
"
:tooltip="$t('pages.sequence.hide_photo_tooltip')"
:disabled="
!picturesToDelete.length || sequence.status === 'hidden'
"
@trigger="patchOrDeleteCollectionItems('PATCH')"
/>
<div class="button-hidde">
<Button
look="button--red background-white no-text"
icon="bi bi-trash"
:tooltip="$t('pages.sequence.delete_photo_tooltip')"
:disabled="!picturesToDelete.length"
@trigger="patchOrDeleteCollectionItems('DELETE')"
/>
</div>
</div>
<div class="wrapper-panel-photo">
<PanelPhotosManagement
:pictures="pictures"
:pictures-to-delete="picturesToDelete"
:sequence="sequence"
:pagination-links="paginationLinks"
:self-link="selfLink"
:full-images-to-delete="fullImagesToDelete"
:is-sequence-owner="isSequenceOwner"
:images-selected-have-different-status="
imagesSelectedHaveDifferentStatus
"
:item-selected="itemSelected"
@triggerInputCheck="triggerCheck"
@triggerGoToNextPage="goToNextPage"
@triggerSelectImageAndMove="selectImageAndMove"
@triggerPatchOrDeleteCollectionItems="patchOrDeleteCollectionItems"
/>
</div>
<ul class="photo-list">
<li
v-for="(item, i) in pictures"
:id="`el-list${i}`"
class="photo-item"
>
<ImageItem
:href="item.assets.thumb.href"
:href-hd="item.assets.hd.href"
:created="formatDate(item.properties.datetime, 'HH:mm:ss')"
:selected="photoToDeleteOrPatchSelected(item, picturesToDelete)"
:selected-on-map="itemSelected === item.id"
:status="
imageStatus(item.properties['geovisio:status'], sequence.status)
"
@trigger="selectImageAndMove(item)"
/>
</li>
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="goToNextPage"
/>
</div>
</ul>
</div>
<p v-else class="no-photo">{{ $t('pages.sequence.no_image') }}</p>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
<div v-else-if="panelView === 'orientation'" class="entry-panel">
<PanelOrientationManagement
:road-degrees="seqDegrees"
:seq-brute-deg="seqBruteDeg"
:is-loading="isLoadingOrient"
@triggerAngle="orientSequence"
/>
</div>
<div v-else class="entry-panel">
<PanelSortManagement
:sequence-sorted="sequence['geovisio:sorted-by']"
@triggerSort="sortSequence"
/>
</div>
</div>
<div v-else class="menu-right wrapper-loader">
<Loader look="sm" :is-loaded="false" />
</div>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</main>
</template>
@@ -209,11 +179,12 @@ import { useCookies } from 'vue3-cookies'
import Button from '@/components/Button.vue'
import Link from '@/components/Link.vue'
import Toast from '@/components/Toast.vue'
import Pagination from '@/components/Pagination.vue'
import InputCheckbox from '@/components/InputCheckbox.vue'
import Loader from '@/components/Loader.vue'
import ImageItem from '@/components/ImageItem.vue'
import EditText from '@/components/EditText.vue'
import TabPanel from '@/components/TabPanel.vue'
import PanelPhotosManagement from '@/components/sequence/PanelPhotosManagement.vue'
import PanelOrientationManagement from '@/components/sequence/PanelOrientationManagement.vue'
import PanelSortManagement from '@/components/sequence/PanelSortManagement.vue'
import Viewer from '@/components/Viewer.vue'
import type ViewerType from '@/components/Viewer.vue'
import { durationCalc, formatDate } from '@/utils/dates'
@@ -228,9 +199,7 @@ import {
} from '@/views/utils/sequence/request'
import {
imageStatus,
scrollIntoSelected,
photoToDeleteOrPatchSelected,
spliceIntoChunks,
formatPaginationItems
} from '@/views/utils/sequence/index'
@@ -258,10 +227,14 @@ let isShiftPressed = ref<boolean>(false)
let itemSelected = ref<string>('')
let isLoading = ref<boolean>(false)
let isLoadingTitle = ref<boolean>(false)
let isLoadingOrient = ref<boolean>(false)
let seqDegrees = ref<number>(0)
let seqBruteDeg = ref<number>(0)
let panelView = ref<string>('photos')
const collapseMenu = ref<HTMLDivElement>()
const deleteAll = ref<HTMLDivElement>()
const menuHeight = ref<string>()
const viewerRef = ref<InstanceType<typeof ViewerType>>()
const menuHeight = ref<string>('0')
const viewerRef = ref<InstanceType<typeof ViewerType> | null>(null)
onMounted(async () => {
try {
@@ -276,40 +249,41 @@ onMounted(async () => {
fetchAllCollectionInfo[1].data.links
)
formatSequenceFetched(fetchAllCollectionInfo[0].data)
const collectionItems = fetchAllCollectionInfo[1].data.features
const collectionItemsReady = collectionItems.filter(
const collItems = fetchAllCollectionInfo[1].data.features
const collItemsReady = collItems.filter(
(el) => el.properties['geovisio:status'] === 'ready'
)
pictures.value = collectionItems
pictures.value = collItems
setHeightValue()
if (
itemSelected.value.length ||
!getCurrentPicId(collectionItemsReady[0].id)
) {
if (itemSelected.value.length || !getCurrentPicId(collItemsReady[0].id)) {
return
}
if (!viewerRef.value) return
viewerRef.value.viewer._api.onceReady().then(() => {
viewerRef.value.viewer.addEventListener('picture-loaded', (): void => {
if (!viewerRef.value) return
const seqRelativeDeg = viewerRef.value.viewer.getPictureRelativeHeading()
seqBruteDeg.value =
viewerRef.value.viewer.getPictureMetadata().properties['view:azimuth']
seqDegrees.value = seqBruteDeg.value - seqRelativeDeg
})
if (!viewerRef.value) return
viewerRef.value.viewer._api.onceReady().then(() => {
if (!sequence.value || !viewerRef.value) return
viewerRef.value.viewer.goToPicture(
getCurrentPicId(collectionItemsReady[0].id),
sequence.value?.id
getCurrentPicId(collItemsReady[0].id),
sequence.value.id
)
})
itemSelected.value = getCurrentPicId(collectionItemsReady[0].id)
if (!pictureExistInList(getCurrentPicId(collectionItemsReady[0].id))) {
await goToTheGoodPage(getCurrentPicId(collectionItemsReady[0].id))
}
scrollIntoSelected(
itemSelected.value,
pictures.value.map((e) => e.id)
)
await goToThePageAndScroll()
} catch (err) {
console.log(err)
}
})
watchEffect(async () => {
watchEffect(() => {
goToThePageAndScroll()
})
async function goToThePageAndScroll() {
if (!viewerRef || !viewerRef.value || !viewerRef.value.viewer) return
viewerRef.value.viewer.addEventListener(
'picture-loaded',
@@ -324,7 +298,12 @@ watchEffect(async () => {
)
}
)
})
}
function setPanelView(value: string): void {
panelView.value = value
if (value === 'orientation') window.location.hash = '#background=aerial'
else window.location.hash = '#background=streets'
}
async function setNewSequenceTitle(value: string | null): Promise<void> {
isLoadingTitle.value = true
if (value && value.length > 0) {
@@ -357,16 +336,12 @@ const sequenceStatus = computed((): string => {
return t('pages.sequence.sequence_hidden')
return t('pages.sequence.sequence_waiting')
})
const picturesToDeleteStatus = computed((): string => {
if (fullImagesToDelete().length) {
return fullImagesToDelete()[0].properties['geovisio:status']
}
return 'hidden'
const fullImagesToDelete = computed((): ResponseUserPhotoInterface[] => {
return pictures.value.filter((el) => picturesToDelete.value.includes(el.id))
})
const imagesSelectedHaveDifferentStatus = computed((): boolean => {
function filterByStatus(status: string): ResponseUserPhotoInterface[] {
return fullImagesToDelete().filter((el) => {
return fullImagesToDelete.value.filter((el) => {
return el.properties['geovisio:status'] === status
})
}
@@ -374,20 +349,6 @@ const imagesSelectedHaveDifferentStatus = computed((): boolean => {
filterByStatus('hidden').length > 0 && filterByStatus('ready').length > 0
)
})
const isIndeterminate = computed(
(): boolean =>
!!picturesToDelete.value.length &&
!!sequence.value &&
pictures.value.length !== picturesToDelete.value.length
)
const selectedText = computed((): string =>
picturesToDelete.value.length === pictures.value.length
? t('pages.sequence.unselect_text')
: t('pages.sequence.select_text')
)
async function goToTheGoodPage(id: string): Promise<void> {
const { data } = await fetchCollectionItems(
route.params.id,
@@ -440,7 +401,7 @@ function hiddeAllPictures(): void {
}
async function deleteCollection(): Promise<void> {
if (confirm(t('pages.sequence.confirm_sequence_dialog'))) {
if (confirm(t('pages.sequence.conf_sequence_msg'))) {
isLoading.value = true
await deleteACollection(route.params.id)
isLoading.value = false
@@ -465,11 +426,6 @@ async function patchCollection(): Promise<void> {
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
isLoading.value = false
}
function fullImagesToDelete(): ResponseUserPhotoInterface[] {
return pictures.value.filter((el) => picturesToDelete.value.includes(el.id))
}
async function goToNextPage(value: string): Promise<void> {
isLoading.value = true
const { data } = await fetchCollectionItemsWithFullUrl(value)
@@ -484,15 +440,31 @@ async function goToNextPage(value: string): Promise<void> {
isLoading.value = false
setHeightValue()
}
function triggerCheck(value: CheckboxInterface): void {
value.isChecked
? (picturesToDelete.value = pictures.value
.filter(
(el) => el.properties['geovisio:status'] !== 'waiting-for-process'
)
.map((el) => el.id))
: (picturesToDelete.value = [])
if (value.isChecked) {
picturesToDelete.value = pictures.value
.filter((e) => e.properties['geovisio:status'] !== 'waiting-for-process')
.map((e) => e.id)
} else picturesToDelete.value = []
}
async function orientSequence(value: number): Promise<void> {
isLoadingOrient.value = true
await patchACollection(route.params.id, {
relative_heading: value
})
const fetchCollectionInfo = await fetchCollection(route.params.id)
formatSequenceFetched(fetchCollectionInfo.data)
if (viewerRef.value) viewerRef.value.viewer.clearPictureMetadataCache()
sequenceStore.addToastText(t('pages.sequence.orientation_updated'), 'success')
isLoadingOrient.value = false
}
async function sortSequence(value: string): Promise<void> {
await patchACollection(route.params.id, {
sortby: value
})
const fetchCollectionInfo = await fetchCollection(route.params.id)
formatSequenceFetched(fetchCollectionInfo.data)
sequenceStore.addToastText(t('pages.sequence.sort_updated'), 'success')
}
function selectPhotoToDeleteOrPatch(
item: ResponseUserPhotoInterface
@@ -550,59 +522,36 @@ async function selectImageAndMove(
)
}
}
async function patchOrDeleteCollectionItems(
requestType: string
): Promise<void> {
if (
requestType === 'DELETE' &&
!confirm(t('pages.sequence.confirm_pictures_dialog'))
)
return
function formatValuesToPatchOrDelete(ele: string): string {
if (imagesSelectedHaveDifferentStatus.value) return 'true'
const imageToDelete = pictures.value.find((elem) => elem.id === ele)
return imageToDelete?.properties['geovisio:status'] === 'ready'
? 'false'
: 'true'
}
async function patchOrDeleteCollectionItems(reqType: string): Promise<void> {
if (reqType === 'DELETE' && !confirm(t('pages.sequence.conf_pic_msg'))) return
isLoading.value = true
toastText.value = ''
const chunksItems = spliceIntoChunks(picturesToDelete.value, 4)
try {
let items: unknown[] = []
if (imagesSelectedHaveDifferentStatus.value) {
for (let el of chunksItems) {
items = [
...items,
...(await Promise.all(
el.map(async (ele) => {
if (requestType === 'PATCH') {
return await patchACollectionItem('true', route.params.id, ele)
}
return await deleteACollectionItem(route.params.id, ele)
})
))
]
}
} else {
for (let el of chunksItems) {
items = [
...items,
...(await Promise.all(
el.map(async (ele) => {
if (requestType === 'PATCH') {
const imageToDelete = pictures.value.find(
(elem) => elem.id === ele
)
const isVisible =
imageToDelete?.properties['geovisio:status'] === 'ready'
? 'false'
: 'true'
return await patchACollectionItem(
isVisible,
route.params.id,
ele
)
}
return await deleteACollectionItem(route.params.id, ele)
})
))
]
}
for (let el of chunksItems) {
items = [
...items,
...(await Promise.all(
el.map(async (ele) => {
if (reqType === 'PATCH') {
return await patchACollectionItem(
formatValuesToPatchOrDelete(ele),
route.params.id,
ele
)
}
return await deleteACollectionItem(route.params.id, ele)
})
))
]
}
const { data } = await fetchCollectionItems(route.params.id, '?limit=100')
pictures.value = data.features
@@ -633,27 +582,36 @@ async function patchOrDeleteCollectionItems(
display: flex;
}
.entry-viewer {
width: 50vw;
width: 55vw;
position: relative;
height: calc(100vh - #{toRem(8)});
}
.menu-right {
width: 50vw;
width: 45vw;
height: calc(100vh - #{toRem(8)});
overflow: auto;
overflow-y: auto;
}
.back-button,
.entry-tab-panel {
margin-left: toRem(2);
margin-right: toRem(2);
margin-top: toRem(2);
}
.back-button {
width: fit-content;
margin-left: toRem(2);
margin-top: toRem(2);
margin-bottom: toRem(2);
}
.wrapper-loader {
display: flex;
justify-content: center;
align-items: center;
}
.entry-edit-text {
display: flex;
width: 100%;
}
.wrapper-title {
min-width: 60%;
width: 100%;
}
.wrapper-button {
display: flex;
@@ -701,13 +659,27 @@ async function patchOrDeleteCollectionItems(
.button-close {
display: none;
}
.entry-panel,
.menu-top {
padding: toRem(2) toRem(2) toRem(3);
border-bottom-right-radius: toRem(0.5);
border-bottom-left-radius: toRem(0.5);
border-top-right-radius: toRem(0.5);
background-color: var(--blue-semi-pale);
margin-right: toRem(2);
margin-left: toRem(2);
}
.wrapper-panel-photo {
margin: toRem(1);
}
.entry-panel-photo {
height: calc(100vh - v-bind(menuHeight));
}
.menu-top {
position: relative;
margin: toRem(2) toRem(2) 0;
padding: toRem(2.5) toRem(2) toRem(1);
border: toRem(0.1) solid var(--grey);
border-radius: toRem(0.5);
background-color: var(--blue-semi);
margin-bottom: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.header-menu {
display: flex;
@@ -715,14 +687,14 @@ async function patchOrDeleteCollectionItems(
margin-bottom: toRem(1);
}
.sequence-status {
position: absolute;
top: 0;
left: 0;
z-index: 1;
@include text(xss-regular);
@include text(xs-r-regular);
height: 100%;
border-radius: toRem(0.4);
padding: toRem(0.3) toRem(0.8);
color: var(--white);
margin-right: toRem(1);
display: flex;
align-items: center;
&.ready {
background-color: var(--green);
border: toRem(0.1) solid var(--green);
@@ -749,67 +721,13 @@ async function patchOrDeleteCollectionItems(
color: var(--grey-dark);
font-size: toRem(3);
}
.photos-wrapper {
padding: toRem(1) 0 toRem(2) toRem(1);
height: calc(100vh - v-bind(menuHeight));
}
.delete-all {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: toRem(1);
margin-right: toRem(2);
margin-bottom: toRem(1);
@include text(xs-r-regular);
}
.wrapper-select {
display: flex;
align-items: center;
}
.wrapper-photo-selected {
@include text(xs-regular);
}
.photo-selected-separator {
margin-right: toRem(0.5);
margin-left: toRem(0.5);
}
.button-hidde {
margin-right: toRem(1);
margin-left: toRem(1);
}
.action-buttons {
display: flex;
align-items: center;
}
.photo-list {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
height: 100%;
width: 100%;
padding: 0;
}
.photo-item {
width: calc(33% - #{toRem(2)});
height: fit-content;
margin: toRem(1);
border-radius: toRem(0.5);
background-color: var(--grey);
}
.no-photo {
@include text(s-regular);
text-align: center;
margin-top: toRem(10);
color: var(--grey-dark);
}
.entry-pagination {
margin-top: toRem(2);
width: 100%;
display: flex;
justify-content: center;
}
@media (max-width: toRem(102.4)) {
.entry-viewer {
width: 45vw;
}
.menu-right {
width: 55vw;
}
.wrapper-title {
width: 90%;
}
@@ -827,9 +745,6 @@ async function patchOrDeleteCollectionItems(
margin-top: 0;
}
}
.photo-item {
width: calc(50% - #{toRem(2)});
}
}
@media (max-width: toRem(76.8)) {
.entry-page {
@@ -841,6 +756,11 @@ async function patchOrDeleteCollectionItems(
.menu-right {
height: calc(100vh - #{toRem(12)});
}
.back-button {
.link--grey {
@include text(xs-r-regular);
}
}
.desktop {
display: none;
}
@@ -865,10 +785,6 @@ async function patchOrDeleteCollectionItems(
top: toRem(-1);
right: 0;
}
.photo-item {
width: 100%;
margin-right: 0;
}
.wrapper-info-top {
width: 100%;
margin-right: 0;
@@ -876,26 +792,6 @@ async function patchOrDeleteCollectionItems(
.wrapper-button {
margin-top: toRem(1);
}
.photo-list {
padding-right: toRem(2);
}
.delete-all {
text-align: left;
}
.wrapper-select {
flex-direction: column;
align-items: initial;
}
.wrapper-photo-selected:nth-child(2) {
margin-top: toRem(0.5);
}
.photo-selected-separator {
display: none;
}
.entry-pagination {
margin-top: toRem(0.5);
margin-bottom: toRem(1);
}
}
@media (max-width: toRem(50)) {
@@ -915,26 +811,16 @@ async function patchOrDeleteCollectionItems(
top: 0;
right: 0;
z-index: 0;
width: 80vw;
width: 85vw;
background-color: var(--white);
border-left: toRem(0.1) solid var(--grey-pale);
}
.menu-top {
width: 0vw;
padding: toRem(2.5) toRem(1);
}
.button-close {
position: absolute;
right: 0;
top: toRem(22);
z-index: 3;
height: toRem(5);
display: flex;
align-items: center;
justify-content: center;
border-top-left-radius: toRem(0.5);
border-bottom-left-radius: toRem(0.5);
background-color: var(--white);
border: toRem(0.1) solid var(--black);
@include switch-button-view();
}
.menu-is-open {
.menu-right {
@@ -944,12 +830,20 @@ async function patchOrDeleteCollectionItems(
width: auto;
}
.button-close {
left: calc(20vw - #{toRem(3)});
left: calc(15vw - #{toRem(3)});
right: initial;
}
}
.entry-pagination {
margin-top: toRem(1);
.entry-edit-text {
flex-direction: column;
}
.sequence-status {
width: fit-content;
}
.entry-panel {
padding-right: toRem(1);
padding-left: toRem(1);
padding-top: toRem(0);
}
}

View File

@@ -1,5 +1,12 @@
<template>
<main class="entry-page">
<main :class="['entry-page', { 'menu-is-open': menuIsOpen }]">
<div class="button-close">
<Button
look="no-text-blue-dark"
:icon="menuIsOpen ? 'bi bi-chevron-right' : 'bi bi-chevron-left'"
@trigger="menuIsOpen = !menuIsOpen"
/>
</div>
<section :style="{ width: `${mapWidth}px` }" class="section-viewer">
<vue-draggable-resizable
:style="{ width: `${mapWidth}px` }"
@@ -54,182 +61,200 @@
class="section-sequence"
@scroll="handleScroll"
>
<div class="header-title">
<h1 id="sequenceTitle" class="sequences-title">
{{ $t('pages.sequences.title') }}
</h1>
<Button
v-if="
filterDate.start || filterDate.end || sortDate.sortBy || filterBbox
"
look="button-border--black"
:text="$t('pages.sequences.reset_filter_button')"
@trigger="resetAllFilter"
/>
</div>
<div
ref="headerList"
:class="['sequence-item sequence-item-head', headerListClass]"
:style="{ width: `${listWidth}px`, borderRadius: '0px' }"
>
<div class="sequence-header-item"></div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_name') }}</span>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_photos') }}</span>
</div>
<div class="sequence-header-item">
<div>
<div class="header-title">
<h1 id="sequenceTitle" class="sequences-title">
{{ $t('pages.sequences.title') }}
</h1>
<Button
look="link--black no-text"
:icon="iconButtonSort('datetime')"
data-test="button-sort-date"
@trigger="sortList('datetime')"
v-if="
filterDate.start ||
filterDate.end ||
sortDate.sortBy ||
filterBbox
"
look="button-border--black"
:text="$t('pages.sequences.reset_filter_button')"
@trigger="resetAllFilter"
/>
<span class="title">{{ $t('pages.sequences.sequence_date') }}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('datetime')"
:tooltip="$t('pages.sequences.sequence_date_tooltip')"
@trigger="displayModal('datetime')"
/>
</div>
</div>
<div class="sequence-header-item">
<Button
look="link--black no-text"
:icon="iconButtonSort('created')"
data-test="button-sort-date"
@trigger="sortList('created')"
/>
<span class="title">{{
$t('pages.sequences.sequence_creation')
}}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('created')"
:tooltip="$t('pages.sequences.sequence_creation_tooltip')"
@trigger="displayModal('created')"
/>
</div>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_status') }}</span>
</div>
</div>
<ul v-if="!isLoading" ref="list" class="sequence-list">
<li
v-if="userSequences.length"
v-for="item in userSequences"
:class="['sequence-item', { 'sequence-selected': item.id === seqId }]"
@mouseover="goToSequence(item)"
<div
ref="headerList"
:class="[
'sequence-item sequence-item-head',
{ 'item-head-fixed': headerListClass }
]"
:style="{ width: `${listWidth}px`, borderRadius: '0px' }"
>
<router-link
class="button-item"
:to="{
name: 'sequence',
params: { id: item.id }
}"
>
<div class="wrapper-thumb">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb"
<div class="sequence-header-item"></div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_name') }}</span>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_photos') }}</span>
</div>
<div class="sequence-header-item">
<Button
look="link--black no-text"
:icon="iconButtonSort('datetime')"
data-test="button-sort-date"
@trigger="sortList('datetime')"
/>
<span class="title">{{ $t('pages.sequences.sequence_date') }}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('datetime')"
:tooltip="$t('pages.sequences.sequence_date_tooltip')"
@trigger="displayModal('datetime')"
/>
<div class="wrapper-thumb-hover">
</div>
</div>
<div class="sequence-header-item">
<Button
look="link--black no-text"
:icon="iconButtonSort('created')"
data-test="button-sort-date"
@trigger="sortList('created')"
/>
<span class="title">{{
$t('pages.sequences.sequence_creation')
}}</span>
<div class="button-filter">
<Button
:look="filterClass"
:icon="iconButtonFilter('created')"
:tooltip="$t('pages.sequences.sequence_creation_tooltip')"
@trigger="displayModal('created')"
/>
</div>
</div>
<div class="sequence-header-item">
<span>{{ $t('pages.sequences.sequence_status') }}</span>
</div>
</div>
<ul v-if="!isLoading" ref="list" class="sequence-list">
<li
v-if="userSequences.length"
v-for="item in userSequences"
:class="[
'sequence-item',
{ 'sequence-selected': item.id === seqId }
]"
@mouseover="goToSequence(item)"
>
<router-link
class="button-item"
:to="{
name: 'sequence',
params: { id: item.id }
}"
>
<div class="wrapper-thumb">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb-hover"
class="thumb"
/>
<i v-else class="bi bi-image no-image"></i>
<div class="wrapper-thumb-hover">
<img
v-if="item['stats:items'].count > 0"
loading="lazy"
:src="`${item.href}/thumb.jpg`"
alt=""
class="thumb-hover"
/>
</div>
</div>
<div class="sequence-title">
<span>
{{ item.title }}
</span>
<div class="responsive wrapper-images">
<i class="bi bi-images"></i>
<span>
{{ item['stats:items'].count }}
</span>
</div>
</div>
<div class="desktop">
<i class="bi bi-images"></i>
<span>
{{ item['stats:items'].count }}
</span>
</div>
<div>
<span>
{{
formatDate(
item.extent.temporal.interval[0][0],
'Do MMM YYYY HH:mm:ss'
)
}}
</span>
</div>
<div>
<span>
{{ formatDate(item.created, 'Do MMM YYYY HH:mm:ss') }}
</span>
</div>
<div class="wrapper-status">
<span :class="item['geovisio:status']">{{
sequenceStatus(item['geovisio:status'])
}}</span>
</div>
</router-link>
<div class="wrapper-button">
<Button
:text="sequenceButtonText(item['geovisio:status'])"
look="link--blue row-reverse"
:icon="
item['geovisio:status'] === 'ready'
? 'bi bi-eye-slash'
: 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection(item)"
/>
<Button
:text="$t('pages.sequences.delete_button')"
look="link--red row-reverse"
icon="bi bi-trash"
@trigger="deleteCollection(item)"
/>
</div>
<div class="sequence-title">
<span>
{{ item.title }}
</span>
</div>
<div>
<i class="bi bi-images"></i>
<span>
{{ item['stats:items'].count }}
</span>
</div>
<div>
<span>
{{
formatDate(
item.extent.temporal.interval[0][0],
'Do MMM YYYY HH:mm:ss'
)
}}
</span>
</div>
<div>
<span>
{{ formatDate(item.created, 'Do MMM YYYY HH:mm:ss') }}
</span>
</div>
<div>
<span :class="item['geovisio:status']">{{
sequenceStatus(item['geovisio:status'])
}}</span>
</div>
</router-link>
<div class="wrapper-button">
<Button
:text="sequenceButtonText(item['geovisio:status'])"
look="link--blue row-reverse"
:icon="
item['geovisio:status'] === 'ready'
? 'bi bi-eye-slash'
: 'bi bi-eye'
"
class="disable-button"
@trigger="patchCollection(item)"
/>
<Button
:text="$t('pages.sequences.delete_button')"
look="link--red row-reverse"
icon="bi bi-trash"
@trigger="deleteCollection(item)"
</li>
<div v-else-if="!userSequences.length && noSequencesFound">
<p class="no-sequence-found">
{{ $t('pages.sequences.no_sequence_found') }}
</p>
</div>
<div v-else class="no-sequence">
<p class="no-sequence-text">
{{ $t('pages.sequences.no_sequences_text') }}
</p>
<Link
:text="$t('general.header.upload_text')"
look="button button--blue"
:route="{ name: 'why-contribute' }"
/>
</div>
</li>
<div v-else-if="!userSequences.length && noSequencesFound">
<p class="no-sequence-found">
{{ $t('pages.sequences.no_sequence_found') }}
</p>
</ul>
<div v-else class="loader">
<Loader look="sm" :is-loaded="false" />
</div>
<div v-else class="no-sequence">
<p class="no-sequence-text">
{{ $t('pages.sequences.no_sequences_text') }}
</p>
<Link
:text="$t('general.header.upload_text')"
look="button button--blue"
:route="{ name: 'why-contribute' }"
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="goToNextPage"
/>
</div>
</ul>
<div v-else class="loader">
<Loader look="sm" :is-loaded="false" />
</div>
<div class="entry-pagination">
<Pagination
v-for="item in paginationLinks"
:type="item.rel"
:href="item.href"
:self-link="selfLink[0]"
@trigger="goToNextPage"
/>
</div>
<Toast :text="toastText" :look="toastLook" @trigger="toastText = ''" />
</section>
@@ -278,7 +303,11 @@ import Loader from '@/components/Loader.vue'
import Modal from '@/components/Modal.vue'
import CalendarFilter from '@/components/filters/CalendarFilter.vue'
import Pagination from '@/components/Pagination.vue'
import type { SequenceLinkInterface } from './interfaces/MySequencesView'
import type {
SequenceLinkInterface,
PositionInterface,
DateInterface
} from './interfaces/MySequencesView'
import type { ResponseUserPhotoLinksInterface } from './interfaces/MySequenceView'
import { formatDate } from '@/utils/dates'
import {
@@ -290,14 +319,6 @@ const { t } = useI18n()
const { cookies } = useCookies()
const sequenceStore = useSequenceStore()
const { toastText, toastLook } = storeToRefs(sequenceStore)
interface PositionInterface {
bottom: number
top: number
right: number
left: number
y: number
x: number
}
let userSequences = ref<SequenceLinkInterface[]>([])
let paginationLinks = ref<ResponseUserPhotoLinksInterface[] | []>([])
let selfLink = ref<ResponseUserPhotoLinksInterface[] | []>([])
@@ -309,21 +330,24 @@ let noSequencesFound = ref<boolean>(false)
let seqId = ref<string>('')
let calendarType = ref<string>('')
let width = ref<number>(0)
let mapWidth = ref<number>(window.innerWidth / 3)
let listWidth = ref<number>(window.innerWidth / 1.5)
let filterDate = ref<{
end: string | null
start: string | null
type: string
}>({
let menuIsOpen = ref<boolean>(true)
let mapWidth = ref<number>(
window.innerWidth > 768 ? window.innerWidth / 3 : window.innerWidth
)
let listWidth = ref<number>(
window.innerWidth > 768
? window.innerWidth / 1.5
: (window.innerWidth * 85) / 100
)
let filterDate = ref<DateInterface>({
start: null,
end: null,
type: ''
})
let modal = ref()
let filterBbox = ref<number[] | null>(null)
let sortDate = ref<{ sortBy: string | null }>({ sortBy: null })
let uri = ref<string>('api/users/me/collection?limit=50')
let modal = ref()
let uri = ref<string>('api/users/me/collection?limit=25')
const windowWidth = ref<number>(window.innerWidth)
const windowHeight = ref<number>(window.innerHeight - 80)
const viewerRef = ref<InstanceType<typeof ViewerType>>()
@@ -370,17 +394,15 @@ function iconButtonSort(type: string): string {
return 'bi bi-sort-numeric-down'
}
function iconButtonFilter(type: string): string {
if (
const isDateSelected =
filterDate.value.start &&
filterDate.value.end &&
filterDate.value.type === type
) {
return 'bi bi-funnel-fill'
}
if (isDateSelected) return 'bi bi-funnel-fill'
return 'bi bi-funnel'
}
async function fetchAndFormatSequence(): Promise<void> {
const { data } = await axios.get('api/users/me/collection?limit=50')
const { data } = await axios.get('api/users/me/collection?limit=25')
collectionBbox.value = data.extent.spatial.bbox[0]
userSequences.value = getLinkByRel(data.links, 'child')
}
@@ -399,7 +421,7 @@ async function deleteCollection(
sequence: SequenceLinkInterface
): Promise<void> {
isLoading.value = true
if (confirm(t('pages.sequence.confirm_sequence_dialog'))) {
if (confirm(t('pages.sequence.conf_sequence_msg'))) {
await deleteACollection(sequence.id)
await fetchAndFormatSequence()
if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles()
@@ -415,11 +437,11 @@ function sequenceButtonText(status: string): string {
if (status === 'hidden') return t('pages.sequences.show_button')
return t('pages.sequences.hide_button')
}
function onResizeMap(width: any): void {
if (width && collectionBbox.value.length) {
width.value = width
mapWidth.value = width.value.width
listWidth.value = window.innerWidth - width.value.width
function onResizeMap(element: { width: number }): void {
if (element && collectionBbox.value.length && window.innerWidth > 768) {
width.value = element.width
mapWidth.value = element.width
listWidth.value = window.innerWidth - element.width
}
}
const handleScroll = async () => {
@@ -513,17 +535,19 @@ const modalTitle = computed<string>((): string => {
return t('pages.sequences.filter_date_upload_title')
})
const getUserId = computed<string>((): string => cookies.get('user_id'))
const headerListClass = computed<string>((): string => {
if (headerLisPos.value && listPos.value) {
const classCondition = headerLisPos.value.y != 0 && listPos.value.top < 174
return classCondition ? 'item-head-fixed' : ''
}
return ''
})
const headerListClass = computed<boolean>(
(): boolean =>
!!(
headerLisPos.value &&
listPos.value &&
headerLisPos.value.y !== 0 &&
listPos.value.top < 174
)
)
onMounted(async () => {
isLoading.value = true
try {
const { data } = await axios.get('api/users/me/collection?limit=50')
const { data } = await axios.get('api/users/me/collection?limit=25')
selfLink.value = getLinkByRel(data.links, 'self')
paginationLinks.value = formatPaginationItems(data.links)
userSequences.value = getLinkByRel(data.links, 'child')
@@ -558,8 +582,8 @@ function formatUri(): void {
}
if (params.length) {
const constructParams = params.join('')
uri.value = `api/users/me/collection?limit=50${constructParams}`
} else uri.value = 'api/users/me/collection?limit=50'
uri.value = `api/users/me/collection?limit=25${constructParams}`
} else uri.value = 'api/users/me/collection?limit=25'
}
async function updateFilters(value: {
start: null
@@ -607,6 +631,14 @@ watchEffect(() => {
cursor: col-resize;
}
}
@media (max-width: toRem(76.8)) {
.resize-handle-map-mr {
display: none !important;
}
.resize-handle-map {
display: none;
}
}
</style>
<style lang="scss" scoped>
.entry-page {
@@ -619,6 +651,9 @@ watchEffect(() => {
height: 100%;
position: relative;
}
.button-close {
display: none;
}
.bbox-filter-button {
padding: toRem(2);
width: fit-content;
@@ -723,6 +758,10 @@ watchEffect(() => {
.wrapper-thumb {
position: relative;
}
.no-image {
font-size: toRem(3.5);
color: var(--grey-semi-dark);
}
.wrapper-thumb-hover {
display: none;
border-radius: toRem(0.3);
@@ -733,7 +772,12 @@ watchEffect(() => {
z-index: 1;
}
.sequence-title {
text-decoration: underline;
span:first-child {
text-decoration: underline;
}
}
.responsive {
display: none;
}
.ready {
color: var(--green);
@@ -859,17 +903,44 @@ watchEffect(() => {
}
}
@media (max-width: toRem(76.8)) {
.entry-page {
padding-right: toRem(2);
padding-left: toRem(2);
padding-top: toRem(14);
height: 100%;
}
.section-viewer {
display: none;
height: calc(100vh - #{toRem(11.6)});
width: 100vw;
position: fixed;
z-index: 1;
top: 0;
left: 0;
margin-top: toRem(11.4);
}
.button-close {
@include switch-button-view();
}
.menu-is-open {
.section-sequence {
z-index: 3;
}
.button-close {
left: calc(15vw - #{toRem(4.1)});
right: initial;
}
}
.item-head-fixed {
position: initial;
top: initial;
width: initial;
z-index: initial;
}
.section-sequence {
width: 100% !important;
z-index: 0;
position: fixed;
right: 0;
background-color: var(--white);
border-left: toRem(0.1) solid var(--grey-pale);
height: calc(100vh - #{toRem(11.4)});
}
.entry-page {
padding-top: toRem(11);
height: 100%;
}
.sequence-item-head {
width: 100% !important;
@@ -877,6 +948,11 @@ watchEffect(() => {
padding-right: 0;
padding-left: 0;
}
.header-title {
margin-top: toRem(2);
margin-left: toRem(1);
margin-bottom: toRem(1);
}
.sequence-header-item {
width: 50%;
justify-content: center;
@@ -890,20 +966,30 @@ watchEffect(() => {
.button-item {
flex-direction: column;
align-items: center;
padding-right: toRem(1);
padding-left: toRem(1);
padding: toRem(1) toRem(1) 0;
& > * {
text-align: center;
width: 100%;
padding: toRem(0.5) 0;
@include text(s-r-regular);
}
.wrapper-thumb {
margin-right: 0;
}
}
.sequence-title {
display: flex;
justify-content: center;
}
.wrapper-images {
margin-left: toRem(0.5);
}
.sequence-item {
border-top-right-radius: toRem(1);
border-top-left-radius: toRem(1);
border-radius: toRem(1);
flex-direction: column;
padding-bottom: toRem(2);
padding-right: 0;
padding-left: 0;
}
.wrapper-button {
flex-direction: row;
@@ -911,12 +997,46 @@ watchEffect(() => {
right: 0;
bottom: 0;
justify-content: center;
margin-top: toRem(1);
margin-bottom: 0;
.button--white:first-child {
margin-right: toRem(1);
.link--blue:first-child {
margin-right: toRem(2);
margin-bottom: 0;
}
}
.wrapper-status:has(> .ready) {
background: var(--green);
}
.wrapper-status:has(> .hidden) {
background: var(--red-pale);
}
.wrapper-status:has(> .waiting-for-process) {
background: var(--yellow);
}
.ready,
.waiting-for-process,
.hidden {
color: var(--white);
@include text(xs-r-regular);
}
.wrapper-status {
position: absolute;
width: fit-content;
border-radius: toRem(0.5);
right: toRem(1);
top: toRem(1);
padding: toRem(0.2) toRem(0.4) toRem(0.4);
max-width: toRem(8);
line-height: 1;
}
.entry-pagination {
margin-top: toRem(2);
margin-bottom: toRem(2);
}
.desktop {
display: none;
}
.responsive {
display: flex;
}
}
</style>

View File

@@ -118,11 +118,13 @@
<span>{{ $t('pages.share_pictures.information_text3') }}</span>
</div>
<div class="entry-information-card">
<InformationCard
:title="$t('pages.share_pictures.information_about_title')"
:text="formatTextInfoCard"
look="blue"
/>
<InformationCard :text="formatTextInfoCard" look="blue">
<template v-slot:title>
<h3 class="subtitle">
{{ $t('pages.upload.title') }}
</h3>
</template>
</InformationCard>
</div>
</section>
<section class="section">
@@ -268,7 +270,7 @@ ul {
.subtitle {
@include text(h2);
color: var(--blue-dark);
margin-bottom: toRem(2);
margin-bottom: 0;
}
.upload-button {
display: flex;
@@ -282,6 +284,10 @@ ul {
.entry-information-card {
margin-top: toRem(2);
}
.subtitle {
@include text(h2);
color: var(--blue-dark);
}
.icon-block {
display: flex;
align-items: center;

View File

@@ -4,7 +4,6 @@
<InformationCard
v-if="informationCardDisplayed"
:text="$t('pages.upload.description')"
:title="$t('pages.upload.title')"
>
<template v-slot:cross>
<Button
@@ -13,6 +12,11 @@
@trigger="informationCardDisplayed = false"
/>
</template>
<template v-slot:title>
<h3 class="subtitle">
{{ $t('pages.upload.title') }}
</h3>
</template>
<template v-slot:button>
<div class="button-know-more">
<Link
@@ -320,6 +324,10 @@ h3 {
margin-right: toRem(3);
margin-bottom: toRem(1);
}
.subtitle {
@include text(h2);
color: var(--blue-dark);
}
.wrapper-section {
display: flex;
width: 100%;

View File

@@ -27,6 +27,7 @@ export interface UserSequenceInterface {
status: string
providers: [{ name: string; roles: [string] }]
extent: { temporal: { interval: [Date[]] }; spatial: { bbox: string[] } }
['geovisio:sorted-by']?: string
}
export interface ResponseUserSequenceInterface extends UserSequenceInterface {
@@ -40,4 +41,5 @@ export interface ResponseUserSequenceInterface extends UserSequenceInterface {
export interface CheckboxInterface {
isChecked: boolean
isIndeterminate?: boolean
name: string
}

View File

@@ -9,3 +9,18 @@ export interface SequenceLinkInterface {
['stats:items']: { count: number }
['geovisio:status']: string
}
export interface PositionInterface {
bottom: number
top: number
right: number
left: number
y: number
x: number
}
export interface DateInterface {
end: string | null
start: string | null
type: string
}

View File

@@ -22,17 +22,6 @@ export default ({ mode }) => {
eslintPlugin(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: 'src/main.ts',
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: '/index.html',
/**
* Data that needs to be injected into the index.html ejs template
*/
@@ -48,7 +37,7 @@ export default ({ mode }) => {
preprocessorOptions: {
scss: {
additionalData:
'@import "@/assets/font-size.scss"; @import "@/assets/rem-calc.scss";'
'@import "@/assets/font-size.scss"; @import "@/assets/rem-calc.scss"; @import "@/assets/components/index.scss";'
}
}
},