Merge branch 'staging' into preprod

This commit is contained in:
Maxime Lalo 2024-07-29 18:39:37 +02:00
commit 38909761ee
153 changed files with 3669 additions and 4275 deletions

View File

@ -1,22 +1,5 @@
<svg width="44" height="56" viewBox="0 0 44 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3466_113133)">
<path d="M43.8828 9.98633C43.9035 9.4084 43.5713 8.81895 42.8538 8.51041L24.3282 0.58968C22.8379 -0.0481226 21.0845 -0.0481226 19.5942 0.58968L1.17241 8.46666C0.443378 8.7798 0.0996282 9.37616 0.115778 9.96331V46.1454L4.25693 48.091L6.73008 46.9098L18.4499 51.7336V54.1858L22.0396 55.869L25.6617 54.1398V51.7544L37.3584 46.9397L39.8201 48.0887L43.8874 46.1477V9.98633H43.8828Z" fill="#C5B2D4"/>
<path d="M22.0369 55.9998L18.3318 54.2614V51.8115L6.73196 47.036L4.2565 48.2172L0 46.2209V9.85003C0.0322987 9.21222 0.454488 8.6481 1.12584 8.36259L19.5499 0.483307C21.061 -0.163706 22.8651 -0.163706 24.374 0.483307L42.8995 8.40403C43.5617 8.68725 43.9723 9.24216 43.9977 9.87075V46.2232L39.8173 48.2195L37.3534 47.0682L25.7743 51.8345V54.2153L22.0369 55.9998ZM18.5625 54.1163L22.0369 55.7465L25.5459 54.0725V51.6825L37.3626 46.8173L39.8196 47.9639L43.7716 46.0781V10.101H43.767V9.98127C43.7924 9.39412 43.4325 8.88296 42.8096 8.61587L24.284 0.69514C22.8305 0.073455 21.0956 0.073455 19.6422 0.69514L1.21812 8.57212C0.583683 8.84382 0.216862 9.36189 0.233012 9.96055V10.078V46.0712L4.25881 47.9616L6.72966 46.7827L18.5671 51.6549V54.1117L18.5625 54.1163Z" fill="#4E1480"/>
<path d="M18.0574 49.0688C17.5222 49.0688 16.9823 48.9629 16.4724 48.7441L4.4412 43.6325C2.94854 42.9993 1.98419 41.5441 1.98419 39.9254L1.97266 17.4319C1.97266 16.0711 2.65093 14.8116 3.79061 14.0633C4.93029 13.315 6.35835 13.1906 7.61108 13.7294L19.6539 18.9171C21.135 19.5549 22.0924 21.0078 22.0924 22.6195V45.037C22.0924 46.3932 21.4141 47.6504 20.2814 48.401C19.61 48.8454 18.8349 49.0711 18.0551 49.0711L18.0574 49.0688ZM6.00999 14.0863C5.36632 14.0863 4.72727 14.2728 4.17127 14.6366C3.22769 15.256 2.66246 16.3014 2.66477 17.4296L2.67631 39.9231C2.67631 41.2632 3.47685 42.4697 4.71343 42.9947L16.7447 48.1063C17.7805 48.5461 18.9617 48.4402 19.9007 47.8185C20.842 47.1991 21.4026 46.1561 21.4026 45.0324V22.6149C21.4026 21.2795 20.609 20.0775 19.3816 19.5479L7.33885 14.3603C6.91204 14.1761 6.45986 14.0863 6.00999 14.0863Z" fill="white"/>
<path d="M3.09274 20.3118V19.1352C3.09274 19.1052 3.07428 19.0776 3.0466 19.0661L1.58855 18.4375C1.53779 18.4168 1.48242 18.4536 1.48242 18.5066V19.6832C1.48242 19.7131 1.50088 19.7407 1.52856 19.7523L2.98431 20.3808C3.03506 20.4016 3.09043 20.3647 3.09043 20.3118H3.09274Z" fill="white"/>
<path d="M3.01696 20.7323C2.95929 20.7323 2.90392 20.7208 2.84855 20.6978L1.3928 20.0715C1.23823 20.0047 1.13672 19.8527 1.13672 19.6846V18.508C1.13672 18.3653 1.20824 18.234 1.3259 18.1558C1.44586 18.0775 1.59582 18.0637 1.72502 18.1212L3.18307 18.7498C3.33764 18.8166 3.43915 18.9686 3.43915 19.1366V20.3132C3.43915 20.456 3.36763 20.5872 3.24767 20.6655C3.17846 20.7116 3.09771 20.7346 3.01696 20.7346V20.7323ZM1.82883 19.505L2.74704 19.9011V19.3116L1.82883 18.9156V19.505Z" fill="white"/>
<path d="M3.14743 38.7512V37.5746C3.14743 37.5447 3.12897 37.5171 3.10129 37.5055L1.64323 36.877C1.59248 36.8562 1.53711 36.8931 1.53711 36.946V38.1226C1.53711 38.1526 1.55557 38.1802 1.58325 38.1917L3.039 38.8203C3.08975 38.841 3.14512 38.8042 3.14512 38.7512H3.14743Z" fill="white"/>
<path d="M3.0713 39.1721C3.01362 39.1721 2.95825 39.1606 2.90519 39.1376L1.44713 38.509C1.29256 38.4422 1.19336 38.2902 1.19336 38.1221V36.9455C1.19336 36.8051 1.26257 36.6715 1.38254 36.5933C1.5025 36.515 1.65015 36.5012 1.78166 36.5587L3.23971 37.1873C3.39428 37.2541 3.49349 37.4061 3.49349 37.5741V38.7507C3.49349 38.8935 3.42197 39.0247 3.30431 39.103C3.2351 39.1491 3.15435 39.1721 3.0713 39.1721ZM1.88317 37.9448L2.80137 38.3409V37.7514L1.88317 37.3554V37.9448Z" fill="white"/>
<path d="M22.415 26.6204V25.4438C22.415 25.4138 22.3965 25.3862 22.3689 25.3747L20.9108 24.7461C20.8601 24.7254 20.8047 24.7622 20.8047 24.8152V25.9918C20.8047 26.0217 20.8231 26.0493 20.8508 26.0608L22.3066 26.6894C22.3573 26.7102 22.4127 26.6733 22.4127 26.6204H22.415Z" fill="white"/>
<path d="M22.3392 27.0412C22.2816 27.0412 22.2262 27.0297 22.1708 27.0067L20.7151 26.3781C20.5605 26.3113 20.459 26.1594 20.459 25.9913V24.8147C20.459 24.6719 20.5305 24.5407 20.6482 24.4624C20.7681 24.3841 20.9158 24.3703 21.0473 24.4279L22.5053 25.0565C22.6599 25.1232 22.7614 25.2752 22.7614 25.4433V26.6199C22.7614 26.7626 22.6899 26.8939 22.5699 26.9722C22.5007 27.0182 22.42 27.0412 22.3392 27.0412ZM21.1511 25.814L22.0693 26.21V25.6206L21.1511 25.2245V25.814Z" fill="white"/>
<path d="M22.4677 45.0618V43.8852C22.4677 43.8552 22.4493 43.8276 22.4216 43.8161L20.9635 43.1875C20.9128 43.1668 20.8574 43.2036 20.8574 43.2566V44.4332C20.8574 44.4631 20.8759 44.4907 20.9036 44.5023L22.3593 45.1308C22.4101 45.1516 22.4654 45.1147 22.4654 45.0618H22.4677Z" fill="white"/>
<path d="M22.3916 45.4823C22.3339 45.4823 22.2786 45.4708 22.2232 45.4478L20.7674 44.8215C20.6129 44.7547 20.5137 44.6027 20.5137 44.4346V43.258C20.5137 43.1176 20.5829 42.984 20.7028 42.9058C20.8228 42.8275 20.9728 42.8137 21.102 42.8712L22.56 43.4998C22.7146 43.5666 22.8138 43.7186 22.8138 43.8866V45.0632C22.8138 45.206 22.7423 45.3372 22.6223 45.4155C22.5531 45.4616 22.4724 45.4846 22.3916 45.4846V45.4823ZM21.2035 44.255L22.1217 44.6511V44.0616L21.2035 43.6656V44.255Z" fill="white"/>
<path d="M8.47164 24.259C6.02617 25.2099 5.44248 29.5617 7.16585 33.9803C8.88921 38.3989 12.269 41.2126 14.7145 40.2616C17.16 39.3107 17.7437 34.9589 16.0203 30.5403C14.2969 26.1217 10.9171 23.308 8.47164 24.259ZM12.5989 32.3478C12.5713 32.2442 12.5367 32.1406 12.4974 32.037C12.4144 31.8228 12.3129 31.6294 12.1975 31.4567L12.7835 27.0911C13.7248 28.0098 14.5876 29.3177 15.1944 30.8742C15.7827 32.3823 16.0364 33.8813 15.9857 35.1661L12.5989 32.3478ZM9.10377 25.804C9.92738 25.4839 10.8802 25.6935 11.8122 26.3036L11.2216 30.6946C11.1178 30.6808 11.0163 30.69 10.924 30.7268C10.8664 30.7498 10.8156 30.7821 10.7671 30.8212L7.43347 28.0467C7.70339 26.9437 8.26862 26.1263 9.10377 25.804ZM8.04945 33.651C7.45192 32.1152 7.19815 30.591 7.26044 29.29L10.458 31.9518C10.4811 32.205 10.5457 32.479 10.6518 32.753C10.7187 32.9234 10.7971 33.08 10.8825 33.2251L10.3334 37.3075C9.4406 36.398 8.62852 35.1362 8.04945 33.651ZM11.3001 38.1341L11.8445 34.077C11.976 34.1092 12.1052 34.1069 12.2229 34.0609C12.3752 34.001 12.4905 33.879 12.5713 33.7132L15.8219 36.4187C15.5566 37.5515 14.9867 38.3897 14.1377 38.7212C13.2772 39.0551 12.2713 38.811 11.2978 38.1341H11.3001Z" fill="white"/>
<path d="M12.1448 31.9302C12.5924 32.6509 12.5878 33.4591 12.1656 33.7147C11.7434 33.9703 11.0167 33.595 10.6199 32.8466C10.1977 32.0523 10.1885 31.3339 10.583 31.0852C11.0421 30.7951 11.6904 31.1957 12.1471 31.9325L12.1448 31.9302Z" fill="#3FA79E"/>
</g>
<defs>
<clipPath id="clip0_3466_113133">
<rect width="44" height="56" fill="white"/>
</clipPath>
</defs>
</svg>
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" rx="128" fill="white"/>
<path d="M185.067 93.7936C185.647 94.7771 186.831 95.2561 187.94 94.9282L206.362 89.4058C207.345 89.1284 208 88.2206 208 87.2372V52.4634C208 49.9922 206.009 48 203.539 48H51.7297C49.4869 48 48.126 49.5131 48.126 51.7069L48 131.341V203.537C48 206.008 49.9909 208 52.4606 208H204.27C206.513 208 207.874 206.487 207.874 204.268L208 124.659V124.407C208 121.456 207.572 119.414 203.565 120.548L196.332 122.692C195.173 123.045 194.417 124.129 194.467 125.314L194.593 127.987C194.593 159.685 178.087 182.43 148.955 191.357C101.3 205.957 57.8283 168.536 61.6841 122.213C64.3554 90.4145 89.909 64.5925 121.662 61.6674C148.526 59.1962 172.442 72.6872 185.042 93.7431L185.067 93.7936Z" fill="#005BCB"/>
<path d="M153.011 159.962C159.739 155.322 164.931 148.615 167.677 140.772C168.408 138.629 168.988 136.385 169.341 134.09C169.567 132.703 168.257 131.543 166.921 131.972L154.548 135.855C154.019 136.032 153.59 136.435 153.389 136.965C149.482 146.724 139.402 153.305 128.011 151.918C116.898 150.556 108.002 141.327 107.019 130.181C106.691 126.449 107.221 122.869 108.405 119.616C111.807 110.437 120.653 103.88 131.01 103.88C136.857 103.88 142.225 105.973 146.408 109.453C146.887 109.857 147.542 110.008 148.172 109.806L159.916 106.125C160.571 105.923 160.999 105.419 161.176 104.864C161.327 104.284 161.251 103.628 160.823 103.124C153.237 94.0206 141.544 88.4478 128.566 89.2295C128.415 89.2547 128.263 89.2799 128.112 89.2799L124.105 76.5202C123.601 74.8559 121.837 73.9228 120.174 74.4272L111.051 77.2767C109.388 77.8062 108.455 79.5714 108.985 81.2357L111.958 90.7425C112.488 92.4068 111.832 94.2224 110.371 95.1554C103.138 99.7448 97.4679 106.654 94.5194 114.824C93.3098 118.103 92.5538 121.582 92.3017 125.188C92.2261 126.525 92.2009 127.836 92.2513 129.122C92.3269 130.862 91.1929 132.426 89.5297 132.955L79.4997 136.183C77.8365 136.713 76.904 138.478 77.4081 140.142L80.2558 149.27C80.785 150.935 82.5491 151.842 84.2123 151.338L94.4186 148.035C96.0315 147.53 97.8207 148.11 98.7532 149.523C105.709 159.937 117.553 166.821 131.01 166.821C131.59 166.821 132.144 166.821 132.699 166.821C134.463 166.72 136.05 167.855 136.58 169.519L139.679 179.505C140.208 181.17 141.973 182.103 143.636 181.573L152.758 178.724C154.422 178.219 155.329 176.429 154.825 174.765L151.549 164.325C151.045 162.711 151.624 160.946 153.011 159.988V159.962Z" fill="#005BCB"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,15 +1,15 @@
{
"short_name": "lecoffre",
"name": "lecoffre",
"icons": [
{
"src": "/favicon.ico",
"sizes": "32x32 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "light",
"background_color": "light"
"short_name": "lecoffre",
"name": "lecoffre",
"icons": [
{
"src": "/favicon.svg",
"sizes": "32x32 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "light",
"background_color": "light"
}

View File

@ -1,22 +0,0 @@
<svg width="44" height="56" viewBox="0 0 44 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3026_107168)">
<path d="M43.8828 9.98633C43.9035 9.4084 43.5713 8.81895 42.8538 8.51041L24.3282 0.58968C22.8379 -0.0481226 21.0845 -0.0481226 19.5942 0.58968L1.17241 8.46666C0.443378 8.7798 0.0996282 9.37616 0.115778 9.96331V46.1454L4.25693 48.091L6.73008 46.9098L18.4499 51.7336V54.1858L22.0396 55.869L25.6617 54.1398V51.7544L37.3584 46.9397L39.8201 48.0887L43.8874 46.1477V9.98633H43.8828Z" fill="#C5B2D4"/>
<path d="M22.0369 55.9998L18.3318 54.2614V51.8115L6.73196 47.036L4.2565 48.2172L0 46.2209V9.85003C0.0322987 9.21222 0.454488 8.6481 1.12584 8.36259L19.5499 0.483307C21.061 -0.163706 22.8651 -0.163706 24.374 0.483307L42.8995 8.40403C43.5617 8.68725 43.9723 9.24216 43.9977 9.87075V46.2232L39.8173 48.2195L37.3534 47.0682L25.7743 51.8345V54.2153L22.0369 55.9998ZM18.5625 54.1163L22.0369 55.7465L25.5459 54.0725V51.6825L37.3626 46.8173L39.8196 47.9639L43.7716 46.0781V10.101H43.767V9.98127C43.7924 9.39412 43.4325 8.88296 42.8096 8.61587L24.284 0.69514C22.8305 0.073455 21.0956 0.073455 19.6422 0.69514L1.21812 8.57212C0.583683 8.84382 0.216862 9.36189 0.233012 9.96055V10.078V46.0712L4.25881 47.9616L6.72966 46.7827L18.5671 51.6549V54.1117L18.5625 54.1163Z" fill="#4E1480"/>
<path d="M18.0574 49.0688C17.5222 49.0688 16.9823 48.9629 16.4724 48.7441L4.4412 43.6325C2.94854 42.9993 1.98419 41.5441 1.98419 39.9254L1.97266 17.4319C1.97266 16.0711 2.65093 14.8116 3.79061 14.0633C4.93029 13.315 6.35835 13.1906 7.61108 13.7294L19.6539 18.9171C21.135 19.5549 22.0924 21.0078 22.0924 22.6195V45.037C22.0924 46.3932 21.4141 47.6504 20.2814 48.401C19.61 48.8454 18.8349 49.0711 18.0551 49.0711L18.0574 49.0688ZM6.00999 14.0863C5.36632 14.0863 4.72727 14.2728 4.17127 14.6366C3.22769 15.256 2.66246 16.3014 2.66477 17.4296L2.67631 39.9231C2.67631 41.2632 3.47685 42.4697 4.71343 42.9947L16.7447 48.1063C17.7805 48.5461 18.9618 48.4402 19.9007 47.8185C20.842 47.1991 21.4026 46.1561 21.4026 45.0324V22.6149C21.4026 21.2795 20.609 20.0775 19.3816 19.5479L7.33885 14.3603C6.91204 14.1761 6.45986 14.0863 6.00999 14.0863Z" fill="white"/>
<path d="M3.09274 20.3118V19.1352C3.09274 19.1052 3.07428 19.0776 3.0466 19.0661L1.58855 18.4375C1.53779 18.4168 1.48242 18.4536 1.48242 18.5066V19.6832C1.48242 19.7131 1.50088 19.7407 1.52856 19.7523L2.98431 20.3808C3.03506 20.4016 3.09043 20.3647 3.09043 20.3118H3.09274Z" fill="white"/>
<path d="M3.01696 20.7323C2.95929 20.7323 2.90392 20.7208 2.84855 20.6978L1.3928 20.0715C1.23823 20.0047 1.13672 19.8527 1.13672 19.6846V18.508C1.13672 18.3653 1.20824 18.234 1.3259 18.1558C1.44586 18.0775 1.59582 18.0637 1.72502 18.1212L3.18307 18.7498C3.33764 18.8166 3.43915 18.9686 3.43915 19.1366V20.3132C3.43915 20.456 3.36763 20.5872 3.24767 20.6655C3.17846 20.7116 3.09771 20.7346 3.01696 20.7346V20.7323ZM1.82883 19.505L2.74704 19.9011V19.3116L1.82883 18.9156V19.505Z" fill="white"/>
<path d="M3.14743 38.7512V37.5746C3.14743 37.5447 3.12897 37.5171 3.10129 37.5055L1.64323 36.877C1.59248 36.8562 1.53711 36.8931 1.53711 36.946V38.1226C1.53711 38.1526 1.55557 38.1802 1.58325 38.1917L3.039 38.8203C3.08975 38.841 3.14512 38.8042 3.14512 38.7512H3.14743Z" fill="white"/>
<path d="M3.0713 39.1721C3.01362 39.1721 2.95825 39.1606 2.90519 39.1376L1.44713 38.509C1.29256 38.4422 1.19336 38.2902 1.19336 38.1221V36.9455C1.19336 36.8051 1.26257 36.6715 1.38254 36.5933C1.5025 36.515 1.65015 36.5012 1.78166 36.5587L3.23971 37.1873C3.39428 37.2541 3.49349 37.4061 3.49349 37.5741V38.7507C3.49349 38.8935 3.42197 39.0247 3.30431 39.103C3.2351 39.1491 3.15435 39.1721 3.0713 39.1721ZM1.88317 37.9448L2.80137 38.3409V37.7514L1.88317 37.3554V37.9448Z" fill="white"/>
<path d="M22.415 26.6204V25.4438C22.415 25.4138 22.3965 25.3862 22.3689 25.3747L20.9108 24.7461C20.8601 24.7254 20.8047 24.7622 20.8047 24.8152V25.9918C20.8047 26.0217 20.8231 26.0493 20.8508 26.0608L22.3066 26.6894C22.3573 26.7102 22.4127 26.6733 22.4127 26.6204H22.415Z" fill="white"/>
<path d="M22.3392 27.0412C22.2816 27.0412 22.2262 27.0297 22.1708 27.0067L20.7151 26.3781C20.5605 26.3113 20.459 26.1594 20.459 25.9913V24.8147C20.459 24.6719 20.5305 24.5407 20.6482 24.4624C20.7681 24.3841 20.9158 24.3703 21.0473 24.4279L22.5053 25.0565C22.6599 25.1232 22.7614 25.2752 22.7614 25.4433V26.6199C22.7614 26.7626 22.6899 26.8939 22.5699 26.9722C22.5007 27.0182 22.42 27.0412 22.3392 27.0412ZM21.1511 25.814L22.0693 26.21V25.6206L21.1511 25.2245V25.814Z" fill="white"/>
<path d="M22.4677 45.0618V43.8852C22.4677 43.8552 22.4493 43.8276 22.4216 43.8161L20.9635 43.1875C20.9128 43.1668 20.8574 43.2036 20.8574 43.2566V44.4332C20.8574 44.4631 20.8759 44.4907 20.9036 44.5023L22.3593 45.1308C22.4101 45.1516 22.4654 45.1147 22.4654 45.0618H22.4677Z" fill="white"/>
<path d="M22.3916 45.4823C22.3339 45.4823 22.2786 45.4708 22.2232 45.4478L20.7674 44.8215C20.6129 44.7547 20.5137 44.6027 20.5137 44.4346V43.258C20.5137 43.1176 20.5829 42.984 20.7028 42.9058C20.8228 42.8275 20.9728 42.8137 21.102 42.8712L22.56 43.4998C22.7146 43.5666 22.8138 43.7186 22.8138 43.8866V45.0632C22.8138 45.206 22.7423 45.3372 22.6223 45.4155C22.5531 45.4616 22.4724 45.4846 22.3916 45.4846V45.4823ZM21.2035 44.255L22.1217 44.6511V44.0616L21.2035 43.6656V44.255Z" fill="white"/>
<path d="M8.47164 24.259C6.02617 25.2099 5.44248 29.5617 7.16585 33.9803C8.88921 38.3989 12.269 41.2126 14.7145 40.2616C17.16 39.3107 17.7437 34.9589 16.0203 30.5403C14.2969 26.1217 10.9171 23.308 8.47164 24.259ZM12.5989 32.3478C12.5713 32.2442 12.5367 32.1406 12.4974 32.037C12.4144 31.8228 12.3129 31.6294 12.1975 31.4567L12.7835 27.0911C13.7248 28.0098 14.5876 29.3177 15.1944 30.8742C15.7827 32.3823 16.0364 33.8813 15.9857 35.1661L12.5989 32.3478ZM9.10377 25.804C9.92738 25.4839 10.8802 25.6935 11.8122 26.3036L11.2216 30.6946C11.1178 30.6808 11.0163 30.69 10.924 30.7268C10.8664 30.7498 10.8156 30.7821 10.7671 30.8212L7.43347 28.0467C7.70339 26.9437 8.26862 26.1263 9.10377 25.804ZM8.04945 33.651C7.45192 32.1152 7.19815 30.591 7.26044 29.29L10.458 31.9518C10.4811 32.205 10.5457 32.479 10.6518 32.753C10.7187 32.9234 10.7971 33.08 10.8825 33.2251L10.3334 37.3075C9.4406 36.398 8.62852 35.1362 8.04945 33.651ZM11.3001 38.1341L11.8445 34.077C11.976 34.1092 12.1052 34.1069 12.2229 34.0609C12.3752 34.001 12.4905 33.879 12.5713 33.7132L15.8219 36.4187C15.5566 37.5515 14.9867 38.3897 14.1377 38.7212C13.2772 39.0551 12.2713 38.811 11.2978 38.1341H11.3001Z" fill="white"/>
<path d="M12.1448 31.9302C12.5924 32.6509 12.5878 33.4591 12.1656 33.7147C11.7434 33.9703 11.0167 33.595 10.6199 32.8466C10.1977 32.0523 10.1885 31.3339 10.583 31.0852C11.0421 30.7951 11.6904 31.1957 12.1471 31.9325L12.1448 31.9302Z" fill="#3FA79E"/>
</g>
<defs>
<clipPath id="clip0_3026_107168">
<rect width="44" height="56" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.87891 7.51884C11.0505 6.49372 12.95 6.49372 14.1215 7.51884C15.2931 8.54397 15.2931 10.206 14.1215 11.2312C13.9176 11.4096 13.6917 11.5569 13.4513 11.6733C12.7056 12.0341 12.0002 12.6716 12.0002 13.5V14.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 17.25H12.0075V17.2575H12V17.25Z" stroke="#24282E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2" height="14" viewBox="0 0 2 14" fill="none">
<path d="M1 1L0.999999 13" stroke="#47535D" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@ -0,0 +1,7 @@
@import "@Themes/constants.scss";
.root {
.label {
padding: 0px var(--spacing-2, 16px);
}
}

View File

@ -0,0 +1,94 @@
import useOpenable from "@Front/Hooks/useOpenable";
import { useCallback, useEffect, useState } from "react";
import DropdownMenu from "../Dropdown/DropdownMenu";
import { IOption } from "../Dropdown/DropdownMenu/DropdownOption";
import SearchBar from "../SearchBar";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import { getLabel } from "../Dropdown";
type IProps = {
options: IOption[];
placeholder?: string;
disabled?: boolean;
label?: string;
onSelectionChange?: (option: IOption | null) => void;
selectedOption?: IOption | null;
};
export default function Autocomplete(props: IProps) {
const { onSelectionChange, options, placeholder, disabled, label, selectedOption: selectedOptionProps } = props;
const [selectedOption, setSelectedOption] = useState<IOption | null>(selectedOptionProps ?? null);
const [searchValue, setSearchValue] = useState("");
const [filteredOptions, setFilteredOptions] = useState<IOption[]>(options);
const openable = useOpenable({ defaultOpen: false });
useEffect(() => {
if (searchValue) {
const filteredOptions = options.filter((option) => getLabel(option)?.toLowerCase().includes(searchValue.toLowerCase()));
console.log(filteredOptions);
if (filteredOptions.length === 0)
return setFilteredOptions([{ id: "no-results", label: "Aucun résulats", notSelectable: true }]);
return setFilteredOptions(filteredOptions);
}
return setFilteredOptions(options);
}, [searchValue, options]);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
if (value) {
openable.open();
} else {
openable.close();
}
},
[openable],
);
const handleChange = useCallback(
(option: IOption | null) => {
setSelectedOption(option);
onSelectionChange?.(option);
},
[onSelectionChange],
);
useEffect(() => {
setSelectedOption(selectedOptionProps ?? null);
}, [selectedOptionProps]);
const handleSelectOption = useCallback(
(newOption: IOption, _options: IOption[]) => {
handleChange(newOption);
setSearchValue(getLabel(newOption) || "");
openable.close();
},
[handleChange, openable],
);
return (
<DropdownMenu
options={filteredOptions}
openable={openable}
onSelect={handleSelectOption}
selectedOptions={selectedOption ? [selectedOption] : []}>
<div className={classes["root"]}>
{label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.CONTRAST_DEFAULT}>
{label}
</Typography>
)}
</div>
<SearchBar
placeholder={placeholder}
disabled={disabled}
onChange={handleSearchChange}
value={searchValue}
onClear={() => handleChange(null)}
onFocus={openable.open}
/>
</DropdownMenu>
);
}

View File

@ -0,0 +1,26 @@
@import "@Themes/constants.scss";
.root {
width: fit-content;
height: 32px;
display: inline-flex;
padding: 4px 12px;
align-items: center;
gap: 8px;
border-radius: var(--input-chip-radius, 360px);
border: 1px solid var(--input-chip-default-border, #b7d1f1);
background: var(--input-chip-default-background, #e5eefa);
&:hover {
background-color: var(--input-chip-hovered-background);
border-color: var(--input-chip-hovered-border);
}
.label{
display: flex;
align-items: center;
gap: 4px;
}
}

View File

@ -0,0 +1,48 @@
import { XMarkIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import React from "react";
import IconButton from "../../IconButton";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
type IProps = {
option: IOption;
className?: string;
onDelete?: () => void;
};
export default function Chip(props: IProps) {
const { className, option, onDelete } = props;
return (
<div className={classNames(classes["root"], className)}>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.INPUT_CHIP_CONTRAST}>
{getLabelContent(option)}
</Typography>
<IconButton icon={<XMarkIcon />} onClick={onDelete} />
</div>
);
}
function getLabelContent(option: IOption) {
if (typeof option.label === "string") {
return (
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.INPUT_CHIP_CONTRAST}>
{option.label}
</Typography>
);
}
return (
<span className={classes["label"]}>
<Typography typo={ETypo.TEXT_MD_LIGHT} color={ETypoColor.INPUT_CHIP_CONTRAST}>
{`${option.label.text} , `}
</Typography>
<Typography typo={ETypo.TEXT_MD_SEMIBOLD} color={ETypoColor.INPUT_CHIP_CONTRAST} type="span">
{option.label.subtext}
</Typography>
</span>
);
}

View File

@ -0,0 +1,67 @@
@import "@Themes/constants.scss";
.root {
border-radius: var(--input-radius, 0px);
border: 1px solid var(--input-main-border-default, #6d7e8a);
background: var(--input-background, #fff);
svg {
stroke: var(--button-icon-button-default-default);
}
&:hover {
border: 1px solid var(--input-main-border-hovered, #b4bec5);
}
&[data-has-value="true"] {
border: 1px solid var(--input-main-border-filled, #6d7e8a);
}
&[data-is-focused="true"] {
border: 1px solid var(--input-main-border-focused, #005bcb);
}
&[data-is-disabled="true"] {
opacity: var(--opacity-disabled, 0.3);
pointer-events: none;
}
.content {
display: flex;
align-items: center;
align-content: center;
gap: 16px var(--spacing-2, 16px);
flex-wrap: wrap;
min-height: 56px;
padding: var(--spacing-1-5, 12px) var(--spacing-sm, 8px);
.input {
flex: 1;
border: none;
color: var(--input-placeholder-filled, #24282e);
/* text/md/semibold */
font-family: var(--font-text-family, Poppins);
font-size: 16px;
font-style: normal;
font-weight: var(--font-text-weight-semibold, 600);
line-height: normal;
letter-spacing: 0.08px;
width: 100%;
&::placeholder {
color: var(--input-placeholder-empty, #6d7e8a);
/* text/md/regular */
font-family: var(--font-text-family, Poppins);
font-size: 16px;
font-style: normal;
font-weight: var(--font-text-weight-regular, 400);
line-height: normal;
letter-spacing: 0.08px;
}
}
}
}

View File

@ -0,0 +1,93 @@
import React, { useCallback, useEffect } from "react";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
import Chip from "../Chip";
import classes from "./classes.module.scss";
type IProps = {
selectedOptions: IOption[];
onSelectedOptionsChange: (options: IOption[]) => void;
onChange?: (input: string) => void;
searchValue?: string;
placeholder?: string;
disabled?: boolean;
onClear?: () => void;
onFocus?: () => void;
onBlur?: () => void;
};
export default function ChipContainer(props: IProps) {
const {
selectedOptions,
onChange,
searchValue,
placeholder = "Rechercher",
disabled = false,
onFocus,
onBlur,
onSelectedOptionsChange,
} = props;
const [isFocused, setIsFocused] = React.useState(false);
const [value, setValue] = React.useState(searchValue || "");
const changeValue = useCallback(
(value: string) => {
setValue(value);
onChange && onChange(value);
},
[onChange],
);
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => changeValue(event.target.value), [changeValue]);
const handleFocus = useCallback(() => {
setIsFocused(true);
onFocus?.();
}, [onFocus]);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement, Element>) => {
setIsFocused(false);
onBlur?.();
},
[onBlur],
);
const onChipDelete = useCallback(
(option: IOption) => {
const newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.id !== option.id);
onSelectedOptionsChange && onSelectedOptionsChange(newSelectedOptions);
},
[selectedOptions, onSelectedOptionsChange],
);
useEffect(() => {
if (searchValue !== undefined) {
setValue(searchValue);
}
}, [searchValue]);
return (
<div
className={classes["root"]}
data-is-focused={isFocused}
data-has-value={selectedOptions && selectedOptions.length > 0}
data-is-disabled={disabled}>
<div className={classes["content"]}>
{selectedOptions.map((option) => (
<Chip key={option.id} option={option} onDelete={() => onChipDelete(option)} />
))}
<input
type="text"
value={value}
placeholder={placeholder}
className={classes["input"]}
onChange={handleOnChange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
@import "@Themes/constants.scss";
.root {
.label {
padding: 0px var(--spacing-2, 16px);
}
}

View File

@ -0,0 +1,95 @@
import useOpenable from "@Front/Hooks/useOpenable";
import { useCallback, useEffect, useState } from "react";
import { getLabel } from "../Dropdown";
import DropdownMenu from "../Dropdown/DropdownMenu";
import { IOption } from "../Dropdown/DropdownMenu/DropdownOption";
import Typography, { ETypo, ETypoColor } from "../Typography";
import ChipContainer from "./ChipContainer";
import classes from "./classes.module.scss";
type IProps = {
options: IOption[];
placeholder?: string;
disabled?: boolean;
label?: string;
onSelectionChange?: (options: IOption[] | null) => void;
selectedOptions?: IOption[] | null;
};
export default function AutocompleteMultiSelect(props: IProps) {
const { onSelectionChange, options, placeholder, disabled, label, selectedOptions: selectedOptionsProps } = props;
const [selectedOptions, setSelectedOptions] = useState<IOption[] | null>(selectedOptionsProps ?? null);
const [searchValue, setSearchValue] = useState("");
const [filteredOptions, setFilteredOptions] = useState<IOption[]>(options);
const openable = useOpenable({ defaultOpen: false });
useEffect(() => {
if (searchValue) {
const filteredOptions = options.filter((option) => getLabel(option)?.toLowerCase().includes(searchValue.toLowerCase()));
if (filteredOptions.length === 0)
return setFilteredOptions([{ id: "no-results", label: "Aucun résulats", notSelectable: true }]);
return setFilteredOptions(filteredOptions);
}
return setFilteredOptions(options);
}, [searchValue, options]);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
if (value) {
openable.open();
} else {
openable.close();
}
},
[openable],
);
const handleChange = useCallback(
(options: IOption[] | null) => {
setSelectedOptions(options);
onSelectionChange?.(options);
},
[onSelectionChange],
);
useEffect(() => {
setSelectedOptions(selectedOptionsProps ?? null);
}, [selectedOptionsProps]);
const handleSelectOption = useCallback(
(_newOption: IOption, options: IOption[]) => {
handleChange(options);
setSearchValue("");
openable.close();
},
[handleChange, openable],
);
return (
<DropdownMenu
options={filteredOptions}
openable={openable}
onSelect={handleSelectOption}
selectedOptions={selectedOptions ? selectedOptions : []}>
<div className={classes["root"]}>
{label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.CONTRAST_DEFAULT}>
{label}
</Typography>
)}
</div>
<ChipContainer
placeholder={placeholder}
disabled={disabled}
onChange={handleSearchChange}
searchValue={searchValue}
onClear={() => handleChange(null)}
onFocus={openable.open}
selectedOptions={selectedOptions ?? []}
onSelectedOptionsChange={handleChange}
/>
</DropdownMenu>
);
}

View File

@ -677,7 +677,8 @@
text-transform: inherit;
}
&[disabled="true"] {
&:disabled {
opacity: var(--opacity-disabled, 0.3);
pointer-events: none;
}
}

View File

@ -2,6 +2,7 @@ import classNames from "classnames";
import React from "react";
import classes from "./classes.module.scss";
import Loader from "../Loader";
export enum EButtonVariant {
PRIMARY = "primary",
@ -57,18 +58,23 @@ export default function Button(props: IButtonProps) {
} = props;
const fullwidthattr = fullwidth.toString();
const isloadingattr = isLoading.toString();
const attributes = { ...props, variant, disabled, type, isloadingattr, fullwidthattr, sizing: size, styletype };
const attributes = { ...props, variant, disabled, type, fullwidthattr, sizing: size, styletype };
delete attributes.fullwidth;
delete attributes.leftIcon;
delete attributes.rightIcon;
delete attributes.isLoading;
return (
<button {...attributes} onClick={onClick} className={classNames(classes["root"], className)} type={type}>
{leftIcon}
<button
{...attributes}
disabled={disabled || isLoading}
onClick={onClick}
className={classNames(classes["root"], className)}
type={type}>
{!isLoading && leftIcon}
{children}
{rightIcon}
{!isLoading ? rightIcon : <Loader />}
</button>
);
}

View File

@ -3,43 +3,77 @@
.root {
cursor: pointer;
display: flex;
align-items: center;
&.disabled {
cursor: not-allowed;
}
align-items: flex-start;
input[type="checkbox"] {
appearance: none;
background-color: transparent;
width: 16px;
height: 16px;
border: 1px solid var(--color-secondary-500);
border-radius: 2px;
margin-right: 16px;
display: grid;
place-content: center;
color: var(--text-primary, #24282e);
font-size: 16px;
font-style: normal;
font-weight: var(--font-text-weight-regular, 400);
line-height: normal;
letter-spacing: 0.08px;
gap: var(--Radius-lg, 16px);
&:disabled {
cursor: not-allowed;
.content {
cursor: pointer;
display: flex;
flex-direction: column;
.label {
color: var(--text-primary, #24282e);
font-weight: var(--font-text-weight-regular, 400);
}
.description {
color: var(--text-secondary, #47535d);
font-weight: var(--font-text-weight-light, 300);
}
}
input[type="checkbox"]::before {
content: url("../../../Assets/Icons/check_white.svg");
place-content: flex-start;
display: grid;
width: 16px;
height: 16px;
background-color: var(--color-secondary-500);
border-radius: 2px;
transform: scale(0);
input[type="checkbox"] {
accent-color: var(--select-option-selected-default-background);
cursor: pointer;
display: flex;
width: 20px;
height: 20px;
min-width: 20px;
max-width: 20px;
flex-direction: column;
align-items: flex-start;
margin-right: 16px;
gap: 8px;
border-radius: var(--radius-xs, 2px);
border: 2px solid var(--select-option-unselected-default-border, #6d7e8a);
&:hover {
border: 2px solid var(--select-option-unselected-pressed-border, #3e474e);
accent-color: var(--select-option-selected-hovered-background);
}
&:active {
border: 2px solid var(--select-option-unselected-pressed-border, #3e474e);
accent-color: var(--select-option-selected-pressed-background);
}
}
input[type="checkbox"]:checked::before {
transform: scale(1);
&.disabled {
pointer-events: none;
cursor: not-allowed;
opacity: var(--opacity-disabled, 0.3);
}
.tooltip {
margin-left: 16px;
}
// input[type="checkbox"]::before {
// content: url("../../../Assets/Icons/check_white.svg");
// place-content: flex-start;
// display: grid;
// width: 16px;
// height: 16px;
// background-color: var(--color-secondary-500);
// border-radius: 2px;
// transform: scale(0);
// }
// input[type="checkbox"]:checked::before {
// transform: scale(1);
// }
// .tooltip {
// }
}

View File

@ -1,14 +1,14 @@
import React from "react";
import { IOption } from "../Form/SelectField";
import Tooltip from "../ToolTip";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import classNames from "classnames";
import { IOptionOld } from "../Form/SelectFieldOld";
type IProps = {
name?: string;
option: IOption;
option: IOptionOld;
toolTip?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
checked: boolean;
@ -45,9 +45,20 @@ export default class CheckBox extends React.Component<IProps, IState> {
value={this.props.option.value as string}
onChange={this.onChange}
checked={this.state.checked}
disabled={this.props.disabled}
/>
{this.props.option.label}
<div className={classes["content"]}>
{this.props.option.label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR}>
{this.props.option.label}
</Typography>
)}
{this.props.option.description && (
<Typography className={classes["description"]} typo={ETypo.TEXT_MD_LIGHT}>
{this.props.option.description}
</Typography>
)}
</div>
{this.props.toolTip && <Tooltip className={classes["tooltip"]} text={this.props.toolTip} />}
</label>
</Typography>

View File

@ -0,0 +1,38 @@
.root {
display: flex;
padding: var(--spacing-1, 8px) var(--spacing-2, 16px);
align-items: center;
gap: var(--spacing-sm, 8px);
justify-content: space-between;
border-radius: var(--dropdown-radius, 0px);
border: 1px solid var(--dropdown-border, rgba(0, 0, 0, 0));
background: var(--dropdown-option-background-default, #fff);
svg {
width: 24px;
height: 24px;
}
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1 0 0;
}
&:hover {
background-color: var(--dropdown-option-background-hovered);
}
&:focus,
&:active {
background-color: var(--dropdown-option-background-pressed);
}
&[data-not-selectable="true"] {
pointer-events: none;
user-select: none;
}
}

View File

@ -0,0 +1,56 @@
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { CheckIcon } from "@heroicons/react/24/outline";
import { useCallback } from "react";
import classes from "./classes.module.scss";
export type IOption = {
id: string;
label: string | { text: string; subtext: string };
notSelectable?: boolean;
};
type IProps = {
option: IOption;
isActive: boolean;
onClick?: (option: IOption) => void;
};
export default function DropdownOption(props: IProps) {
const { option, onClick, isActive } = props;
const handleOnClick = useCallback(() => onClick && onClick(option), [onClick, option]);
return (
<div className={classes["root"]} data-not-selectable={!!option.notSelectable} onClick={handleOnClick}>
{getContent(option.label)}
{isActive && <CheckIcon />}
</div>
);
function getContent(label: string | { text: string; subtext: string }) {
if (typeof label === "string") {
return (
<Typography
typo={isActive ? ETypo.TEXT_MD_SEMIBOLD : ETypo.TEXT_MD_REGULAR}
color={isActive ? ETypoColor.DROPDOWN_CONTRAST_ACTIVE : ETypoColor.DROPDOWN_CONTRAST_DEFAULT}>
{label}
</Typography>
);
}
return (
<div className={classes["content"]}>
<Typography
typo={ETypo.TEXT_MD_LIGHT}
color={isActive ? ETypoColor.NAVIGATION_BUTTON_CONTRAST_ACTIVE : ETypoColor.NAVIGATION_BUTTON_CONTRAST_DEFAULT}>
{label.text}
</Typography>
<Typography
typo={ETypo.TEXT_MD_BOLD}
color={isActive ? ETypoColor.NAVIGATION_BUTTON_CONTRAST_ACTIVE : ETypoColor.NAVIGATION_BUTTON_CONTRAST_DEFAULT}>
{label.subtext}
</Typography>
</div>
);
}
}

View File

@ -0,0 +1,37 @@
.root {
position: relative;
overflow: hidden;
.content {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 3;
padding: var(--spacing-sm, 8px);
border-radius: var(--dropdown-radius, 0px);
background: var(--dropdown-menu-background, #fff);
border: 1px solid var(--dropdown-menu-border-primary, #005bcb);
max-height: 0;
opacity: 0;
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
position: absolute;
top: 100%;
left: 0;
transform: translateY(8px);
}
&.open {
overflow: visible;
.content {
max-height: 500px;
overflow: auto;
opacity: 1;
}
}
}

View File

@ -0,0 +1,63 @@
import classNames from "classnames";
import React, { useCallback, useEffect, useRef } from "react";
import classes from "./classes.module.scss";
import DropdownOption, { IOption } from "./DropdownOption";
type IProps = {
options: IOption[];
selectedOptions: IOption[];
children: React.ReactNode;
openable: {
isOpen: boolean;
open: () => void;
close: () => void;
toggle: () => void;
};
onSelect?: (newOption: IOption, options: IOption[]) => void;
};
export default function DropdownMenu(props: IProps) {
const { children, options, onSelect, openable, selectedOptions } = props;
const ref = useRef<HTMLDivElement>(null);
const handleSelect = useCallback(
(option: IOption) => {
const newOptions = selectedOptions.some((selectedOption) => selectedOption.id === option.id)
? selectedOptions
: [...selectedOptions, option];
onSelect?.(option, newOptions);
openable.close();
},
[onSelect, openable, selectedOptions],
);
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
openable.close();
}
},
[openable],
);
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [handleClickOutside]);
return (
<div className={classNames([classes["root"], openable.isOpen && classes["open"]])} ref={ref}>
{children}
<div className={classes["content"]}>
{options.map((option) => {
return <DropdownOption key={option.id} option={option} onClick={handleSelect} isActive={isActive(option)} />;
})}
</div>
</div>
);
function isActive(option: IOption): boolean {
return selectedOptions.some((selectedOption) => selectedOption.id === option.id);
}
}

View File

@ -0,0 +1,65 @@
@import "@Themes/constants.scss";
.root {
.label {
padding: 0px var(--spacing-2, 16px);
}
.container {
cursor: pointer;
display: flex;
align-items: center;
height: 56px;
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
gap: var(--spacing-2, 16px);
border-radius: var(--input-radius, 0px);
border: 1px solid var(--dropdown-input-border-default, #d7dce0);
background: var(--dropdown-input-background, #fff);
.content {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
.value {
width: 100%;
display: flex;
padding: 0px var(--spacing-2, 16px);
align-items: center;
gap: 4px;
flex: 1 0 0;
}
svg {
width: 24px;
height: 24px;
cursor: pointer;
stroke: var(--button-icon-button-default-default);
}
}
&:hover {
border-color: var(--dropdown-input-border-hovered);
}
&.active {
border-color: var(--dropdown-input-border-filled);
}
&.open {
border-color: var(--dropdown-input-border-expanded);
svg {
transform: rotate(180deg);
}
}
&.disabled {
opacity: var(--opacity-disabled, 0.3);
pointer-events: none;
}
}
}

View File

@ -0,0 +1,112 @@
import useOpenable from "@Front/Hooks/useOpenable";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import { useCallback, useEffect, useState } from "react";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import DropdownMenu from "./DropdownMenu";
import { IOption } from "./DropdownMenu/DropdownOption";
type IProps = {
options: IOption[];
label?: string;
placeholder?: string;
disabled?: boolean;
onSelectionChange?: (option: IOption) => void;
selectedOption?: IOption | null;
};
export default function Dropdown(props: IProps) {
const { options, placeholder, disabled, onSelectionChange, selectedOption: selectedOptionProps, label } = props;
const [selectedOption, setSelectedOption] = useState<IOption | null>(selectedOptionProps ?? null);
const openable = useOpenable({ defaultOpen: false });
useEffect(() => {
setSelectedOption(selectedOptionProps ?? null);
}, [selectedOptionProps]);
const handleOnSelect = useCallback(
(newOption: IOption, _options: IOption[]) => {
setSelectedOption(newOption);
onSelectionChange?.(newOption);
},
[onSelectionChange],
);
return (
<DropdownMenu
options={options}
openable={openable}
onSelect={handleOnSelect}
selectedOptions={selectedOption ? [selectedOption] : []}>
<div className={classes["root"]}>
{label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.CONTRAST_DEFAULT}>
{label}
</Typography>
)}
<div
className={classNames([
classes["container"],
openable.isOpen && classes["open"],
disabled && classes["disabled"],
!!selectedOption && classes["active"],
])}
onClick={() => openable.toggle()}>
<div className={classes["content"]}>
{getLabelContent(selectedOption, placeholder)}
<ChevronDownIcon />
</div>
</div>
</div>
</DropdownMenu>
);
}
export function getLabel(option: IOption | null): string | null {
if (!option) return null;
if (typeof option.label === "string") {
return option.label;
}
return `${option.label.text}, ${option.label.subtext}`;
}
function getLabelContent(option: IOption | null, placeholder?: string) {
if (!option)
return (
<Typography
className={classes["value"]}
typo={!!option ? ETypo.TEXT_MD_SEMIBOLD : ETypo.TEXT_MD_REGULAR}
color={!!option ? ETypoColor.DROPDOWN_CONTRAST_ACTIVE : ETypoColor.DROPDOWN_CONTRAST_DEFAULT}>
{placeholder}
</Typography>
);
if (typeof option.label === "string") {
return (
<Typography
className={classes["value"]}
typo={!!option ? ETypo.TEXT_MD_SEMIBOLD : ETypo.TEXT_MD_REGULAR}
color={!!option ? ETypoColor.DROPDOWN_CONTRAST_ACTIVE : ETypoColor.DROPDOWN_CONTRAST_DEFAULT}>
{option.label}
</Typography>
);
}
return (
<span className={classes["value"]}>
<Typography
typo={ETypo.TEXT_MD_LIGHT}
color={!!option ? ETypoColor.NAVIGATION_BUTTON_CONTRAST_ACTIVE : ETypoColor.NAVIGATION_BUTTON_CONTRAST_DEFAULT}>
{`${option.label.text} , `}
</Typography>
<Typography
typo={ETypo.TEXT_MD_BOLD}
type="span"
color={!!option ? ETypoColor.NAVIGATION_BUTTON_CONTRAST_ACTIVE : ETypoColor.NAVIGATION_BUTTON_CONTRAST_DEFAULT}>
{option.label.subtext}
</Typography>
</span>
);
}

View File

@ -1,14 +0,0 @@
@import "@Themes/constants.scss";
.root {
height: calc(100vh - 290px);
overflow-y: scroll;
&.archived {
height: calc(100vh - 220px);
}
.active {
background-color: var(--color-neutral-200);
}
}

View File

@ -1,76 +0,0 @@
import Module from "@Front/Config/Module";
import classNames from "classnames";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import FolderContainer from "../FolderContainer";
import classes from "./classes.module.scss";
type IProps = {
folders: OfficeFolder[];
isArchived: boolean;
onSelectedFolder?: (folder: OfficeFolder) => void;
onCloseLeftSide?: () => void;
};
type IPropsClass = IProps & {
selectedFolder: string;
};
type IState = {};
class FolderListClass extends React.Component<IPropsClass, IState> {
private redirectPath: string = this.props.isArchived
? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path
: Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path;
public override render(): JSX.Element {
return <div className={classNames(classes["root"], this.props.isArchived ? classes["archived"] : "")}>{this.renderFolders()}</div>;
}
private renderFolders(): JSX.Element[] {
const pendingFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length >= 1;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
const otherFolders = this.props.folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length === 0;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
return [...pendingFolders, ...otherFolders].map((folder) => {
return (
<div
onClick={this.props.onCloseLeftSide}
key={folder.uid}
className={folder.uid === this.props.selectedFolder ? classes["active"] : ""}>
<Link href={this.redirectPath.replace("[folderUid]", folder.uid ?? "")}>
<FolderContainer folder={folder} onSelectedFolder={this.props.onSelectedFolder} />;
</Link>
</div>
);
});
}
}
export default function FolderList(props: IProps) {
const router = useRouter();
let { folderUid } = router.query;
folderUid = folderUid as string;
return <FolderListClass {...props} selectedFolder={folderUid} />;
}

View File

@ -1,23 +0,0 @@
@import "@Themes/constants.scss";
.root {
width: calc(100vh - 83px);
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: calc(100vh - 290px);
height: calc(100vh - 290px);
overflow: auto;
border-right: 1px solid var(--color-neutral-200);
}
}

View File

@ -1,91 +0,0 @@
import Module from "@Front/Config/Module";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import { useRouter } from "next/router";
import React, { useCallback, useEffect } from "react";
import classes from "./classes.module.scss";
import { IBlock } from "../SearchBlockList/BlockList/Block";
import SearchBlockList from "../SearchBlockList";
type IProps = {
folders: OfficeFolder[];
isArchived: boolean;
onSelectedFolder?: (folder: OfficeFolder) => void;
onCloseLeftSide?: () => void;
};
export default function FolderListContainer(props: IProps) {
const router = useRouter();
const { folderUid } = router.query;
const { folders, isArchived } = props;
const redirectPath: string = isArchived
? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path
: Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path;
const getBlocks = useCallback(
(folders: OfficeFolder[]): IBlock[] => {
const pendingFolders = folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length >= 1;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
const otherFolders = folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length === 0;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
return [...pendingFolders, ...otherFolders].map((folder) => {
return {
id: folder.uid!,
primaryText: folder.name,
secondaryText: folder.folder_number,
isActive: folderUid === folder.uid,
showAlert: folder.documents?.some((document) => document.document_status === EDocumentStatus.DEPOSITED),
};
});
},
[folderUid],
);
const [blocks, setBlocks] = React.useState<IBlock[]>(getBlocks(folders));
const onSelectedFolder = (block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const folder = folders.find((folder) => folder.uid === block.id);
if (!folder) return;
props.onSelectedFolder && props.onSelectedFolder(folder);
const path = redirectPath.replace("[folderUid]", folder.uid ?? "");
router.push(path);
};
useEffect(() => {
setBlocks(getBlocks(folders));
}, [folders, getBlocks]);
return (
<div className={classes["root"]}>
<SearchBlockList
blocks={blocks}
onSelectedBlock={onSelectedFolder}
bottomButton={{
link: Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
text: "Créer un dossier",
}}
/>
</div>
);
}

View File

@ -3,26 +3,33 @@
.root {
border-top: 1px solid var(--footer-border, #d7dce0);
background: var(--footer-background, #fff);
padding: var(--spacing-1-5) 0;
font-family: var(--font-title-family, Poppins);
font-size: 12px;
font-weight: var(--font-text-weight-regular, 400);
letter-spacing: 0.06px;
.sub-root {
display: flex;
align-items: center;
gap: var(--Radius-lg, 16px);
white-space: nowrap;
padding: 0 360px;
//make it sticky
}
@media (max-width: 1023px) {
padding: 0 12px;
.desktop {
padding: var(--spacing-1-5, 12px) var(--Radius-xl, 24px);
}
.tablet {
padding: var(--spacing-1-5, 12px) var(--Radius-xl, 24px);
}
.mobile {
padding: var(--spacing-1-5, 12px) var(--Radius-lg, 16px);
}
&[data-has-left-padding="true"] {
.desktop {
padding: var(--spacing-1-5, 12px) var(--spacing-15, 120px);
}
}
@media (max-width: 660px) or (min-width: 768px) {
@media (max-width: 660px) or (min-width: 1023px) {
.tablet {
display: none;
}
@ -34,7 +41,7 @@
}
}
@media (max-width: 769px) {
@media (max-width: 1023px) {
.desktop {
display: none;
}
@ -45,4 +52,11 @@
content: "|";
}
}
a {
color: var(--footer-contrast, #47535d);
font-family: var(--font-title-family, Poppins);
font-size: 12px;
font-weight: var(--font-text-weight-regular, 400);
letter-spacing: 0.06px;
}
}

View File

@ -1,20 +1,27 @@
import React from "react";
import classes from "./classes.module.scss";
import Link from "next/link";
import Module from "@Front/Config/Module";
import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import VerticalSeparator from "@Assets/Icons/vertical-separator.svg";
import Image from "next/image";
type IProps = {
className?: string;
};
const legalPages = Module.getInstance().get().modules.pages.Legal.pages.LegalInformations.props.path;
export default function Desktop({ className }: IProps) {
return (
<div className={[classes["sub-root"], className].join(" ")}>
<span>© Copyright lecoffre 2024</span>
<span className={classes["separator"]} />
<a href="/terms">Conditions d'utilisation</a>
<span className={classes["separator"]} />
<a href="/privacy">Politique de confidentialité</a>
<span className={classes["separator"]} />
<a href="/cookies">Politique des cookies</a>
<Link href={legalPages.replace("[legalUid]", ELegalOptions.LEGAL_MENTIONS)}>© Copyright lecoffre 2024</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.CGU)}>Juridiques</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE)}>Politique de confidentialité</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES)}>Politique des cookies</Link>
</div>
);
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import classes from "./classes.module.scss";
import Mobile from "./mobile";
import Desktop from "./desktop";
@ -6,11 +6,18 @@ import Tablet from "./tablet";
type IProps = {
className?: string;
hasLeftPadding?: boolean;
};
export default function Footer({ className }: IProps) {
export default function Footer({ className, hasLeftPadding = false }: IProps) {
const footerRef = React.useRef<HTMLDivElement>(null);
useEffect(() => {
if (!footerRef.current) return;
const footerHeight = footerRef.current.clientHeight + 1;
document.documentElement.style.setProperty("--footer-height", `${footerHeight}px`);
});
return (
<footer className={[classes["root"], className].join(" ")}>
<footer className={[classes["root"], className].join(" ")} data-has-left-padding={hasLeftPadding} ref={footerRef}>
<Mobile className={classes["mobile"]} />
<Tablet className={classes["tablet"]} />
<Desktop className={classes["desktop"]} />

View File

@ -1,18 +1,25 @@
import React from "react";
import classes from "./classes.module.scss";
import Link from "next/link";
import Module from "@Front/Config/Module";
import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import VerticalSeparator from "@Assets/Icons/vertical-separator.svg";
import Image from "next/image";
type IProps = {
className?: string;
};
const legalPages = Module.getInstance().get().modules.pages.Legal.pages.LegalInformations.props.path;
export default function Mobile({ className }: IProps) {
return (
<div className={[classes["sub-root"], className].join(" ")}>
<span>© Lecoffre 2024</span>
<span className={classes["separator"]} />
<a href="/terms">Juridiques</a>
<span className={classes["separator"]} />
<a href="/cookies">Cookies</a>
<Link href={legalPages.replace("[legalUid]", ELegalOptions.LEGAL_MENTIONS)}>© Lecoffre 2024</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.LEGAL_MENTIONS)}>Juridiques</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES)}>Cookies</Link>
</div>
);
}

View File

@ -1,20 +1,25 @@
import React from "react";
import classes from "./classes.module.scss";
import Module from "@Front/Config/Module";
import { ELegalOptions } from "@Front/Components/LayoutTemplates/DefaultLegalDashboard";
import Link from "next/link";
import VerticalSeparator from "@Assets/Icons/vertical-separator.svg";
import Image from "next/image";
type IProps = {
className?: string;
};
const legalPages = Module.getInstance().get().modules.pages.Legal.pages.LegalInformations.props.path;
export default function Tablet({ className }: IProps) {
return (
<div className={[classes["sub-root"], className].join(" ")}>
<span>© Lecoffre 2024</span>
<span className={classes["separator"]} />
<a href="/terms">Conditions d'utilisation</a>
<span className={classes["separator"]} />
<a href="/privacy">Politique de confidentialité</a>
<span className={classes["separator"]} />
<a href="/cookies">Politique des cookies</a>
<Link href={legalPages.replace("[legalUid]", ELegalOptions.LEGAL_MENTIONS)}>© Copyright lecoffre 2024</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.CGU)}>Juridiques</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE)}>Politique de confidentialité</Link>
<Image src={VerticalSeparator} alt="Separator" />
<Link href={legalPages.replace("[legalUid]", ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES)}>Gestion des cookies</Link>
</div>
);
}

View File

@ -0,0 +1,11 @@
@import "@Themes/constants.scss";
.root {
.hidden-input {
position: absolute;
opacity: 0;
}
.errors-container {
margin-top: 8px;
}
}

View File

@ -0,0 +1,66 @@
import React from "react";
import { ReactNode } from "react";
import Autocomplete from "../../Autocomplete";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
import BaseField, { IProps as IBaseFieldProps, IState as IBaseFieldState } from "../BaseField";
import classes from "./classes.module.scss";
export type IProps = IBaseFieldProps & {
onSelectionChange?: (option: IOption | null) => void;
options: IOption[];
selectedOption?: IOption | null;
label?: string;
};
type IState = IBaseFieldState & {
selectedOption: IOption | null;
};
export default class AutocompleteField extends BaseField<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
selectedOption: this.props.selectedOption ?? null,
...this.getDefaultState(),
};
this.handleOnChange = this.handleOnChange.bind(this);
}
private handleOnChange = (option: IOption | null) => {
this.setState({ selectedOption: option });
this.props.onSelectionChange?.(option);
};
public override componentDidUpdate(prevProps: IProps): void {
if (prevProps.selectedOption !== this.props.selectedOption) {
this.setState({ selectedOption: this.props.selectedOption ?? null });
}
}
public override render(): ReactNode {
return (
<div className={classes["root"]}>
<Autocomplete
options={this.props.options}
placeholder={this.props.placeholder}
onSelectionChange={this.handleOnChange}
selectedOption={this.state.selectedOption}
label={this.props.label}
disabled={this.props.disabled}
/>
{this.state.selectedOption && (
<input
className={classes["hidden-input"]}
name={this.props.name}
type="text"
defaultValue={this.state.selectedOption.id}
hidden
/>
)}
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
}

View File

@ -0,0 +1,11 @@
@import "@Themes/constants.scss";
.root {
.hidden-input {
position: absolute;
opacity: 0;
}
.errors-container {
margin-top: 8px;
}
}

View File

@ -0,0 +1,66 @@
import React from "react";
import { ReactNode } from "react";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
import BaseField, { IProps as IBaseFieldProps, IState as IBaseFieldState } from "../BaseField";
import classes from "./classes.module.scss";
import AutocompleteMultiSelect from "../../AutocompleteMultiSelect";
export type IProps = IBaseFieldProps & {
onSelectionChange?: (options: IOption[] | null) => void;
options: IOption[];
selectedOptions?: IOption[] | null;
label?: string;
};
type IState = IBaseFieldState & {
selectedOptions: IOption[] | null;
};
export default class AutocompleteMultiSelectField extends BaseField<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
selectedOptions: this.props.selectedOptions ?? null,
...this.getDefaultState(),
};
this.handleOnChange = this.handleOnChange.bind(this);
}
private handleOnChange = (options: IOption[] | null) => {
this.setState({ selectedOptions: options });
this.props.onSelectionChange?.(options);
};
public override componentDidUpdate(prevProps: IProps): void {
if (prevProps.selectedOptions !== this.props.selectedOptions) {
this.setState({ selectedOptions: this.props.selectedOptions ?? null });
}
}
public override render(): ReactNode {
return (
<div className={classes["root"]}>
<AutocompleteMultiSelect
options={this.props.options}
placeholder={this.props.placeholder}
onSelectionChange={this.handleOnChange}
selectedOptions={this.state.selectedOptions}
label={this.props.label}
disabled={this.props.disabled}
/>
{this.state.selectedOptions && (
<input
className={classes["hidden-input"]}
name={this.props.name}
type="text"
defaultValue={JSON.stringify(this.state.selectedOptions)}
hidden
/>
)}
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
}

View File

@ -18,7 +18,7 @@ export type IProps = {
label?: string;
};
type IState = {
export type IState = {
value: string;
validationError: ValidationError | null;
};

View File

@ -1,143 +1,11 @@
@import "@Themes/constants.scss";
.root {
display: flex;
position: relative;
flex-direction: column;
width: 100%;
border: 1px solid var(--color-neutral-200);
&[data-errored="true"] {
border: 1px solid var(--color-error-600);
.hidden-input {
position: absolute;
opacity: 0;
}
&[data-disabled="true"] {
.container-label {
cursor: not-allowed;
}
opacity: 0.6;
}
.container-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--color-generic-white);
cursor: pointer;
padding: 24px;
z-index: 1;
&[data-border-right-collapsed="true"] {
border-radius: 8px 0 0 8px;
}
.container-input chevron-icon {
display: flex;
align-items: center;
span {
display: flex;
.icon {
display: flex;
margin-right: 8px;
align-items: center;
}
}
.placeholder {
position: absolute;
top: 24px;
left: 8px;
background-color: var(--color-generic-white);
padding: 0 16px;
&[data-open="true"] {
transform: translateY(-36px);
}
}
}
.chevron-icon {
height: 24px;
fill: var(--color-neutral-500);
transition: all 350ms $custom-easing;
transform: rotate(90deg);
&[data-open="true"] {
transform: rotate(-90deg);
}
}
}
.container-ul {
padding-left: 24px;
z-index: 3;
list-style: none;
margin: 0;
outline: 0;
display: flex;
flex-direction: column;
width: 100%;
transition: height 350ms $custom-easing, opacity 350ms $custom-easing;
opacity: 1;
overflow: hidden;
top: 50px;
background-color: var(--color-generic-white);
&[data-open="false"] {
height: 0;
opacity: 0;
border: none;
}
}
.container-li {
display: flex;
justify-content: flex-start;
align-items: center;
padding-bottom: 24px;
border-radius: 8px;
cursor: pointer;
background: var(--color-neutral-50);
&:hover {
background: var(--color-neutral-100);
}
&:active {
background: var(--color-neutral-200);
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.token-icon {
max-width: 20px;
display: flex;
align-items: center;
margin-right: 11px;
> svg {
height: 20px;
margin-right: 11px;
}
> img {
height: 20px;
width: 20px;
}
}
.backdrop {
position: fixed;
z-index: 1;
inset: 0;
.errors-container {
margin-top: 8px;
}
}

View File

@ -1,212 +1,66 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import WindowStore from "@Front/Stores/WindowStore";
import { ValidationError } from "class-validator";
import classNames from "classnames";
import Image from "next/image";
import React, { FormEvent, ReactNode } from "react";
import React from "react";
import { ReactNode } from "react";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
import BaseField, { IProps as IBaseFieldProps, IState as IBaseFieldState } from "../BaseField";
import classes from "./classes.module.scss";
import { NextRouter, useRouter } from "next/router";
import Dropdown from "../../Dropdown";
type IProps = {
selectedOption?: IOption;
onChange?: (selectedOption: IOption) => void;
export type IProps = IBaseFieldProps & {
onSelectionChange?: (option: IOption) => void;
options: IOption[];
hasBorderRightCollapsed?: boolean;
placeholder?: string;
className?: string;
name: string;
disabled?: boolean;
errors?: ValidationError;
selectedOption?: IOption | null;
label?: string;
};
export type IOption = {
value: unknown;
label: string;
icon?: ReactNode;
description?: string;
};
type IState = {
isOpen: boolean;
listWidth: number;
listHeight: number;
type IState = IBaseFieldState & {
selectedOption: IOption | null;
errors: ValidationError | null;
};
type IPropsClass = IProps & {
router: NextRouter;
};
class SelectFieldClass extends React.Component<IPropsClass, IState> {
private contentRef = React.createRef<HTMLUListElement>();
private rootRef = React.createRef<HTMLDivElement>();
private removeOnresize = () => {};
static defaultProps = {
disabled: false,
};
constructor(props: IPropsClass) {
export default class SelectField extends BaseField<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
isOpen: false,
listHeight: 0,
listWidth: 0,
selectedOption: null,
errors: this.props.errors ?? null,
selectedOption: this.props.selectedOption ?? null,
...this.getDefaultState(),
};
this.toggle = this.toggle.bind(this);
this.onSelect = this.onSelect.bind(this);
this.handleOnChange = this.handleOnChange.bind(this);
}
public override render(): JSX.Element {
const selectedOption = this.state.selectedOption ?? this.props.selectedOption;
private handleOnChange = (option: IOption) => {
this.setState({ selectedOption: option });
this.props.onSelectionChange?.(option);
};
public override componentDidUpdate(prevProps: IProps): void {
if (prevProps.selectedOption !== this.props.selectedOption) {
this.setState({ selectedOption: this.props.selectedOption ?? null });
}
}
public override render(): ReactNode {
return (
<div className={classes["container"]}>
<div
className={classNames(classes["root"], this.props.className)}
ref={this.rootRef}
data-disabled={this.props.disabled?.toString()}
data-errored={(this.state.errors !== null).toString()}>
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
<label
className={classNames(classes["container-label"])}
data-open={this.state.isOpen}
onClick={this.toggle}
data-border-right-collapsed={this.props.hasBorderRightCollapsed}>
<div className={classNames(classes["container-input"])}>
{selectedOption && (
<>
<span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span>
<Typography typo={ETypo.TEXT_LG_REGULAR}>
<span className={classes["text"]}>{selectedOption?.label}</span>
</Typography>
</>
)}
{!selectedOption && (
<div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>
<span className={classes["text"]}>{this.props.placeholder ?? ""}</span>
</Typography>
</div>
)}
</div>
<Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" />
</label>
<ul
className={classes["container-ul"]}
data-open={this.state.isOpen}
ref={this.contentRef}
style={{
height: this.state.listHeight + "px",
}}>
{this.props.options.map((option, index) => (
<li
key={`${index}-${option.value}`}
className={classes["container-li"]}
onClick={(e) => this.onSelect(option, e)}>
<div className={classes["token-icon"]}>{option.icon}</div>
<Typography typo={ETypo.TEXT_LG_REGULAR}>{option.label}</Typography>
</li>
))}
</ul>
{this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />}
</div>
{this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
<div className={classes["root"]}>
<Dropdown
options={this.props.options}
placeholder={this.props.placeholder}
onSelectionChange={this.handleOnChange}
selectedOption={this.state.selectedOption}
label={this.props.label}
disabled={this.props.disabled}
/>
{this.state.selectedOption && (
<input
className={classes["hidden-input"]}
name={this.props.name}
type="text"
defaultValue={this.state.selectedOption.id}
hidden
/>
)}
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
public override componentDidMount(): void {
this.onResize();
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
this.props.router.events.on("routeChangeStart", () => {
this.setState({
isOpen: false,
selectedOption: null,
listHeight: 0,
listWidth: 0,
});
});
}
public override componentWillUnmount() {
this.removeOnresize();
}
public override componentDidUpdate(prevProps: IProps) {
if (this.props.errors !== prevProps.errors) {
this.setState({
errors: this.props.errors ?? null,
});
}
if (this.props.selectedOption !== prevProps.selectedOption) {
this.setState({
selectedOption: this.props.selectedOption ?? null,
});
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
if (props.selectedOption?.value) {
return {
value: props.selectedOption?.value,
};
}
return null;
}
private onResize() {
let listHeight = 0;
let listWidth = 0;
listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState({ listHeight, listWidth });
}
private toggle(e: FormEvent) {
if (this.props.disabled) return;
e.preventDefault();
let listHeight = 0;
let listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (!this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState((state) => {
return { isOpen: !state.isOpen, listHeight, listWidth };
});
}
private onSelect(option: IOption, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
if (this.props.disabled) return;
this.props.onChange && this.props.onChange(option);
this.setState({
selectedOption: option,
});
this.toggle(e);
}
private renderErrors(): JSX.Element | null {
if (!this.state.errors) return null;
return (
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_600}>
{this.props.placeholder} ne peut pas être vide
</Typography>
);
}
}
export default function SelectField(props: IProps) {
const router = useRouter();
return <SelectFieldClass {...props} router={router} />;
}

View File

@ -0,0 +1,11 @@
@import "@Themes/constants.scss";
.root {
.hidden-input {
position: absolute;
opacity: 0;
}
.errors-container {
margin-top: 8px;
}
}

View File

@ -0,0 +1,212 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import WindowStore from "@Front/Stores/WindowStore";
import { ValidationError } from "class-validator";
import classNames from "classnames";
import Image from "next/image";
import React, { FormEvent, ReactNode } from "react";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import { NextRouter, useRouter } from "next/router";
type IProps = {
selectedOption?: IOptionOld;
onChange?: (selectedOption: IOptionOld) => void;
options: IOptionOld[];
hasBorderRightCollapsed?: boolean;
placeholder?: string;
className?: string;
name: string;
disabled?: boolean;
errors?: ValidationError;
};
export type IOptionOld = {
value: unknown;
label: string;
icon?: ReactNode;
description?: string;
};
type IState = {
isOpen: boolean;
listWidth: number;
listHeight: number;
selectedOption: IOptionOld | null;
errors: ValidationError | null;
};
type IPropsClass = IProps & {
router: NextRouter;
};
class SelectFieldClass extends React.Component<IPropsClass, IState> {
private contentRef = React.createRef<HTMLUListElement>();
private rootRef = React.createRef<HTMLDivElement>();
private removeOnresize = () => {};
static defaultProps = {
disabled: false,
};
constructor(props: IPropsClass) {
super(props);
this.state = {
isOpen: false,
listHeight: 0,
listWidth: 0,
selectedOption: null,
errors: this.props.errors ?? null,
};
this.toggle = this.toggle.bind(this);
this.onSelect = this.onSelect.bind(this);
}
public override render(): JSX.Element {
const selectedOption = this.state.selectedOption ?? this.props.selectedOption;
return (
<div className={classes["container"]}>
<div
className={classNames(classes["root"], this.props.className)}
ref={this.rootRef}
data-disabled={this.props.disabled?.toString()}
data-errored={(this.state.errors !== null).toString()}>
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
<label
className={classNames(classes["container-label"])}
data-open={this.state.isOpen}
onClick={this.toggle}
data-border-right-collapsed={this.props.hasBorderRightCollapsed}>
<div className={classNames(classes["container-input"])}>
{selectedOption && (
<>
<span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span>
<Typography typo={ETypo.TEXT_LG_REGULAR}>
<span className={classes["text"]}>{selectedOption?.label}</span>
</Typography>
</>
)}
{!selectedOption && (
<div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}>
<Typography typo={ETypo.TEXT_MD_REGULAR}>
<span className={classes["text"]}>{this.props.placeholder ?? ""}</span>
</Typography>
</div>
)}
</div>
<Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" />
</label>
<ul
className={classes["container-ul"]}
data-open={this.state.isOpen}
ref={this.contentRef}
style={{
height: this.state.listHeight + "px",
}}>
{this.props.options.map((option, index) => (
<li
key={`${index}-${option.value}`}
className={classes["container-li"]}
onClick={(e) => this.onSelect(option, e)}>
<div className={classes["token-icon"]}>{option.icon}</div>
<Typography typo={ETypo.TEXT_LG_REGULAR}>{option.label}</Typography>
</li>
))}
</ul>
{this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />}
</div>
{this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
public override componentDidMount(): void {
this.onResize();
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
this.props.router.events.on("routeChangeStart", () => {
this.setState({
isOpen: false,
selectedOption: null,
listHeight: 0,
listWidth: 0,
});
});
}
public override componentWillUnmount() {
this.removeOnresize();
}
public override componentDidUpdate(prevProps: IProps) {
if (this.props.errors !== prevProps.errors) {
this.setState({
errors: this.props.errors ?? null,
});
}
if (this.props.selectedOption !== prevProps.selectedOption) {
this.setState({
selectedOption: this.props.selectedOption ?? null,
});
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
if (props.selectedOption?.value) {
return {
value: props.selectedOption?.value,
};
}
return null;
}
private onResize() {
let listHeight = 0;
let listWidth = 0;
listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState({ listHeight, listWidth });
}
private toggle(e: FormEvent) {
if (this.props.disabled) return;
e.preventDefault();
let listHeight = 0;
let listWidth = this.rootRef.current?.scrollWidth ?? 0;
if (!this.state.listHeight) {
listHeight = this.contentRef.current?.scrollHeight ?? 0;
}
this.setState((state) => {
return { isOpen: !state.isOpen, listHeight, listWidth };
});
}
private onSelect(option: IOptionOld, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
if (this.props.disabled) return;
this.props.onChange && this.props.onChange(option);
this.setState({
selectedOption: option,
});
this.toggle(e);
}
private renderErrors(): JSX.Element | null {
if (!this.state.errors) return null;
return (
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_600}>
{this.props.placeholder} ne peut pas être vide
</Typography>
);
}
}
export default function SelectField(props: IProps) {
const router = useRouter();
return <SelectFieldClass {...props} router={router} />;
}

View File

@ -1,18 +0,0 @@
@import "@Themes/constants.scss";
.root {
.content {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
}
.sub-menu {
padding: 24px;
text-align: center;
gap: 24px;
display: flex;
flex-direction: column;
}
}

View File

@ -1,58 +0,0 @@
import classNames from "classnames";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import classes from "./classes.module.scss";
import { IAppRule } from "@Front/Api/Entities/rule";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { IHeaderLinkProps } from "../../../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import HeaderSubmenuLink from "../../../HeaderSubmenu/HeaderSubmenuLink";
import useToggle from "@Front/Hooks/useToggle";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
type IProps = {
text: string | JSX.Element;
links: (IHeaderLinkProps & {
rules?: IAppRule[];
})[];
};
export default function HeaderSubmenu(props: IProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = useState(true);
const { active: isSubmenuOpened, toggle } = useToggle();
useEffect(() => {
setIsActive(false);
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
}, [isActive, pathname, props.links]);
return (
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
<div className={classes["container"]}>
<div className={classNames(classes["root"], (isActive || isSubmenuOpened) && classes["active"])}>
<div className={classes["content"]} onClick={toggle}>
<Typography
typo={isActive || isSubmenuOpened ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isSubmenuOpened ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
{isSubmenuOpened ? <ChevronUpIcon height="20" width="20" /> : <ChevronDownIcon height="20" width="20" />}
</div>
<div className={classes["underline"]} data-active={(isActive || isSubmenuOpened).toString()} />
{isSubmenuOpened && (
<div className={classes["sub-menu"]}>
{props.links.map((link) => (
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
<HeaderSubmenuLink {...link} />
</Rules>
))}
</div>
)}
</div>
</div>
</Rules>
);
}

View File

@ -1,21 +1,22 @@
@import "@Themes/constants.scss";
.root {
position: absolute;
top: var(--header-height);
left: 0;
width: 100%;
max-height: calc(100vh - var(--header-height));
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
display: flex;
flex-direction: column;
background-color: var(--color-generic-white);
box-shadow: $shadow-nav;
padding: 24px;
position: absolute;
top: 83px;
width: 100%;
left: 0;
text-align: center;
max-height: calc(100vh - 83px);
overflow: auto;
> *:not(:last-child) {
margin-bottom: 24px;
}
border-radius: var(--menu-radius, 0px);
background: var(--color-generic-white, #FFF);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.10);
.separator {
width: 100%;

View File

@ -1,170 +1,196 @@
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import LogOutButton from "@Front/Components/DesignSystem/LogOutButton";
import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import Module from "@Front/Config/Module";
import React from "react";
import NavigationLink from "../../NavigationLink";
import classes from "./classes.module.scss";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import BurgerModalSubmenu from "./BurgerModalSubmenu";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
type IProps = {
isOpen: boolean;
closeModal: () => void;
};
type IState = {};
export default class BurgerModal extends React.Component<IProps, IState> {
// TODO isEnabled depending on role given by DB
public override render(): JSX.Element | null {
if (!this.props.isOpen) return null;
return (
<>
<div className={classes["background"]} onClick={this.props.closeModal} />
<div className={classes["root"]}>
<Rules
mode={RulesMode.OPTIONAL}
rules={[
{
action: AppRuleActions.read,
name: AppRuleNames.officeFolders,
},
]}>
<>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.props.path}
text="Dossiers en cours"
routesActive={[
export default function BurgerModal(props: IProps) {
const { isOpen, closeModal } = props;
if (!isOpen) return null;
return (
<>
<div className={classes["background"]} onClick={closeModal} />
<div className={classes["root"]}>
<Rules
mode={RulesMode.OPTIONAL}
rules={[
{
action: AppRuleActions.read,
name: AppRuleNames.officeFolders,
},
]}>
<>
<MenuItem
item={{
text: "Dossiers en cours",
routesActive: [
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]}
/>
<NavigationLink
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
text="Dossiers archivés"
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
<div className={classes["separator"]} />
</>
</Rules>
],
link: Module.getInstance().get().modules.pages.Folder.props.path,
}}
/>
<BurgerModalSubmenu
text={"Espace super admin"}
links={[
{
text: "Gestion des utilisateurs",
path: Module.getInstance().get().modules.pages.Users.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
Module.getInstance().get().modules.pages.Users.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.offices,
},
],
},
{
text: "Gestion des offices",
path: Module.getInstance().get().modules.pages.Offices.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path,
Module.getInstance().get().modules.pages.Offices.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.offices,
},
],
},
]}
/>
<BurgerModalSubmenu
text="Espace office"
links={[
{
text: "Collaborateurs",
path: Module.getInstance().get().modules.pages.Collaborators.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
Module.getInstance().get().modules.pages.Collaborators.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.users,
},
],
},
{
text: "Gestion des rôles",
path: Module.getInstance().get().modules.pages.Roles.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
Module.getInstance().get().modules.pages.Roles.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
],
},
{
text: "Paramétrage des listes de pièces",
path: Module.getInstance().get().modules.pages.DeedTypes.props.path,
routesActive: [
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
Module.getInstance().get().modules.pages.DeedTypes.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.deedTypes,
},
],
},
{
text: "RIB Office",
path: Module.getInstance().get().modules.pages.OfficesRib.props.path,
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.rib,
},
],
},
{
text: "Abonnement",
path: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.New.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path,
],
},
]}
/>
<div className={classes["separator"]} />
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" />
<NavigationLink target="blank" path="https://ressources.lecoffre.io/" text="Guide de Prise en Main" />
<NavigationLink target="blank" path="https://tally.so/r/mBGaNY" text="Support" />
<NavigationLink target="blank" path="/CGU_LeCoffre_io.pdf" text="CGU" />
<LogOutButton />
</div>
</>
);
}
<MenuItem
item={{
text: "Dossiers archivés",
routesActive: [Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path],
link: Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path,
hasSeparator: true,
}}
/>
</>
</Rules>
<MenuItem
item={{
text: "Espace super admin",
dropdown: {
items: [
{
text: "Gestion des utilisateurs",
link: Module.getInstance().get().modules.pages.Users.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Users.pages.UsersInformations.props.path,
Module.getInstance().get().modules.pages.Users.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.offices,
},
],
},
],
},
}}
/>
<MenuItem
item={{
text: "Espace office",
hasSeparator: true,
dropdown: {
items: [
{
text: "Collaborateurs",
link: Module.getInstance().get().modules.pages.Collaborators.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path,
Module.getInstance().get().modules.pages.Collaborators.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.users,
},
],
},
{
text: "Gestion des rôles",
link: Module.getInstance().get().modules.pages.Roles.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Roles.pages.Create.props.path,
Module.getInstance().get().modules.pages.Roles.pages.RolesInformations.props.path,
Module.getInstance().get().modules.pages.Roles.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
],
},
{
text: "Paramétrage des listes de pièces",
link: Module.getInstance().get().modules.pages.DeedTypes.props.path,
routesActive: [
Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DeedTypes.pages.Edit.props.path,
Module.getInstance().get().modules.pages.DeedTypes.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.Edit.props.path,
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path,
],
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.deedTypes,
},
],
},
{
text: "RIB Office",
link: Module.getInstance().get().modules.pages.OfficesRib.props.path,
rules: [
{
action: AppRuleActions.update,
name: AppRuleNames.rib,
},
],
},
{
text: "Abonnement",
link: Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
routesActive: [
Module.getInstance().get().modules.pages.Subscription.pages.Error.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Success.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Invite.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Manage.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.ManageCollaborators.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.New.props.path,
Module.getInstance().get().modules.pages.Subscription.pages.Subscribe.props.path,
],
},
],
},
}}
/>
<MenuItem
item={{
text: "Mon compte",
link: Module.getInstance().get().modules.pages.MyAccount.props.path,
hasSeparator: true,
}}
/>
<MenuItem
item={{
text: "Guide de Prise en Main",
link: "https://ressources.lecoffre.io/",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "Support",
link: "https://tally.so/r/mBGaNY",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "CGU",
link: "/CGU_LeCoffre_io.pdf",
hasSeparator: true,
}}
/>
<LogOutButton />
</div>
</>
);
}

View File

@ -1,39 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import useHoverable from "@Front/Hooks/useHoverable";
type IHeaderLinkProps = {
text: string | JSX.Element;
path: string;
routesActive?: string[];
};
export default function HeaderSubmenuLink(props: IHeaderLinkProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = React.useState(props.path === pathname);
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable();
useEffect(() => {
if (props.path === pathname) setIsActive(true);
if (props.routesActive) {
for (const routeActive of props.routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) setIsActive(true);
}
}
}, [isActive, pathname, props.path, props.routesActive]);
return (
<Link href={props.path} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<Typography
typo={isActive || isHovered ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isHovered ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
</Link>
);
}

View File

@ -1,44 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: flex;
position: relative;
width: fit-content;
margin: auto;
height: 83px;
padding: 10px 16px;
.content {
margin: auto;
}
.underline {
width: 100%;
height: 3px;
background-color: var(--color-generic-white);
position: absolute;
bottom: 0;
left: 0;
&[data-active="true"] {
background-color: var(--color-generic-black);
}
}
&.desactivated {
cursor: not-allowed;
}
.sub-menu {
box-shadow: 0px 8px 10px 0px #00000012;
padding: 24px;
text-align: center;
gap: 24px;
left: 0;
transform: translateX(-25%);
width: 300px;
top: 84px;
display: flex;
flex-direction: column;
background: white;
position: absolute;
}
}

View File

@ -1,57 +0,0 @@
import classNames from "classnames";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { IHeaderLinkProps } from "../ButtonHeader";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
import useHoverable from "@Front/Hooks/useHoverable";
import HeaderSubmenuLink from "./HeaderSubmenuLink";
import { IAppRule } from "@Front/Api/Entities/rule";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
type IProps = {
text: string | JSX.Element;
links: (IHeaderLinkProps & {
rules?: IAppRule[];
})[];
};
export default function HeaderSubmenu(props: IProps) {
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = useState(false);
const { handleMouseLeave, handleMouseEnter, isHovered } = useHoverable(100);
useEffect(() => {
setIsActive(false);
if (props.links.some((link) => link.path === pathname)) setIsActive(true);
if (props.links.some((link) => link.routesActive?.some((routeActive) => pathname.includes(routeActive)))) setIsActive(true);
}, [isActive, pathname, props.links]);
return (
<Rules mode={RulesMode.OPTIONAL} rules={props.links.flatMap((link) => link.rules ?? [])}>
<div className={classes["container"]} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div className={classNames(classes["root"], (isActive || isHovered) && classes["active"])}>
<div className={classes["content"]}>
<Typography
typo={isActive || isHovered ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={isActive || isHovered ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{props.text}
</Typography>
</div>
<div className={classes["underline"]} data-active={(isActive || isHovered).toString()} />
{isHovered && (
<div className={classes["sub-menu"]}>
{props.links.map((link) => (
<Rules mode={RulesMode.NECESSARY} rules={link.rules ?? []} key={link.path}>
<HeaderSubmenuLink {...link} />
</Rules>
))}
</div>
)}
</div>
</div>
</Rules>
);
}

View File

@ -8,7 +8,8 @@ import { AdjustmentsVerticalIcon, BanknotesIcon, Square3Stack3DIcon, TagIcon, Us
import { usePathname } from "next/navigation";
import React, { useCallback, useEffect } from "react";
import Menu, { IItem } from "../../Menu";
import Menu from "../../Menu";
import { IItem } from "../../Menu/MenuItem";
import ButtonHeader from "../ButtonHeader";
import classes from "./classes.module.scss";
@ -175,6 +176,7 @@ const officeItems: IItem[] = [
icon: <BanknotesIcon />,
text: "RIB Office",
link: Module.getInstance().get().modules.pages.OfficesRib.props.path,
routesActive: [Module.getInstance().get().modules.pages.OfficesRib.props.path],
rules: [
{
action: AppRuleActions.update,

View File

@ -1,12 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: flex;
position: relative;
width: fit-content;
margin: auto;
.content {
align-content: center;
}
}

View File

@ -1,54 +0,0 @@
import React from "react";
import classes from "./classes.module.scss";
import Link from "next/link";
import classNames from "classnames";
import { useRouter } from "next/router";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
type IProps = {
text: string | JSX.Element;
path?: string;
onClick?: () => void;
isEnabled?: boolean;
isActive?: boolean;
routesActive?: string[];
target?: "blank" | "self" | "_blank";
};
type IPropsClass = IProps;
type IStateClass = {};
class NavigationLinkClass extends React.Component<IPropsClass, IStateClass> {
static defaultProps = { isEnabled: true };
public override render(): JSX.Element | null {
if (!this.props.isEnabled) return null;
return (
<Link
href={this.props.path ?? ""}
className={classNames(classes["root"], this.props.isActive && [classes["active"]])}
onClick={this.props.onClick}
target={this.props.target}>
<div className={classes["content"]}>
<Typography
typo={this.props.isActive ? ETypo.TEXT_LG_SEMIBOLD : ETypo.TEXT_LG_REGULAR}
color={this.props.isActive ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_500}>
{this.props.text}
</Typography>
</div>
</Link>
);
}
}
export default function NavigationLink(props: IProps) {
const router = useRouter();
const { pathname } = router;
let isActive = props.path === pathname;
if (props.routesActive) {
for (const routeActive of props.routesActive) {
if (isActive) break;
isActive = pathname.includes(routeActive);
}
}
return <NavigationLinkClass {...props} isActive={isActive} />;
}

View File

@ -1,14 +1,26 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
background-color: var(--color-generic-white);
box-shadow: $shadow-nav;
padding: 24px;
position: absolute;
top: 107px;
right: 66px;
top: 48px;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
border-radius: var(--menu-radius, 0);
border: 1px solid var(--menu-border, #d7dce0);
background: var(--color-generic-white, #fff);
text-wrap: nowrap;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
z-index: 3;
top: calc(var(--header-height) + 10px);
right: 32px;
text-align: center;
animation: smooth-appear 0.2s ease forwards;
@ -20,15 +32,6 @@
opacity: 1;
}
}
> *:not(:last-child) {
margin-bottom: 24px;
}
.separator {
width: 100%;
border: 1px solid var(--color-neutral-200);
}
}
.background {

View File

@ -2,8 +2,8 @@ import LogOutButton from "@Front/Components/DesignSystem/LogOutButton";
import Module from "@Front/Config/Module";
import React from "react";
import NavigationLink from "../../NavigationLink";
import classes from "./classes.module.scss";
import MenuItem from "@Front/Components/DesignSystem/Menu/MenuItem";
type IProps = {
isOpen: boolean;
@ -19,10 +19,28 @@ export default class ProfileModal extends React.Component<IProps, IState> {
<>
<div className={classes["background"]} onClick={this.props.closeModal} />
<div className={classes["root"]}>
<NavigationLink path={Module.getInstance().get().modules.pages.MyAccount.props.path} text="Mon compte" />
<NavigationLink target="_blank" path="https://ressources.lecoffre.io/" text="Guide de Prise en Main" />
<NavigationLink target="_blank" path="/CGU_LeCoffre_io.pdf" text="CGU" />
<div className={classes["separator"]} />
<MenuItem
item={{
text: "Mon compte",
link: Module.getInstance().get().modules.pages.MyAccount.props.path,
}}
/>
<MenuItem
item={{
text: "Guide de Prise en Main",
link: "https://ressources.lecoffre.io/",
target: "_blank",
}}
/>
<MenuItem
item={{
text: "CGU",
link: "/CGU_LeCoffre_io.pdf",
hasSeparator: true,
}}
/>
<LogOutButton />
</div>
</>

View File

@ -6,7 +6,7 @@
align-items: center;
flex-shrink: 0;
height: 75px;
height: var(--header-height);
padding: 0px var(--spacing-lg, 24px);
border-bottom: 1px solid var(--menu-border, #d7dce0);

View File

@ -23,6 +23,8 @@ type IProps = {
isUserConnected: boolean;
};
const headerHeight = 75;
export default function Header(props: IProps) {
const { isUserConnected } = props;
@ -44,6 +46,7 @@ export default function Header(props: IProps) {
}, []);
useEffect(() => {
document.documentElement.style.setProperty("--header-height", `${headerHeight}px`);
loadSubscription();
}, [loadSubscription]);

View File

@ -94,6 +94,10 @@
}
}
&.default {
padding: 0;
}
&.disabled {
cursor: default;
opacity: var(--opacity-disabled, 0.3);

View File

@ -5,11 +5,5 @@
}
.root {
width: 100%;
height: 100%;
border-radius: 50%;
border: 8px solid;
border-color: var(--color-neutral-50);
border-right-color: var(--color-info-500-soft);
animation: s2 1s infinite linear;
}

View File

@ -1,4 +1,6 @@
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import React from "react";
import classes from "./classes.module.scss";
interface IProps {
@ -6,6 +8,6 @@ interface IProps {
}
export default class Loader extends React.Component<IProps> {
public override render(): JSX.Element {
return <div className={classes["root"]}></div>;
return <ArrowPathIcon className={classes["root"]} />;
}
}

View File

@ -1,27 +1,20 @@
import React from "react";
import Image from "next/image";
import DisconnectIcon from "@Assets/Icons/disconnect.svg";
import classes from "./classes.module.scss";
import Typography, { ETypo, ETypoColor } from "../Typography";
import { useRouter } from "next/router";
import UserStore from "@Front/Stores/UserStore";
import { FrontendVariables } from "@Front/Config/VariablesFront";
import UserStore from "@Front/Stores/UserStore";
import { PowerIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import MenuItem from "../Menu/MenuItem";
export default function LogOut() {
const router = useRouter();
const variables = FrontendVariables.getInstance();
const disconnect = async () => {
await UserStore.instance.disconnect();
router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`);
};
const disconnect = useCallback(() => {
UserStore.instance
.disconnect()
.then(() => router.push(`https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=${variables.FRONT_APP_HOST}`));
}, [router, variables.FRONT_APP_HOST]);
return (
<div className={classes["root"]} onClick={disconnect}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
Déconnexion
</Typography>
<Image src={DisconnectIcon} className={classes["disconnect-icon"]} alt="disconnect" />
</div>
);
return <MenuItem item={{ text: "Déconnexion", icon: <PowerIcon />, onClick: disconnect }} />;
}

View File

@ -1,5 +1,6 @@
.root {
width: 100%;
.menu-item {
display: flex;
padding: var(--spacing-md, 16px);
@ -7,12 +8,16 @@
align-items: center;
gap: var(--spacing-lg, 24px);
cursor: pointer;
}
> svg {
width: 24px;
height: 24px;
transition: all ease-in-out 0.1s;
}
svg {
width: 24px;
height: 24px;
transition: transform 0.3s ease-in-out;
}
.chevron.open {
transform: rotate(180deg);
}
.separator {
@ -20,4 +25,14 @@
height: 1px;
background-color: var(--separator-stroke-light, #d7dce0);
}
.dropdown {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
.dropdown.open {
max-height: 500px;
}
}

View File

@ -3,27 +3,71 @@ import classes from "./classes.module.scss";
import { useRouter } from "next/router";
import React, { useCallback, useEffect } from "react";
import useHoverable from "@Front/Hooks/useHoverable";
import { IItem } from "..";
import classNames from "classnames";
import { IAppRule } from "@Front/Api/Entities/rule";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import useOpenable from "@Front/Hooks/useOpenable";
type IProps = {
item: IItem;
closeMenuCb: () => void;
};
type IItemBase = {
text: string;
icon?: JSX.Element;
hasSeparator?: boolean;
color?: ETypoColor;
onClose?: () => void;
};
type IItemWithLink = IItemBase & {
link: string;
rules?: IAppRule[];
routesActive?: string[];
onClick?: never;
dropdown?: never;
target?: "_blank";
};
type IItemWithOnClick = IItemBase & {
onClick: () => void;
link?: never;
rules?: never;
routesActive?: never;
dropdown?: never;
target?: never;
};
type IItemWithDropdown = IItemBase & {
dropdown: {
items: IItem[];
};
routesActive?: never;
link?: never;
rules?: never;
onClick?: never;
target?: never;
};
export type IItem = IItemWithLink | IItemWithOnClick | IItemWithDropdown;
export default function MenuItem(props: IProps) {
const { item, closeMenuCb } = props;
const { item } = props;
const router = useRouter();
const { pathname } = router;
const [isActive, setIsActive] = React.useState(item.link === pathname);
const { isOpen, toggle, open } = useOpenable();
const handleClickElement = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
closeMenuCb();
item.onClose?.();
const link = e.currentTarget.getAttribute("data-link");
if (item.target === "_blank") window.open(item.link, "_blank");
if (link) router.push(link);
if (item.onClick) item.onClick();
},
[closeMenuCb, item, router],
[item, router],
);
const { handleMouseEnter, handleMouseLeave, isHovered } = useHoverable();
@ -44,7 +88,25 @@ export default function MenuItem(props: IProps) {
if (pathname.includes(routeActive)) setIsActive(true);
}
}
}, [isActive, item.link, item.routesActive, pathname]);
if (item.dropdown) {
for (const subItem of item.dropdown.items) {
if (isActive) break;
if (subItem.link === pathname) {
!isOpen && open();
setIsActive(true);
}
if (subItem.routesActive) {
for (const routeActive of subItem.routesActive) {
if (isActive) break;
if (pathname.includes(routeActive)) {
!isOpen && open();
setIsActive(true);
}
}
}
}
}
}, [isActive, isOpen, item.dropdown, item.link, item.routesActive, open, pathname]);
return (
<div
@ -53,12 +115,23 @@ export default function MenuItem(props: IProps) {
data-link={item.link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className={classes["menu-item"]}>
{React.cloneElement(item.icon, { color: `var(${getColor()})` })}
<div className={classes["menu-item"]} onClick={item.dropdown && toggle}>
{item.icon && React.cloneElement(item.icon, { color: `var(${getColor()})` })}
<Typography typo={ETypo.TEXT_LG_REGULAR} color={getColor()}>
{item.text}
</Typography>
{item.dropdown &&
React.cloneElement(<ChevronDownIcon className={classNames(classes["chevron"], isOpen && [classes["open"]])} />, {
color: `var(${getColor()})`,
})}
</div>
{item.dropdown && (
<div className={classNames(classes["dropdown"], isOpen && [classes["open"]])}>
{item.dropdown.items.map((subItem, index) => (
<MenuItem key={index} item={subItem} />
))}
</div>
)}
{item.hasSeparator && <div className={classes["separator"]} />}
</div>
);

View File

@ -1,35 +1,10 @@
import { IAppRule } from "@Front/Api/Entities/rule";
import { ETypoColor } from "@Front/Components/DesignSystem/Typography";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import useHoverable from "@Front/Hooks/useHoverable";
import useOpenable from "@Front/Hooks/useOpenable";
import React, { useEffect, useRef } from "react";
import classes from "./classes.module.scss";
import MenuItem from "./MenuItem";
type IItemBase = {
icon: JSX.Element;
text: string;
hasSeparator?: boolean;
color?: ETypoColor;
};
type IItemWithLink = IItemBase & {
link: string;
rules?: IAppRule[];
routesActive?: string[];
onClick?: never;
};
type IItemWithOnClick = IItemBase & {
onClick: () => void;
link?: never;
rules?: never;
routesActive?: never;
};
export type IItem = IItemWithLink | IItemWithOnClick;
import MenuItem, { IItem } from "./MenuItem";
type IProps = {
children: React.ReactNode;
@ -79,7 +54,7 @@ export default function Menu(props: IProps) {
{items.map((item, index) => {
return (
<Rules mode={RulesMode.NECESSARY} rules={item.rules ?? []} key={item.link}>
<MenuItem item={item} key={index} closeMenuCb={close} />
<MenuItem item={item} key={index} />
</Rules>
);
})}

View File

@ -46,7 +46,8 @@ export default function Modal(props: IProps) {
<Button
{...firstButton}
variant={firstButton.variant ?? EButtonVariant.PRIMARY}
styletype={firstButton.styletype ?? EButtonstyletype.OUTLINED}>
styletype={firstButton.styletype ?? EButtonstyletype.OUTLINED}
fullwidth={firstButton.fullwidth ?? true}>
{firstButton.children}
</Button>
)}
@ -54,7 +55,8 @@ export default function Modal(props: IProps) {
<Button
{...secondButton}
variant={secondButton.variant ?? EButtonVariant.PRIMARY}
styletype={secondButton.styletype ?? EButtonstyletype.CONTAINED}>
styletype={secondButton.styletype ?? EButtonstyletype.CONTAINED}
fullwidth={secondButton.fullwidth ?? true}>
{secondButton.children}
</Button>
)}

View File

@ -1,79 +0,0 @@
@import "@Themes/constants.scss";
.root {
.label-container {
cursor: pointer;
display: flex;
flex-direction: column;
flex: 1;
position: relative;
border: 1px solid var(--color-neutral-200);
background-color: transparent;
.placeholder {
position: absolute;
top: 24px;
left: 24px;
pointer-events: none;
display: flex;
align-items: center;
transition: top 0.3s ease-in-out;
background-color: white;
padding: 0 4px;
&[data-selected="true"] {
top: -12px;
}
}
.label {
font-family: var(--font-text-family);
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 18px;
text-transform: uppercase;
color: var(--color-primary-8);
pointer-events: none;
}
.input-container {
display: flex;
outline: none;
gap: 16px;
border: none;
width: 100%;
> div {
width: 100%;
> div:first-of-type > div:first-of-type {
padding: 24px;
}
}
}
&.active {
.input-container {
> div > div > div {
padding: 24px 16px 16px 16px !important;
}
}
}
.is-active-placeholder {
position: absolute;
top: -11px;
left: 8px;
background-color: #ffffff;
z-index: 1;
padding: 0 16px;
}
&[data-is-errored="true"] {
.input {
border: 1px solid var(--color-error-600);
~ .fake-placeholder {
color: var(--color-error-600);
}
}
}
}
}

View File

@ -1,158 +0,0 @@
import classNames from "classnames";
import React from "react";
import ReactSelect, { ActionMeta, MultiValue, Options, PropsValue } from "react-select";
import { IOption } from "../Form/SelectField";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import { styles } from "./styles";
import { ValidationError } from "class-validator";
type IProps = {
options: IOption[];
label?: string | JSX.Element;
placeholder?: string;
onChange?: (newValue: MultiValue<IOption>, actionMeta: ActionMeta<IOption>) => void;
defaultValue?: PropsValue<IOption>;
value?: PropsValue<IOption>;
isMulti?: boolean;
shouldCloseMenuOnSelect: boolean;
isOptionDisabled?: (option: IOption, selectValue: Options<IOption>) => boolean;
validationError?: ValidationError;
};
type IState = {
isFocused: boolean;
selectedOptions: MultiValue<IOption>;
validationError: ValidationError | null;
};
export default class MultiSelect extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
placeholder: "Sélectionner une option...",
shouldCloseMenuOnSelect: false,
};
constructor(props: IProps) {
super(props);
this.state = {
isFocused: false,
selectedOptions: [],
validationError: this.props.validationError ?? null,
};
this.hasError = this.hasError.bind(this);
this.onChange = this.onChange.bind(this);
this.onEmptyResearch = this.onEmptyResearch.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.renderErrors = this.renderErrors.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<div className={classNames(classes["label-container"], this.state.selectedOptions.length >= 1 && classes["active"])}>
{this.props.label && <div className={classes["label"]}>{this.props.label}</div>}
{this.props.placeholder && (
<div
className={classes["placeholder"]}
data-selected={(this.state.isFocused || this.state.selectedOptions.length >= 1).toString()}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{this.props.placeholder}
</Typography>
</div>
)}
<div className={classes["input-container"]}>
<ReactSelect
placeholder={""}
options={this.props.options}
styles={styles}
onChange={this.onChange}
value={this.props.defaultValue}
defaultValue={this.state.selectedOptions}
closeMenuOnSelect={this.props.shouldCloseMenuOnSelect}
isMulti
isOptionDisabled={this.props.isOptionDisabled}
noOptionsMessage={this.onEmptyResearch}
onFocus={this.onFocus}
onBlur={this.onBlur}
classNamePrefix="react-select"
/>
</div>
</div>
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div>
);
}
public override componentDidMount(): void {
if (this.props.defaultValue) {
// If default value contains multiple IOptions
if (Array.isArray(this.props.defaultValue)) {
this.setState({ selectedOptions: this.props.defaultValue });
}
// If default value is a single IOption
if ("label" in this.props.defaultValue) {
this.setState({ selectedOptions: [this.props.defaultValue] });
}
}
}
public override componentDidUpdate(prevProps: IProps): void {
if (this.props.validationError !== prevProps.validationError) {
this.setState({
validationError: this.props.validationError ?? null,
});
}
if (this.props.defaultValue && this.props.defaultValue !== prevProps.defaultValue) {
// If default value contains multiple IOptions
if (Array.isArray(this.props.defaultValue)) {
this.setState({ selectedOptions: this.props.defaultValue });
}
// If default value is a single IOption
if ("label" in this.props.defaultValue) {
this.setState({ selectedOptions: [this.props.defaultValue] });
}
}
}
private onFocus() {
this.setState({ isFocused: true });
}
private onBlur() {
this.setState({ isFocused: false });
}
private onChange(newValue: MultiValue<IOption>, actionMeta: ActionMeta<IOption>) {
this.props.onChange && this.props.onChange(newValue, actionMeta);
this.setState({
selectedOptions: newValue,
validationError: null,
});
}
private onEmptyResearch() {
if (this.state.selectedOptions.length === this.props.options.length) {
return null;
}
return "Aucune option trouvée";
}
protected hasError(): boolean {
return this.state.validationError !== null;
}
protected renderErrors(): JSX.Element[] | null {
if (!this.state.validationError || !this.state.validationError.constraints) return null;
let errors: JSX.Element[] = [];
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
errors.push(
<Typography key={key} typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_600}>
{value}
</Typography>,
);
});
return errors;
}
}

View File

@ -1,100 +0,0 @@
export const styles = {
option: (provided: any, props: { isSelected: boolean; isFocused: boolean }) => ({
...provided,
cursor: "pointer",
padding: "8px 24px",
fontFamily: "var(--font-text-family)",
fontStyle: "normal",
fontWeight: "400",
fontSize: "18px",
lineHeight: "21.78px",
color: "#939393",
backgroundColor: props.isSelected ? "var(--color-primary-3)" : props.isFocused ? "var(--color-primary-3)" : undefined,
":active": {
...provided[":active"],
backgroundColor: props.isSelected ? "var(--color-primary-3)" : undefined,
},
}),
control: () => ({
width: "100%",
display: "flex",
background: "transparent",
}),
valueContainer: (provided: any) => ({
...provided,
padding: 0,
minWidth: "100px",
fontFamily: "var(--font-text-family)",
fontStyle: "normal",
fontWeight: "600",
fontSize: "16px",
lineHeight: "22px",
color: "#939393",
letter: "0.5 px",
}),
multiValue: (provided: any) => ({
...provided,
margin: "4px",
padding: "8px 16px",
fontStyle: "normal",
fontWeight: "400",
fontSize: "16px",
lineHeight: "22px",
background: "transparent",
border: "1px solid black",
borderRadius: "100px",
}),
multiValueLabel: (provided: any) => ({
...provided,
fontSize: "16px",
color: "#939393",
fontWeight: "400",
}),
input: (provided: any) => ({
...provided,
margin: 0,
padding: 0,
}),
placeholder: (provided: any) => ({
...provided,
fontSize: "16px",
lineHeight: "22px",
fontWeight: "400",
color: "#939393",
}),
indicatorSeparator: () => ({
display: "none",
}),
menu: (provided: any) => ({
...provided,
position: "static",
border: "0",
boxShadow: "none",
}),
menuList: (provided: any) => ({
...provided,
}),
multiValueRemove: (provided: any) => ({
...provided,
backgroundColor: "transparent",
color: "black",
"&:hover": {
color: "grey",
backgroundColor: "transparent",
},
">svg": {
width: "20px",
height: "20px",
},
}),
indicatorsContainer: (provided: any) => ({
...provided,
display: "none",
}),
listBox: (provided: any) => ({
...provided,
color: "red",
fontSize: "16px",
}),
};

View File

@ -4,37 +4,78 @@
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
align-items: flex-start;
color: var(--text-primary, #24282e);
font-size: 16px;
font-style: normal;
font-weight: var(--font-text-weight-regular, 400);
line-height: normal;
letter-spacing: 0.08px;
gap: var(--Radius-lg, 16px);
.content {
cursor: pointer;
display: flex;
flex-direction: column;
.label {
color: var(--text-primary, #24282e);
font-weight: var(--font-text-weight-regular, 400);
}
.description {
color: var(--text-secondary, #47535d);
font-weight: var(--font-text-weight-light, 300);
}
}
input[type="radio"] {
appearance: none;
background-color: transparent;
width: 15px;
height: 15px;
border: 1px solid var(--color-secondary-500);
border-radius: 100px;
cursor: pointer;
width: 20px;
height: 20px;
min-width: 20px;
max-width: 20px;
border-radius: var(--radius-full, 360px);
border: 2px solid var(--select-option-unselected-default-border, #6d7e8a);
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-top: auto;
margin-bottom: auto;
align-items: flex-start;
flex-direction: column;
gap: 8px;
&:hover {
border: 2px solid var(--select-option-unselected-default-border, #6d7e8a);
}
&:active {
border: 2px solid var(--select-option-unselected-pressed-border, #3e474e);
}
&:disabled {
cursor: not-allowed;
border: 2px solid var(--select-option-unselected-default-border, #6d7e8a);
}
}
input[type="radio"]::before {
content: "";
width: 11px;
height: 11px;
background-color: var(--color-secondary-500);
border-radius: 100px;
transform: scale(0);
&.disabled {
pointer-events: none;
cursor: not-allowed;
opacity: var(--opacity-disabled, 0.3);
}
input[type="radio"]:checked::before {
transform: scale(1);
}
// input[type="radio"]::before {
// content: "";
// width: 11px;
// height: 11px;
// background-color: var(--color-secondary-500);
// border-radius: 100px;
// transform: scale(0);
// }
.tooltip {
margin-left: 16px;
}
// input[type="radio"]:checked::before {
// transform: scale(1);
// }
// .tooltip {
// margin-left: 16px;
// }
}

View File

@ -3,9 +3,9 @@ import React from "react";
import Tooltip from "../ToolTip";
import Typography, { ETypo, ETypoColor } from "../Typography";
import classes from "./classes.module.scss";
import classNames from "classnames";
type IProps = {
children: React.ReactNode;
name: string;
toolTip?: string;
checked?: boolean;
@ -13,6 +13,8 @@ type IProps = {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
value: string;
disabled: boolean;
description?: string;
label?: string;
};
export default class RadioBox extends React.Component<IProps> {
@ -21,8 +23,8 @@ export default class RadioBox extends React.Component<IProps> {
};
public override render(): JSX.Element {
return (
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}>
<label className={classes["root"]}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}>
<label className={classNames(classes["root"], this.props.disabled && classes["disabled"])}>
<input
type="radio"
name={this.props.name}
@ -30,9 +32,20 @@ export default class RadioBox extends React.Component<IProps> {
defaultChecked={this.props.defaultChecked}
onChange={this.props.onChange}
value={this.props.value}
disabled={this.props.disabled}
/>
{this.props.children}
<div className={classes["content"]}>
{this.props.label && (
<Typography className={classes["label"]} typo={ETypo.TEXT_MD_REGULAR}>
{this.props.label}
</Typography>
)}
{this.props.description && (
<Typography className={classes["description"]} typo={ETypo.TEXT_MD_LIGHT}>
{this.props.description}
</Typography>
)}
</div>
{this.props.toolTip && <Tooltip className={classes["tooltip"]} text={this.props.toolTip} />}
</label>
</Typography>

View File

@ -1,6 +1,8 @@
@import "@Themes/constants.scss";
.root {
height: 56px;
display: flex;
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
align-items: flex-start;
@ -10,6 +12,10 @@
border: 1px solid var(--input-main-border-default, #d7dce0);
background: var(--input-background, #fff);
svg {
stroke: var(--button-icon-button-default-default);
}
&:hover {
border-radius: var(--input-radius, 0px);
border: 1px solid var(--input-main-border-hovered, #b4bec5);
@ -28,8 +34,14 @@
background: var(--input-background, #fff);
}
&[data-is-disabled="true"] {
opacity: var(--opacity-disabled, 0.3);
pointer-events: none;
}
.input-container {
display: flex;
align-items: center;
flex: 1;
gap: 8px;
padding: 0px var(--spacing-2, 16px);

View File

@ -1,16 +1,23 @@
import React, { useCallback } from "react";
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import React, { useCallback, useEffect } from "react";
import classes from "./classes.module.scss";
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
type IProps = {
onChange?: (input: string) => void;
value?: string;
placeholder?: string;
disabled?: boolean;
onClear?: () => void;
onFocus?: () => void;
onBlur?: () => void;
};
export default function SearchBar({ onChange, placeholder = "Rechercher" }: IProps) {
export default function SearchBar(props: IProps) {
const { onChange, value: propValue, placeholder = "Rechercher", disabled = false, onClear, onFocus, onBlur } = props;
const [isFocused, setIsFocused] = React.useState(false);
const [value, setValue] = React.useState("");
const [value, setValue] = React.useState(propValue || "");
const changeValue = useCallback(
(value: string) => {
@ -20,13 +27,34 @@ export default function SearchBar({ onChange, placeholder = "Rechercher" }: IPro
[onChange],
);
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => changeValue(event.target.value);
const handleFocus = () => setIsFocused(true);
const handleBlur = () => setIsFocused(false);
const clearValue = () => changeValue("");
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => changeValue(event.target.value), [changeValue]);
const handleFocus = useCallback(() => {
setIsFocused(true);
onFocus?.();
}, [onFocus]);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement, Element>) => {
setIsFocused(false);
onBlur?.();
},
[onBlur],
);
const clearValue = useCallback(() => {
changeValue("");
onClear?.();
}, [changeValue, onClear]);
useEffect(() => {
if (propValue !== undefined) {
setValue(propValue);
}
}, [propValue]);
return (
<label className={classes["root"]} data-is-focused={isFocused} data-has-value={value !== ""}>
<label className={classes["root"]} data-is-focused={isFocused} data-has-value={value !== ""} data-is-disabled={disabled}>
<div className={classes["input-container"]}>
<MagnifyingGlassIcon width="24" height="24" />
<input

View File

@ -1,35 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: inline-flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 24px;
border: 1px solid var(--color-neutral-200);
cursor: pointer;
&:hover {
background-color: var(--color-neutral-200);
}
&[data-selected="true"] {
background-color: var(--color-neutral-200);
}
.left-side {
display: inline-flex;
justify-content: space-between;
align-items: center;
overflow-wrap: anywhere;
.warning {
margin-left: 32px;
}
}
.right-side {
display: flex;
align-items: center;
gap: 16px;
}
}

View File

@ -0,0 +1,60 @@
@import "@Themes/constants.scss";
.root {
.dropdown-header {
cursor: pointer;
display: flex;
padding: var(--spacing-2, 16px) var(--spacing-sm, 8px);
align-items: center;
gap: var(--spacing-2, 16px);
border-radius: var(--input-radius, 0px);
border: 1px solid var(--dropdown-input-border-hovered, #b4bec5);
background: var(--dropdown-input-background, #fff);
.text-container {
display: flex;
flex-direction: column;
height: var(--spacing-6, 48px);
padding: 0px var(--spacing-2, 16px);
flex: 1;
}
> svg {
transition: transform 200ms ease-in-out;
}
}
.dropdown-content {
margin-top: 8px;
display: flex;
padding: var(--spacing-sm, 8px);
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
max-height: 200px;
overflow-y: auto;
border-radius: var(--dropdown-radius, 0px);
border: 1px solid var(--dropdown-menu-border-primary, #005bcb);
background: var(--dropdown-menu-background, #fff);
.dropdown-item {
cursor: pointer;
padding: var(--spacing-1, 8px) var(--spacing-2, 16px);
}
}
&[data-is-opened="true"] {
.dropdown-header {
border-radius: var(--input-radius, 0px);
border: 1px solid var(--dropdown-input-border-expanded, #005bcb);
background: var(--dropdown-input-background, #fff);
> svg {
transform: rotate(180deg);
}
}
}
}

View File

@ -0,0 +1,50 @@
import React, { useEffect } from "react";
import { IBlock } from "../BlockList/Block";
import classes from "./classes.module.scss";
import Dropdown from "../../Dropdown";
import { IOption } from "../../Dropdown/DropdownMenu/DropdownOption";
type IProps = {
blocks: IBlock[];
onSelectedBlock: (block: IBlock) => void;
defaultSelectedBlock?: IBlock;
};
export default function DropdownNavigation({ blocks, onSelectedBlock, defaultSelectedBlock = blocks[0] }: IProps) {
const [selectedBlock, setSelectedBlock] = React.useState<IBlock | null>(defaultSelectedBlock ?? null);
useEffect(() => {
if (defaultSelectedBlock) setSelectedBlock(defaultSelectedBlock);
}, [defaultSelectedBlock]);
const handleDropDownSelect = (option: IOption) => {
const block = blocks.find((block) => block.id === option.id);
if (block) {
setSelectedBlock(block);
onSelectedBlock
? onSelectedBlock(block)
: console.error("DropdownNavigation: onSelectedBlock prop is required to handle block selection");
}
};
return (
<div className={classes["root"]}>
<Dropdown
options={blocks.map((block) => {
return {
id: block.id,
label: {
subtext: block.primaryText,
text: block.secondaryText,
},
} as IOption;
})}
onSelectionChange={handleDropDownSelect}
selectedOption={
selectedBlock
? { id: selectedBlock.id, label: { text: selectedBlock.secondaryText ?? "", subtext: selectedBlock.primaryText } }
: undefined
}
/>
</div>
);
}

View File

@ -1,5 +1,7 @@
@import "@Themes/constants.scss";
.root {
width: 336px;
min-width: 336px;
height: 100%;
max-height: 100%;
@ -9,6 +11,12 @@
flex-direction: column;
justify-content: flex-start;
@media (max-width: $screen-m) {
height: auto;
gap: 16px;
padding: var(--spacing-lg, 24px);
width: auto;
}
.block-list {
overflow-y: auto;
::-webkit-scrollbar {
@ -16,13 +24,55 @@
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
@media (max-width: $screen-m) {
display: none;
}
}
.responsive-dropdown {
display: none;
@media (max-width: $screen-m) {
display: block;
}
}
.searchbar {
padding: var(--spacing-md, 16px);
@media (max-width: $screen-m) {
padding: 0px;
display: flex;
gap: var(--spacing-md, 16px);
}
> label {
flex: 1;
}
.responsive-button-container {
display: none;
@media (max-width: $screen-m) {
display: block;
flex: 1;
}
@media (max-width: $screen-s) {
display: none;
}
}
}
.bottom-container {
margin-top: auto;
@media (max-width: $screen-m) {
display: none;
}
@media (max-width: $screen-s) {
display: block;
}
}
}

View File

@ -5,8 +5,9 @@ import classes from "./classes.module.scss";
import SearchBar from "../SearchBar";
import Button from "../Button";
import { useRouter } from "next/router";
import DropdownNavigation from "./DropdownNavigation";
type IProps = {
export type ISearchBlockListProps = {
blocks: IBlock[];
onSelectedBlock: (block: IBlock) => void;
bottomButton?: {
@ -14,9 +15,10 @@ type IProps = {
link: string;
};
};
export default function SearchBlockList(props: IProps) {
export default function SearchBlockList(props: ISearchBlockListProps) {
const { blocks, onSelectedBlock, bottomButton } = props;
const [selectedBlock, setSelectedBlock] = useState<IBlock | null>(null);
const router = useRouter();
const [blocksShowing, setBlocksShowing] = useState<IBlock[]>(blocks);
@ -54,6 +56,14 @@ export default function SearchBlockList(props: IProps) {
router.push(bottomButton.link);
}, [bottomButton, router]);
const handleSelectedBlock = useCallback(
(block: IBlock) => {
setSelectedBlock(block);
onSelectedBlock(block);
},
[onSelectedBlock],
);
useEffect(() => {
setBlocksShowing(blocks);
}, [blocks]);
@ -62,9 +72,23 @@ export default function SearchBlockList(props: IProps) {
<div className={classes["root"]}>
<div className={classes["searchbar"]} ref={searchBarRef}>
<SearchBar placeholder="Chercher" onChange={handleSearchChange} />
{bottomButton && (
<div className={classes["responsive-button-container"]}>
<Button fullwidth onClick={redirectOnBottomButtonClick}>
{bottomButton.text}
</Button>
</div>
)}
</div>
<div className={classes["block-list"]} style={getHeight()}>
<BlockList blocks={blocksShowing} onSelectedBlock={onSelectedBlock} />
<BlockList blocks={blocksShowing} onSelectedBlock={handleSelectedBlock} />
</div>
<div className={classes["responsive-dropdown"]}>
<DropdownNavigation
blocks={blocksShowing}
onSelectedBlock={handleSelectedBlock}
defaultSelectedBlock={selectedBlock ?? undefined}
/>
</div>
{bottomButton && (
<div className={classes["bottom-container"]}>

View File

@ -0,0 +1,46 @@
@import "@Themes/constants.scss";
.root {
cursor: pointer;
height: fit-content;
display: flex;
width: 48px;
padding: var(--Radius-xs, 2px);
gap: 8px;
border-radius: var(--toggle-radius, 360px);
background: var(--toggle-off-background, #edeff1);
.circle {
width: 20px;
height: 20px;
flex-shrink: 0;
border-radius: var(--toggle-radius, 360px);
background: var(--toggle-switch-background, #fff);
transition: transform 0.3s ease-in-out;
&.active {
transform: translateX(24px);
}
}
&.sm {
width: 40px;
.circle {
width: 16px;
height: 16px;
&.active {
transform: translateX(20px);
}
}
}
&.active {
background-color: var(--toggle-on-background, #005bcb);
}
}

View File

@ -0,0 +1,38 @@
import useToggle from "@Front/Hooks/useToggle";
import classNames from "classnames";
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
export enum EToggleSize {
MD = "md",
SM = "sm",
}
export enum ETagVariant {
REGULAR = "regular",
SEMI_BOLD = "semi_bold",
}
type IProps = {
onChange?: (value: boolean) => void;
defaultActive?: boolean;
size?: EToggleSize;
className?: string;
};
export default function Toggle(props: IProps) {
const { className, defaultActive = false, size = EToggleSize.MD } = props;
const { active, toggle } = useToggle(defaultActive);
const handleToggle = useCallback(() => {
toggle((isOpen) => props.onChange?.(isOpen));
}, [toggle, props]);
return (
<div className={classNames(classes["root"], className, classes[size], active && classes["active"])} onClick={handleToggle}>
<div className={classNames([classes["circle"], active && classes["active"]])} />
</div>
);
}

View File

@ -1,4 +1,5 @@
import ToolTipIcon from "@Assets/Icons/tool-tip.svg";
// import ToolTipIcon from "@Assets/Icons/tool-tip.svg";
import QuestionMark from "@Assets/Icons/question-mark-circle.svg";
import styled from "@emotion/styled";
import TooltipMUI, { tooltipClasses, TooltipProps } from "@mui/material/Tooltip";
import Image from "next/image";
@ -48,7 +49,7 @@ export default class Tooltip extends React.Component<IProps, IState> {
return (
<LightTooltip title={this.props.text} placement="top" arrow>
<span className={this.props.className} style={!this.props.isNotFlex ? { display: "flex" } : {}}>
<Image src={ToolTipIcon} alt="toolTip icon" />
<Image src={QuestionMark} alt="toolTip icon" />
</span>
</LightTooltip>
);

View File

@ -156,6 +156,11 @@ export enum ETypoColor {
CONTRAST_ACTIVED = "--contrast-actived",
NAVIGATION_BUTTON_CONTRAST_DEFAULT = "--navigation-button-contrast-default",
NAVIGATION_BUTTON_CONTRAST_ACTIVE = "--navigation-button-contrast-active",
DROPDOWN_CONTRAST_DEFAULT = "--dropdown-contrast-default",
DROPDOWN_CONTRAST_ACTIVE = "--dropdown-contrast-active",
INPUT_CHIP_CONTRAST = "--input-chip-contrast",
}
export default function Typography(props: IProps) {

View File

@ -1,23 +0,0 @@
@import "@Themes/constants.scss";
.root {
width: calc(100vh - 83px);
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: calc(100vh - 215px);
height: 100%;
border-right: 1px solid var(--color-neutral-200);
overflow: auto;
}
}

View File

@ -1,44 +0,0 @@
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
import User from "le-coffre-resources/dist/Notary";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import SearchBlockList from "@Front/Components/DesignSystem/SearchBlockList";
type IProps = {
collaborators: User[];
onSelectedCollaborator?: (user: User) => void;
onCloseLeftSide?: () => void;
};
export default function CollaboratorListContainer(props: IProps) {
const router = useRouter();
const { collaboratorUid } = router.query;
const onSelectedBlock = useCallback(
(block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const redirectPath = Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path;
router.push(redirectPath.replace("[uid]", block.id));
},
[props, router],
);
return (
<div className={classes["root"]}>
<SearchBlockList
blocks={props.collaborators.map((user) => {
return {
primaryText: user.contact?.first_name + " " + user.contact?.last_name,
id: user.uid!,
isActive: user.uid === collaboratorUid,
};
})}
onSelectedBlock={onSelectedBlock}
/>
</div>
);
}

View File

@ -1,116 +0,0 @@
@import "@Themes/constants.scss";
@keyframes growWidth {
0% {
width: 100%;
}
100% {
width: 200%;
}
}
.root {
.content {
display: flex;
overflow: hidden;
height: calc(100vh - 83px);
.overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--color-generic-white);
opacity: 0.5;
z-index: 2;
transition: all 0.3s $custom-easing;
}
.left-side {
background-color: var(--color-generic-white);
z-index: 3;
display: flex;
width: 336px;
min-width: 336px;
transition: all 0.3s $custom-easing;
overflow: hidden;
@media (max-width: ($screen-m - 1px)) {
width: 56px;
min-width: 56px;
transform: translateX(-389px);
&.opened {
transform: translateX(0px);
width: 336px;
min-width: 336px;
}
}
@media (max-width: $screen-s) {
width: 0px;
min-width: 0px;
&.opened {
width: 100vw;
min-width: 100vw;
}
}
}
.closable-left-side {
position: absolute;
background-color: var(--color-generic-white);
z-index: 0;
display: flex;
justify-content: center;
min-width: 56px;
max-width: 56px;
height: calc(100vh - 83px);
border-right: 1px var(--color-neutral-200) solid;
@media (min-width: $screen-m) {
display: none;
}
.chevron-icon {
margin-top: 21px;
transform: rotate(180deg);
cursor: pointer;
}
@media (max-width: $screen-s) {
display: none;
}
}
.right-side {
min-width: calc(100vw - 389px);
padding: 24px;
overflow-y: auto;
@media (max-width: ($screen-m - 1px)) {
min-width: calc(100vw - 56px);
}
@media (max-width: $screen-s) {
flex: 1;
min-width: unset;
}
.back-arrow-mobile {
display: none;
@media (max-width: $screen-s) {
display: block;
margin-bottom: 24px;
}
}
.back-arrow-desktop {
@media (max-width: $screen-s) {
display: none;
}
}
}
}
}

View File

@ -1,93 +1,20 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/Admin/Users/Users";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
import User from "le-coffre-resources/dist/Notary";
import JwtService from "@Front/Services/JwtService/JwtService";
import WindowStore from "@Front/Stores/WindowStore";
import classNames from "classnames";
import User from "le-coffre-resources/dist/Admin";
import Image from "next/image";
import React, { ReactNode } from "react";
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/Admin/Users/Users";
import classes from "./classes.module.scss";
import CollaboratorListContainer from "./CollaboratorListContainer";
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
type IProps = IPropsDashboardWithList;
type IProps = {
title: string;
children?: ReactNode;
onSelectedUser: (user: User) => void;
hasBackArrow: boolean;
backArrowUrl?: string;
mobileBackText?: string;
};
type IState = {
collaborators: User[] | null;
isLeftSideOpen: boolean;
leftSideCanBeClosed: boolean;
};
export default class DefaultCollaboratorDashboard extends React.Component<IProps, IState> {
private onWindowResize = () => {};
public static defaultProps: Partial<IProps> = {
hasBackArrow: false,
};
public constructor(props: IProps) {
super(props);
this.state = {
collaborators: null,
isLeftSideOpen: false,
leftSideCanBeClosed: typeof window !== "undefined" ? window.innerWidth < 1024 : false,
};
this.onOpenLeftSide = this.onOpenLeftSide.bind(this);
this.onCloseLeftSide = this.onCloseLeftSide.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Header isUserConnected={true} />
<div className={classes["content"]}>
{this.state.isLeftSideOpen && <div className={classes["overlay"]} onClick={this.onCloseLeftSide} />}
<div className={classNames(classes["left-side"], this.state.isLeftSideOpen && classes["opened"])}>
{this.state.collaborators && (
<CollaboratorListContainer collaborators={this.state.collaborators} onCloseLeftSide={this.onCloseLeftSide} />
)}
</div>
<div className={classNames(classes["closable-left-side"])}>
<Image alt="open side menu" src={ChevronIcon} className={classes["chevron-icon"]} onClick={this.onOpenLeftSide} />
</div>
<div className={classes["right-side"]}>
{this.props.hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={this.props.backArrowUrl ?? ""} />
</div>
)}
{this.props.mobileBackText && (
<div className={classes["back-arrow-mobile"]}>
<Button
leftIcon={<ChevronLeftIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
onClick={this.onOpenLeftSide}>
{this.props.mobileBackText ?? "Retour"}
</Button>
</div>
)}
{this.props.children}
</div>
</div>
<Version />
</div>
);
}
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
export default function DefaultCollaboratorDashboard(props: IProps) {
const [collaborators, setCollaborators] = React.useState<User[] | null>(null);
const router = useRouter();
const { collaboratorUid } = router.query;
useEffect(() => {
const jwt = JwtService.getInstance().decodeJwt();
if (!jwt) return;
const query: IGetUsersparams = {
@ -102,27 +29,30 @@ export default class DefaultCollaboratorDashboard extends React.Component<IProps
},
};
const collaborators = await Users.getInstance().get(query);
this.setState({ collaborators });
}
public override componentWillUnmount() {
this.onWindowResize();
}
Users.getInstance()
.get(query)
.then((users) => setCollaborators(users));
}, []);
private onOpenLeftSide() {
this.setState({ isLeftSideOpen: true });
}
const onSelectedBlock = (block: IBlock) => {
router.push(
Module.getInstance().get().modules.pages.Collaborators.pages.CollaboratorInformations.props.path.replace("[uid]", block.id),
);
};
private onCloseLeftSide() {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ isLeftSideOpen: false });
}
private onResize(window: Window) {
if (window.innerWidth > 1023) {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ leftSideCanBeClosed: false });
}
this.setState({ leftSideCanBeClosed: true });
}
return (
<DefaultDashboardWithList
{...props}
onSelectedBlock={onSelectedBlock}
blocks={
collaborators
? collaborators.map((collaborator) => ({
id: collaborator.uid!,
primaryText: collaborator.contact?.first_name + " " + collaborator.contact?.last_name,
isActive: collaborator.uid === collaboratorUid,
}))
: []
}
/>
);
}

View File

@ -0,0 +1,38 @@
@import "@Themes/constants.scss";
.root {
position: relative;
.content {
display: flex;
justify-content: flex-start;
min-height: calc(100vh - var(--header-height));
height: calc(100vh - var(--header-height));
.right-side {
min-width: calc(100% - 336px);
flex: 1;
display: flex;
justify-content: space-between;
flex-direction: column;
.right-side-content {
overflow-y: auto;
padding: var(--spacing-lg, 24px);
height: calc(100% - var(--footer-height));
}
&[data-no-padding="true"] {
.right-side-content {
padding: 0;
}
}
}
@media (max-width: $screen-m) {
width: 100%;
flex-direction: column;
.right-side {
min-width: 100%;
}
}
}
}

View File

@ -0,0 +1,54 @@
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import React, { ReactNode } from "react";
import classes from "./classes.module.scss";
import SearchBlockList, { ISearchBlockListProps } from "@Front/Components/DesignSystem/SearchBlockList";
import Footer from "@Front/Components/DesignSystem/Footer";
export type IPropsDashboardWithList = {
title?: string;
children?: ReactNode;
hasBackArrow?: boolean;
backArrowUrl?: string;
mobileBackText?: string;
headerConnected?: boolean;
noPadding?: boolean;
};
type IProps = IPropsDashboardWithList & ISearchBlockListProps;
export default function DefaultDashboardWithList(props: IProps) {
const {
hasBackArrow,
backArrowUrl,
children,
blocks,
onSelectedBlock,
headerConnected = true,
bottomButton,
noPadding = false,
} = props;
return (
<div className={classes["root"]}>
<Header isUserConnected={headerConnected} />
<div className={classes["content"]}>
<SearchBlockList blocks={blocks} onSelectedBlock={onSelectedBlock} bottomButton={bottomButton} />
<div className={classes["right-side"]} data-no-padding={noPadding}>
<div className={classes["right-side-content"]}>
{hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={backArrowUrl ?? ""} />
</div>
)}
{children}
</div>
<Footer />
</div>
</div>
<Version />
</div>
);
}

View File

@ -1,32 +0,0 @@
@import "@Themes/constants.scss";
.root {
width: calc(100vh - 83px);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: calc(100vh - 290px);
height: calc(100vh - 290px);
overflow: auto;
border-right: 1px solid var(--color-neutral-200);
}
.create-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
}

View File

@ -1,48 +0,0 @@
import DeedTypes from "@Front/Api/LeCoffreApi/Admin/DeedTypes/DeedTypes";
import Module from "@Front/Config/Module";
import { DeedType } from "le-coffre-resources/dist/Admin";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import SearchBlockList from "@Front/Components/DesignSystem/SearchBlockList";
type IProps = {
deedTypes: DeedType[];
onSelectedDeed?: (deed: DeedTypes) => void;
onCloseLeftSide?: () => void;
};
export default function DeedListContainer(props: IProps) {
const router = useRouter();
const { deedTypeUid } = router.query;
const onSelectedBlock = useCallback(
(block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const redirectPath = Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path;
router.push(redirectPath.replace("[uid]", block.id));
},
[props, router],
);
return (
<div className={classes["root"]}>
<SearchBlockList
blocks={props.deedTypes.map((deed) => {
return {
primaryText: deed.name,
id: deed.uid!,
isActive: deedTypeUid === deed.uid,
};
})}
onSelectedBlock={onSelectedBlock}
bottomButton={{
link: Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
text: "Créer un type d'acte",
}}
/>
</div>
);
}

View File

@ -1,116 +0,0 @@
@import "@Themes/constants.scss";
@keyframes growWidth {
0% {
width: 100%;
}
100% {
width: 200%;
}
}
.root {
.content {
display: flex;
overflow: hidden;
height: calc(100vh - 83px);
.overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--color-generic-white);
opacity: 0.5;
z-index: 2;
transition: all 0.3s $custom-easing;
}
.left-side {
background-color: var(--color-generic-white);
z-index: 3;
display: flex;
width: 336px;
min-width: 336px;
transition: all 0.3s $custom-easing;
overflow: hidden;
@media (max-width: ($screen-m - 1px)) {
width: 56px;
min-width: 56px;
transform: translateX(-389px);
&.opened {
transform: translateX(0px);
width: 336px;
min-width: 336px;
}
}
@media (max-width: $screen-s) {
width: 0px;
min-width: 0px;
&.opened {
width: 100vw;
min-width: 100vw;
}
}
}
.closable-left-side {
position: absolute;
background-color: var(--color-generic-white);
z-index: 0;
display: flex;
justify-content: center;
min-width: 56px;
max-width: 56px;
height: calc(100vh - 83px);
border-right: 1px var(--color-neutral-200) solid;
@media (min-width: $screen-m) {
display: none;
}
.chevron-icon {
margin-top: 21px;
transform: rotate(180deg);
cursor: pointer;
}
@media (max-width: $screen-s) {
display: none;
}
}
.right-side {
min-width: calc(100vw - 389px);
padding: 24px;
overflow-y: auto;
@media (max-width: ($screen-m - 1px)) {
min-width: calc(100vw - 56px);
}
@media (max-width: $screen-s) {
flex: 1;
min-width: unset;
}
.back-arrow-mobile {
display: none;
@media (max-width: $screen-s) {
display: block;
margin-bottom: 24px;
}
}
.back-arrow-desktop {
@media (max-width: $screen-s) {
display: none;
}
}
}
}
}

View File

@ -1,92 +1,19 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
import { DeedType } from "le-coffre-resources/dist/Notary";
import DeedTypes, { IGetDeedTypesParams } from "@Front/Api/LeCoffreApi/Notary/DeedTypes/DeedTypes";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import WindowStore from "@Front/Stores/WindowStore";
import classNames from "classnames";
import { Deed, DeedType } from "le-coffre-resources/dist/Notary";
import Image from "next/image";
import React, { ReactNode } from "react";
import classes from "./classes.module.scss";
import DeedListContainer from "./DeedTypeListContainer";
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
type IProps = IPropsDashboardWithList;
type IProps = {
title: string;
children?: ReactNode;
onSelectedDeed: (deed: Deed) => void;
hasBackArrow: boolean;
backArrowUrl?: string;
mobileBackText?: string;
};
type IState = {
deedTypes: DeedType[] | null;
isLeftSideOpen: boolean;
leftSideCanBeClosed: boolean;
};
export default class DefaultDeedTypesDashboard extends React.Component<IProps, IState> {
private onWindowResize = () => {};
public static defaultProps: Partial<IProps> = {
hasBackArrow: false,
};
public constructor(props: IProps) {
super(props);
this.state = {
deedTypes: null,
isLeftSideOpen: false,
leftSideCanBeClosed: typeof window !== "undefined" ? window.innerWidth < 1024 : false,
};
this.onOpenLeftSide = this.onOpenLeftSide.bind(this);
this.onCloseLeftSide = this.onCloseLeftSide.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Header isUserConnected={true} />
<div className={classes["content"]}>
{this.state.isLeftSideOpen && <div className={classes["overlay"]} onClick={this.onCloseLeftSide} />}
<div className={classNames(classes["left-side"], this.state.isLeftSideOpen && classes["opened"])}>
{this.state.deedTypes && (
<DeedListContainer deedTypes={this.state.deedTypes} onCloseLeftSide={this.onCloseLeftSide} />
)}
</div>
<div className={classNames(classes["closable-left-side"])}>
<Image alt="open side menu" src={ChevronIcon} className={classes["chevron-icon"]} onClick={this.onOpenLeftSide} />
</div>
<div className={classes["right-side"]}>
{this.props.hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={this.props.backArrowUrl ?? ""} />
</div>
)}
{this.props.mobileBackText && (
<div className={classes["back-arrow-mobile"]}>
<Button
leftIcon={<ChevronLeftIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
onClick={this.onOpenLeftSide}>
{this.props.mobileBackText ?? "Retour"}
</Button>
</div>
)}
{this.props.children}
</div>
</div>
<Version />
</div>
);
}
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
export default function DefaultDeedTypeDashboard(props: IProps) {
const [deedTypes, setDeedTypes] = React.useState<DeedType[] | null>(null);
const router = useRouter();
const { deedTypeUid } = router.query;
useEffect(() => {
const query: IGetDeedTypesParams = {
where: {
archived_at: null,
@ -96,28 +23,32 @@ export default class DefaultDeedTypesDashboard extends React.Component<IProps, I
},
};
const deedTypes = await DeedTypes.getInstance().get(query);
this.setState({ deedTypes });
}
DeedTypes.getInstance()
.get(query)
.then((deedTypes) => setDeedTypes(deedTypes));
}, []);
public override componentWillUnmount() {
this.onWindowResize();
}
const onSelectedBlock = (block: IBlock) => {
router.push(Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path.replace("[uid]", block.id));
};
private onOpenLeftSide() {
this.setState({ isLeftSideOpen: true });
}
private onCloseLeftSide() {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ isLeftSideOpen: false });
}
private onResize(window: Window) {
if (window.innerWidth > 1023) {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ leftSideCanBeClosed: false });
}
this.setState({ leftSideCanBeClosed: true });
}
return (
<DefaultDashboardWithList
{...props}
onSelectedBlock={onSelectedBlock}
blocks={
deedTypes
? deedTypes.map((deedTypes) => ({
id: deedTypes.uid!,
primaryText: deedTypes.name,
isActive: deedTypes.uid === deedTypeUid,
}))
: []
}
bottomButton={{
link: Module.getInstance().get().modules.pages.DeedTypes.pages.Create.props.path,
text: "Créer une liste de pièces",
}}
/>
);
}

View File

@ -1,30 +0,0 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: calc(100vh - 290px);
height: calc(100vh - 290px);
overflow: auto;
border-right: 1px solid var(--color-neutral-200);
}
.create-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
}

View File

@ -1,48 +0,0 @@
import Module from "@Front/Config/Module";
import { DocumentType } from "le-coffre-resources/dist/SuperAdmin";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import SearchBlockList from "@Front/Components/DesignSystem/SearchBlockList";
type IProps = {
documentTypes: DocumentType[];
onSelectedDocumentType?: (documentType: DocumentType) => void;
onCloseLeftSide?: () => void;
};
export default function DocumentTypeListContainer(props: IProps) {
const router = useRouter();
const { documentTypeUid } = router.query;
const onSelectedBlock = useCallback(
(block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const redirectPath = Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path;
router.push(redirectPath.replace("[uid]", block.id));
},
[props, router],
);
return (
<div className={classes["root"]}>
<SearchBlockList
blocks={props.documentTypes.map((documentType) => {
return {
primaryText: documentType.name,
id: documentType.uid!,
isActive: documentType.uid === documentTypeUid,
};
})}
onSelectedBlock={onSelectedBlock}
bottomButton={{
link: Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
text: "Créer un type de document",
}}
/>
</div>
);
}

View File

@ -1,116 +0,0 @@
@import "@Themes/constants.scss";
@keyframes growWidth {
0% {
width: 100%;
}
100% {
width: 200%;
}
}
.root {
.content {
display: flex;
overflow: hidden;
height: calc(100vh - 83px);
.overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--color-generic-white);
opacity: 0.5;
z-index: 2;
transition: all 0.3s $custom-easing;
}
.left-side {
background-color: var(--color-generic-white);
z-index: 3;
display: flex;
width: 336px;
min-width: 336px;
transition: all 0.3s $custom-easing;
overflow: hidden;
@media (max-width: ($screen-m - 1px)) {
width: 56px;
min-width: 56px;
transform: translateX(-389px);
&.opened {
transform: translateX(0px);
width: 336px;
min-width: 336px;
}
}
@media (max-width: $screen-s) {
width: 0px;
min-width: 0px;
&.opened {
width: 100vw;
min-width: 100vw;
}
}
}
.closable-left-side {
position: absolute;
background-color: var(--color-generic-white);
z-index: 0;
display: flex;
justify-content: center;
min-width: 56px;
max-width: 56px;
height: calc(100vh - 83px);
border-right: 1px var(--color-neutral-200) solid;
@media (min-width: $screen-m) {
display: none;
}
.chevron-icon {
margin-top: 21px;
transform: rotate(180deg);
cursor: pointer;
}
@media (max-width: $screen-s) {
display: none;
}
}
.right-side {
min-width: calc(100vw - 389px);
padding: 24px;
overflow-y: auto;
@media (max-width: ($screen-m - 1px)) {
min-width: calc(100vw - 56px);
}
@media (max-width: $screen-s) {
flex: 1;
min-width: unset;
}
.back-arrow-mobile {
display: none;
@media (max-width: $screen-s) {
display: block;
margin-bottom: 24px;
}
}
.back-arrow-desktop {
@media (max-width: $screen-s) {
display: none;
}
}
}
}
}

View File

@ -1,124 +1,57 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import DocumentTypes from "@Front/Api/LeCoffreApi/Notary/DocumentTypes/DocumentTypes";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import Module from "@Front/Config/Module";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
import JwtService from "@Front/Services/JwtService/JwtService";
import WindowStore from "@Front/Stores/WindowStore";
import classNames from "classnames";
import DocumentTypes from "@Front/Api/LeCoffreApi/Notary/DocumentTypes/DocumentTypes";
import { DocumentType } from "le-coffre-resources/dist/Notary";
import Image from "next/image";
import React, { ReactNode } from "react";
import classes from "./classes.module.scss";
import DocumentTypeListContainer from "./DocumentTypeListContainer";
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
type IProps = IPropsDashboardWithList;
type IProps = {
title: string;
children?: ReactNode;
onSelectedDocumentType: (documentType: DocumentType) => void;
hasBackArrow: boolean;
backArrowUrl?: string;
mobileBackText?: string;
};
type IState = {
documentTypes: DocumentType[] | null;
isLeftSideOpen: boolean;
leftSideCanBeClosed: boolean;
};
export default class DefaultDocumentTypesDashboard extends React.Component<IProps, IState> {
private onWindowResize = () => {};
public static defaultProps: Partial<IProps> = {
hasBackArrow: false,
};
public constructor(props: IProps) {
super(props);
this.state = {
documentTypes: null,
isLeftSideOpen: false,
leftSideCanBeClosed: typeof window !== "undefined" ? window.innerWidth < 1024 : false,
};
this.onOpenLeftSide = this.onOpenLeftSide.bind(this);
this.onCloseLeftSide = this.onCloseLeftSide.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Header isUserConnected={true} />
<div className={classes["content"]}>
{this.state.isLeftSideOpen && <div className={classes["overlay"]} onClick={this.onCloseLeftSide} />}
<div className={classNames(classes["left-side"], this.state.isLeftSideOpen && classes["opened"])}>
{this.state.documentTypes && (
<DocumentTypeListContainer documentTypes={this.state.documentTypes} onCloseLeftSide={this.onCloseLeftSide} />
)}
</div>
<div className={classNames(classes["closable-left-side"])}>
<Image alt="open side menu" src={ChevronIcon} className={classes["chevron-icon"]} onClick={this.onOpenLeftSide} />
</div>
<div className={classes["right-side"]}>
{this.props.hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={this.props.backArrowUrl ?? ""} />
</div>
)}
{this.props.mobileBackText && (
<div className={classes["back-arrow-mobile"]}>
<Button
leftIcon={<ChevronLeftIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
onClick={this.onOpenLeftSide}>
{this.props.mobileBackText ?? "Retour"}
</Button>
</div>
)}
{this.props.children}
</div>
</div>
<Version />
</div>
);
}
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
export default function DefaultDocumentTypeDashboard(props: IProps) {
const [documentTypes, setDocumentTypes] = React.useState<DocumentType[] | null>(null);
const router = useRouter();
const { documentTypeUid } = router.query;
useEffect(() => {
const jwt = JwtService.getInstance().decodeJwt();
if (!jwt) return;
const documentTypes = await DocumentTypes.getInstance().get({
where: {
office_uid: jwt.office_Id,
},
orderBy: {
name: "asc",
},
});
this.setState({ documentTypes });
}
DocumentTypes.getInstance()
.get({
where: {
office_uid: jwt.office_Id,
},
orderBy: {
name: "asc",
},
})
.then((documentTypes) => setDocumentTypes(documentTypes));
}, []);
public override componentWillUnmount() {
this.onWindowResize();
}
const onSelectedBlock = (block: IBlock) => {
router.push(
Module.getInstance().get().modules.pages.DocumentTypes.pages.DocumentTypesInformations.props.path.replace("[uid]", block.id),
);
};
private onOpenLeftSide() {
this.setState({ isLeftSideOpen: true });
}
private onCloseLeftSide() {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ isLeftSideOpen: false });
}
private onResize(window: Window) {
if (window.innerWidth > 1023) {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ leftSideCanBeClosed: false });
}
this.setState({ leftSideCanBeClosed: true });
}
return (
<DefaultDashboardWithList
{...props}
onSelectedBlock={onSelectedBlock}
blocks={
documentTypes
? documentTypes.map((documentType) => ({
id: documentType.uid!,
primaryText: documentType.name,
isActive: documentType.uid === documentTypeUid,
}))
: []
}
bottomButton={{
link: Module.getInstance().get().modules.pages.DocumentTypes.pages.Create.props.path,
text: "Créer un type de document",
}}
/>
);
}

View File

@ -5,7 +5,7 @@
.content {
display: flex;
.sides {
min-height: calc(100vh - 83px);
min-height: calc(100vh - var(--header-height) - var(--footer-height));
width: 50%;
@media (max-width: $screen-m) {
width: 100%;
@ -28,10 +28,11 @@
.background-image-container {
position: fixed;
z-index: -1;
width: 50vw;
top: 83px;
right: 0;
height: calc(100vh - 83px);
height: calc(100vh - var(--header-height));
@media (max-width: $screen-m) {
display: none;
}

View File

@ -5,6 +5,7 @@ import Image, { StaticImageData } from "next/image";
import React, { ReactNode, useEffect } from "react";
import classes from "./classes.module.scss";
import Footer from "@Front/Components/DesignSystem/Footer";
type IProps = {
title: string;
@ -45,6 +46,7 @@ export default function DefaultDoubleSidePage(props: IProps) {
<Image alt={"right side image"} src={image} className={classes["background-image"]} priority />
</div>
)}
<Footer hasLeftPadding />
</div>
);
}

View File

@ -0,0 +1,57 @@
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import { useRouter } from "next/router";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
import { useMemo } from "react";
import Module from "@Front/Config/Module";
type IProps = IPropsDashboardWithList;
export enum ELegalOptions {
LEGAL_MENTIONS = "mentions-legales",
CGU = "cgu",
CGS = "cgs",
POLITIQUE_DE_CONFIDENTIALITE = "politique-de-confidentialite",
POLITIQUE_DE_GESTION_DES_COOKIES = "politique-de-gestion-des-cookies",
}
export default function DefaultLegalDashboard(props: IProps) {
const router = useRouter();
const { legalUid } = router.query;
const onSelectedBlock = (block: IBlock) => {
router.push(Module.getInstance().get().modules.pages.Legal.pages.LegalInformations.props.path.replace("[legalUid]", block.id));
};
const blocks: IBlock[] = useMemo<IBlock[]>(
() => [
{
id: ELegalOptions.LEGAL_MENTIONS,
primaryText: "Mentions légales",
isActive: legalUid === ELegalOptions.LEGAL_MENTIONS,
},
{
id: ELegalOptions.CGU,
primaryText: "CGU",
isActive: legalUid === ELegalOptions.CGU,
},
{
id: ELegalOptions.CGS,
primaryText: "CGS",
isActive: legalUid === ELegalOptions.CGS,
},
{
id: ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE,
primaryText: "Politique de confidentialité",
isActive: legalUid === ELegalOptions.POLITIQUE_DE_CONFIDENTIALITE,
},
{
id: ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES,
primaryText: "Politique de gestion des cookies",
isActive: legalUid === ELegalOptions.POLITIQUE_DE_GESTION_DES_COOKIES,
},
],
[legalUid],
);
return <DefaultDashboardWithList {...props} onSelectedBlock={onSelectedBlock} blocks={blocks} noPadding />;
}

View File

@ -1,132 +0,0 @@
@import "@Themes/constants.scss";
@keyframes growWidth {
0% {
width: 100%;
}
100% {
width: 200%;
}
}
.root {
position: relative;
.content {
display: flex;
overflow: hidden;
height: calc(100vh - 83px);
.overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--color-generic-white);
opacity: 0.5;
z-index: 2;
transition: all 0.3s $custom-easing;
}
.left-side {
background-color: var(--color-generic-white);
z-index: 3;
display: flex;
width: 336px;
min-width: 336px;
transition: all 0.3s $custom-easing;
overflow: hidden;
@media (max-width: ($screen-m - 1px)) {
width: 56px;
min-width: 56px;
transform: translateX(-389px);
&.opened {
transform: translateX(0px);
width: 336px;
min-width: 336px;
}
}
@media (max-width: $screen-s) {
width: 0px;
min-width: 0px;
&.opened {
width: 100vw;
min-width: 100vw;
}
}
}
.closable-left-side {
position: absolute;
background-color: var(--color-generic-white);
z-index: 0;
display: flex;
justify-content: center;
min-width: 56px;
max-width: 56px;
height: calc(100vh - 83px);
border-right: 1px var(--color-neutral-200) solid;
@media (min-width: $screen-m) {
display: none;
}
.chevron-icon {
margin-top: 21px;
transform: rotate(180deg);
cursor: pointer;
}
@media (max-width: $screen-s) {
display: none;
}
}
.right-side {
min-width: calc(100vw - 389px);
padding: 24px;
overflow-y: auto;
@media (max-width: ($screen-m - 1px)) {
min-width: calc(100vw - 56px);
}
@media (max-width: $screen-s) {
flex: 1;
min-width: unset;
}
.back-arrow-mobile {
display: none;
@media (max-width: $screen-s) {
display: block;
margin-bottom: 24px;
}
}
.back-arrow-desktop {
@media (max-width: $screen-s) {
display: none;
}
}
}
}
.background-image-container {
position: fixed;
top: 0;
right: 0;
@media (max-width: $screen-l) {
display: none;
}
.background-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}

View File

@ -1,117 +1,81 @@
import ChevronIcon from "@Assets/Icons/chevron.svg";
import Folders, { IGetFoldersParams } from "@Front/Api/LeCoffreApi/Notary/Folders/Folders";
import Button, { EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import FolderArchivedListContainer from "@Front/Components/DesignSystem/FolderArchivedListContainer";
import FolderListContainer from "@Front/Components/DesignSystem/FolderListContainer";
import Header from "@Front/Components/DesignSystem/Header";
import Version from "@Front/Components/DesignSystem/Version";
import BackArrow from "@Front/Components/Elements/BackArrow";
import WindowStore from "@Front/Stores/WindowStore";
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import EFolderStatus from "le-coffre-resources/dist/Customer/EFolderStatus";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import Image, { StaticImageData } from "next/image";
import React, { ReactNode } from "react";
import React, { useCallback, useEffect } from "react";
import { EDocumentStatus } from "le-coffre-resources/dist/Notary/Document";
import classes from "./classes.module.scss";
import Module from "@Front/Config/Module";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import { useRouter } from "next/router";
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
type IProps = {
title: string;
children?: ReactNode;
type IProps = IPropsDashboardWithList & {
isArchived?: boolean;
onSelectedFolder: (folder: OfficeFolder) => void;
hasBackArrow: boolean;
backArrowUrl?: string;
mobileBackText?: string;
image?: StaticImageData;
};
type IState = {
folders: OfficeFolder[] | null;
isLeftSideOpen: boolean;
leftSideCanBeClosed: boolean;
};
export default class DefaultNotaryDashboard extends React.Component<IProps, IState> {
private onWindowResize = () => {};
public static defaultProps: Partial<IProps> = {
hasBackArrow: false,
isArchived: false,
export default function DefaultNotaryDashboard(props: IProps) {
const { isArchived = false } = props;
const router = useRouter();
const [folders, setFolders] = React.useState<OfficeFolder[]>([]);
const { folderUid } = router.query;
const redirectPath: string = isArchived
? Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.pages.FolderInformation.props.path
: Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path;
const getBlocks = useCallback(
(folders: OfficeFolder[]): IBlock[] => {
const pendingFolders = folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length >= 1;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
const otherFolders = folders
.filter((folder) => {
const pendingDocuments = (folder.documents ?? []).filter(
(document) => document.document_status === EDocumentStatus.DEPOSITED,
);
return pendingDocuments.length === 0;
})
.sort((folder1, folder2) => {
return folder1.created_at! > folder2.created_at! ? -1 : 1;
});
return [...pendingFolders, ...otherFolders].map((folder) => {
return {
id: folder.uid!,
primaryText: folder.name,
secondaryText: folder.folder_number,
isActive: folderUid === folder.uid,
showAlert: folder.documents?.some((document) => document.document_status === EDocumentStatus.DEPOSITED),
};
});
},
[folderUid],
);
const [blocks, setBlocks] = React.useState<IBlock[]>(getBlocks(folders));
const onSelectedBlock = (block: IBlock) => {
const folder = folders.find((folder) => folder.uid === block.id);
if (!folder) return;
const path = redirectPath.replace("[folderUid]", folder.uid ?? "");
router.push(path);
};
public constructor(props: IProps) {
super(props);
this.state = {
folders: null,
isLeftSideOpen: false,
leftSideCanBeClosed: typeof window !== "undefined" ? window.innerWidth < 1024 : false,
};
this.onOpenLeftSide = this.onOpenLeftSide.bind(this);
this.onCloseLeftSide = this.onCloseLeftSide.bind(this);
}
useEffect(() => {
setBlocks(getBlocks(folders));
}, [folders, getBlocks]);
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Header isUserConnected={true} />
<div className={classes["content"]}>
{this.state.isLeftSideOpen && <div className={classes["overlay"]} onClick={this.onCloseLeftSide} />}
<div className={classNames(classes["left-side"], this.state.isLeftSideOpen && classes["opened"])}>
{this.props.isArchived && this.state.folders && (
<FolderArchivedListContainer
folders={this.state.folders}
onSelectedFolder={this.props.onSelectedFolder}
onCloseLeftSide={this.onCloseLeftSide}
isArchived={true}
/>
)}
{!this.props.isArchived && this.state.folders && (
<FolderListContainer
folders={this.state.folders}
onSelectedFolder={this.props.onSelectedFolder}
onCloseLeftSide={this.onCloseLeftSide}
isArchived={false}
/>
)}
</div>
<div className={classNames(classes["closable-left-side"])}>
<Image alt="open side menu" src={ChevronIcon} className={classes["chevron-icon"]} onClick={this.onOpenLeftSide} />
</div>
<div className={classes["right-side"]}>
{this.props.hasBackArrow && (
<div className={classes["back-arrow-desktop"]}>
<BackArrow url={this.props.backArrowUrl ?? ""} />
</div>
)}
{this.props.mobileBackText && (
<div className={classes["back-arrow-mobile"]}>
<Button
leftIcon={<ChevronLeftIcon />}
variant={EButtonVariant.PRIMARY}
styletype={EButtonstyletype.TEXT}
onClick={this.onOpenLeftSide}>
{this.props.mobileBackText ?? "Retour"}
</Button>
</div>
)}
{this.props.children}
</div>
{this.props.image && (
<div className={classes["background-image-container"]}>
<Image alt={"right side image"} src={this.props.image} className={classes["background-image"]} priority />
</div>
)}
</div>
<Version />
</div>
);
}
public override async componentDidMount() {
this.onWindowResize = WindowStore.getInstance().onResize((window) => this.onResize(window));
useEffect(() => {
let targetedStatus: EFolderStatus = EFolderStatus["LIVE" as keyof typeof EFolderStatus];
if (this.props.isArchived) targetedStatus = EFolderStatus.ARCHIVED;
if (isArchived) targetedStatus = EFolderStatus.ARCHIVED;
const query: IGetFoldersParams = {
q: {
where: { status: targetedStatus },
@ -143,27 +107,20 @@ export default class DefaultNotaryDashboard extends React.Component<IProps, ISta
},
};
const folders = await Folders.getInstance().get(query);
this.setState({ folders: folders });
}
public override componentWillUnmount() {
this.onWindowResize();
}
Folders.getInstance()
.get(query)
.then((folders) => setFolders(folders));
}, [isArchived]);
private onOpenLeftSide() {
this.setState({ isLeftSideOpen: true });
}
private onCloseLeftSide() {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ isLeftSideOpen: false });
}
private onResize(window: Window) {
if (window.innerWidth > 1023) {
if (!this.state.leftSideCanBeClosed) return;
this.setState({ leftSideCanBeClosed: false });
}
this.setState({ leftSideCanBeClosed: true });
}
return (
<DefaultDashboardWithList
{...props}
onSelectedBlock={onSelectedBlock}
blocks={blocks}
bottomButton={{
link: Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
text: "Créer un dossier",
}}
/>
);
}

View File

@ -1,23 +0,0 @@
@import "@Themes/constants.scss";
.root {
width: calc(100vh - 83px);
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
flex: 1;
}
.searchbar {
padding: 40px 24px 24px 24px;
}
.folderlist-container {
max-height: 100vh;
height: 100vh;
overflow: auto;
border-right: 1px solid var(--color-neutral-200);
}
}

View File

@ -1,44 +0,0 @@
import Module from "@Front/Config/Module";
import { Office } from "le-coffre-resources/dist/SuperAdmin";
import { useRouter } from "next/router";
import React, { useCallback } from "react";
import classes from "./classes.module.scss";
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
import SearchBlockList from "@Front/Components/DesignSystem/SearchBlockList";
type IProps = {
offices: Office[];
onSelectedOffice?: (office: Office) => void;
onCloseLeftSide?: () => void;
};
export default function OfficeListContainer(props: IProps) {
const router = useRouter();
const { officeUid } = router.query;
const onSelectedBlock = useCallback(
(block: IBlock) => {
props.onCloseLeftSide && props.onCloseLeftSide();
const redirectPath = Module.getInstance().get().modules.pages.Offices.pages.OfficesInformations.props.path;
router.push(redirectPath.replace("[uid]", block.id));
},
[props, router],
);
return (
<div className={classes["root"]}>
<SearchBlockList
blocks={props.offices.map((office) => {
return {
primaryText: office.crpcen + " - " + office.name,
id: office.uid!,
isActive: office.uid === officeUid,
};
})}
onSelectedBlock={onSelectedBlock}
/>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More