Add owner portal, i18n, contact API, and region/footer assets
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,4 @@
|
||||
<svg width="56" height="53" viewBox="0 0 56 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.4054 36.8816C41.5072 46.627 41.1617 47.7533 38.079 48.6702C34.3853 49.7607 30.7938 50.2257 29.4319 50.2257C27.3314 50.2257 25.2738 49.5827 23.5529 48.3763C20.7284 46.4962 20.3436 45.8765 19.0641 45.6328C18.9284 45.1354 19.0699 45.5578 13.2585 33.4462L16.3572 31.5233C20.0255 29.2513 24.9124 30.1882 27.4722 33.6606C27.9193 34.2608 28.6235 34.616 29.3768 34.573L37.0256 34.2485C39.0179 34.1881 40.5109 35.62 40.7919 37.2922L46.922 32.6991C49.2117 30.9652 52.3239 31.1161 54.4422 32.8155C55.7296 33.8503 55.748 35.8389 54.4054 36.8816ZM16.2273 48.5657L8.63873 52.1744C7.6212 52.6642 6.39922 52.2364 5.90942 51.2188L0.202796 39.3632C-0.287005 38.3457 0.140883 37.1237 1.15841 36.6339L8.74698 33.0252C9.76451 32.5354 10.9865 32.9632 11.4763 33.9808L17.1828 45.8363C17.6726 46.8539 17.2448 48.0757 16.2273 48.5657Z" fill="#F5F5F5"/>
|
||||
<path d="M35.679 24.7862C37.1181 25.466 41.9745 27.9217 45.5814 31.4062L41.4783 34.4804C39.6083 32.143 37.0539 32.4347 36.9466 32.4105L29.255 32.7413C29.1447 32.7413 29.0284 32.6738 28.9488 32.5698C28.1343 31.4675 27.1055 30.5489 25.9419 29.8753C28.9855 27.4012 32.3659 25.6314 33.8111 24.9272C34.083 22.7524 34.0188 20.5103 33.407 18.3684C21.8972 18.6018 16.2285 12.5572 16.2048 1.6255C16.2048 0.731331 16.909 0.0149466 17.8032 0.0026577C28.9081 -0.141883 34.773 5.61144 34.7971 16.7395C35.3258 9.52575 39.3562 5.6476 47.3634 5.62443C48.0371 5.61834 48.5821 6.15718 48.5882 6.82476C48.6819 14.9604 44.6221 19.6051 35.9852 19.6299C35.8685 19.8839 35.7642 20.1435 35.6728 20.4076C35.8383 21.7304 35.8505 23.1879 35.679 24.7862Z" fill="#F5F5F5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M36.0996 28.449C36.3757 29.3654 36.5156 30.3175 36.5147 31.2746C36.5147 36.6729 32.1319 41.0557 26.7337 41.0557C21.3354 41.0557 16.9526 36.6729 16.9526 31.2746C16.9518 30.3175 17.0916 29.3654 17.3677 28.449" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M47.9257 28.449V34.1003L43.3459 34.6727C42.906 36.8305 42.0558 38.8398 40.8806 40.6155L43.7166 44.2617L39.7206 48.2578L36.0742 45.4217C34.2989 46.5971 32.2895 47.4474 30.1314 47.887L29.559 52.467H23.9077L23.3352 47.8871C21.1773 47.4472 19.168 46.5971 17.3924 45.4219L13.7461 48.2579L9.75007 44.2618L12.5861 40.6156C11.4108 38.8402 10.5605 36.8307 10.1208 34.6728L5.54102 34.1004V28.4491" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M50.7515 28.239L49.0359 19.8713H4.43117L2.71553 28.239M1 28.344H52.467M31.8802 7.86223V1H21.5868V7.86223M6.43187 19.8713C7.78272 11.8357 13.7946 5.37365 21.5868 3.36528M31.8802 3.36528C39.6724 5.37365 45.6843 11.8357 47.0352 19.8713" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="59" height="59" viewBox="0 0 59 59" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.399 18.7769V12.7757M48.4031 12.7873V18.7769M17.8722 12.6099H8.23572M50.5665 12.6099H41.0856M40.8189 1.15456V21.7573M18.0857 21.8106V1.13911M25.8044 8.79072V11.5186M32.9979 8.79072V11.5186M25.8044 19.3812V22.109M32.9979 19.3812V22.109M15.5797 1H43.2224M21.0981 30.0597H37.8385M19.5252 46.9185C16.1001 45.0464 13.2672 42.2307 11.3783 38.8222M47.4387 38.7955C45.5497 42.2161 42.711 45.0415 39.2769 46.9185" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8759 32.0372C12.8777 32.0372 14.5005 30.4188 14.5005 28.4224C14.5005 26.426 12.8777 24.8076 10.8759 24.8076C8.87412 24.8076 7.25134 26.426 7.25134 28.4224C7.25134 30.4188 8.87412 32.0372 10.8759 32.0372Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.85718 37.181V37.042C5.85718 34.2777 8.1041 32.0369 10.8758 32.0369C13.6474 32.0369 15.8944 34.2777 15.8944 37.042V37.181" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M48.023 32.0372C50.0248 32.0372 51.6476 30.4188 51.6476 28.4224C51.6476 26.426 50.0248 24.8076 48.023 24.8076C46.0212 24.8076 44.3984 26.426 44.3984 28.4224C44.3984 30.4188 46.0212 32.0372 48.023 32.0372Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M43.0043 37.181V37.042C43.0043 34.2777 45.2512 32.0369 48.0229 32.0369C50.7945 32.0369 53.0415 34.2777 53.0415 37.042V37.181" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M29.4011 51.1779C31.4029 51.1779 33.0256 49.5595 33.0256 47.5631C33.0256 45.5667 31.4029 43.9482 29.4011 43.9482C27.3993 43.9482 25.7765 45.5667 25.7765 47.5631C25.7765 49.5595 27.3993 51.1779 29.4011 51.1779Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.3823 56.3228V56.1838C24.3823 53.4196 26.6292 51.1787 29.4009 51.1787C32.1726 51.1787 34.4195 53.4196 34.4195 56.1838V56.3228" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8759 38.7582C16.3302 38.7582 20.7518 34.3485 20.7518 28.9089C20.7518 23.4693 16.3302 19.0596 10.8759 19.0596C5.42159 19.0596 1 23.4693 1 28.9089C1 34.3485 5.42159 38.7582 10.8759 38.7582Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M48.0229 38.7582C53.4772 38.7582 57.8987 34.3485 57.8987 28.9089C57.8987 23.4693 53.4772 19.0596 48.0229 19.0596C42.5686 19.0596 38.147 23.4693 38.147 28.9089C38.147 34.3485 42.5686 38.7582 48.0229 38.7582Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M29.401 57.8988C34.8553 57.8988 39.2769 53.4891 39.2769 48.0495C39.2769 42.6099 34.8553 38.2002 29.401 38.2002C23.9467 38.2002 19.5251 42.6099 19.5251 48.0495C19.5251 53.4891 23.9467 57.8988 29.401 57.8988Z" stroke="#0F1419" stroke-opacity="0.5" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 619 KiB After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 60H60V4.8399e-05H0V60Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 60H60V4.8399e-05H0V60Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 50H50V-3.57628e-07H0V50Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 60H60V4.8399e-05H0V60Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 154 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.726632 5.07752C0.533917 5.07752 0.349095 5.15407 0.212825 5.29034C0.0765554 5.42661 -2.6213e-07 5.61143 -2.53707e-07 5.80415C-2.45283e-07 5.99686 0.0765554 6.18168 0.212825 6.31795C0.349095 6.45422 0.533917 6.53078 0.726632 6.53078L10.5987 6.53078L6.75259 10.3768C6.62023 10.5139 6.54699 10.6974 6.54865 10.888C6.5503 11.0785 6.62672 11.2607 6.76145 11.3955C6.89617 11.5302 7.07842 11.6066 7.26894 11.6082C7.45946 11.6099 7.64301 11.5367 7.78005 11.4043L12.8665 6.31788C13.0027 6.18161 13.0792 5.99682 13.0792 5.80415C13.0792 5.61147 13.0027 5.42668 12.8665 5.29042L7.78005 0.203992C7.64301 0.0716301 7.45946 -0.00161013 7.26894 4.54586e-05C7.07842 0.00170105 6.89617 0.0781199 6.76144 0.212844C6.62672 0.347567 6.5503 0.529816 6.54865 0.720337C6.54699 0.910857 6.62023 1.09441 6.75259 1.23145L10.5987 5.07751L0.726632 5.07752Z" fill="#0B1220"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 961 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="12" viewBox="0 0 21 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.726632 5.07752C0.533917 5.07752 0.349095 5.15407 0.212825 5.29034C0.0765554 5.42661 -2.6213e-07 5.61143 -2.53707e-07 5.80415C-2.45283e-07 5.99686 0.0765554 6.18168 0.212825 6.31795C0.349095 6.45422 0.533917 6.53078 0.726632 6.53078L17.5987 6.53078L13.7526 10.3768C13.6202 10.5139 13.547 10.6974 13.5486 10.888C13.5503 11.0785 13.6267 11.2607 13.7614 11.3954C13.8962 11.5302 14.0784 11.6066 14.2689 11.6082C14.4595 11.6099 14.643 11.5367 14.7801 11.4043L19.8665 6.31788C20.0027 6.18161 20.0792 5.99682 20.0792 5.80415C20.0792 5.61147 20.0027 5.42668 19.8665 5.29042L14.7801 0.203991C14.643 0.0716289 14.4595 -0.00161043 14.2689 4.51527e-05C14.0784 0.00170074 13.8962 0.0781196 13.7614 0.212843C13.6267 0.347567 13.5503 0.529816 13.5486 0.720336C13.547 0.910857 13.6202 1.09441 13.7526 1.23145L17.5987 5.07751L0.726632 5.07752Z" fill="#151515"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 959 B |
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="17" viewBox="0 0 8 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.66703 16.2328C1.66703 16.347 1.75898 16.4398 1.87218 16.4398H4.79711C4.91031 16.4398 5.00168 16.347 5.00168 16.2328V8.15525H7.12211C7.22841 8.15525 7.31748 8.07289 7.3261 7.96559L7.5301 5.53195C7.53255 5.50341 7.52909 5.47468 7.51995 5.44757C7.5108 5.42045 7.49618 5.39556 7.477 5.37445C7.45782 5.35335 7.4345 5.33649 7.40852 5.32497C7.38255 5.31344 7.35448 5.30749 7.3261 5.30749H5.00168V3.58086C5.00168 3.17603 5.32693 2.84833 5.72803 2.84833H7.36173C7.47551 2.84833 7.56688 2.75553 7.56688 2.64128V0.207057C7.56688 0.0927984 7.47551 0 7.36173 0H4.60115C2.98067 0 1.66703 1.32586 1.66703 2.96085V5.30749H0.205147C0.0919422 5.30749 0 5.40029 0 5.51455V7.94819C0 8.06303 0.0919422 8.15525 0.205147 8.15525H1.66703V16.2328Z" fill="#BABABA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 894 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M12.2661 3.64912C12.2373 2.98961 12.1316 2.53624 11.9802 2.14335C11.824 1.72633 11.5837 1.35299 11.2689 1.0426C10.9613 0.727398 10.5889 0.482459 10.1805 0.327329C9.7889 0.174576 9.34194 0.0678905 8.68841 0.0388431C8.02999 0.00727398 7.82097 0 6.15106 0C4.48114 0 4.27212 0.00727398 3.61615 0.0363699C2.96262 0.0654659 2.51333 0.172199 2.12411 0.324856C1.71074 0.482459 1.34076 0.724925 1.03319 1.0426C0.720827 1.35296 0.478221 1.72878 0.324372 2.14098C0.172998 2.53624 0.0673012 2.98718 0.0384922 3.64664C0.0072323 4.31107 0 4.52199 0 6.20713C0 7.89227 0.0072323 8.10322 0.0360413 8.76515C0.0648744 9.42466 0.170644 9.87802 0.322041 10.2709C0.478221 10.6879 0.720827 11.0613 1.03319 11.3717C1.34076 11.6869 1.71319 11.9318 2.12166 12.0869C2.51331 12.2397 2.96017 12.3464 3.61382 12.3754C4.26967 12.4046 4.47881 12.4118 6.14872 12.4118C7.81864 12.4118 8.02766 12.4046 8.68363 12.3754C9.33716 12.3464 9.78645 12.2397 10.1757 12.0869C10.5845 11.9274 10.9557 11.6835 11.2656 11.3708C11.5756 11.0581 11.8173 10.6834 11.9754 10.2709C12.1267 9.87567 12.2325 9.42464 12.2613 8.76515C12.2901 8.10322 12.2973 7.89227 12.2973 6.20713C12.2973 4.52199 12.2949 4.31105 12.2661 3.64912ZM11.1585 8.71666C11.132 9.32282 11.0311 9.65015 10.947 9.86837C10.7403 10.4091 10.3151 10.8382 9.77922 11.0468C9.56297 11.1317 9.23627 11.2335 8.63791 11.2601C7.98916 11.2893 7.79461 11.2965 6.15351 11.2965C4.5124 11.2965 4.3154 11.2893 3.66898 11.2601C3.0683 11.2335 2.74392 11.1317 2.52768 11.0468C2.26104 10.9473 2.01834 10.7897 1.82131 10.5836C1.61708 10.3824 1.4609 10.1399 1.36234 9.87085C1.27824 9.65263 1.17735 9.32282 1.15099 8.71913C1.12206 8.06447 1.11495 7.86803 1.11495 6.21196C1.11495 4.55589 1.12206 4.35709 1.15099 3.70491C1.17735 3.09874 1.27824 2.77141 1.36234 2.55319C1.4609 2.28401 1.61708 2.03914 1.82376 1.84027C2.0231 1.63417 2.26337 1.47657 2.53013 1.37723C2.74637 1.29237 3.0732 1.19053 3.67144 1.16384C4.32018 1.13474 4.51485 1.12747 6.15584 1.12747C7.79939 1.12747 7.99394 1.13474 8.64036 1.16384C9.24105 1.19056 9.56542 1.29234 9.78167 1.37721C10.0483 1.47657 10.291 1.63417 10.488 1.84027C10.6923 2.04154 10.8484 2.28401 10.947 2.55319C11.0311 2.77141 11.132 3.10109 11.1584 3.70491C11.1873 4.35957 11.1945 4.55589 11.1945 6.21196C11.1945 7.86803 11.1873 8.062 11.1585 8.71666Z" fill="#BABABA"/>
|
||||
<path d="M6.1161 3.03646C4.36882 3.03646 2.95117 4.455 2.95117 6.20359C2.95117 7.95218 4.36882 9.37072 6.1161 9.37072C7.86343 9.37072 9.28102 7.95218 9.28102 6.20359C9.28102 4.455 7.86343 3.03646 6.1161 3.03646ZM6.1161 8.25801C4.98255 8.25801 4.06306 7.33802 4.06306 6.20359C4.06306 5.06916 4.98255 4.14917 6.11607 4.14917C7.24972 4.14917 8.16909 5.06916 8.16909 6.20359C8.16909 7.33802 7.2497 8.25801 6.1161 8.25801ZM10.1451 2.91122C10.1451 3.31955 9.81425 3.65062 9.40613 3.65062C8.99811 3.65062 8.66727 3.31955 8.66727 2.91122C8.66727 2.50285 8.99811 2.17188 9.40615 2.17188C9.81425 2.17188 10.1451 2.50282 10.1451 2.91122Z" fill="#BABABA"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M13.5386 13.6646V8.65992C13.5386 6.2003 13.0139 4.32142 10.1708 4.32142C8.80006 4.32142 7.88621 5.07297 7.5139 5.79036H7.48005V4.54347H4.78926V13.6646H7.59851V9.13818C7.59851 7.94253 7.81851 6.79812 9.27391 6.79812C10.7124 6.79812 10.7293 8.1475 10.7293 9.2065V13.6475H13.5386V13.6646ZM0.220002 4.54347H3.02925V13.6646H0.220002V4.54347ZM1.62463 0C0.727697 0 0 0.734471 0 1.63975C0 2.54503 0.727697 3.29658 1.62463 3.29658C2.52156 3.29658 3.24925 2.54503 3.24925 1.63975C3.24925 0.734471 2.52156 0 1.62463 0Z" fill="#BABABA"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 661 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M7.14163 5.19284L11.6089 0H10.5503L6.67137 4.50887L3.57328 0H0L4.68492 6.81821L0 12.2637H1.05866L5.15491 7.50218L8.42672 12.2637H12L7.14137 5.19284H7.14163ZM5.69165 6.87828L5.21697 6.19934L1.44011 0.79694H3.06615L6.11412 5.15685L6.5888 5.83579L10.5508 11.503H8.92476L5.69165 6.87854V6.87828Z" fill="#BABABA"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="13" height="9" viewBox="0 0 13 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M12.4153 1.40173C12.2692 0.85361 11.841 0.421537 11.298 0.273941C10.306 -1.90652e-08 6.33807 0 6.33807 0C6.33807 0 2.37026 0 1.37829 0.263566C0.845738 0.411002 0.407132 0.85371 0.261056 1.40173C0 2.40286 0 4.4791 0 4.4791C0 4.4791 0 6.56577 0.261056 7.55642C0.407271 8.10447 0.835299 8.53656 1.37837 8.68413C2.3807 8.95816 6.33823 8.95816 6.33823 8.95816C6.33823 8.95816 10.306 8.95816 11.298 8.69459C11.8411 8.54707 12.2692 8.115 12.4154 7.56696C12.6764 6.56575 12.6764 4.48961 12.6764 4.48961C12.6764 4.48961 12.6868 2.40284 12.4153 1.40173ZM5.07474 6.39717V2.56097L8.37431 4.47906L5.07474 6.39717Z" fill="#BABABA"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 752 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="44" height="59" viewBox="0 0 44 59" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.176 0.103878C27.2012 -0.486482 25.6287 1.55899 25.1568 3.22198C24.5332 5.43271 25.3356 7.6133 26.9518 8.08621C28.9068 8.66929 30.5085 6.61655 30.971 4.96811C31.5977 2.75738 30.7922 0.57575 29.176 0.103878ZM30.1426 31.1913C27.4703 31.1913 24.8579 31.9838 22.6359 33.4685C20.4139 34.9532 18.6821 37.0634 17.6594 39.5324C16.6367 42.0013 16.3691 44.7181 16.8905 47.3391C17.4118 49.9601 18.6987 52.3677 20.5884 54.2573C22.478 56.147 24.8856 57.4339 27.5066 57.9552C30.1276 58.4766 32.8444 58.209 35.3134 57.1863C37.7823 56.1636 39.8926 54.4318 41.3772 52.2098C42.8619 49.9878 43.6544 47.3755 43.6544 44.7031C43.6503 41.1208 42.2254 37.6864 39.6923 35.1534C37.1593 32.6203 33.7249 31.1954 30.1426 31.1913ZM21.8277 47.8212C22.1034 47.8212 22.3677 47.7117 22.5626 47.5168C22.7576 47.3218 22.8671 47.0575 22.8671 46.7818C22.8671 46.5062 22.9766 46.2418 23.1715 46.0469C23.3664 45.8519 23.6308 45.7424 23.9064 45.7424C24.1821 45.7424 24.4465 45.8519 24.6414 46.0469C24.8363 46.2418 24.9458 46.5062 24.9458 46.7818C24.9458 47.6088 24.6173 48.4019 24.0325 48.9866C23.4478 49.5714 22.6547 49.8999 21.8277 49.8999C21.0007 49.8999 20.2076 49.5714 19.6229 48.9866C19.0381 48.4019 18.7096 47.6088 18.7096 46.7818V42.6243C18.7096 41.7974 19.0381 41.0043 19.6229 40.4195C20.2076 39.8348 21.0007 39.5062 21.8277 39.5062C22.6547 39.5062 23.4478 39.8348 24.0325 40.4195C24.6173 41.0043 24.9458 41.7974 24.9458 42.6243C24.9458 42.9 24.8363 43.1644 24.6414 43.3593C24.4465 43.5542 24.1821 43.6637 23.9064 43.6637C23.6308 43.6637 23.3664 43.5542 23.1715 43.3593C22.9766 43.1644 22.8671 42.9 22.8671 42.6243C22.8671 42.3487 22.7576 42.0843 22.5626 41.8894C22.3677 41.6945 22.1034 41.585 21.8277 41.585C21.552 41.585 21.2877 41.6945 21.0928 41.8894C20.8978 42.0843 20.7883 42.3487 20.7883 42.6243V46.7818C20.7883 47.0575 20.8978 47.3218 21.0928 47.5168C21.2877 47.7117 21.552 47.8212 21.8277 47.8212ZM33.2607 46.7818C33.2607 47.6088 32.9322 48.4019 32.3475 48.9866C31.7627 49.5714 30.9696 49.8999 30.1426 49.8999C29.3157 49.8999 28.5226 49.5714 27.9378 48.9866C27.353 48.4019 27.0245 47.6088 27.0245 46.7818V42.6243C27.0245 41.7974 27.353 41.0043 27.9378 40.4195C28.5226 39.8348 29.3157 39.5062 30.1426 39.5062C30.9696 39.5062 31.7627 39.8348 32.3475 40.4195C32.9322 41.0043 33.2607 41.7974 33.2607 42.6243V46.7818ZM40.5363 44.1834C40.4735 44.9104 40.2471 45.6138 39.874 46.2409C39.5009 46.868 38.9907 47.4026 38.3817 47.8046L38.363 47.8212H39.4969C39.7726 47.8212 40.0369 47.9307 40.2319 48.1256C40.4268 48.3205 40.5363 48.5849 40.5363 48.8605C40.5363 49.1362 40.4268 49.4006 40.2319 49.5955C40.0369 49.7904 39.7726 49.8999 39.4969 49.8999H36.3788C36.1032 49.8999 35.8388 49.7904 35.6439 49.5955C35.449 49.4006 35.3395 49.1362 35.3395 48.8605C35.4087 48.3253 35.5956 47.812 35.8867 47.3576C36.1779 46.9031 36.566 46.5187 37.0232 46.232C37.916 45.4618 38.4576 44.9401 38.4576 44.1834C38.4576 44.0456 38.4028 43.9134 38.3053 43.8159C38.2079 43.7185 38.0757 43.6637 37.9379 43.6637C37.8141 43.663 37.6927 43.6979 37.5883 43.7643C37.4838 43.8306 37.4006 43.9256 37.3485 44.0379C37.2994 44.1652 37.2257 44.2817 37.1315 44.3805C37.0374 44.4794 36.9247 44.5587 36.7999 44.614C36.6751 44.6693 36.5407 44.6995 36.4042 44.7028C36.2678 44.7062 36.132 44.6826 36.0047 44.6334C35.8773 44.5843 35.7609 44.5106 35.662 44.4164C35.5632 44.3223 35.4838 44.2096 35.4285 44.0848C35.3732 43.96 35.3431 43.8256 35.3397 43.6891C35.3364 43.5527 35.36 43.4169 35.4091 43.2895C35.6125 42.7856 35.962 42.354 36.4126 42.0502C36.8632 41.7465 37.3944 41.5845 37.9379 41.585C38.2791 41.585 38.617 41.6522 38.9322 41.7828C39.2475 41.9134 39.5339 42.1048 39.7752 42.346C40.0165 42.5873 40.2079 42.8738 40.3385 43.189C40.4691 43.5043 40.5363 43.8422 40.5363 44.1834Z" fill="#F5F5F5"/>
|
||||
<path d="M30.1434 41.5847C29.8678 41.5847 29.6034 41.6942 29.4085 41.8891C29.2136 42.0841 29.1041 42.3484 29.1041 42.6241V46.7816C29.1041 47.0572 29.2136 47.3216 29.4085 47.5165C29.6034 47.7114 29.8678 47.8209 30.1434 47.8209C30.4191 47.8209 30.6835 47.7114 30.8784 47.5165C31.0733 47.3216 31.1828 47.0572 31.1828 46.7816V42.6241C31.1828 42.3484 31.0733 42.0841 30.8784 41.8891C30.6835 41.6942 30.4191 41.5847 30.1434 41.5847ZM1.86436 8.53288C1.63168 8.36838 1.34355 8.30221 1.06241 8.34871C0.781278 8.39521 0.529795 8.55063 0.362478 8.78129C0.0414866 9.31981 -0.0716237 9.95707 0.0444321 10.5732C0.221124 11.6946 1.02455 12.5396 1.7968 12.4243C2.77069 12.2684 2.96609 10.9307 2.83617 10.1491C2.78995 9.82857 2.68017 9.5205 2.51329 9.24296C2.34642 8.96543 2.12579 8.72401 1.86436 8.53288ZM17.671 13.5218C9.37586 13.5218 3.11992 17.0962 3.11992 21.8368C3.22513 24.3547 3.865 26.8213 4.99701 29.0728C7.32311 34.5867 7.49149 39.6993 6.70469 45.5551C6.42642 47.337 6.27049 49.1358 6.23801 50.939C6.23671 52.4283 6.69291 53.882 7.54489 55.1035C8.39687 56.325 9.60351 57.2552 11.0015 57.7685C12.3996 58.2817 13.9215 58.3531 15.3614 57.9729C16.8014 57.5928 18.0898 56.7796 19.0524 55.6432C17.386 53.9595 16.1264 51.9174 15.3696 49.6728C14.6128 47.4281 14.3788 45.0402 14.6855 42.6913C14.9922 40.3424 15.8315 38.0946 17.1394 36.1196C18.4472 34.1445 20.189 32.4943 22.2318 31.295C22.608 30.7462 22.9999 30.2068 23.4 29.6653C25.2647 27.1375 27.0253 24.7574 27.0253 20.7974C27.0253 15.9695 23.8781 13.5218 17.671 13.5218ZM17.7947 1.05568C16.5132 1.04945 15.7773 2.4765 15.6276 3.57927C15.4198 5.11441 16.166 6.54977 17.2574 6.71191C18.6148 6.91979 19.466 5.30669 19.6178 4.19041C19.781 2.99826 19.2976 1.0692 17.7947 1.05568ZM8.80213 8.25537C9.91113 8.25641 10.3955 6.86262 10.3955 5.95733C10.3955 4.69554 9.65441 3.62084 8.77823 3.60941C8.61097 3.6077 8.44513 3.64019 8.29088 3.70489C8.13663 3.76959 7.99724 3.86514 7.88125 3.98566C7.42648 4.5179 7.18016 5.19702 7.18799 5.89705C7.18176 7.16196 7.92075 8.23874 8.80213 8.25537Z" fill="#F5F5F5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="58" height="60" viewBox="0 0 58 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.1538 36.5769L7.9551 33.3781H4.63584C4.38994 33.3781 4.15411 33.2804 3.98023 33.1066C3.80635 32.9327 3.70867 32.6969 3.70867 32.451C3.70867 32.2051 3.80635 31.9692 3.98023 31.7953C4.15411 31.6215 4.38994 31.5238 4.63584 31.5238H6.10076L3.64377 29.0668C3.46918 28.8922 3.3711 28.6554 3.3711 28.4085C3.3711 28.1616 3.46918 27.9248 3.64377 27.7502C3.81836 27.5756 4.05515 27.4775 4.30206 27.4775C4.54896 27.4775 4.78576 27.5756 4.96035 27.7502L9.0028 31.7927L10.8757 33.6655L10.032 24.7926C8.58615 24.3367 7.07897 24.1053 5.563 24.1064H2.7815C2.04448 24.1087 1.33827 24.4024 0.817115 24.9236C0.295958 25.4447 0.00220118 26.1509 0 26.888V29.6695C0.00392094 33.0895 1.18766 36.4035 3.35143 39.052C5.51519 41.7005 8.52647 43.5215 11.877 44.2074L11.1538 36.5769Z" fill="#F5F5F5"/>
|
||||
<path d="M46.3584 11.126H11.1261C10.1019 11.126 9.27173 11.9562 9.27173 12.9803V16.689C9.27173 17.7131 10.1019 18.5433 11.1261 18.5433H46.3584C47.3825 18.5433 48.2128 17.7131 48.2128 16.689V12.9803C48.2128 11.9562 47.3825 11.126 46.3584 11.126Z" fill="#F5F5F5"/>
|
||||
<path d="M43.9302 6.65706C43.7622 6.07572 43.4102 5.56452 42.9271 5.20011C42.444 4.83569 41.8558 4.63771 41.2507 4.63584H16.2357C15.6306 4.63771 15.0424 4.83569 14.5593 5.20011C14.0762 5.56452 13.7242 6.07572 13.5562 6.65706L12.8145 9.27167H44.6719L43.9302 6.65706ZM34.3062 1.85433C34.3047 1.36299 34.1089 0.892181 33.7614 0.544743C33.414 0.197306 32.9432 0.00146745 32.4519 0L25.0345 0C24.5432 0.00146745 24.0724 0.197306 23.7249 0.544743C23.3775 0.892181 23.1817 1.36299 23.1802 1.85433V2.7815H34.3062V1.85433Z" fill="#F5F5F5"/>
|
||||
<path d="M15.7612 54.703C16.2732 54.703 16.6883 54.2879 16.6883 53.7758C16.6883 53.2637 16.2732 52.8486 15.7612 52.8486C15.2491 52.8486 14.834 53.2637 14.834 53.7758C14.834 54.2879 15.2491 54.703 15.7612 54.703Z" fill="#F5F5F5"/>
|
||||
<path d="M15.7622 48.2129C14.662 48.2129 13.5864 48.5392 12.6716 49.1504C11.7568 49.7617 11.0437 50.6305 10.6227 51.647C10.2016 52.6635 10.0915 53.7821 10.3061 54.8612C10.5208 55.9403 11.0506 56.9315 11.8286 57.7095C12.6066 58.4875 13.5978 59.0174 14.6769 59.232C15.7561 59.4467 16.8746 59.3365 17.8911 58.9154C18.9076 58.4944 19.7764 57.7814 20.3877 56.8665C20.999 55.9517 21.3252 54.8762 21.3252 53.7759C21.3235 52.301 20.7369 50.887 19.694 49.8442C18.6511 48.8013 17.2371 48.2146 15.7622 48.2129ZM15.7622 56.5574C15.2121 56.5574 14.6743 56.3943 14.2169 56.0886C13.7595 55.783 13.403 55.3486 13.1925 54.8403C12.9819 54.3321 12.9268 53.7728 13.0342 53.2333C13.1415 52.6937 13.4064 52.1981 13.7954 51.8091C14.1844 51.4201 14.68 51.1552 15.2196 51.0478C15.7591 50.9405 16.3184 50.9956 16.8267 51.2061C17.3349 51.4166 17.7693 51.7732 18.075 52.2306C18.3806 52.688 18.5437 53.2258 18.5437 53.7759C18.543 54.5134 18.2497 55.2204 17.7282 55.7419C17.2068 56.2634 16.4997 56.5567 15.7622 56.5574Z" fill="#F5F5F5"/>
|
||||
<path d="M44.2441 38.9686L46.015 20.3975H11.4688L13.9628 46.5807C14.5506 46.4309 15.155 46.3561 15.7615 46.3581C17.728 46.3606 19.6132 47.1429 21.0037 48.5333C22.3942 49.9238 23.1764 51.809 23.1789 53.7755C23.1793 54.7294 22.9935 55.6743 22.6318 56.557H35.232V53.7755C35.2314 50.7182 36.0718 47.7197 37.6613 45.1081C39.2509 42.4965 41.5282 40.3725 44.2441 38.9686ZM26.0809 27.8797L27.9352 24.579C28.0225 24.4445 28.142 24.3339 28.2829 24.2574C28.4238 24.1808 28.5815 24.1407 28.7419 24.1407C28.9022 24.1407 29.06 24.1808 29.2009 24.2574C29.3418 24.3339 29.4613 24.4445 29.5485 24.579L33.2108 31.0784L33.4333 30.448C33.5145 30.2168 33.6841 30.0274 33.905 29.9213C34.1258 29.8152 34.3797 29.8012 34.6108 29.8824C34.842 29.9635 35.0314 30.1332 35.1375 30.354C35.2436 30.5748 35.2576 30.8287 35.1764 31.0599L34.2493 33.6838C34.1864 33.8649 34.0687 34.022 33.9125 34.1333C33.7564 34.2446 33.5695 34.3046 33.3777 34.305C33.2733 34.3045 33.1697 34.2857 33.0717 34.2493L30.4479 33.3222C30.2167 33.241 30.0273 33.0714 29.9212 32.8506C29.8151 32.6297 29.8011 32.3758 29.8823 32.1447C29.9634 31.9135 30.1331 31.7241 30.3539 31.618C30.5747 31.512 30.8287 31.498 31.0598 31.5791L31.4307 31.7089L28.7419 26.9247L27.6942 28.7883C27.5737 29.0023 27.3731 29.1596 27.1367 29.2256C26.9002 29.2917 26.6472 29.2611 26.4332 29.1406C26.2193 29.0202 26.062 28.8196 25.9959 28.5831C25.9298 28.3467 25.9604 28.0936 26.0809 27.8797ZM26.2663 35.0189C26.1684 35.0552 26.0648 35.0741 25.9604 35.0745C25.7686 35.0742 25.5817 35.0142 25.4256 34.9029C25.2694 34.7916 25.1517 34.6345 25.0888 34.4533L24.8663 33.8228L21.9828 38.9408H24.106C24.3519 38.9408 24.5878 39.0385 24.7616 39.2124C24.9355 39.3862 25.0332 39.6221 25.0332 39.868C25.0332 40.1139 24.9355 40.3497 24.7616 40.5236C24.5878 40.6975 24.3519 40.7951 24.106 40.7951H20.3974C20.2353 40.7954 20.0761 40.7526 19.936 40.6712C19.7959 40.5897 19.68 40.4725 19.6 40.3316C19.5166 40.1933 19.4718 40.0353 19.4702 39.8739C19.4686 39.7125 19.5102 39.5536 19.5907 39.4137L23.0862 33.1924L22.7153 33.3222C22.6008 33.3624 22.4796 33.3796 22.3585 33.3729C22.2374 33.3662 22.1187 33.3358 22.0094 33.2833C21.9001 33.2307 21.8021 33.1572 21.7212 33.0668C21.6403 32.9765 21.578 32.8711 21.5378 32.7566C21.4976 32.6422 21.4804 32.5209 21.487 32.3998C21.4937 32.2787 21.5242 32.1601 21.5767 32.0507C21.6292 31.9414 21.7028 31.8434 21.7931 31.7625C21.8835 31.6816 21.9889 31.6193 22.1034 31.5791L24.7272 30.6519C24.8416 30.6107 24.9631 30.5926 25.0845 30.5988C25.2059 30.605 25.3249 30.6353 25.4345 30.688C25.5441 30.7406 25.6421 30.8145 25.7229 30.9054C25.8036 30.9964 25.8654 31.1024 25.9047 31.2175L26.8319 33.8414C26.8731 33.9558 26.8912 34.0772 26.885 34.1986C26.8788 34.3201 26.8485 34.4391 26.7959 34.5487C26.7432 34.6583 26.6693 34.7563 26.5784 34.837C26.4875 34.9178 26.3814 34.9796 26.2663 35.0189ZM37.8837 40.3316C37.8038 40.4725 37.6878 40.5897 37.5477 40.6712C37.4076 40.7526 37.2484 40.7954 37.0864 40.7951H30.0492L30.3273 41.064C30.501 41.2391 30.5985 41.4757 30.5985 41.7223C30.5985 41.9689 30.501 42.2055 30.3273 42.3806C30.1516 42.553 29.9152 42.6495 29.669 42.6495C29.4229 42.6495 29.1865 42.553 29.0108 42.3806L27.1564 40.5263C27.0726 40.4376 27.0065 40.3338 26.9617 40.2203C26.869 39.9946 26.869 39.7414 26.9617 39.5157C27.0065 39.4022 27.0726 39.2983 27.1564 39.2097L29.0108 37.3554C29.1853 37.1808 29.4221 37.0827 29.669 37.0827C29.916 37.0827 30.1527 37.1808 30.3273 37.3554C30.5019 37.5299 30.6 37.7667 30.6 38.0136C30.6 38.2605 30.5019 38.4973 30.3273 38.6719L30.0492 38.9408H35.5009L34.7128 37.5408C34.6532 37.4349 34.615 37.3182 34.6004 37.1975C34.5858 37.0768 34.5951 36.9544 34.6278 36.8373C34.6606 36.7203 34.716 36.6108 34.7911 36.5151C34.8661 36.4194 34.9592 36.3395 35.0652 36.2798C35.1711 36.2202 35.2877 36.182 35.4084 36.1674C35.5291 36.1528 35.6515 36.1621 35.7686 36.1948C35.8857 36.2276 35.9952 36.283 36.0909 36.3581C36.1865 36.4331 36.2664 36.5262 36.3261 36.6322L37.893 39.4137C37.9736 39.5536 38.0152 39.7125 38.0136 39.8739C38.0119 40.0353 37.9671 40.1933 37.8837 40.3316Z" fill="#F5F5F5"/>
|
||||
<path d="M55.6293 38.9404H51.9206C47.9876 38.9448 44.2169 40.5092 41.4358 43.2903C38.6547 46.0714 37.0904 49.8421 37.0859 53.7751V58.0271L41.0663 54.0468L44.5033 50.6098V45.4306C44.5033 45.1847 44.601 44.9489 44.7748 44.775C44.9487 44.6011 45.1845 44.5034 45.4304 44.5034C45.6763 44.5034 45.9122 44.6011 46.0861 44.775C46.2599 44.9489 46.3576 45.1847 46.3576 45.4306V48.7554L49.4108 45.7023C49.5856 45.5334 49.8198 45.4399 50.0629 45.442C50.306 45.4441 50.5386 45.5416 50.7105 45.7136C50.8824 45.8855 50.9799 46.118 50.982 46.3611C50.9841 46.6042 50.8907 46.8384 50.7218 47.0133L46.086 51.6491L43.96 53.7751H46.3576C46.6035 53.7751 46.8393 53.8728 47.0132 54.0467C47.1871 54.2205 47.2848 54.4564 47.2848 54.7023C47.2848 54.9482 47.1871 55.184 47.0132 55.3579C46.8393 55.5318 46.6035 55.6294 46.3576 55.6294H42.1056L38.397 59.3381H42.6489C46.582 59.3337 50.3527 57.7693 53.1338 54.9883C55.9148 52.2072 57.4792 48.4365 57.4836 44.5034V40.7948C57.4831 40.3031 57.2876 39.8317 56.94 39.4841C56.5923 39.1364 56.1209 38.9409 55.6293 38.9404Z" fill="#F5F5F5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="45" height="37" viewBox="0 0 45 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.44799 36.3484C3.33458 36.3484 0 33.0138 0 28.9004C0.0856092 26.4177 0.171218 24.1491 0.256827 22.0945C0.428046 14.7321 1.58377 9.2103 3.724 5.52911C5.52987 2.35078 8.67664 0.513342 13.1643 0.0168069C14.6991 -0.153009 16.0448 0.941209 16.3985 2.44431C16.882 4.49899 15.2559 6.33237 13.3597 7.25977C12.2202 7.81712 11.4053 8.65278 10.9152 9.76676C10.4278 10.8745 10.0665 12.1428 9.83134 13.5717C9.43404 15.986 11.4861 17.9852 13.9329 17.9852C16.3797 17.9852 18.3632 19.9687 18.3632 22.4155V28.3484C18.3632 32.7667 14.7814 36.3484 10.3632 36.3484H7.44799ZM33.6444 36.3484C29.531 36.3484 26.1964 33.0138 26.1964 28.9004C26.282 26.4177 26.3676 24.1491 26.4532 22.0945C26.6244 14.7321 27.7802 9.2103 29.9204 5.52911C31.7263 2.35078 34.873 0.513342 39.3607 0.0168068C40.8955 -0.153009 42.2412 0.941208 42.5949 2.44431C43.0784 4.49899 41.4523 6.33237 39.5561 7.25977C38.4166 7.81712 37.6017 8.65278 37.1116 9.76676C36.6242 10.8745 36.2629 12.1428 36.0277 13.5717C35.6304 15.986 37.6825 17.9852 40.1293 17.9852C42.5761 17.9852 44.5596 19.9687 44.5596 22.4155V28.3484C44.5596 32.7667 40.9778 36.3484 36.5596 36.3484H33.6444Z" fill="#E4F1FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20C4.48667 20 0 15.5133 0 10C0 4.48667 4.48667 0 10 0C15.5133 0 20 4.48667 20 10C20 15.5133 15.5133 20 10 20ZM10 1.5C5.31333 1.5 1.5 5.31333 1.5 10C1.5 14.6867 5.31333 18.5 10 18.5C14.6867 18.5 18.5 14.6867 18.5 10C18.5 5.31333 14.6867 1.5 10 1.5ZM9.555 13.055L14.105 8.505C14.3983 8.21167 14.3983 7.73667 14.105 7.445C13.8117 7.15333 13.3367 7.15167 13.045 7.445L9.025 11.465L6.955 9.395C6.66167 9.10167 6.18667 9.10167 5.895 9.395C5.60333 9.68833 5.60167 10.1633 5.895 10.455L8.495 13.055C8.64167 13.2017 8.83333 13.275 9.025 13.275C9.21667 13.275 9.40833 13.2017 9.555 13.055Z" fill="#1D9E16"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.3933 31.8438C12.0875 31.8438 11.7878 31.9286 11.5273 32.0888C11.2669 32.249 11.0561 32.4784 10.9182 32.7513L6.00694 42.4747H26.5527V31.8438H12.3933ZM45.4922 32.7513C45.3544 32.4784 45.1435 32.249 44.8831 32.0888C44.6227 31.9286 44.3229 31.8438 44.0172 31.8438H29.8578V42.4747H50.4035L45.4922 32.7513ZM56.2315 54.0131L52.0731 45.78H29.8578V56.4109H54.7565C55.9855 56.4109 56.7864 55.1119 56.2315 54.0131ZM4.33753 45.78L0.179108 54.0131C-0.374864 55.11 0.423151 56.4109 1.65405 56.4109H26.5527V45.78H4.33753Z" fill="#F5F5F5"/>
|
||||
<path d="M28.2061 8.48487C24.8648 8.48487 22.1467 11.2033 22.1467 14.5446C22.1467 17.886 24.8649 20.6044 28.2061 20.6044C31.5472 20.6044 34.2655 17.886 34.2655 14.5446C34.2655 11.2033 31.5473 8.48487 28.2061 8.48487ZM28.2061 0C27.2934 0 26.5534 0.739951 26.5534 1.65266V5.28929C26.5534 6.202 27.2934 6.94195 28.2061 6.94195C29.1188 6.94195 29.8587 6.202 29.8587 5.28929V1.65266C29.8587 0.739951 29.1188 0 28.2061 0ZM28.2061 22.1474C27.2934 22.1474 26.5534 22.8874 26.5534 23.8001V27.4367C26.5534 28.3494 27.2934 29.0894 28.2061 29.0894C29.1188 29.0894 29.8587 28.3494 29.8587 27.4367V23.8001C29.8587 22.8874 29.1188 22.1474 28.2061 22.1474ZM22.8305 6.83155L20.2593 4.26012C19.6139 3.6147 18.5675 3.6147 17.922 4.26012C17.2767 4.90543 17.2766 5.95189 17.922 6.59731L20.4932 9.16874C21.1389 9.81427 22.1853 9.81405 22.8305 9.16874C23.4758 8.52332 23.4759 7.47697 22.8305 6.83155ZM38.4901 22.4922L35.9189 19.9207C35.2735 19.2753 34.2272 19.2753 33.5816 19.9207C32.9363 20.566 32.9362 21.6125 33.5816 22.2579L36.1528 24.8294C36.7986 25.4749 37.8449 25.4746 38.4901 24.8294C39.1355 24.1839 39.1356 23.1376 38.4901 22.4922ZM18.9512 12.892H15.3148C14.4021 12.892 13.6621 13.6319 13.6621 14.5446C13.6621 15.4573 14.4021 16.1973 15.3148 16.1973H18.9512C19.8639 16.1973 20.6038 15.4573 20.6038 14.5446C20.6038 13.6319 19.8639 12.892 18.9512 12.892ZM41.0974 12.892H37.461C36.5483 12.892 35.8083 13.6319 35.8083 14.5446C35.8083 15.4573 36.5483 16.1973 37.461 16.1973H41.0974C42.0101 16.1973 42.75 15.4573 42.75 14.5446C42.75 13.6319 42.0101 12.892 41.0974 12.892ZM22.8305 19.9206C22.185 19.2752 21.1389 19.2753 20.4932 19.9206L17.922 22.4921C17.2766 23.1375 17.2767 24.1838 17.922 24.8293C18.5676 25.4747 19.6138 25.4746 20.2593 24.8293L22.8305 22.2578C23.4759 21.6124 23.4758 20.5659 22.8305 19.9206ZM38.4901 4.26012C37.8446 3.61481 36.7985 3.61481 36.1528 4.26012L33.5816 6.83155C32.9362 7.47697 32.9363 8.52332 33.5816 9.16874C34.2273 9.81416 35.2734 9.81405 35.9189 9.16874L38.4901 6.59731C39.1356 5.95189 39.1355 4.90543 38.4901 4.26012Z" fill="#F5F5F5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Aurum Estates — Luxury Real Estate Dashboard</title>
|
||||
<link rel="stylesheet" href="lux.css" />
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
// Entrance animations are opt-in: enable only while the document is visible,
|
||||
// so preview/capture/print (document.hidden) always render full content.
|
||||
(function () {
|
||||
function enable() { if (!document.hidden) document.body.classList.add('anim-on'); }
|
||||
enable();
|
||||
document.addEventListener('visibilitychange', enable);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="image-slot.js"></script>
|
||||
<script src="lux-data.js"></script>
|
||||
<script type="text/babel" src="icons.jsx"></script>
|
||||
<script type="text/babel" src="lux-ui.jsx"></script>
|
||||
<script type="text/babel" src="lux-shell.jsx"></script>
|
||||
<script type="text/babel" src="lux-dashboard.jsx"></script>
|
||||
<script type="text/babel" src="lux-pages.jsx"></script>
|
||||
<script type="text/babel" src="lux-pages2.jsx"></script>
|
||||
<script type="text/babel" src="lux-app.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>YEIDA Portal — Yamuna Expressway Property Management</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
// Entrance animations are opt-in: only enable them while the document is actually
|
||||
// visible, so preview/capture/print (document.hidden) always render full content.
|
||||
(function () {
|
||||
function enable() { document.body.classList.toggle('anim-on', !document.hidden); }
|
||||
enable();
|
||||
document.addEventListener('visibilitychange', enable);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="data.js"></script>
|
||||
<script type="text/babel" src="icons.jsx"></script>
|
||||
<script type="text/babel" src="ui.jsx"></script>
|
||||
<script type="text/babel" src="map.jsx"></script>
|
||||
<script type="text/babel" src="auth.jsx"></script>
|
||||
<script type="text/babel" src="onboarding.jsx"></script>
|
||||
<script type="text/babel" src="shell.jsx"></script>
|
||||
<script type="text/babel" src="dashboard.jsx"></script>
|
||||
<script type="text/babel" src="plot.jsx"></script>
|
||||
<script type="text/babel" src="construction.jsx"></script>
|
||||
<script type="text/babel" src="sampleplans.jsx"></script>
|
||||
<script type="text/babel" src="myjourney.jsx"></script>
|
||||
<script type="text/babel" src="payments.jsx"></script>
|
||||
<script type="text/babel" src="documents.jsx"></script>
|
||||
<script type="text/babel" src="assistant.jsx"></script>
|
||||
<script type="text/babel" src="floating-assistant.jsx"></script>
|
||||
<script type="text/babel" src="transfer.jsx"></script>
|
||||
<script type="text/babel" src="extras.jsx"></script>
|
||||
<script type="text/babel" src="app.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Main app router */
|
||||
(function () {
|
||||
const { useState, useEffect } = React;
|
||||
const Icon = window.Icon, Logo = window.Logo, Shell = window.Shell, useToast = window.useToast;
|
||||
|
||||
const APP_ROUTES = ['dashboard', 'plot', 'construction', 'payments', 'documents', 'notices', 'sectormaps', 'transfer', 'assistant', 'support', 'profile', 'sampleplans', 'journey', 'myjourney'];
|
||||
|
||||
function App() {
|
||||
const [route, setRoute] = useState(() => localStorage.getItem('yeida_route') || 'welcome');
|
||||
const [toastNode, toast] = useToast();
|
||||
const go = (r) => { setRoute(r); localStorage.setItem('yeida_route', r); window.scrollTo(0, 0); };
|
||||
useEffect(() => { document.body.style.background = APP_ROUTES.includes(route) ? 'var(--bg)' : '#fff'; }, [route]);
|
||||
|
||||
// Auth + public
|
||||
if (route === 'welcome') return React.createElement(window.Welcome, { go });
|
||||
if (route === 'login') return React.createElement(window.LoginSelect, { go });
|
||||
if (route === 'login-allotment') return React.createElement(window.AllotmentLookup, { go });
|
||||
if (route === 'verify' || route === 'otp') return React.createElement(window.Verify, { go });
|
||||
if (route === 'register') return React.createElement(window.Register, { go });
|
||||
if (route === 'app') { go('dashboard'); return null; }
|
||||
if (route === 'sectormaps-public') return React.createElement(PublicMaps, { go });
|
||||
|
||||
// Authenticated app
|
||||
const content = {
|
||||
dashboard: window.Dashboard, plot: window.MyPlot, construction: window.Construction,
|
||||
payments: window.Payments, documents: window.Documents, notices: window.Notices,
|
||||
sectormaps: window.SectorMaps, transfer: window.Transfer, assistant: window.Assistant,
|
||||
support: window.Support, profile: window.Profile,
|
||||
sampleplans: window.SamplePlans, journey: window.Journey, myjourney: window.MyJourney,
|
||||
}[route] || window.Dashboard;
|
||||
|
||||
return React.createElement(React.Fragment, null,
|
||||
React.createElement(Shell, { route, go },
|
||||
React.createElement(content, { go, toast })),
|
||||
toastNode
|
||||
);
|
||||
}
|
||||
|
||||
// Public sector maps (pre-login)
|
||||
function PublicMaps({ go }) {
|
||||
return React.createElement('div', { style: { minHeight: '100vh', background: 'var(--bg)' } },
|
||||
React.createElement('header', { style: { height: 64, background: '#fff', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', position: 'sticky', top: 0, zIndex: 50 } },
|
||||
React.createElement('button', { onClick: () => go('welcome'), style: { background: 'none', border: 'none', cursor: 'pointer' } }, React.createElement(Logo, { size: 36 })),
|
||||
React.createElement('div', { className: 'row gap-3' },
|
||||
React.createElement('button', { className: 'btn btn-ghost', onClick: () => go('welcome') }, React.createElement(Icon, { name: 'chevL', size: 16 }), 'Home'),
|
||||
React.createElement('button', { className: 'btn btn-primary', onClick: () => go('login') }, 'Login to Portal', React.createElement(Icon, { name: 'arrowR', size: 17 })))
|
||||
),
|
||||
React.createElement('div', { style: { maxWidth: 1600, margin: '0 auto', padding: '28px 36px' } },
|
||||
React.createElement(window.PageHead, { title: 'Explore Sector Maps', subtitle: 'Browse plotted layouts and master plans across YEIDA sectors — no login required' }),
|
||||
React.createElement(window.SectorMaps, { go })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
|
||||
})();
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,133 @@
|
||||
/* AI Assistant */
|
||||
(function () {
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const Icon = window.Icon;
|
||||
const D = window.YEIDA;
|
||||
|
||||
const ANSWERS = [
|
||||
{ match: ['construction', 'start', 'build'], reply: "To start construction on Plot B-47:\n\n1. Ensure possession is granted (✓ done for your plot)\n2. Prepare your architect-stamped building plan\n3. Upload required documents in the Construction module\n4. Submit for YEIDA scrutiny\n\nYour application is currently at Step 4 — Authority Review. Estimated decision: 12 Jun 2026.", action: ['construction', 'Open Construction'] },
|
||||
{ match: ['document', 'required', 'papers'], reply: "For building plan approval you'll need:\n\n• Approved Building Plan (architect-stamped)\n• Structural Stability Certificate\n• Architect Registration (COA)\n• Soil Test Report\n• Indemnity Bond\n• Possession Certificate copy\n\n5 of 6 are already uploaded. The Soil Test Report is still pending.", action: ['documents', 'View Documents'] },
|
||||
{ match: ['notice', 'latest', 'update'], reply: "Here are the latest notices:\n\n📌 Revised Annual Lease Rent rates FY 2026-27 (01 Jun)\n📌 Construction guidelines — FAR & setback (24 May)\n• Water supply augmentation, Sector 18 & 20 (18 May)\n\nThe lease rent revision affects your next due on 30 Jun.", action: ['notices', 'Open Notices'] },
|
||||
{ match: ['transfer', 'sell', 'ownership'], reply: "To transfer Plot B-47:\n\n1. Pass the eligibility check (5-year lock-in — ✓ you're eligible)\n2. Pay transfer charges (~₹2.02 L incl. fees & GST)\n3. Submit the transfer application with transferee KYC\n4. Authority review & approval\n\nWould you like me to open the Transfer module?", action: ['transfer', 'Start Transfer'] },
|
||||
{ match: ['payment', 'pending', 'due', 'pay'], reply: "You have 3 outstanding dues totalling ₹1,47,585:\n\n• Water Connection Charges — ₹12,400 (Overdue)\n• Annual Lease Rent 2026-27 — ₹38,625 (due 30 Jun)\n• Location Premium 14/16 — ₹96,560 (due 15 Jul)\n\nClear the overdue water charge first to avoid interest.", action: ['payments', 'Pay Dues'] },
|
||||
{ match: ['map', 'sector', '18', 'plot location'], reply: "Plot B-47 is in Sector 18, a developed residential sector along the Yamuna Expressway. It's an east-facing 300 sq.m plot on a 12 m road in Block B.\n\nI can open the interactive Sector 18 map so you can locate it.", action: ['sectormaps', 'Open Map'] },
|
||||
];
|
||||
const fallback = "I can help with construction approvals, payments, documents, transfers, notices and sector maps for your plot. Try one of the suggested questions, or ask me anything about Plot B-47.";
|
||||
|
||||
function reply(text) {
|
||||
const t = text.toLowerCase();
|
||||
const hit = ANSWERS.find(a => a.match.some(m => t.includes(m)));
|
||||
return hit || { reply: fallback };
|
||||
}
|
||||
|
||||
function Assistant({ go }) {
|
||||
const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hello Rajeev 👋 I'm your YEIDA assistant. Ask me anything about Plot B-47 — construction, payments, documents, transfers or sector maps." }]);
|
||||
const [input, setInput] = useState('');
|
||||
const [typing, setTyping] = useState(false);
|
||||
const scroller = useRef(null);
|
||||
useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing]);
|
||||
|
||||
const send = (text) => {
|
||||
if (!text.trim()) return;
|
||||
setMsgs(m => [...m, { role: 'user', text }]);
|
||||
setInput(''); setTyping(true);
|
||||
setTimeout(() => {
|
||||
const r = reply(text);
|
||||
setTyping(false);
|
||||
setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]);
|
||||
}, 900);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 270px', gap: 18, height: 'calc(100vh - 124px)' }}>
|
||||
{/* chat */}
|
||||
<div className="card" style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div className="row spread" style={{ padding: '16px 22px', borderBottom: '1px solid var(--line)' }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 42, height: 42, borderRadius: 12, display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff' }}><Icon name="sparkles" size={21} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 15.5 }}>YEIDA AI Assistant</div><div className="row gap-2" style={{ fontSize: 12, color: 'var(--emerald-700)', fontWeight: 600 }}><span style={{ width: 7, height: 7, borderRadius: 99, background: 'var(--emerald-500)' }} />Online · Knowledge base connected</div></div>
|
||||
</div>
|
||||
<button className="btn btn-outline btn-sm"><Icon name="history" size={15} />History</button>
|
||||
</div>
|
||||
|
||||
<div ref={scroller} style={{ flex: 1, overflowY: 'auto', padding: '24px 26px', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{msgs.length === 1 && (
|
||||
<div className="anim-fade-up" style={{ textAlign: 'center', padding: '20px 0 8px' }}>
|
||||
<div style={{ width: 60, height: 60, borderRadius: 16, margin: '0 auto 14px', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff', boxShadow: 'var(--sh-glow)' }}><Icon name="sparkles" size={30} /></div>
|
||||
<h2 style={{ fontSize: 23 }}>Ask anything about your plot</h2>
|
||||
<p className="muted" style={{ fontSize: 14, marginTop: 6 }}>Choose a question below or type your own.</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, maxWidth: 540, margin: '22px auto 0' }}>
|
||||
{D.assistantSuggestions.map(s => (
|
||||
<button key={s} onClick={() => send(s)} className="card-hover" style={{ textAlign: 'left', padding: '13px 15px', borderRadius: 12, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, fontSize: 13.5, fontWeight: 600, color: 'var(--ink-2)' }}>
|
||||
<Icon name="chat" size={16} style={{ color: 'var(--blue-500)', flex: 'none' }} />{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{msgs.map((m, i) => <Bubble key={i} m={m} go={go} />)}
|
||||
{typing && (
|
||||
<div className="row gap-3" style={{ alignItems: 'flex-end' }}>
|
||||
<span style={{ width: 32, height: 32, borderRadius: 9, flex: 'none', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff' }}><Icon name="sparkles" size={16} /></span>
|
||||
<div style={{ background: 'var(--bg-2)', borderRadius: '14px 14px 14px 4px', padding: '14px 18px', display: 'flex', gap: 5 }}>
|
||||
{[0, 1, 2].map(i => <span key={i} style={{ width: 7, height: 7, borderRadius: 99, background: 'var(--muted)', animation: `pulseDot 1.2s ${i * 0.2}s infinite` }} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '14px 18px', borderTop: '1px solid var(--line)' }}>
|
||||
<div className="row gap-3" style={{ background: 'var(--bg)', borderRadius: 13, padding: '6px 6px 6px 16px', border: '1.5px solid var(--line)' }}>
|
||||
<input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask about construction, payments, documents…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 14.5, color: 'var(--ink)' }} />
|
||||
<button className="btn btn-primary btn-sm" style={{ borderRadius: 10 }} onClick={() => send(input)}><Icon name="send" size={17} /></button>
|
||||
</div>
|
||||
<div className="faint" style={{ fontSize: 11, textAlign: 'center', marginTop: 8 }}>AI responses are informational. Verify critical actions with the Authority.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* side panel */}
|
||||
<div className="col gap-4">
|
||||
<div className="card card-pad">
|
||||
<h4 style={{ fontSize: 14.5, marginBottom: 12 }}>Smart Suggestions</h4>
|
||||
<div className="col gap-2">
|
||||
{['What payments are pending?', 'Show Sector 18 map', 'How can I transfer my plot?'].map(s => (
|
||||
<button key={s} onClick={() => send(s)} style={{ textAlign: 'left', fontSize: 13, fontWeight: 500, color: 'var(--ink-2)', padding: '10px 12px', borderRadius: 10, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer' }} className="card-hover">{s}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h4 style={{ fontSize: 14.5, marginBottom: 12 }}>Knowledge Sources</h4>
|
||||
<div className="col gap-3">
|
||||
{[['Notices & Circulars', 'notices'], ['Document Center', 'documents'], ['Building Byelaws', 'file'], ['Sector Master Plans', 'map']].map(([l, ic]) => (
|
||||
<div key={l} className="row gap-3" style={{ fontSize: 13, color: 'var(--muted)' }}><Icon name={ic} size={16} style={{ color: 'var(--blue-500)' }} />{l}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad" style={{ background: 'var(--navy-800)', color: '#fff' }}>
|
||||
<Icon name="support" size={22} style={{ color: 'var(--emerald-500)' }} />
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5, marginTop: 10 }}>Need a human?</div>
|
||||
<p style={{ fontSize: 12.5, color: 'rgba(200,216,240,.8)', marginTop: 4, lineHeight: 1.5 }}>Escalate to a YEIDA support officer for complex queries.</p>
|
||||
<button className="btn btn-emerald btn-sm btn-block" style={{ marginTop: 12 }} onClick={() => go('support')}>Talk to an Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Bubble({ m, go }) {
|
||||
const isUser = m.role === 'user';
|
||||
return (
|
||||
<div className="anim-fade-up row gap-3" style={{ alignItems: 'flex-end', flexDirection: isUser ? 'row-reverse' : 'row' }}>
|
||||
<span style={{ width: 32, height: 32, borderRadius: 9, flex: 'none', display: 'grid', placeItems: 'center', background: isUser ? 'var(--bg-2)' : 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: isUser ? 'var(--muted)' : '#fff' }}>
|
||||
<Icon name={isUser ? 'user' : 'sparkles'} size={16} />
|
||||
</span>
|
||||
<div style={{ maxWidth: '76%' }}>
|
||||
<div style={{ background: isUser ? 'var(--blue-600)' : 'var(--bg-2)', color: isUser ? '#fff' : 'var(--ink)', borderRadius: isUser ? '14px 14px 4px 14px' : '14px 14px 14px 4px', padding: '13px 17px', fontSize: 14, lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>{m.text}</div>
|
||||
{m.action && <button className="btn btn-outline btn-sm" style={{ marginTop: 9 }} onClick={() => go(m.action[0])}>{m.action[1]}<Icon name="arrowR" size={15} /></button>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Assistant = Assistant;
|
||||
})();
|
||||
@@ -0,0 +1,317 @@
|
||||
/* Authentication: Welcome, Login select, shared Verification (email+phone OTP) */
|
||||
(function () {
|
||||
const { useState, useEffect, useRef } = React;
|
||||
const Icon = window.Icon, Logo = window.Logo;
|
||||
const D = window.YEIDA;
|
||||
|
||||
/* ---- Brand panel (navy decorative side) ---- */
|
||||
function BrandPanel({ compact }) {
|
||||
return (
|
||||
<div style={{ position: 'relative', overflow: 'hidden', color: '#fff', padding: compact ? '38px 40px' : '48px 46px', background: 'radial-gradient(120% 90% at 100% 0%, #16407e 0%, #0b2548 45%, #081a39 100%)', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: .5, backgroundImage: 'linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px)', backgroundSize: '46px 46px', maskImage: 'radial-gradient(80% 80% at 70% 30%,#000,transparent)' }} />
|
||||
<div style={{ position: 'absolute', top: -120, right: -100, width: 380, height: 380, borderRadius: '50%', background: 'radial-gradient(circle, rgba(22,182,126,.32), transparent 65%)', filter: 'blur(10px)' }} />
|
||||
<div style={{ position: 'relative', zIndex: 2 }}><Logo light size={42} /></div>
|
||||
<div style={{ position: 'relative', zIndex: 2 }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '7px 13px', borderRadius: 99, background: 'rgba(255,255,255,.08)', border: '1px solid rgba(255,255,255,.12)', fontSize: 12.5, fontWeight: 600, color: '#cfe0f7', marginBottom: 22 }}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: 99, background: 'var(--emerald-500)', boxShadow: '0 0 0 3px rgba(22,182,126,.25)' }} />Official Allottee Portal
|
||||
</div>
|
||||
<h2 style={{ color: '#fff', fontSize: 32, fontWeight: 800, lineHeight: 1.12, letterSpacing: '-.02em', maxWidth: 380 }}>Your complete Yamuna Expressway property portal.</h2>
|
||||
<p style={{ color: 'rgba(220,232,248,.82)', fontSize: 15, lineHeight: 1.55, maxWidth: 360, marginTop: 16 }}>Verify your allotment, view official authority records, track construction and manage documents — securely.</p>
|
||||
</div>
|
||||
<div style={{ position: 'relative', zIndex: 2, display: 'flex', gap: 30 }}>
|
||||
{[['7+', 'Active Sectors'], ['62,000+', 'Allottees'], ['100%', 'Digital']].map((x, i) => (
|
||||
<div key={i}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 24, color: '#fff' }}>{x[0]}</div>
|
||||
<div style={{ fontSize: 12.5, color: 'rgba(200,216,240,.7)', fontWeight: 600, marginTop: 2 }}>{x[1]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthShell({ wide, children }) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'grid', placeItems: 'center', padding: 24, background: 'linear-gradient(180deg,#eef3fb,#e6eefa)' }}>
|
||||
<div className="anim-pop card" style={{ width: '100%', maxWidth: wide ? 1240 : 1080, display: 'grid', gridTemplateColumns: '1fr 1fr', overflow: 'hidden', borderRadius: 24, boxShadow: 'var(--sh-lg)', minHeight: 600 }}>
|
||||
<BrandPanel />
|
||||
<div style={{ padding: '44px 50px', display: 'flex', flexDirection: 'column', justifyContent: 'center', background: '#fff', maxHeight: '94vh', overflowY: 'auto' }}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- WELCOME ---------------- */
|
||||
function Welcome({ go }) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', position: 'relative', overflow: 'hidden', background: 'radial-gradient(130% 100% at 80% -10%, #16407e 0%, #0b2548 42%, #06122b 100%)', color: '#fff' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: .55, backgroundImage: 'linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.045) 1px,transparent 1px)', backgroundSize: '54px 54px', maskImage: 'radial-gradient(90% 70% at 60% 20%,#000,transparent)' }} />
|
||||
<div style={{ position: 'absolute', top: -160, right: -120, width: 520, height: 520, borderRadius: '50%', background: 'radial-gradient(circle, rgba(22,182,126,.26), transparent 62%)' }} />
|
||||
<div style={{ position: 'absolute', bottom: -200, left: -120, width: 520, height: 520, borderRadius: '50%', background: 'radial-gradient(circle, rgba(38,112,230,.28), transparent 62%)' }} />
|
||||
<header style={{ position: 'relative', zIndex: 3, maxWidth: 1180, margin: '0 auto', padding: '26px 40px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Logo light size={40} />
|
||||
<div className="row gap-3">
|
||||
<button className="btn btn-ghost" style={{ color: '#cfe0f7' }} onClick={() => go('login')}>Sign In</button>
|
||||
<button className="btn" style={{ background: 'rgba(255,255,255,.1)', border: '1px solid rgba(255,255,255,.16)', color: '#fff' }} onClick={() => go('sectormaps-public')}>Explore Sector Maps</button>
|
||||
</div>
|
||||
</header>
|
||||
<main style={{ position: 'relative', zIndex: 3, maxWidth: 1180, margin: '0 auto', padding: '40px 40px 60px', display: 'grid', gridTemplateColumns: '1.05fr .95fr', gap: 50, alignItems: 'center' }}>
|
||||
<div className="anim-fade-up">
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, padding: '8px 15px', borderRadius: 99, background: 'rgba(255,255,255,.07)', border: '1px solid rgba(255,255,255,.13)', fontSize: 13, fontWeight: 600, color: '#cfe0f7', marginBottom: 26 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 99, background: 'var(--emerald-500)', boxShadow: '0 0 0 4px rgba(22,182,126,.22)' }} />Government of Uttar Pradesh · Official Portal
|
||||
</div>
|
||||
<h1 style={{ color: '#fff', fontSize: 54, lineHeight: 1.05, fontWeight: 800, letterSpacing: '-.025em' }}>Your Complete <span style={{ background: 'linear-gradient(100deg,#5ad6a6,#7fb3ff)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>Yamuna Expressway</span> Property Management Portal</h1>
|
||||
<p style={{ color: 'rgba(214,228,247,.85)', fontSize: 17.5, lineHeight: 1.55, maxWidth: 520, marginTop: 22 }}>Verify your allotment, view official authority payment records, track construction approvals and manage documents — all in one secure place.</p>
|
||||
<div className="row gap-3" style={{ marginTop: 34, flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-lg" style={{ background: '#0077FF', color: '#fff', borderRadius: 999, boxShadow: '0 16px 40px -12px rgba(0,119,255,.7)' }} onClick={() => go('login')}>Login to Portal<Icon name="arrowR" size={19} /></button>
|
||||
<button className="btn btn-lg" style={{ background: 'rgba(255,255,255,.1)', border: '1px solid rgba(255,255,255,.18)', color: '#fff', borderRadius: 999 }} onClick={() => go('sectormaps-public')}><Icon name="map" size={19} />Explore Sector Maps</button>
|
||||
</div>
|
||||
<div className="row gap-4" style={{ marginTop: 40, color: 'rgba(200,216,240,.7)', fontSize: 13.5, fontWeight: 500, flexWrap: 'wrap' }}>
|
||||
<span className="row gap-2"><Icon name="shield" size={17} style={{ color: 'var(--emerald-500)' }} />SSL Secured</span>
|
||||
<span className="row gap-2"><Icon name="lock" size={17} style={{ color: 'var(--emerald-500)' }} />OTP Verified Access</span>
|
||||
<span className="row gap-2"><Icon name="check" size={17} style={{ color: 'var(--emerald-500)' }} />DigiLocker Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
<HeroCard />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCard() {
|
||||
const plots = [[20, 18], [64, 18], [108, 18], [186, 18], [230, 18], [274, 18], [20, 116], [64, 116], [186, 116], [230, 116], [274, 116]];
|
||||
return (
|
||||
<div className="anim-fade-up" style={{ animationDelay: '.1s', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', inset: '-14px', borderRadius: 28, background: 'linear-gradient(160deg,rgba(127,179,255,.25),transparent)', filter: 'blur(2px)' }} />
|
||||
<div style={{ position: 'relative', background: 'rgba(255,255,255,.97)', borderRadius: 22, padding: 22, boxShadow: '0 40px 80px -30px rgba(0,0,0,.55)', color: 'var(--ink)' }}>
|
||||
<div className="row spread" style={{ marginBottom: 16 }}>
|
||||
<div className="row gap-3">
|
||||
<div style={{ width: 40, height: 40, borderRadius: 11, background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', display: 'grid', placeItems: 'center', color: '#fff' }}><Icon name="building" size={21} /></div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5 }}>Plot B-47, Sector 18</div>
|
||||
<div className="muted" style={{ fontSize: 12.5 }}>Residential · 300 sq.m</div>
|
||||
</div>
|
||||
</div>
|
||||
<window.StatusBadge code="LD_COMPLETED" />
|
||||
</div>
|
||||
<div style={{ borderRadius: 14, overflow: 'hidden', border: '1px solid var(--line)', background: 'linear-gradient(135deg,#eef4ff,#e7f7f0)' }}>
|
||||
<svg viewBox="0 0 320 180" width="100%" style={{ display: 'block' }}>
|
||||
<rect width="320" height="180" fill="#eaf1fa" />
|
||||
<rect x="0" y="78" width="320" height="24" fill="#dde6f1" />
|
||||
<rect x="150" y="0" width="22" height="180" fill="#dde6f1" />
|
||||
<line x1="0" y1="90" x2="320" y2="90" stroke="#fff" strokeWidth="2" strokeDasharray="8 7" />
|
||||
<line x1="161" y1="0" x2="161" y2="180" stroke="#fff" strokeWidth="2" strokeDasharray="8 7" />
|
||||
{plots.map((p, i) => <rect key={i} x={p[0]} y={p[1]} width="34" height="44" rx="3" fill="#fff" stroke="#c9d6e6" strokeWidth="1" />)}
|
||||
<rect x="108" y="116" width="34" height="44" rx="3" fill="var(--emerald-500)" stroke="#fff" strokeWidth="2" />
|
||||
<circle cx="125" cy="138" r="14" fill="none" stroke="var(--emerald-500)" strokeWidth="2" opacity=".5" style={{ animation: 'ping 2s infinite' }} />
|
||||
<text x="125" y="141" textAnchor="middle" fontSize="9" fontWeight="700" fill="#fff">B-47</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- LOGIN SELECTION ---------------- */
|
||||
function LoginSelect({ go }) {
|
||||
const [mode, setMode] = useState('allotment');
|
||||
const opts = [
|
||||
{ k: 'allotment', icon: 'building', title: 'Allotment Number', desc: 'Login with your YEIDA allotment number' },
|
||||
{ k: 'invite', icon: 'key', title: 'Invitation Code', desc: 'Access via authority-issued invite code' },
|
||||
];
|
||||
return (
|
||||
<AuthShell>
|
||||
<div className="anim-fade-up">
|
||||
<div className="kicker" style={{ marginBottom: 10 }}>Welcome back</div>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 800 }}>Sign in to your portal</h1>
|
||||
<p className="muted" style={{ fontSize: 14.5, marginTop: 6 }}>Verify yourself using your allotment number or an invitation code.</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 11, margin: '26px 0 20px' }}>
|
||||
{opts.map(o => (
|
||||
<button key={o.k} onClick={() => setMode(o.k)} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '15px 16px', borderRadius: 14, textAlign: 'left', border: '1.5px solid ' + (mode === o.k ? 'var(--blue-400)' : 'var(--line)'), background: mode === o.k ? 'var(--blue-50)' : '#fff', boxShadow: mode === o.k ? '0 0 0 4px rgba(38,112,230,.1)' : 'none', transition: 'all .15s', cursor: 'pointer' }}>
|
||||
<span style={{ width: 42, height: 42, borderRadius: 11, flex: 'none', display: 'grid', placeItems: 'center', background: mode === o.k ? 'var(--blue-500)' : 'var(--bg-2)', color: mode === o.k ? '#fff' : 'var(--muted)' }}><Icon name={o.icon} size={21} /></span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 15 }}>{o.title}</div>
|
||||
<div className="muted" style={{ fontSize: 12.8, marginTop: 1 }}>{o.desc}</div>
|
||||
</div>
|
||||
<span style={{ width: 20, height: 20, borderRadius: 99, border: '2px solid ' + (mode === o.k ? 'var(--blue-500)' : '#cdd7e4'), display: 'grid', placeItems: 'center' }}>{mode === o.k && <span style={{ width: 9, height: 9, borderRadius: 99, background: 'var(--blue-500)' }} />}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="field" style={{ marginBottom: 16 }}>
|
||||
<label>{mode === 'allotment' ? 'Allotment Number' : 'Invitation Code'}</label>
|
||||
<div className="input-icon">
|
||||
<Icon name={mode === 'allotment' ? 'building' : 'key'} />
|
||||
<input className="input" maxLength={mode === 'allotment' ? 10 : undefined} placeholder={mode === 'allotment' ? 'e.g. YEA-654321' : 'Enter your code'} defaultValue={mode === 'allotment' ? 'YEA-654321' : ''} />
|
||||
</div>
|
||||
{mode === 'allotment' && <button onClick={() => go('login-allotment')} style={{ alignSelf: 'flex-start', background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 600, fontSize: 12.5, cursor: 'pointer', padding: 0, marginTop: 2 }}>I don't remember my allotment number</button>}
|
||||
</div>
|
||||
<button className="btn btn-primary btn-block btn-lg" onClick={() => go('verify')}>Continue<Icon name="arrowR" size={19} /></button>
|
||||
<div style={{ textAlign: 'center', marginTop: 22, fontSize: 14, color: 'var(--muted)' }}>
|
||||
New allottee or representative?{' '}
|
||||
<button onClick={() => go('register')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 14 }}>Register here</button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- SHARED VERIFICATION (email + phone, same screen) ---------------- */
|
||||
// VerifyPanel renders both channels and reports completion via onChange(bothVerified)
|
||||
function VerifyPanel({ email, phone, onChange }) {
|
||||
const [cc, setCc] = useState(D.countryCodes[0]);
|
||||
const [state, setState] = useState({ email: false, phone: false });
|
||||
return (
|
||||
<div className="col gap-3">
|
||||
<VerifyChannelLifted kind="email" value={email} onVerified={v => setState(s => { const n = { ...s, email: v }; onChange && onChange(n.email && n.phone); return n; })} />
|
||||
<VerifyChannelLifted kind="phone" value={phone} cc={cc} setCc={setCc} onVerified={v => setState(s => { const n = { ...s, phone: v }; onChange && onChange(n.email && n.phone); return n; })} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// channel that lifts verified state up to the panel
|
||||
function VerifyChannelLifted({ kind, value, cc, setCc, onVerified }) {
|
||||
const [sent, setSent] = useState(false);
|
||||
const [otp, setOtp] = useState(['', '', '', '', '', '']);
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [api, setApi] = useState('idle'); // idle|checking|ok|invalid_email|mailbox_full|bounce_risk|invalid_mobile
|
||||
const [otpErr, setOtpErr] = useState(false);
|
||||
const [field, setField] = useState(value || '');
|
||||
const refs = useRef([]);
|
||||
useEffect(() => { if (cooldown <= 0) return; const t = setInterval(() => setCooldown(s => s - 1), 1000); return () => clearInterval(t); }, [cooldown]);
|
||||
useEffect(() => { onVerified && onVerified(verified); }, [verified]);
|
||||
|
||||
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field || '');
|
||||
const digits = (field || '').replace(/\D/g, '').length;
|
||||
const phoneOk = digits >= (cc ? cc.digits : 10);
|
||||
const valid = kind === 'email' ? emailOk : phoneOk;
|
||||
|
||||
// simulate an email/phone validation API — keywords in the value trigger states (demo)
|
||||
const runApi = () => {
|
||||
setApi('checking'); setChecking(true);
|
||||
setTimeout(() => {
|
||||
setChecking(false);
|
||||
const v = (field || '').toLowerCase();
|
||||
if (kind === 'email') {
|
||||
if (!emailOk) return setApi('invalid_email');
|
||||
if (v.includes('full')) return setApi('mailbox_full');
|
||||
if (v.includes('bounce') || v.includes('temp')) return setApi('bounce_risk');
|
||||
setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50);
|
||||
} else {
|
||||
if (!phoneOk) return setApi('invalid_mobile');
|
||||
setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50);
|
||||
}
|
||||
}, 950);
|
||||
};
|
||||
|
||||
const API_MSG = {
|
||||
checking: { tone: 'blue', icon: 'clock', text: kind === 'email' ? 'Validating email via API…' : 'Validating mobile via API…' },
|
||||
ok: { tone: 'green', icon: 'checkCircle', text: kind === 'email' ? 'Email deliverable · OTP sent' : 'Mobile valid · OTP sent' },
|
||||
invalid_email: { tone: 'red', icon: 'alert', text: 'Invalid email address' },
|
||||
mailbox_full: { tone: 'amber', icon: 'alert', text: 'Mailbox full — try another email' },
|
||||
bounce_risk: { tone: 'amber', icon: 'alert', text: 'High bounce risk — verify spelling' },
|
||||
invalid_mobile: { tone: 'red', icon: 'alert', text: `Invalid mobile number for ${cc ? cc.label : 'IN'}` },
|
||||
};
|
||||
|
||||
const set = (i, v) => {
|
||||
if (!/^\d?$/.test(v)) return;
|
||||
const d = otp.slice(); d[i] = v; setOtp(d); setOtpErr(false);
|
||||
if (v && i < 5) refs.current[i + 1] && refs.current[i + 1].focus();
|
||||
if (d.every(x => x !== '')) {
|
||||
// demo: "000000" simulates an incorrect OTP
|
||||
if (d.join('') === '000000') { setOtpErr(true); setOtp(['', '', '', '', '', '']); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50); }
|
||||
else setVerified(true);
|
||||
}
|
||||
};
|
||||
const onKey = (i, e) => { if (e.key === 'Backspace' && !otp[i] && i > 0) refs.current[i - 1] && refs.current[i - 1].focus(); };
|
||||
|
||||
const apiMsg = API_MSG[api];
|
||||
const toneColor = { red: 'var(--red-600)', amber: 'var(--amber-600)', blue: 'var(--blue-600)', green: 'var(--emerald-700)' };
|
||||
const toneBg = { red: 'var(--red-50)', amber: 'var(--amber-50)', blue: 'var(--blue-50)', green: 'var(--emerald-50)' };
|
||||
|
||||
return (
|
||||
<div style={{ border: '1.5px solid ' + (verified ? '#bfe6d3' : 'var(--line)'), borderRadius: 16, padding: 18, background: verified ? 'var(--emerald-50)' : '#fff', transition: 'all .2s' }}>
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 38, height: 38, borderRadius: 10, flex: 'none', display: 'grid', placeItems: 'center', background: verified ? 'var(--emerald-500)' : 'var(--blue-50)', color: verified ? '#fff' : 'var(--blue-600)' }}><Icon name={kind === 'email' ? 'mail' : 'phone'} size={19} /></span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5 }}>{kind === 'email' ? 'Verify Email' : 'Verify Phone Number'}</div>
|
||||
<div className="faint" style={{ fontSize: 12 }}>{verified ? 'Verified successfully' : sent ? 'Enter the 6-digit OTP' : 'OTP verification required'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{verified && <span className="badge badge-green"><Icon name="check" size={13} />Verified</span>}
|
||||
</div>
|
||||
{kind === 'email' ? (
|
||||
<div className="input-icon"><Icon name="mail" /><input className="input" value={field} onChange={e => { setField(e.target.value); setApi('idle'); }} disabled={sent} /></div>
|
||||
) : (
|
||||
<div className="row gap-2">
|
||||
<select className="select" value={cc.code} onChange={e => setCc(D.countryCodes.find(c => c.code === e.target.value))} disabled={sent} style={{ width: 100, flex: 'none' }}>
|
||||
{D.countryCodes.map(c => <option key={c.code} value={c.code}>{c.label} {c.code}</option>)}
|
||||
</select>
|
||||
<div className="input-icon" style={{ flex: 1 }}><Icon name="phone" /><input className="input" value={field} onChange={e => { setField(e.target.value); setApi('idle'); }} disabled={sent} /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API validation status banner */}
|
||||
{apiMsg && (
|
||||
<div className="row gap-2" style={{ fontSize: 12, color: toneColor[apiMsg.tone], background: toneBg[apiMsg.tone], borderRadius: 9, padding: '7px 11px', marginTop: 9, fontWeight: 600 }}>
|
||||
{api === 'checking' ? <span style={{ width: 13, height: 13, border: '2px solid rgba(38,112,230,.3)', borderTopColor: 'var(--blue-600)', borderRadius: 99, animation: 'spin .7s linear infinite' }} /> : <Icon name={apiMsg.icon} size={14} />}
|
||||
{apiMsg.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!sent ? (
|
||||
<button className="btn btn-outline btn-sm btn-block" style={{ marginTop: 12 }} disabled={checking} onClick={runApi}>
|
||||
{checking ? <><span style={{ width: 15, height: 15, border: '2px solid rgba(38,112,230,.3)', borderTopColor: 'var(--blue-600)', borderRadius: 99, animation: 'spin .7s linear infinite' }} />{kind === 'email' ? 'Checking email…' : 'Sending OTP…'}</> : <><Icon name="send" size={15} />Send OTP</>}
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div className="row gap-2" style={{ justifyContent: 'space-between' }}>
|
||||
{otp.map((d, i) => (
|
||||
<input key={i} ref={el => refs.current[i] = el} value={d} inputMode="numeric" maxLength={1} disabled={verified}
|
||||
onChange={e => set(i, e.target.value)} onKeyDown={e => onKey(i, e)}
|
||||
style={{ width: 44, height: 50, textAlign: 'center', fontSize: 20, fontWeight: 700, fontFamily: 'var(--font-display)', border: '1.5px solid ' + (otpErr ? 'var(--red-500)' : d ? (verified ? '#7fd3ad' : 'var(--blue-400)') : 'var(--line)'), borderRadius: 11, color: 'var(--ink)', outline: 'none', background: otpErr ? 'var(--red-50)' : d ? (verified ? '#fff' : 'var(--blue-50)') : '#fff' }} />
|
||||
))}
|
||||
</div>
|
||||
{otpErr && <div className="row gap-2" style={{ fontSize: 12, color: 'var(--red-600)', marginTop: 8, fontWeight: 600 }}><Icon name="alert" size={14} />OTP incorrect — please try again</div>}
|
||||
{!verified && (
|
||||
<div className="row spread" style={{ marginTop: 10, fontSize: 12.5 }}>
|
||||
<span className="faint">Didn't get the code?</span>
|
||||
<div className="row gap-3">
|
||||
{kind === 'email' && cooldown <= 0 && <button onClick={() => setCooldown(60)} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 12.5 }}>Resend Email</button>}
|
||||
{cooldown > 0
|
||||
? <span className="muted">Resend in <b className="num" style={{ color: 'var(--ink-2)' }}>0:{String(cooldown).padStart(2, '0')}</b></span>
|
||||
: <button onClick={() => setCooldown(60)} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 12.5 }}>Resend OTP</button>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* standalone Verify screen (login path) */
|
||||
function Verify({ go }) {
|
||||
const [both, setBoth] = useState(false);
|
||||
return (
|
||||
<AuthShell>
|
||||
<div className="anim-fade-up">
|
||||
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, marginLeft: -8 }} onClick={() => go('login')}><Icon name="chevL" size={16} />Back</button>
|
||||
<div style={{ width: 54, height: 54, borderRadius: 14, background: 'var(--blue-50)', display: 'grid', placeItems: 'center', color: 'var(--blue-600)', marginBottom: 16 }}><Icon name="shield" size={27} /></div>
|
||||
<h1 style={{ fontSize: 27, fontWeight: 800 }}>Verify it's you</h1>
|
||||
<p className="muted" style={{ fontSize: 14.5, marginTop: 6, marginBottom: 20 }}>Confirm both your email and mobile to securely access your records.</p>
|
||||
<VerifyPanel email={D.user.email} phone="9876500012" onChange={setBoth} />
|
||||
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 20, opacity: both ? 1 : .55 }} disabled={!both} onClick={() => go('app')}>{both ? 'Continue to Portal' : 'Verify email & phone to continue'}<Icon name="arrowR" size={19} /></button>
|
||||
<p className="faint" style={{ fontSize: 11.5, textAlign: 'center', marginTop: 14 }}>Demo: enter any 6 digits to verify · type "000000" for an incorrect-OTP state · use an email with "full"/"bounce" to see API states</p>
|
||||
</div>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { Welcome, LoginSelect, Verify, AuthShell, VerifyPanel, BrandPanel });
|
||||
const s = document.createElement('style');
|
||||
s.textContent = '@keyframes ping{0%{transform:scale(.8);opacity:.6}100%{transform:scale(1.8);opacity:0}}';
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,218 @@
|
||||
/* Construction module — 6-step stepper */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead;
|
||||
const D = window.YEIDA;
|
||||
|
||||
function Construction({ go, toast }) {
|
||||
const steps = D.constructionSteps;
|
||||
const [active, setActive] = useState(3); // Authority Review
|
||||
const [docs, setDocs] = useState(D.requiredDocs);
|
||||
|
||||
const upload = (i) => { setDocs(d => d.map((x, j) => j === i ? { ...x, status: 'uploaded' } : x)); toast && toast('Document uploaded for scrutiny'); };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Construction" subtitle="Building plan approval & on-site progress for Plot B-47">
|
||||
<button className="btn btn-outline" onClick={() => go('documents')}><Icon name="documents" size={18} />Building Docs</button>
|
||||
</PageHead>
|
||||
|
||||
{/* Stepper rail */}
|
||||
<div className="card card-pad anim-fade-up" style={{ marginBottom: 18 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', position: 'relative' }}>
|
||||
{steps.map((s, i) => {
|
||||
const state = s.done ? 'done' : i === active ? 'active' : 'todo';
|
||||
const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : 'var(--line)';
|
||||
return (
|
||||
<div key={s.key} style={{ flex: 1, position: 'relative' }}>
|
||||
{i < steps.length - 1 && <div style={{ position: 'absolute', left: '50%', right: '-50%', top: 21, height: 3, background: s.done ? 'var(--emerald-500)' : 'var(--line)' }} />}
|
||||
<button onClick={() => setActive(i)} style={{ position: 'relative', zIndex: 1, background: 'none', border: 'none', cursor: 'pointer', width: '100%', textAlign: 'center', padding: 0 }}>
|
||||
<span style={{ width: 44, height: 44, borderRadius: 99, display: 'grid', placeItems: 'center', margin: '0 auto', background: state === 'todo' ? '#fff' : cc, border: '2.5px solid ' + (state === 'todo' ? 'var(--line)' : cc), color: state === 'todo' ? 'var(--faint)' : '#fff', boxShadow: state === 'active' ? '0 0 0 5px rgba(38,112,230,.15)' : 'none', transition: 'all .2s' }}>
|
||||
{s.done ? <Icon name="check" size={20} /> : <span style={{ fontWeight: 800, fontFamily: 'var(--font-display)', fontSize: 16 }}>{i + 1}</span>}
|
||||
</span>
|
||||
<div style={{ fontWeight: i === active ? 700 : 600, fontSize: 13, marginTop: 9, color: i === active ? 'var(--ink)' : 'var(--muted)' }}>{s.title}</div>
|
||||
<div className="faint" style={{ fontSize: 11, marginTop: 2 }}>{s.desc}</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div key={active} className="anim-fade-up">
|
||||
{active === 0 && <Overview go={go} />}
|
||||
{active === 1 && <Required docs={docs} setActive={setActive} />}
|
||||
{active === 2 && <Upload docs={docs} upload={upload} setActive={setActive} />}
|
||||
{active === 3 && <Review setActive={setActive} />}
|
||||
{active === 4 && <Approval />}
|
||||
{active === 5 && <Progress />}
|
||||
</div>
|
||||
|
||||
{/* nav */}
|
||||
<div className="row spread" style={{ marginTop: 18 }}>
|
||||
<button className="btn btn-outline" disabled={active === 0} style={{ opacity: active === 0 ? .4 : 1 }} onClick={() => setActive(a => Math.max(0, a - 1))}><Icon name="chevL" size={17} />Previous</button>
|
||||
<button className="btn btn-primary" disabled={active === 5} style={{ opacity: active === 5 ? .4 : 1 }} onClick={() => setActive(a => Math.min(5, a + 1))}>Next Step<Icon name="chevR" size={17} /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ children, ...p }) { return <div className="card card-pad" {...p}>{children}</div>; }
|
||||
|
||||
function Overview({ go }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18 }}>
|
||||
<Card>
|
||||
<span className="badge badge-blue" style={{ marginBottom: 12 }}>Step 1 · Overview</span>
|
||||
<h3 style={{ fontSize: 20 }}>Start construction on Plot B-47</h3>
|
||||
<p className="muted" style={{ fontSize: 14.5, lineHeight: 1.6, marginTop: 10 }}>Before beginning construction, your building plan must be sanctioned by YEIDA in accordance with the prevailing building byelaws, FAR and setback norms for {D.user.sector}.</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginTop: 18 }}>
|
||||
{[['Max Ground Coverage', '50%'], ['Permissible FAR', '1.5'], ['Max Height', '15 m'], ['Front Setback', '3 m']].map(([l, v]) => (
|
||||
<div key={l} style={{ background: 'var(--bg)', borderRadius: 11, padding: '13px 15px' }}>
|
||||
<div className="faint" style={{ fontSize: 11.5, fontWeight: 600, textTransform: 'uppercase' }}>{l}</div>
|
||||
<div style={{ fontWeight: 800, fontSize: 19, fontFamily: 'var(--font-display)', marginTop: 3 }}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ background: 'var(--bg)' }}>
|
||||
<h4 style={{ fontSize: 16, marginBottom: 12 }}>Eligibility</h4>
|
||||
{[['Possession granted', true], ['100% premium not mandatory', true], ['No pending dues for plan', false], ['Architect empanelled', true]].map(([l, ok], i) => (
|
||||
<div key={i} className="row gap-3" style={{ padding: '9px 0', borderBottom: '1px solid var(--line-2)', fontSize: 13.5 }}>
|
||||
<span style={{ color: ok ? 'var(--emerald-600)' : 'var(--amber-600)' }}><Icon name={ok ? 'checkCircle' : 'alert'} size={18} /></span>
|
||||
<span style={{ color: 'var(--ink-2)' }}>{l}</span>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ background: 'var(--amber-50)', borderRadius: 11, padding: '12px 14px', marginTop: 14, fontSize: 13, color: 'var(--amber-600)', display: 'flex', gap: 9 }}>
|
||||
<Icon name="info" size={17} style={{ flex: 'none', marginTop: 1 }} />
|
||||
<span>Clear the Water Connection due of ₹12,400 before plan submission.</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Required({ docs }) {
|
||||
return (
|
||||
<Card>
|
||||
<span className="badge badge-blue" style={{ marginBottom: 12 }}>Step 2 · Required Documents</span>
|
||||
<h3 style={{ fontSize: 20, marginBottom: 6 }}>Document checklist</h3>
|
||||
<p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>Prepare the following before submitting your building plan for scrutiny.</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
{docs.map((d, i) => (
|
||||
<div key={i} className="row gap-3" style={{ padding: '14px 16px', borderRadius: 12, border: '1px solid var(--line)', background: '#fff' }}>
|
||||
<span style={{ width: 40, height: 40, borderRadius: 10, flex: 'none', display: 'grid', placeItems: 'center', background: d.status === 'uploaded' ? 'var(--emerald-50)' : 'var(--bg-2)', color: d.status === 'uploaded' ? 'var(--emerald-600)' : 'var(--faint)' }}><Icon name={d.status === 'uploaded' ? 'checkCircle' : 'file'} size={20} /></span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{d.name}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 1 }}>{d.note}</div>
|
||||
</div>
|
||||
<span className={'badge badge-' + (d.status === 'uploaded' ? 'green' : 'amber')} style={{ fontSize: 11 }}>{d.status === 'uploaded' ? 'Ready' : 'Pending'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Upload({ docs, upload }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 18 }}>
|
||||
<Card>
|
||||
<span className="badge badge-blue" style={{ marginBottom: 12 }}>Step 3 · Upload</span>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 12 }}>Upload documents</h3>
|
||||
<div style={{ border: '2px dashed var(--blue-100)', borderRadius: 14, padding: '32px 20px', textAlign: 'center', background: 'var(--blue-50)' }}>
|
||||
<span style={{ width: 52, height: 52, borderRadius: 14, display: 'grid', placeItems: 'center', margin: '0 auto 12px', background: '#fff', color: 'var(--blue-600)', boxShadow: 'var(--sh-sm)' }}><Icon name="upload" size={25} /></span>
|
||||
<div style={{ fontWeight: 700, fontSize: 15 }}>Drag & drop files here</div>
|
||||
<div className="muted" style={{ fontSize: 13, margintop: 4 }}>PDF, DWG, JPG · up to 25 MB each</div>
|
||||
<button className="btn btn-primary btn-sm" style={{ marginTop: 14 }}>Browse Files</button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<h4 style={{ fontSize: 16, marginBottom: 14 }}>Submission status</h4>
|
||||
<div className="col gap-3">
|
||||
{docs.map((d, i) => (
|
||||
<div key={i} className="row spread" style={{ padding: '11px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 34, height: 34, borderRadius: 9, display: 'grid', placeItems: 'center', background: d.status === 'uploaded' ? 'var(--emerald-50)' : 'var(--bg-2)', color: d.status === 'uploaded' ? 'var(--emerald-600)' : 'var(--faint)' }}><Icon name="file" size={17} /></span>
|
||||
<div><div style={{ fontWeight: 600, fontSize: 13.5 }}>{d.name}</div><div className="faint" style={{ fontSize: 11.5 }}>{d.note}</div></div>
|
||||
</div>
|
||||
{d.status === 'uploaded'
|
||||
? <span className="badge badge-green"><Icon name="check" size={13} />Uploaded</span>
|
||||
: <button className="btn btn-outline btn-sm" onClick={() => upload(i)}><Icon name="upload" size={15} />Upload</button>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Review({ setActive }) {
|
||||
return (
|
||||
<Card style={{ textAlign: 'center', padding: '44px 30px' }}>
|
||||
<div style={{ width: 76, height: 76, borderRadius: 99, margin: '0 auto 18px', display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)', position: 'relative' }}>
|
||||
<Icon name="clock" size={36} />
|
||||
<span style={{ position: 'absolute', inset: -6, borderRadius: 99, border: '2px solid var(--blue-400)', opacity: .4, animation: 'ping 2.2s infinite' }} />
|
||||
</div>
|
||||
<span className="badge badge-blue">Step 4 · Authority Review</span>
|
||||
<h3 style={{ fontSize: 23, marginTop: 12 }}>Your application is under scrutiny</h3>
|
||||
<p className="muted" style={{ fontSize: 14.5, maxWidth: 460, margin: '10px auto 0', lineHeight: 1.6 }}>YEIDA's Building Plan section is reviewing your submission against byelaws. You'll be notified once the sanction letter is issued.</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 30, marginTop: 26, flexWrap: 'wrap' }}>
|
||||
{[['Submitted', '16 Feb 2026'], ['Application No.', 'BP-2026-18-0472'], ['Est. Decision', '12 Jun 2026'], ['Reviewing Officer', 'AE (Planning)']].map(([l, v]) => (
|
||||
<div key={l}><div className="faint" style={{ fontSize: 11.5, fontWeight: 600, textTransform: 'uppercase' }}>{l}</div><div style={{ fontWeight: 700, fontSize: 15, marginTop: 3 }}>{v}</div></div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ maxWidth: 520, margin: '26px auto 0' }}>
|
||||
<div className="row spread" style={{ fontSize: 12.5, marginBottom: 7, color: 'var(--muted)', fontWeight: 600 }}><span>Document Check</span><span>Technical Scrutiny</span><span>Sanction</span></div>
|
||||
<div className="progress" style={{ height: 9 }}><span style={{ width: '58%' }} /></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Approval() {
|
||||
return (
|
||||
<Card style={{ textAlign: 'center', padding: '40px 30px', background: 'linear-gradient(180deg,#fff,#f3fbf7)' }}>
|
||||
<div style={{ width: 76, height: 76, borderRadius: 99, margin: '0 auto 16px', display: 'grid', placeItems: 'center', background: 'var(--emerald-500)', color: '#fff', boxShadow: '0 14px 30px -10px rgba(15,157,107,.6)' }}><Icon name="check" size={40} /></div>
|
||||
<span className="badge badge-green">Step 5 · Approval</span>
|
||||
<h3 style={{ fontSize: 23, marginTop: 12 }}>Sanction letter ready</h3>
|
||||
<p className="muted" style={{ fontSize: 14.5, maxWidth: 440, margin: '10px auto 0', lineHeight: 1.6 }}>Once approved, your building plan sanction letter will be available to download here, valid for 3 years from the date of issue.</p>
|
||||
<div className="row gap-3 center" style={{ marginTop: 22 }}>
|
||||
<button className="btn btn-emerald"><Icon name="download" size={18} />Download Sanction Letter</button>
|
||||
<button className="btn btn-outline"><Icon name="eye" size={18} />Preview</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Progress() {
|
||||
return (
|
||||
<Card>
|
||||
<div className="row spread" style={{ marginBottom: 18 }}>
|
||||
<div><span className="badge badge-blue">Step 6 · Progress Tracker</span><h3 style={{ fontSize: 20, marginTop: 10 }}>On-site construction progress</h3></div>
|
||||
<div style={{ textAlign: 'right' }}><div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 30, color: 'var(--emerald-700)' }}>41%</div><div className="faint" style={{ fontSize: 12 }}>overall complete</div></div>
|
||||
</div>
|
||||
<div className="col gap-4">
|
||||
{D.constructionMilestones.map((m, i) => (
|
||||
<div key={i}>
|
||||
<div className="row spread" style={{ marginBottom: 7 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 30, height: 30, borderRadius: 8, display: 'grid', placeItems: 'center', background: m.pct === 100 ? 'var(--emerald-50)' : m.pct > 0 ? 'var(--blue-50)' : 'var(--bg-2)', color: m.pct === 100 ? 'var(--emerald-600)' : m.pct > 0 ? 'var(--blue-600)' : 'var(--faint)' }}><Icon name={m.pct === 100 ? 'check' : 'building'} size={16} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 14.5 }}>{m.label}</div><div className="faint" style={{ fontSize: 12 }}>{m.date}</div></div>
|
||||
</div>
|
||||
<div className="row gap-3">
|
||||
<span className={'badge badge-' + (m.pct === 100 ? 'green' : m.pct > 0 ? 'blue' : 'gray')} style={{ fontSize: 11 }}>{m.status}</span>
|
||||
<span className="num" style={{ fontWeight: 700, fontSize: 14, width: 42, textAlign: 'right' }}>{m.pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress"><span style={{ width: m.pct + '%', background: m.pct === 100 ? 'linear-gradient(90deg,var(--emerald-500),var(--emerald-600))' : 'linear-gradient(90deg,var(--blue-400),var(--blue-600))' }} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
window.Construction = Construction;
|
||||
})();
|
||||
@@ -0,0 +1,404 @@
|
||||
/* Dashboard — split into A) Authority Status & Records and B) Construction Progress */
|
||||
(function () {
|
||||
const Icon = window.Icon, Donut = window.Donut, StatusBadge = window.StatusBadge;
|
||||
const D = window.YEIDA, fmt = D.fmtINR;
|
||||
|
||||
function SectionLabel({ n, title, sub, children }) {
|
||||
return (
|
||||
<div className="row spread" style={{ margin: '6px 0 16px', flexWrap: 'wrap', gap: 12 }}>
|
||||
<div>
|
||||
<div className="sec-eyebrow" style={{ marginBottom: 8 }}>Module {n}</div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 800, letterSpacing: '-.02em' }}>{title}</h2>
|
||||
{sub && <div className="muted" style={{ fontSize: 13, marginTop: 3 }}>{sub}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusCard({ icon, title, code, date, color }) {
|
||||
const s = D.STATUS_CODES[code] || {};
|
||||
const tone = s.tone || 'gray';
|
||||
const tc = { green: ['var(--emerald-50)', 'var(--emerald-700)', 'var(--emerald-500)'], amber: ['var(--amber-50)', 'var(--amber-600)', 'var(--amber-500)'], blue: ['var(--blue-50)', 'var(--blue-600)', 'var(--blue-500)'], gray: ['var(--bg-2)', 'var(--muted)', '#c2cedd'] }[tone];
|
||||
return (
|
||||
<div className="card stat-card card-pad" style={{ '--accent': tc[2], display: 'flex', flexDirection: 'column', gap: 14, paddingTop: 22 }}>
|
||||
<div className="row spread">
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, display: 'grid', placeItems: 'center', background: tc[0], color: tc[1] }}><Icon name={icon} size={21} /></span>
|
||||
{tone === 'green' ? <Icon name="checkCircle" size={20} style={{ color: 'var(--emerald-500)' }} /> : tone === 'amber' ? <Icon name="alert" size={20} style={{ color: 'var(--amber-500)' }} /> : <Icon name="clock" size={20} style={{ color: 'var(--blue-500)' }} />}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12.5, color: 'var(--faint)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.04em' }}>{title}</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 17.5, marginTop: 5, color: tc[1] }}>{s.label}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 5 }}>{date}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dashboard({ go }) {
|
||||
const [view, setView] = React.useState(() => localStorage.getItem('yeida_dash') || 'plot');
|
||||
const A = D.authorityStatus;
|
||||
const switchTo = (v) => { setView(v); localStorage.setItem('yeida_dash', v); };
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Welcome banner */}
|
||||
<div className="anim-fade-up" style={{ position: 'relative', overflow: 'hidden', borderRadius: 20, padding: '30px 34px', marginBottom: 18, background: 'radial-gradient(130% 170% at 92% -10%, #1b4d8f 0%, #0b2548 52%, #071b38 100%)', color: '#fff', boxShadow: '0 18px 44px -22px rgba(8,26,57,.7)' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: .45, backgroundImage: 'linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px)', backgroundSize: '38px 38px', maskImage: 'radial-gradient(70% 90% at 85% 0%,#000,transparent)' }} />
|
||||
{/* decorative concentric rings */}
|
||||
<svg viewBox="0 0 200 200" style={{ position: 'absolute', right: -30, top: -40, width: 280, height: 280, opacity: .5 }} aria-hidden="true">
|
||||
<circle cx="100" cy="100" r="90" fill="none" stroke="rgba(127,179,255,.18)" strokeWidth="1" />
|
||||
<circle cx="100" cy="100" r="64" fill="none" stroke="rgba(127,179,255,.16)" strokeWidth="1" />
|
||||
<circle cx="100" cy="100" r="38" fill="none" stroke="rgba(22,182,126,.3)" strokeWidth="1.5" strokeDasharray="4 5" />
|
||||
</svg>
|
||||
<div className="row spread" style={{ position: 'relative', flexWrap: 'wrap', gap: 18 }}>
|
||||
<div>
|
||||
<div className="row gap-2" style={{ fontSize: 12.5, color: '#9ff0cd', fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase' }}><span style={{ width: 7, height: 7, borderRadius: 99, background: 'var(--emerald-500)', boxShadow: '0 0 0 3px rgba(22,182,126,.25)' }} />Official Allottee Portal</div>
|
||||
<h1 style={{ color: '#fff', fontSize: 30, fontWeight: 800, marginTop: 10, letterSpacing: '-.02em' }}>Welcome back, {D.user.firstName}</h1>
|
||||
<div className="row gap-2" style={{ marginTop: 14, flexWrap: 'wrap' }}>
|
||||
<span className="badge" style={{ background: 'rgba(255,255,255,.1)', color: '#dce8f8', border: '1px solid rgba(255,255,255,.14)' }}><Icon name="building" size={13} />Plot {D.user.plot} · {D.user.sector}</span>
|
||||
<span className="badge" style={{ background: 'rgba(255,255,255,.1)', color: '#dce8f8', border: '1px solid rgba(255,255,255,.14)' }}><Icon name="user" size={13} />{D.user.relationship} · {D.user.isAllottee ? 'Allottee' : 'Representative'}</span>
|
||||
<span className="badge" style={{ background: 'rgba(22,182,126,.2)', color: '#9ff0cd', border: '1px solid rgba(22,182,126,.3)' }}><Icon name="file" size={12} />{A.leaseDeed.label}</span>
|
||||
<span className="badge" style={{ background: 'rgba(232,147,12,.2)', color: '#f7cd8a', border: '1px solid rgba(232,147,12,.3)' }}><Icon name="clock" size={12} />{A.atl.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row gap-3">
|
||||
<button className="btn" style={{ background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.18)', color: '#fff' }} onClick={() => go('payments')}><Icon name="rupee" size={18} />Pay Dues</button>
|
||||
<button className="btn btn-emerald" onClick={() => go('journey')}><Icon name="sparkles" size={18} />What's next?</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== Module switcher: Plot Dashboard (primary) · Construction Project Dashboard (secondary) ===== */}
|
||||
<div className="anim-fade-up" style={{ display: 'flex', gap: 12, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ k: 'plot', icon: 'building', title: 'Yamuna Expressway · Plot Dashboard', sub: 'Authority status, records & payments', tag: 'Primary' },
|
||||
{ k: 'construction', icon: 'construction', title: 'Construction Project Dashboard', sub: 'Build progress, milestones & contractor', tag: D.journey.constructionStarted ? 'Active' : 'Not started' },
|
||||
].map(m => {
|
||||
const on = view === m.k;
|
||||
return (
|
||||
<button key={m.k} onClick={() => switchTo(m.k)} className="card mod-switch" style={{ flex: 1, minWidth: 280, textAlign: 'left', cursor: 'pointer', padding: '16px 18px', display: 'flex', alignItems: 'center', gap: 14, border: on ? '1.5px solid var(--blue-400)' : '1px solid var(--line)', background: on ? 'linear-gradient(100deg, var(--blue-50), #fff)' : '#fff', boxShadow: on ? '0 0 0 4px rgba(38,112,230,.1)' : 'var(--sh-sm)' }}>
|
||||
<span style={{ width: 48, height: 48, borderRadius: 13, flex: 'none', display: 'grid', placeItems: 'center', background: on ? 'linear-gradient(150deg,var(--blue-500),var(--navy-800))' : 'var(--bg-2)', color: on ? '#fff' : 'var(--muted)' }}><Icon name={m.icon} size={24} /></span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row gap-2" style={{ flexWrap: 'wrap' }}>
|
||||
<span style={{ fontWeight: 800, fontSize: 15.5, fontFamily: 'var(--font-display)', color: on ? 'var(--navy-800)' : 'var(--ink)' }}>{m.title}</span>
|
||||
<span className={'badge badge-' + (m.tag === 'Primary' ? 'navy' : m.tag === 'Active' ? 'green' : 'gray')} style={{ fontSize: 10.5 }}>{m.tag}</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>{m.sub}</div>
|
||||
</div>
|
||||
<span style={{ width: 22, height: 22, borderRadius: 99, flex: 'none', border: '2px solid ' + (on ? 'var(--blue-500)' : '#cdd7e4'), display: 'grid', placeItems: 'center' }}>{on && <span style={{ width: 10, height: 10, borderRadius: 99, background: 'var(--blue-500)' }} />}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{view === 'plot' ? <PlotDashboard go={go} key="plot" /> : <ConstructionDashboard go={go} key="cons" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================ MODULE 1 — PLOT DASHBOARD (PRIMARY) ============================ */
|
||||
function PlotDashboard({ go }) {
|
||||
const totalDue = D.dues.reduce((s, d) => s + d.amount, 0);
|
||||
const paidPct = Math.round(D.property.paid / D.property.totalValue * 100);
|
||||
const A = D.authorityStatus;
|
||||
return (
|
||||
<div className="anim-fade-in">
|
||||
{/* Pending actions strip */}
|
||||
<div className="card anim-fade-up" style={{ padding: '14px 18px', marginBottom: 24, borderLeft: '4px solid var(--amber-500)', display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
|
||||
<span style={{ width: 38, height: 38, borderRadius: 10, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--amber-50)', color: 'var(--amber-600)' }}><Icon name="alert" size={20} /></span>
|
||||
<div style={{ flex: 1, minWidth: 200 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 14 }}>3 pending actions need your attention</div>
|
||||
<div className="muted" style={{ fontSize: 12.5 }}>ATL pending · 1 overdue payment · Soil Test Report awaited</div>
|
||||
</div>
|
||||
<div className="row gap-2">
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('payments')}>Clear dues</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => go('construction')}>Resolve<Icon name="arrowR" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionLabel n="A" title="Authority Status & Records" sub="Official statuses and payment records from the Authority">
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('payments')}>View all records<Icon name="arrowR" size={15} /></button>
|
||||
</SectionLabel>
|
||||
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16, marginBottom: 18 }}>
|
||||
<StatusCard icon="file" title="Lease Deed Status" code={A.leaseDeed.code} date={'Registered ' + A.leaseDeed.date} />
|
||||
<StatusCard icon="checkCircle" title="ATL Status" code={A.atl.code} date={A.atl.date} />
|
||||
<StatusCard icon="shield" title="Verification" code={A.verification.code} date={'Verified ' + A.verification.date} />
|
||||
<StatusCard icon="home" title="Possession" code={A.possession.code} date={'Granted ' + A.possession.date} />
|
||||
</div>
|
||||
|
||||
{/* Authority Status — key metrics */}
|
||||
{(() => {
|
||||
const last = D.authorityRecords[D.authorityRecords.length - 1] || D.authorityRecords[0];
|
||||
const nextDue = [...D.dues].sort((a, b) => a.due.localeCompare(b.due))[0] || D.dues[0];
|
||||
const metrics = [
|
||||
{ label: 'Total Paid', value: fmt(D.property.paid), sub: D.authorityRecords.length + ' receipts', icon: 'checkCircle', color: 'emerald' },
|
||||
{ label: 'Outstanding Dues', value: fmt(totalDue), sub: D.dues.length + ' pending', icon: 'rupee', color: 'amber' },
|
||||
{ label: 'Last Payment', value: fmt(last.amount), sub: last.date, icon: 'history', color: 'blue' },
|
||||
{ label: 'Next Due', value: nextDue.due, sub: fmt(nextDue.amount) + ' · ' + nextDue.type, icon: 'calendar', color: 'navy' },
|
||||
];
|
||||
const C = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'] };
|
||||
return (
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16, marginBottom: 18 }}>
|
||||
{metrics.map(m => {
|
||||
const cc = C[m.color];
|
||||
return (
|
||||
<div key={m.label} className="card metric-tile card-pad" style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, flex: 'none', display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={m.icon} size={21} /></span>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>{m.label}</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20, marginTop: 1 }}>{m.value}</div>
|
||||
<div className="faint" style={{ fontSize: 11.5, marginTop: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginBottom: 18 }}>
|
||||
{/* Authority payments summary */}
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<div className="row spread" style={{ marginBottom: 16 }}>
|
||||
<div><div className="kicker" style={{ color: 'var(--muted)' }}>Authority Payments</div><h3 style={{ fontSize: 17, marginTop: 6 }}>Premium & lease account</h3></div>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => go('payments')}>Details</button>
|
||||
</div>
|
||||
<div className="row gap-5" style={{ flexWrap: 'wrap', marginBottom: 16 }}>
|
||||
<div><div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>Total Premium</div><div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20 }}>{fmt(D.property.totalValue)}</div></div>
|
||||
<div><div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>Deposited</div><div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20, color: 'var(--emerald-700)' }}>{fmt(D.property.paid)}</div></div>
|
||||
<div><div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>Balance</div><div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20, color: 'var(--amber-600)' }}>{fmt(D.property.totalValue - D.property.paid)}</div></div>
|
||||
</div>
|
||||
<div className="progress" style={{ marginBottom: 8 }}><span style={{ width: paidPct + '%' }} /></div>
|
||||
<div className="row spread faint" style={{ fontSize: 12 }}><span>{paidPct}% of premium deposited</span><span>{D.authorityRecords.length} official receipts</span></div>
|
||||
</div>
|
||||
|
||||
{/* Pending dues + due dates */}
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<div className="row spread" style={{ marginBottom: 14 }}>
|
||||
<h3 style={{ fontSize: 17 }}>Pending Dues & Due Dates</h3>
|
||||
<span className="badge badge-amber">{D.dues.length} pending</span>
|
||||
</div>
|
||||
<div className="col gap-2">
|
||||
{D.dues.map(d => (
|
||||
<div key={d.id} className="row spread" style={{ padding: '9px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{d.label}</div>
|
||||
<div className="row gap-2" style={{ marginTop: 3 }}>
|
||||
<span className={'badge badge-' + (d.status === 'overdue' ? 'red' : d.status === 'due' ? 'amber' : 'gray')} style={{ fontSize: 10.5 }}>{d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'}</span>
|
||||
<span className="faint" style={{ fontSize: 11.5 }}><Icon name="calendar" size={11} style={{ verticalAlign: -1, marginRight: 3 }} />{d.due}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="num" style={{ fontWeight: 700, fontSize: 14 }}>{fmt(d.amount)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm btn-block" style={{ marginTop: 12 }} onClick={() => go('payments')}>Pay {fmt(totalDue)}<Icon name="arrowR" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Official records table */}
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', marginBottom: 24 }}>
|
||||
<div className="row spread" style={{ padding: '18px 22px 6px' }}>
|
||||
<div><h3 style={{ fontSize: 17 }}>Official Authority Records</h3><p className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>Verified deposits & challans on record with YEIDA</p></div>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('payments')}><Icon name="download" size={15} />Statement</button>
|
||||
</div>
|
||||
<table className="tbl tbl-clean">
|
||||
<thead><tr><th>Head of Account</th><th>Challan No.</th><th>Date</th><th>Mode</th><th>Status</th><th style={{ textAlign: 'right' }}>Amount</th></tr></thead>
|
||||
<tbody>
|
||||
{D.authorityRecords.map(r => (
|
||||
<tr key={r.id}>
|
||||
<td style={{ fontWeight: 600, color: 'var(--ink)' }}>{r.head}</td>
|
||||
<td className="num faint">{r.challan}</td>
|
||||
<td className="num">{r.date}</td>
|
||||
<td>{r.mode}</td>
|
||||
<td><span className="badge badge-green"><Icon name="check" size={12} />{r.status}</span></td>
|
||||
<td className="num" style={{ textAlign: 'right', fontWeight: 700, color: 'var(--ink)' }}>{fmt(r.amount)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Latest Notices · AI Assistant · Recent Documents */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 18 }}>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<div className="row spread" style={{ marginBottom: 14 }}>
|
||||
<h3 style={{ fontSize: 16 }}>Latest Notices</h3>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => go('notices')}>All</button>
|
||||
</div>
|
||||
<div className="col gap-3">
|
||||
{D.notices.slice(0, 3).map(n => (
|
||||
<button key={n.id} onClick={() => go('notices')} style={{ textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: '8px 0', borderBottom: '1px solid var(--line-2)', display: 'flex', gap: 11 }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 8, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name={n.pinned ? 'flag' : 'notices'} size={14} /></span>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12.5, lineHeight: 1.3, color: 'var(--ink)' }}>{n.title}</div>
|
||||
<div className="faint" style={{ fontSize: 11, marginTop: 2 }}>{n.cat} · {n.date}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s', background: 'radial-gradient(130% 130% at 100% 0%,#16407e,#0b2548)', color: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="row gap-3" style={{ marginBottom: 12 }}>
|
||||
<span style={{ width: 40, height: 40, borderRadius: 11, display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,.14)' }}><Icon name="sparkles" size={20} /></span>
|
||||
<div><h3 style={{ color: '#fff', fontSize: 16 }}>AI Assistant</h3><div style={{ fontSize: 12, color: '#9ff0cd' }}>Online · ask anything</div></div>
|
||||
</div>
|
||||
<div className="col gap-2" style={{ flex: 1 }}>
|
||||
{['What payments are pending?', 'What is my ATL status?'].map(q => (
|
||||
<button key={q} onClick={() => go('assistant')} style={{ textAlign: 'left', fontSize: 12.5, fontWeight: 500, color: '#dce8f8', padding: '9px 12px', borderRadius: 10, border: '1px solid rgba(255,255,255,.14)', background: 'rgba(255,255,255,.06)', cursor: 'pointer' }}>{q}</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn btn-emerald btn-sm btn-block" style={{ marginTop: 12 }} onClick={() => go('assistant')}><Icon name="chat" size={15} />Open Assistant</button>
|
||||
</div>
|
||||
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.12s' }}>
|
||||
<div className="row spread" style={{ marginBottom: 14 }}>
|
||||
<h3 style={{ fontSize: 16 }}>Recent Documents</h3>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => go('documents')}>All</button>
|
||||
</div>
|
||||
<div className="col gap-2">
|
||||
{D.documents.slice(0, 4).map((d, i) => (
|
||||
<button key={i} onClick={() => go('documents')} className="row spread" style={{ width: '100%', textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: '8px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="row gap-3" style={{ minWidth: 0 }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 8, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--bg-2)', color: 'var(--muted)' }}><Icon name="file" size={14} /></span>
|
||||
<span style={{ minWidth: 0 }}><span style={{ fontWeight: 600, fontSize: 12.5, color: 'var(--ink)', display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.name}</span><span className="faint" style={{ fontSize: 11 }}>{d.date}</span></span>
|
||||
</span>
|
||||
<Icon name="download" size={16} style={{ color: 'var(--faint)', flex: 'none' }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================ MODULE 2 — CONSTRUCTION PROJECT DASHBOARD (SECONDARY) ============================ */
|
||||
function ConstructionDashboard({ go }) {
|
||||
const J = D.journey;
|
||||
return (
|
||||
<div className="anim-fade-in">
|
||||
<SectionLabel n="B" title="Construction Project Dashboard" sub="Your build journey, milestones and next required action">
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('construction')}>Open module<Icon name="arrowR" size={15} /></button>
|
||||
</SectionLabel>
|
||||
|
||||
{/* construction financial / site widgets */}
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 18, marginBottom: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<span style={{ width: 42, height: 42, borderRadius: 11, display: 'grid', placeItems: 'center', background: '#e9eefb', color: 'var(--navy-700)' }}><Icon name="building" size={21} /></span>
|
||||
<span className="badge badge-blue">{Math.round(D.construction.spent / D.construction.estimatedCost * 100)}% spent</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: 13, fontWeight: 600 }}>Estimated Build Cost</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 23, marginTop: 3 }}>{fmt(D.construction.estimatedCost)}</div>
|
||||
<div className="progress" style={{ height: 6, marginTop: 10 }}><span style={{ width: Math.round(D.construction.spent / D.construction.estimatedCost * 100) + '%' }} /></div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 6 }}>{fmt(D.construction.spent)} spent to date</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<span style={{ width: 42, height: 42, borderRadius: 11, display: 'grid', placeItems: 'center', background: 'var(--emerald-50)', color: 'var(--emerald-700)' }}><Icon name="users" size={21} /></span>
|
||||
<span className="badge badge-green"><span className="dot" />{D.construction.contractor.status}</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: 13, fontWeight: 600 }}>Contractor Status</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 15.5, marginTop: 3 }}>{D.construction.contractor.name}</div>
|
||||
<div className="row gap-3 faint" style={{ fontSize: 12, marginTop: 8 }}>
|
||||
<span className="row gap-1"><Icon name="star" size={13} style={{ color: 'var(--amber-500)' }} />{D.construction.contractor.rating}</span>
|
||||
<span>Engaged since {D.construction.contractor.since}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<span style={{ width: 42, height: 42, borderRadius: 11, display: 'grid', placeItems: 'center', background: 'var(--amber-50)', color: 'var(--amber-600)' }}><Icon name="calendar" size={21} /></span>
|
||||
<span className={'badge badge-' + (D.construction.onSchedule ? 'green' : 'amber')}>{D.construction.onSchedule ? 'On schedule' : 'Delayed'}</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: 13, fontWeight: 600 }}>Completion Forecast</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 23, marginTop: 3 }}>{D.construction.completionForecast}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 6 }}>Current: {D.construction.currentMilestone}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.1fr 1fr', gap: 18 }}>
|
||||
{/* status + progress */}
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<div className="row spread" style={{ marginBottom: 8 }}>
|
||||
<span className="badge badge-blue"><span className="dot" />{J.constructionStarted ? 'Construction Started' : 'Not Started'}</span>
|
||||
</div>
|
||||
<div className="row center" style={{ margin: '12px 0' }}><Donut value={J.progressPct} label={J.progressPct + '%'} sub="complete" color="var(--blue-500)" /></div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>Current Stage</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 15, marginTop: 2 }}>{J.stage}</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 14, padding: '11px 13px', borderRadius: 11, background: 'var(--blue-50)' }}>
|
||||
<div className="kicker" style={{ color: 'var(--blue-600)', fontSize: 11 }}>Next Required Action</div>
|
||||
<div className="row spread" style={{ marginTop: 6 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 13, color: 'var(--blue-700)' }}>{J.nextAction}</span>
|
||||
<button className="btn btn-primary btn-sm" style={{ padding: '6px 10px' }} onClick={() => go('construction')}><Icon name="upload" size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* milestones */}
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<h3 style={{ fontSize: 16, marginBottom: 14 }}>Milestones</h3>
|
||||
<div className="col gap-4">
|
||||
{D.constructionMilestones.map((m, i) => (
|
||||
<div key={i}>
|
||||
<div className="row spread" style={{ marginBottom: 6 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 13 }}>{m.label}</span>
|
||||
<span className={'badge badge-' + (m.pct === 100 ? 'green' : m.pct > 0 ? 'blue' : 'gray')} style={{ fontSize: 10.5 }}>{m.status}</span>
|
||||
</div>
|
||||
<div className="progress" style={{ height: 6 }}><span style={{ width: m.pct + '%', background: m.pct === 100 ? 'linear-gradient(90deg,var(--emerald-500),var(--emerald-600))' : undefined }} /></div>
|
||||
<div className="faint" style={{ fontSize: 11, marginTop: 4 }}>{m.date}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.12s' }}>
|
||||
<h3 style={{ fontSize: 16, marginBottom: 16 }}>Activity Timeline</h3>
|
||||
<div style={{ position: 'relative', paddingLeft: 4 }}>
|
||||
{D.activity.slice(0, 4).map((a, i, arr) => {
|
||||
const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)', navy: 'var(--navy-700)' }[a.color];
|
||||
return (
|
||||
<div key={i} className="row" style={{ alignItems: 'flex-start', gap: 12, paddingBottom: i === arr.length - 1 ? 0 : 15, position: 'relative' }}>
|
||||
{i !== arr.length - 1 && <span style={{ position: 'absolute', left: 13, top: 28, bottom: 0, width: 2, background: 'var(--line)' }} />}
|
||||
<span style={{ width: 28, height: 28, borderRadius: 99, flex: 'none', display: 'grid', placeItems: 'center', background: '#fff', border: '2px solid ' + cc, color: cc, zIndex: 1 }}><Icon name={a.icon} size={13} /></span>
|
||||
<div style={{ paddingTop: 2 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12.5 }}>{a.title}</div>
|
||||
<div className="muted" style={{ fontSize: 11.5, marginTop: 1 }}>{a.desc}</div>
|
||||
<div className="faint" style={{ fontSize: 11, marginTop: 2 }}>{a.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* construction payments shortcut + sample plans CTA */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginTop: 18 }}>
|
||||
<div className="card card-pad anim-fade-up row spread" style={{ alignItems: 'center', gap: 14 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--emerald-50)', color: 'var(--emerald-700)' }}><Icon name="rupee" size={21} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 15 }}>Construction Payments</div><div className="muted" style={{ fontSize: 12.5 }}>Milestone invoices & receipts</div></div>
|
||||
</div>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('payments')}>Open<Icon name="arrowR" size={15} /></button>
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up row spread" style={{ animationDelay: '.06s', alignItems: 'center', gap: 14 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name="grid" size={21} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 15 }}>Sample Plans & Tours</div><div className="muted" style={{ fontSize: 12.5 }}>Designs matched to your plot</div></div>
|
||||
</div>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('sampleplans')}>Open<Icon name="arrowR" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Dashboard = Dashboard;
|
||||
})();
|
||||
@@ -0,0 +1,304 @@
|
||||
/* Mock data for the YEIDA portal — realistic Yamuna Expressway style. */
|
||||
window.YEIDA = (function () {
|
||||
const fmtINR = (n) => '₹' + n.toLocaleString('en-IN');
|
||||
const user = {
|
||||
name: 'Rajeev Malhotra',
|
||||
firstName: 'Rajeev',
|
||||
lastName: 'Malhotra',
|
||||
initials: 'RM',
|
||||
allotment: 'YEA-654321',
|
||||
sector: 'Sector 18',
|
||||
pocket: 'Pocket B',
|
||||
plot: 'B-47',
|
||||
category: 'Residential',
|
||||
size: '300 sq.m',
|
||||
mobile: '+91 98xxxxxx12',
|
||||
email: 'r.malhotra@email.com',
|
||||
memberSince: 'Mar 2019',
|
||||
relationship: 'Self', // Self | Son | Daughter | Spouse | Relative | Authorized Representative
|
||||
isAllottee: true, // true = self registration (Allottee = User)
|
||||
allotteeFirstName: 'Rajeev', // original allottee (may differ from registrant)
|
||||
allotteeLastName: 'Malhotra',
|
||||
registeredAddress: {
|
||||
line1: 'Plot B-47, Pocket B, Sector 18',
|
||||
line2: 'Yamuna Expressway, Gautam Buddh Nagar',
|
||||
city: 'Greater Noida', state: 'Uttar Pradesh', pin: '203201',
|
||||
},
|
||||
mailingAddress: {
|
||||
line1: 'C-204, Orchid Residency, Sector 50',
|
||||
line2: 'Near City Centre',
|
||||
city: 'Noida', state: 'Uttar Pradesh', pin: '201301',
|
||||
},
|
||||
};
|
||||
|
||||
// Real authority statuses (NOT a generic "Verified") — driven by backend status codes
|
||||
const authorityStatus = {
|
||||
leaseDeed: { code: 'LD_COMPLETED', label: 'Lease Deed Completed', state: 'done', date: '05 Sep 2019' },
|
||||
atl: { code: 'ATL_PENDING', label: 'ATL Pending', state: 'pending', date: 'Action required' },
|
||||
verification: { code: 'VERIFIED_AUTH', label: 'Authority Verified', state: 'done', date: '12 Mar 2019' },
|
||||
possession: { code: 'POSS_GRANTED', label: 'Possession Granted', state: 'done', date: '22 Aug 2019' },
|
||||
};
|
||||
|
||||
// status-code → human label + colour (single source of truth for badges)
|
||||
const STATUS_CODES = {
|
||||
LD_COMPLETED: { label: 'Lease Deed Completed', tone: 'green', icon: 'file' },
|
||||
LD_PENDING: { label: 'Lease Deed Pending', tone: 'amber', icon: 'file' },
|
||||
ATL_COMPLETED: { label: 'ATL Completed', tone: 'green', icon: 'checkCircle' },
|
||||
ATL_PENDING: { label: 'ATL Pending', tone: 'amber', icon: 'clock' },
|
||||
VERIFICATION_REVIEW: { label: 'Verification Under Review', tone: 'blue', icon: 'clock' },
|
||||
VERIFIED_AUTH: { label: 'Authority Verified', tone: 'green', icon: 'shield' },
|
||||
POSS_GRANTED: { label: 'Possession Granted', tone: 'green', icon: 'home' },
|
||||
};
|
||||
|
||||
const property = {
|
||||
allotmentNo: 'YEA-654321',
|
||||
sector: 'Sector 18',
|
||||
plot: 'B-47',
|
||||
block: 'Block B',
|
||||
size: '300 sq.m',
|
||||
category: 'Residential',
|
||||
facing: 'East',
|
||||
road: '12 m road',
|
||||
allottedOn: '14 Mar 2019',
|
||||
possession: '22 Aug 2019',
|
||||
leasePeriod: '90 years',
|
||||
rate: '₹25,750 / sq.m',
|
||||
totalValue: 7725000,
|
||||
paid: 6952500,
|
||||
coords: { col: 3, row: 2 },
|
||||
};
|
||||
|
||||
const dues = [
|
||||
{ id: 'D-2026-Q1', label: 'Annual Lease Rent 2026-27', due: '30 Jun 2026', amount: 38625, status: 'due', type: 'Lease Rent' },
|
||||
{ id: 'D-2026-LP', label: 'Location Premium Instalment 14/16', due: '15 Jul 2026', amount: 96560, status: 'upcoming', type: 'Premium' },
|
||||
{ id: 'D-2026-WC', label: 'Water Connection Charges', due: '10 Jun 2026', amount: 12400, status: 'overdue', type: 'Utility' },
|
||||
];
|
||||
|
||||
const payments = [
|
||||
{ id: 'TXN-849213', date: '12 Mar 2026', label: 'Location Premium Instalment 13/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2026-0312-18' },
|
||||
{ id: 'TXN-831077', date: '28 Jan 2026', label: 'Annual Lease Rent 2025-26', mode: 'UPI', amount: 36750, status: 'Success', challan: 'CH-2026-0128-04' },
|
||||
{ id: 'TXN-802461', date: '15 Nov 2025', label: 'Building Plan Scrutiny Fee', mode: 'Debit Card', amount: 8500, status: 'Success', challan: 'CH-2025-1115-77' },
|
||||
{ id: 'TXN-778190', date: '12 Sep 2025', label: 'Location Premium Instalment 12/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2025-0912-21' },
|
||||
{ id: 'TXN-751002', date: '30 Jun 2025', label: 'Annual Lease Rent 2024-25', mode: 'UPI', amount: 35000, status: 'Success', challan: 'CH-2025-0630-09' },
|
||||
];
|
||||
|
||||
const constructionSteps = [
|
||||
{ key: 'overview', title: 'Overview', desc: 'Project & eligibility', done: true },
|
||||
{ key: 'required', title: 'Required Documents', desc: 'Checklist', done: true },
|
||||
{ key: 'upload', title: 'Upload Documents', desc: 'Building plans & approvals', done: true },
|
||||
{ key: 'review', title: 'Authority Review', desc: 'YEIDA scrutiny', done: false, active: true },
|
||||
{ key: 'approval', title: 'Approval', desc: 'Sanction letter', done: false },
|
||||
{ key: 'progress', title: 'Progress Tracker', desc: 'On-site milestones', done: false },
|
||||
];
|
||||
|
||||
const requiredDocs = [
|
||||
{ name: 'Approved Building Plan', note: 'Architect-stamped, 4 sets', status: 'uploaded' },
|
||||
{ name: 'Structural Stability Certificate', note: 'By registered structural engineer', status: 'uploaded' },
|
||||
{ name: 'Architect Registration (COA)', note: 'Valid council certificate', status: 'uploaded' },
|
||||
{ name: 'Soil Test Report', note: 'Geotechnical investigation', status: 'pending' },
|
||||
{ name: 'Indemnity Bond', note: '₹100 stamp paper, notarised', status: 'uploaded' },
|
||||
{ name: 'Possession Certificate Copy', note: 'Self-attested', status: 'uploaded' },
|
||||
];
|
||||
|
||||
const constructionMilestones = [
|
||||
{ label: 'Foundation & Plinth', pct: 100, status: 'Completed', date: 'Done · Apr 2026' },
|
||||
{ label: 'Ground Floor Structure', pct: 65, status: 'In Progress', date: 'Est. Aug 2026' },
|
||||
{ label: 'First Floor Structure', pct: 0, status: 'Not Started', date: 'Est. Nov 2026' },
|
||||
{ label: 'Finishing & Handover', pct: 0, status: 'Not Started', date: 'Est. Mar 2027' },
|
||||
];
|
||||
|
||||
const documents = [
|
||||
{ name: 'Allotment Letter', type: 'Allotment', date: '14 Mar 2019', size: '1.2 MB', tag: 'blue' },
|
||||
{ name: 'Possession Letter', type: 'Possession', date: '22 Aug 2019', size: '0.9 MB', tag: 'green' },
|
||||
{ name: 'Lease Deed (Registered)', type: 'Lease Deed', date: '05 Sep 2019', size: '3.4 MB', tag: 'navy' },
|
||||
{ name: 'Building Plan Sanction', type: 'Building Approvals', date: '18 Feb 2026', size: '5.1 MB', tag: 'amber' },
|
||||
{ name: 'Fire NOC', type: 'NOCs', date: '02 Mar 2026', size: '0.6 MB', tag: 'green' },
|
||||
{ name: 'Sector 18 Master Plan', type: 'Master Plans', date: '11 Jan 2024', size: '8.7 MB', tag: 'navy' },
|
||||
{ name: 'Circular: Transfer Policy 2026', type: 'Circulars', date: '20 Apr 2026', size: '0.4 MB', tag: 'blue' },
|
||||
{ name: 'Govt. Order — Lease Rent Revision', type: 'Government Orders', date: '01 Apr 2026', size: '0.3 MB', tag: 'amber' },
|
||||
];
|
||||
|
||||
const notices = [
|
||||
{ id: 1, title: 'Revised Annual Lease Rent rates effective FY 2026-27', cat: 'Policy Update', date: '01 Jun 2026', pinned: true, body: 'The Authority has revised annual lease rent rates for all residential and institutional allottees with effect from 1 April 2026. Allottees are advised to clear outstanding dues at the revised rate before 30 June 2026 to avoid interest.', attachments: ['GO-LeaseRent-2026.pdf'] },
|
||||
{ id: 2, title: 'Construction guidelines update — setback & FAR norms', cat: 'Construction Guidelines', date: '24 May 2026', pinned: true, body: 'Updated Floor Area Ratio (FAR) and setback norms apply to building plans submitted after 1 May 2026. Refer to the attached circular for sector-wise ground coverage limits.', attachments: ['FAR-Norms-2026.pdf', 'Setback-Chart.pdf'] },
|
||||
{ id: 3, title: 'Water supply augmentation work in Sector 18 & 20', cat: 'Sector Update', date: '18 May 2026', pinned: false, body: 'Scheduled maintenance and pipeline augmentation will be carried out from 25–28 May. Intermittent supply disruption is expected.', attachments: [] },
|
||||
{ id: 4, title: 'Online possession application window now open', cat: 'Announcement', date: '10 May 2026', pinned: false, body: 'Allottees who have completed payment of 100% premium can now apply for possession online through the portal.', attachments: ['Possession-SOP.pdf'] },
|
||||
{ id: 5, title: 'Authority Circular: e-Challan mandatory for all dues', cat: 'Authority Circular', date: '02 May 2026', pinned: false, body: 'With effect from 1 June 2026, all payments must be made via e-Challan generated through the portal. Offline challans will be discontinued.', attachments: [] },
|
||||
];
|
||||
|
||||
const activity = [
|
||||
{ icon: 'payments', color: 'emerald', title: 'Payment received', desc: 'Location Premium Instalment 13/16 — ₹96,560', time: '12 Mar 2026' },
|
||||
{ icon: 'upload', color: 'blue', title: 'Documents uploaded', desc: 'Building plan & structural certificate', time: '15 Feb 2026' },
|
||||
{ icon: 'construction', color: 'amber', title: 'Construction application submitted', desc: 'Under authority review', time: '16 Feb 2026' },
|
||||
{ icon: 'check', color: 'emerald', title: 'Fire NOC approved', desc: 'Valid till Mar 2031', time: '02 Mar 2026' },
|
||||
{ icon: 'notices', color: 'navy', title: 'New notice published', desc: 'Revised lease rent rates FY 2026-27', time: '01 Jun 2026' },
|
||||
];
|
||||
|
||||
const sectors = [
|
||||
{ id: '18', name: 'Sector 18', type: 'Residential', plots: 1240, status: 'Developed', mine: true },
|
||||
{ id: '20', name: 'Sector 20', type: 'Residential', plots: 1810, status: 'Developed', mine: false },
|
||||
{ id: '22D', name: 'Sector 22D', type: 'Institutional', plots: 420, status: 'Developing', mine: false },
|
||||
{ id: '24A', name: 'Sector 24A', type: 'Industrial', plots: 690, status: 'Developing', mine: false },
|
||||
{ id: '25', name: 'Sector 25', type: 'Mixed Use', plots: 980, status: 'Developed', mine: false },
|
||||
{ id: '28', name: 'Sector 28', type: 'Residential', plots: 1530, status: 'Planned', mine: false },
|
||||
{ id: '29', name: 'Sector 29', type: 'Commercial', plots: 350, status: 'Planned', mine: false },
|
||||
];
|
||||
|
||||
const transferFees = [
|
||||
{ label: 'Transfer Charges (2.5% of value)', amount: 193125 },
|
||||
{ label: 'Processing Fee', amount: 5000 },
|
||||
{ label: 'Documentation Charges', amount: 2500 },
|
||||
{ label: 'GST (18% on fees)', amount: 1350 },
|
||||
];
|
||||
|
||||
const assistantSuggestions = [
|
||||
'How do I start construction?',
|
||||
'What documents are required?',
|
||||
'Show latest notices',
|
||||
'How can I transfer my plot?',
|
||||
'What payments are pending?',
|
||||
'Show Sector 18 map',
|
||||
];
|
||||
|
||||
const tickets = [
|
||||
{ id: 'TKT-4821', subject: 'Lease deed correction request', status: 'Open', priority: 'High', updated: '2h ago' },
|
||||
{ id: 'TKT-4790', subject: 'Challan not reflecting after UPI payment', status: 'Resolved', priority: 'Medium', updated: '3d ago' },
|
||||
{ id: 'TKT-4755', subject: 'Building plan re-submission query', status: 'In Progress', priority: 'Medium', updated: '5d ago' },
|
||||
];
|
||||
|
||||
// Official authority deposit/challan records (read-only, from authority backend) —
|
||||
// shown immediately after login to build trust
|
||||
const authorityRecords = [
|
||||
{ id: 'AR-2019-001', date: '14 Mar 2019', head: 'Registration / Earnest Money', challan: 'CH-2019-0314-01', amount: 772500, mode: 'Demand Draft', status: 'Deposited' },
|
||||
{ id: 'AR-2019-014', date: '20 Aug 2019', head: 'Allotment Premium (Lump-sum 25%)', challan: 'CH-2019-0820-14', amount: 1158750, mode: 'RTGS', status: 'Deposited' },
|
||||
{ id: 'AR-2021-077', date: '12 Jul 2021', head: 'Location Premium Instalment 6/16', challan: 'CH-2021-0712-77', amount: 96560, mode: 'Net Banking', status: 'Deposited' },
|
||||
{ id: 'AR-2024-052', date: '30 Jun 2024', head: 'Annual Lease Rent 2023-24', challan: 'CH-2024-0630-52', amount: 33500, mode: 'UPI', status: 'Deposited' },
|
||||
{ id: 'AR-2025-091', date: '12 Sep 2025', head: 'Location Premium Instalment 12/16', challan: 'CH-2025-0912-21', amount: 96560, mode: 'Net Banking', status: 'Deposited' },
|
||||
];
|
||||
|
||||
// Construction journey state — drives the first-time "what next?" experience
|
||||
const journey = {
|
||||
constructionStarted: true, // toggleable in the demo via the journey screen
|
||||
stage: 'Authority Review',
|
||||
nextAction: 'Upload pending Soil Test Report',
|
||||
progressPct: 41,
|
||||
};
|
||||
|
||||
// Architectural sample plans (pre-construction guidance)
|
||||
const samplePlans = [
|
||||
{ id: 'SP-300-E', name: 'Aangan — Courtyard Villa', size: '300 sq.m', facing: 'East', floors: 'G+1', beds: 4, style: 'Contemporary', tour: true, accent: '#1d5fbf' },
|
||||
{ id: 'SP-300-N', name: 'Surya — Sunlit Modern', size: '300 sq.m', facing: 'North', floors: 'G+2', beds: 5, style: 'Modern', tour: true, accent: '#0f9d6b' },
|
||||
{ id: 'SP-200-E', name: 'Nivaas — Compact Smart', size: '200 sq.m', facing: 'East', floors: 'G+1', beds: 3, style: 'Minimal', tour: false, accent: '#c77a08' },
|
||||
{ id: 'SP-450-S', name: 'Vista — Luxury Duplex', size: '450 sq.m', facing: 'South', floors: 'G+2', beds: 6, style: 'Luxury', tour: true, accent: '#103263' },
|
||||
{ id: 'SP-200-W', name: 'Saral — Efficient Living', size: '200 sq.m', facing: 'West', floors: 'G+1', beds: 3, style: 'Contemporary', tour: false, accent: '#7a3fbf' },
|
||||
{ id: 'SP-300-S', name: 'Prakriti — Green Home', size: '300 sq.m', facing: 'South', floors: 'G+1', beds: 4, style: 'Eco / Sustainable', tour: true, accent: '#0a7d56' },
|
||||
];
|
||||
|
||||
// Construction financials & site (Section B dashboard widgets)
|
||||
const construction = {
|
||||
estimatedCost: 9800000,
|
||||
spent: 4018000,
|
||||
contractor: { name: 'Shreeji Constructions Pvt. Ltd.', status: 'On Site', rating: 4.6, since: 'Feb 2026' },
|
||||
completionForecast: 'Mar 2027',
|
||||
onSchedule: true,
|
||||
currentMilestone: 'Ground Floor Structure',
|
||||
siteUpdates: [
|
||||
{ title: 'RCC slab casting completed', date: '04 Jun 2026', icon: 'check', color: 'emerald' },
|
||||
{ title: 'Steel reinforcement inspection passed', date: '28 May 2026', icon: 'shield', color: 'blue' },
|
||||
{ title: 'Brickwork — ground floor started', date: '20 May 2026', icon: 'construction', color: 'amber' },
|
||||
],
|
||||
};
|
||||
|
||||
// Construction-side payments (milestones / invoices / receipts)
|
||||
const constructionPayments = [
|
||||
{ id: 'CINV-014', milestone: 'Ground Floor Structure (40%)', vendor: 'Shreeji Constructions', date: '02 Jun 2026', amount: 1568000, status: 'Paid' },
|
||||
{ id: 'CINV-009', milestone: 'Foundation & Plinth (25%)', vendor: 'Shreeji Constructions', date: '14 Apr 2026', amount: 2450000, status: 'Paid' },
|
||||
{ id: 'CINV-018', milestone: 'First Floor Structure (20%)', vendor: 'Shreeji Constructions', date: 'Due 15 Aug 2026', amount: 1960000, status: 'Upcoming' },
|
||||
{ id: 'CINV-006', milestone: 'Architect & Design Fees', vendor: 'Studio Aangan', date: '10 Feb 2026', amount: 320000, status: 'Paid' },
|
||||
];
|
||||
|
||||
// Onboarding decision — 7 action cards
|
||||
const onboardingActions = [
|
||||
{ id: 'plot', icon: 'plot', title: 'View My Plot Status', desc: 'Authority records, lease deed & status', route: 'plot', color: 'blue' },
|
||||
{ id: 'paperwork', icon: 'documents', title: 'Complete Authority Paperwork', desc: 'ATL, lease deed & pending formalities', route: 'documents', color: 'navy' },
|
||||
{ id: 'construction', icon: 'construction', title: 'Start Construction', desc: 'Plans, approvals & build tracking', route: 'construction', color: 'amber' },
|
||||
{ id: 'plans', icon: 'grid', title: 'View Sample Plans', desc: 'Architect designs for your plot', route: 'sampleplans', color: 'emerald' },
|
||||
{ id: 'tours', icon: 'eye', title: 'Explore Virtual Tours', desc: '3D walkthroughs of ready designs', route: 'sampleplans', color: 'blue' },
|
||||
{ id: 'sell', icon: 'transfer', title: 'Sell My Plot', desc: 'Eligibility, fees & transfer application', route: 'transfer', color: 'navy' },
|
||||
{ id: 'expert', icon: 'support', title: 'Contact Expert', desc: 'Talk to a YEIDA support officer', route: 'support', color: 'emerald' },
|
||||
];
|
||||
|
||||
const relationshipOptions = ['Self', 'Son', 'Daughter', 'Spouse', 'Relative', 'Authorized Representative'];
|
||||
|
||||
// ---- Construction Journey (8-step survey workflow) ----
|
||||
const journeyGoals = [
|
||||
{ id: 'start', icon: 'construction', title: 'Start construction soon', desc: 'Begin your build journey with plans & approvals' },
|
||||
{ id: 'completion', icon: 'checkCircle', title: 'Obtain a completion certificate', desc: 'Finalise an existing or finished build' },
|
||||
{ id: 'full', icon: 'building', title: 'Complete full construction', desc: 'End-to-end managed construction' },
|
||||
{ id: 'consult', icon: 'support', title: 'Need construction consultation', desc: 'Talk to an expert before deciding' },
|
||||
{ id: 'explore', icon: 'grid', title: 'Explore design options', desc: 'Browse plans & 3D walkthroughs first' },
|
||||
];
|
||||
|
||||
const journeySteps = [
|
||||
{ key: 'goal', title: 'Goal Selection', desc: 'What you want to achieve', icon: 'flag',
|
||||
summary: 'Start construction soon', done: true },
|
||||
{ key: 'plot', title: 'Plot Verification', desc: 'Confirm your allotment & plot', icon: 'building',
|
||||
summary: 'YEA-654321 · Plot B-47, Sector 18', done: true },
|
||||
{ key: 'design', title: 'Design Selection', desc: 'Choose a house design', icon: 'grid',
|
||||
summary: 'Aangan — Courtyard Villa (G+1)', done: true },
|
||||
{ key: 'planning', title: 'Construction Planning', desc: 'Scope, timeline & permissions', icon: 'calendar',
|
||||
summary: 'G+1 · est. 11 months · plan sanctioned', done: true },
|
||||
{ key: 'budget', title: 'Budget & Payment Setup', desc: 'Cost plan & milestones', icon: 'rupee',
|
||||
summary: '₹98,00,000 budget · 5 milestones', done: false, active: true },
|
||||
{ key: 'contractor', title: 'Contractor Assignment', desc: 'Assign your project team', icon: 'users',
|
||||
summary: 'Pending assignment', done: false },
|
||||
{ key: 'progress', title: 'Construction Progress', desc: 'Track on-site milestones', icon: 'trend',
|
||||
summary: 'Not started', done: false },
|
||||
{ key: 'certificate', title: 'Completion Certificate', desc: 'Final inspection & handover', icon: 'checkCircle',
|
||||
summary: 'Not started', done: false },
|
||||
];
|
||||
|
||||
const projectTeam = [
|
||||
{ role: 'Project Manager', name: 'Anil Verma', org: 'YEIDA PMC', phone: '+91 98xxxxxx01', avatar: 'AV', online: true },
|
||||
{ role: 'Site Engineer', name: 'Priya Nair', org: 'Shreeji Constructions', phone: '+91 98xxxxxx02', avatar: 'PN', online: true },
|
||||
{ role: 'Architect', name: 'Studio Aangan', org: 'Lead: R. Mehta', phone: '+91 98xxxxxx03', avatar: 'SA', online: false },
|
||||
{ role: 'Construction Team', name: '12-member crew', org: 'Shreeji Constructions', phone: '+91 98xxxxxx04', avatar: 'CT', online: true },
|
||||
];
|
||||
|
||||
const teamUpdates = [
|
||||
{ who: 'Priya Nair', text: 'RCC slab casting for ground floor completed and cured.', time: '2h ago', avatar: 'PN' },
|
||||
{ who: 'Anil Verma', text: 'First-floor material procurement scheduled for next week.', time: 'Yesterday', avatar: 'AV' },
|
||||
{ who: 'Studio Aangan', text: 'Revised electrical layout shared for approval.', time: '3 days ago', avatar: 'SA' },
|
||||
];
|
||||
|
||||
// Project Progress Center — visual construction stages
|
||||
const progressStages = [
|
||||
{ name: 'Site Cleaning', pct: 100, date: '02 Mar 2026', note: 'Site cleared & leveled', photos: 4 },
|
||||
{ name: 'Excavation', pct: 100, date: '14 Mar 2026', note: 'Foundation trench dug', photos: 6 },
|
||||
{ name: 'Foundation', pct: 100, date: '04 Apr 2026', note: 'PCC & footing laid', photos: 8 },
|
||||
{ name: 'Boundary Wall', pct: 100, date: '20 Apr 2026', note: 'Perimeter wall complete', photos: 3 },
|
||||
{ name: 'Columns', pct: 100, date: '06 May 2026', note: 'GF columns cast', photos: 5 },
|
||||
{ name: 'Slab Casting', pct: 65, date: 'In progress', note: 'GF roof slab — curing', photos: 7 },
|
||||
{ name: 'Brick Work', pct: 20, date: 'Started', note: 'GF walls begun', photos: 2 },
|
||||
{ name: 'Electrical', pct: 0, date: 'Upcoming', note: '', photos: 0 },
|
||||
{ name: 'Plumbing', pct: 0, date: 'Upcoming', note: '', photos: 0 },
|
||||
{ name: 'Finishing', pct: 0, date: 'Upcoming', note: '', photos: 0 },
|
||||
{ name: 'Completion', pct: 0, date: 'Est. Mar 2027', note: '', photos: 0 },
|
||||
];
|
||||
const countryCodes = [
|
||||
{ code: '+91', label: 'IN', digits: 10 },
|
||||
{ code: '+971', label: 'AE', digits: 9 },
|
||||
{ code: '+1', label: 'US', digits: 10 },
|
||||
{ code: '+44', label: 'UK', digits: 10 },
|
||||
{ code: '+65', label: 'SG', digits: 8 },
|
||||
];
|
||||
|
||||
return {
|
||||
fmtINR, user, authorityStatus, STATUS_CODES, authorityRecords, journey, samplePlans,
|
||||
relationshipOptions, countryCodes, construction, constructionPayments, onboardingActions,
|
||||
journeyGoals, journeySteps, projectTeam, teamUpdates, progressStages,
|
||||
property, dues, payments, constructionSteps, requiredDocs,
|
||||
constructionMilestones, documents, notices, activity, sectors, transferFees,
|
||||
assistantSuggestions, tickets,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,116 @@
|
||||
/* Document Center */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead;
|
||||
const D = window.YEIDA;
|
||||
|
||||
const CATS = ['All', 'Allotment', 'Possession', 'Lease Deed', 'Building Approvals', 'NOCs', 'Circulars', 'Master Plans', 'Government Orders'];
|
||||
|
||||
function Documents({ toast }) {
|
||||
const [cat, setCat] = useState('All');
|
||||
const [q, setQ] = useState('');
|
||||
const [view, setView] = useState('grid');
|
||||
const [bookmarks, setBookmarks] = useState({});
|
||||
const [preview, setPreview] = useState(null);
|
||||
const tagColor = { blue: ['var(--blue-50)', 'var(--blue-600)'], green: ['var(--emerald-50)', 'var(--emerald-700)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'] };
|
||||
|
||||
const list = D.documents.filter(d => (cat === 'All' || d.type === cat) && d.name.toLowerCase().includes(q.toLowerCase()));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Document Center" subtitle="All your property documents — verified, searchable, downloadable">
|
||||
<div className="row gap-2" style={{ background: 'var(--bg-2)', padding: 3, borderRadius: 10 }}>
|
||||
{[['grid', 'grid'], ['list', 'menu']].map(([k, ic]) => (
|
||||
<button key={k} onClick={() => setView(k)} className="icon-btn" style={{ width: 34, height: 34, background: view === k ? '#fff' : 'transparent', boxShadow: view === k ? 'var(--sh-xs)' : 'none', color: view === k ? 'var(--ink)' : 'var(--muted)' }}><Icon name={ic} size={18} /></button>
|
||||
))}
|
||||
</div>
|
||||
</PageHead>
|
||||
|
||||
{/* search + filters */}
|
||||
<div className="card card-pad anim-fade-up" style={{ marginBottom: 18, padding: 16 }}>
|
||||
<div className="row gap-3" style={{ flexWrap: 'wrap' }}>
|
||||
<div className="input-icon" style={{ flex: 1, minWidth: 240 }}>
|
||||
<Icon name="search" />
|
||||
<input className="input" placeholder="Search documents…" value={q} onChange={e => setQ(e.target.value)} />
|
||||
</div>
|
||||
<button className="btn btn-outline"><Icon name="filter" size={17} />Filters</button>
|
||||
</div>
|
||||
<div className="row gap-2" style={{ marginTop: 14, flexWrap: 'wrap' }}>
|
||||
{CATS.map(c => <button key={c} className={'chip' + (cat === c ? ' active' : '')} onClick={() => setCat(c)}>{c}</button>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === 'grid' ? (
|
||||
<div className="anim-fade-up" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
|
||||
{list.map((d, i) => {
|
||||
const tc = tagColor[d.tag];
|
||||
return (
|
||||
<div key={i} className="card card-pad card-hover" style={{ position: 'relative' }}>
|
||||
<button onClick={() => setBookmarks(b => ({ ...b, [d.name]: !b[d.name] }))} style={{ position: 'absolute', top: 14, right: 14, background: 'none', border: 'none', cursor: 'pointer', color: bookmarks[d.name] ? 'var(--amber-500)' : 'var(--faint)' }}>
|
||||
<Icon name="bookmark" size={18} style={{ fill: bookmarks[d.name] ? 'var(--amber-500)' : 'none' }} />
|
||||
</button>
|
||||
<span style={{ width: 48, height: 48, borderRadius: 12, display: 'grid', placeItems: 'center', background: tc[0], color: tc[1], marginBottom: 14 }}><Icon name="file" size={24} /></span>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5, lineHeight: 1.3, minHeight: 38 }}>{d.name}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 6 }}>{d.type}</div>
|
||||
<div className="row spread" style={{ marginTop: 4, fontSize: 12 }}><span className="faint num">{d.date}</span><span className="faint">{d.size}</span></div>
|
||||
<div className="row gap-2" style={{ marginTop: 14 }}>
|
||||
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={() => setPreview(d)}><Icon name="eye" size={15} />Preview</button>
|
||||
<button className="btn btn-ghost btn-sm" style={{ border: '1px solid var(--line)' }} onClick={() => toast && toast('Downloading ' + d.name)}><Icon name="download" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Document</th><th>Category</th><th>Date</th><th>Size</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{list.map((d, i) => {
|
||||
const tc = tagColor[d.tag];
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td><div className="row gap-3"><span style={{ width: 36, height: 36, borderRadius: 9, display: 'grid', placeItems: 'center', background: tc[0], color: tc[1] }}><Icon name="file" size={18} /></span><span style={{ fontWeight: 600, color: 'var(--ink)' }}>{d.name}</span></div></td>
|
||||
<td>{d.type}</td><td className="num">{d.date}</td><td className="num faint">{d.size}</td>
|
||||
<td style={{ textAlign: 'right' }}><div className="row gap-2" style={{ justifyContent: 'flex-end' }}><button className="icon-btn" onClick={() => setPreview(d)}><Icon name="eye" size={18} /></button><button className="icon-btn" onClick={() => toast && toast('Downloading…')}><Icon name="download" size={18} /></button></div></td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview && <PreviewModal doc={preview} onClose={() => setPreview(null)} toast={toast} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewModal({ doc, onClose, toast }) {
|
||||
return (
|
||||
<div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(8,26,57,.55)', backdropFilter: 'blur(3px)', display: 'grid', placeItems: 'center', zIndex: 1000, padding: 24 }}>
|
||||
<div onClick={e => e.stopPropagation()} className="anim-pop card" style={{ width: 720, maxWidth: '100%', maxHeight: '88vh', padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="row spread" style={{ padding: '16px 22px', borderBottom: '1px solid var(--line)' }}>
|
||||
<div className="row gap-3"><span style={{ width: 38, height: 38, borderRadius: 10, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name="file" size={19} /></span><div><div style={{ fontWeight: 700, fontSize: 15 }}>{doc.name}</div><div className="faint" style={{ fontSize: 12 }}>{doc.type} · {doc.size}</div></div></div>
|
||||
<div className="row gap-2"><button className="btn btn-outline btn-sm" onClick={() => toast && toast('Downloaded')}><Icon name="download" size={15} />Download</button><button className="icon-btn" onClick={onClose}><Icon name="x" /></button></div>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', background: 'var(--bg-2)', padding: 30, display: 'grid', placeItems: 'center' }}>
|
||||
{/* faux document page */}
|
||||
<div style={{ width: 460, background: '#fff', boxShadow: 'var(--sh-lg)', borderRadius: 6, padding: '44px 46px', minHeight: 560 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 18 }}><window.Logo size={40} sub={false} /></div>
|
||||
<div style={{ textAlign: 'center', fontSize: 11, color: 'var(--muted)', letterSpacing: '.06em', textTransform: 'uppercase', fontWeight: 700 }}>Yamuna Expressway Industrial Development Authority</div>
|
||||
<div style={{ height: 2, background: 'var(--navy-800)', margin: '14px 0 20px' }} />
|
||||
<div style={{ textAlign: 'center', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 18, marginBottom: 18 }}>{doc.name}</div>
|
||||
{[...Array(8)].map((_, i) => <div key={i} style={{ height: 9, background: i % 4 === 3 ? 'transparent' : '#eef2f7', borderRadius: 4, marginBottom: 11, width: i % 3 === 0 ? '70%' : '100%' }} />)}
|
||||
<div style={{ height: 80, background: 'var(--bg)', borderRadius: 8, margin: '18px 0', border: '1px solid var(--line)' }} />
|
||||
{[...Array(4)].map((_, i) => <div key={i} style={{ height: 9, background: '#eef2f7', borderRadius: 4, marginBottom: 11, width: i === 3 ? '50%' : '100%' }} />)}
|
||||
<div className="row spread" style={{ marginTop: 40 }}><div><div style={{ height: 30, width: 90, borderBottom: '1.5px solid var(--ink-2)' }} /><div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 4 }}>Authorised Signatory</div></div><div style={{ width: 60, height: 60, borderRadius: 99, border: '2px dashed var(--blue-200,#bcd0f5)', display: 'grid', placeItems: 'center', color: 'var(--blue-400)', fontSize: 9, fontWeight: 700, textAlign: 'center' }}>OFFICIAL SEAL</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Documents = Documents;
|
||||
})();
|
||||
@@ -0,0 +1,277 @@
|
||||
/* Notices, Sector Maps, Support, Profile */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead, SectorMap = window.SectorMap, Avatar = window.Avatar;
|
||||
const D = window.YEIDA;
|
||||
|
||||
/* ---------------- NOTICES ---------------- */
|
||||
const NCATS = ['All', 'Policy Update', 'Construction Guidelines', 'Sector Update', 'Authority Circular', 'Announcement'];
|
||||
function Notices() {
|
||||
const [cat, setCat] = useState('All');
|
||||
const [sel, setSel] = useState(D.notices[0]);
|
||||
const list = D.notices.filter(n => cat === 'All' || n.cat === cat);
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Notices" subtitle="Important notices, circulars and policy updates from YEIDA" />
|
||||
<div className="row gap-2" style={{ marginBottom: 18, flexWrap: 'wrap' }}>
|
||||
{NCATS.map(c => <button key={c} className={'chip' + (cat === c ? ' active' : '')} onClick={() => setCat(c)}>{c}</button>)}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '380px 1fr', gap: 18 }}>
|
||||
<div className="col gap-3 anim-fade-up">
|
||||
{list.map(n => (
|
||||
<button key={n.id} onClick={() => setSel(n)} className="card card-hover" style={{ textAlign: 'left', padding: '16px 18px', cursor: 'pointer', border: sel.id === n.id ? '1.5px solid var(--blue-400)' : '1px solid var(--line)', background: sel.id === n.id ? 'var(--blue-50)' : '#fff' }}>
|
||||
<div className="row spread" style={{ marginBottom: 7 }}>
|
||||
<span className={'badge badge-' + (n.cat === 'Policy Update' ? 'navy' : n.cat === 'Construction Guidelines' ? 'amber' : n.cat === 'Authority Circular' ? 'blue' : 'gray')} style={{ fontSize: 11 }}>{n.cat}</span>
|
||||
{n.pinned && <Icon name="flag" size={15} style={{ color: 'var(--red-500)' }} />}
|
||||
</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5, lineHeight: 1.35 }}>{n.title}</div>
|
||||
<div className="faint num" style={{ fontSize: 12, marginTop: 6 }}>{n.date}{n.attachments.length > 0 && ' · ' + n.attachments.length + ' attachment' + (n.attachments.length > 1 ? 's' : '')}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ alignSelf: 'start' }} key={sel.id}>
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<span className={'badge badge-' + (sel.cat === 'Policy Update' ? 'navy' : sel.cat === 'Construction Guidelines' ? 'amber' : sel.cat === 'Authority Circular' ? 'blue' : 'gray')}>{sel.cat}</span>
|
||||
<div className="row gap-2"><button className="icon-btn"><Icon name="bookmark" size={18} /></button><button className="icon-btn"><Icon name="download" size={18} /></button></div>
|
||||
</div>
|
||||
<h2 style={{ fontSize: 24, lineHeight: 1.2 }}>{sel.title}</h2>
|
||||
<div className="row gap-3 faint" style={{ fontSize: 13, marginTop: 10 }}><span className="row gap-2"><Icon name="calendar" size={14} />{sel.date}</span><span className="row gap-2"><Icon name="building" size={14} />YEIDA Authority</span></div>
|
||||
<div style={{ height: 1, background: 'var(--line)', margin: '18px 0' }} />
|
||||
<p style={{ fontSize: 15, lineHeight: 1.7, color: 'var(--ink-2)' }}>{sel.body}</p>
|
||||
{sel.attachments.length > 0 && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, marginBottom: 10 }}>Attachments</div>
|
||||
<div className="col gap-2">
|
||||
{sel.attachments.map(a => (
|
||||
<div key={a} className="row spread" style={{ padding: '12px 14px', borderRadius: 11, border: '1px solid var(--line)', background: 'var(--bg)' }}>
|
||||
<span className="row gap-3" style={{ fontSize: 13.5, fontWeight: 600 }}><span style={{ width: 34, height: 34, borderRadius: 8, display: 'grid', placeItems: 'center', background: 'var(--red-50)', color: 'var(--red-600)' }}><Icon name="file" size={17} /></span>{a}</span>
|
||||
<button className="btn btn-outline btn-sm"><Icon name="download" size={15} />Download</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- SECTOR MAPS ---------------- */
|
||||
function SectorMaps({ go }) {
|
||||
const [active, setActive] = useState(D.sectors[0]);
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Sector Maps" subtitle="Explore plotted layouts, master plans and infrastructure across YEIDA sectors" />
|
||||
<div className="row gap-2" style={{ marginBottom: 18, flexWrap: 'wrap', overflowX: 'auto' }}>
|
||||
{D.sectors.map(s => (
|
||||
<button key={s.id} onClick={() => setActive(s)} className={'chip' + (active.id === s.id ? ' active' : '')} style={{ flex: 'none' }}>
|
||||
{s.mine && <Icon name="mapPin" size={14} style={{ color: active.id === s.id ? 'var(--emerald-500)' : 'var(--emerald-600)' }} />}{s.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 18 }}>
|
||||
<div className="anim-fade-up"><SectorMap sectorName={active.name} myPlot={active.mine ? 'B-47' : ''} height={520} /></div>
|
||||
<div className="col gap-4 anim-fade-up">
|
||||
<div className="card card-pad">
|
||||
<div className="row spread"><h3 style={{ fontSize: 18 }}>{active.name}</h3>{active.mine && <span className="badge badge-green">Your sector</span>}</div>
|
||||
<div className="col" style={{ marginTop: 10 }}>
|
||||
{[['Type', active.type], ['Total Plots', active.plots.toLocaleString('en-IN')], ['Status', active.status], ['Location', 'Yamuna Expressway']].map(([l, v]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '11px 0', borderBottom: '1px solid var(--line-2)' }}><span className="muted" style={{ fontSize: 13.5 }}>{l}</span><span style={{ fontWeight: 700, fontSize: 13.5 }}>{v}</span></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h4 style={{ fontSize: 14.5, marginBottom: 12 }}>Map Layers</h4>
|
||||
<div className="col gap-3">
|
||||
{[['Road Network', 'map'], ['Infrastructure', 'building'], ['Master Plan Overlay', 'grid'], ['Satellite View', 'layers']].map(([l, ic]) => (
|
||||
<div key={l} className="row gap-3" style={{ fontSize: 13.5, color: 'var(--ink-2)' }}><span style={{ width: 30, height: 30, borderRadius: 8, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name={ic} size={16} /></span>{l}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad" style={{ background: 'var(--navy-800)', color: '#fff' }}>
|
||||
<Icon name="documents" size={22} style={{ color: 'var(--emerald-500)' }} />
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5, marginTop: 10 }}>Master Plan PDF</div>
|
||||
<p style={{ fontSize: 12.5, color: 'rgba(200,216,240,.8)', marginTop: 4 }}>Download the official {active.name} master plan.</p>
|
||||
<button className="btn btn-emerald btn-sm btn-block" style={{ marginTop: 12 }} onClick={() => go('documents')}><Icon name="download" size={15} />Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- SUPPORT ---------------- */
|
||||
function Support() {
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Support Center" subtitle="Raise tickets, chat with us, or browse the help center">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={18} />New Ticket</button>
|
||||
</PageHead>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 16, marginBottom: 18 }}>
|
||||
{[['Live Chat', 'Avg. response 2 min', 'chat', 'emerald'], ['Call Request', 'Request a callback', 'phone', 'blue'], ['Email Support', 'support@yeida.gov.in', 'mail', 'navy']].map(([t, s, ic, c]) => {
|
||||
const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c];
|
||||
return (
|
||||
<div key={t} className="card card-pad card-hover" style={{ cursor: 'pointer' }}>
|
||||
<span style={{ width: 46, height: 46, borderRadius: 12, display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={ic} size={23} /></span>
|
||||
<div style={{ fontWeight: 700, fontSize: 16, marginTop: 14 }}>{t}</div>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>{s}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18 }}>
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<div style={{ padding: '16px 20px', borderBottom: '1px solid var(--line)' }}><h3 style={{ fontSize: 17 }}>My Tickets</h3></div>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Ticket</th><th>Subject</th><th>Priority</th><th>Status</th><th>Updated</th></tr></thead>
|
||||
<tbody>
|
||||
{D.tickets.map(t => (
|
||||
<tr key={t.id}>
|
||||
<td className="num" style={{ fontWeight: 600, color: 'var(--ink)' }}>{t.id}</td>
|
||||
<td>{t.subject}</td>
|
||||
<td><span className={'badge badge-' + (t.priority === 'High' ? 'red' : 'gray')}>{t.priority}</span></td>
|
||||
<td><span className={'badge badge-' + (t.status === 'Resolved' ? 'green' : t.status === 'Open' ? 'amber' : 'blue')}>{t.status}</span></td>
|
||||
<td className="faint">{t.updated}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 17, marginBottom: 14 }}>Frequently Asked</h3>
|
||||
<div className="col gap-2">
|
||||
{['How do I generate an e-Challan?', 'What is the construction approval timeline?', 'How is transfer fee calculated?', 'How do I update my KYC?', 'Where can I find my lease deed?'].map(q => (
|
||||
<button key={q} className="row spread" style={{ width: '100%', padding: '13px 14px', borderRadius: 11, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer', textAlign: 'left' }}>
|
||||
<span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink-2)' }}>{q}</span><Icon name="chevR" size={16} style={{ color: 'var(--faint)' }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- PROFILE ---------------- */
|
||||
function Profile({ go }) {
|
||||
const [tab, setTab] = useState('personal');
|
||||
const tabs = [['personal', 'Personal Info', 'user'], ['kyc', 'KYC Verification', 'shield'], ['security', 'Security', 'lock'], ['notifications', 'Notifications', 'bell'], ['devices', 'Devices', 'device']];
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Profile" subtitle="Manage your account, security and preferences" />
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', marginBottom: 18 }}>
|
||||
<div style={{ height: 100, background: 'radial-gradient(120% 200% at 90% 0%,#16407e,#0b2548)' }} />
|
||||
<div style={{ padding: '0 28px 24px', marginTop: -40 }}>
|
||||
<div className="row spread" style={{ alignItems: 'flex-end', flexWrap: 'wrap', gap: 14 }}>
|
||||
<div className="row gap-4" style={{ alignItems: 'flex-end' }}>
|
||||
<Avatar initials={D.user.initials} size={84} style={{ border: '4px solid #fff', fontSize: 32, boxShadow: 'var(--sh-md)' }} />
|
||||
<div style={{ paddingBottom: 6 }}><h2 style={{ fontSize: 22 }}>{D.user.name}</h2><div className="muted" style={{ fontSize: 13.5 }}>Allottee · {D.user.allotment}</div></div>
|
||||
</div>
|
||||
<div className="row gap-2" style={{ paddingBottom: 6 }}>
|
||||
<span className="badge badge-green"><Icon name="shield" size={13} />KYC Verified</span>
|
||||
<span className="badge badge-navy">Member since {D.user.memberSince}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '230px 1fr', gap: 18 }}>
|
||||
<div className="card" style={{ padding: 10, alignSelf: 'start' }}>
|
||||
{tabs.map(([k, l, ic]) => (
|
||||
<button key={k} onClick={() => setTab(k)} className="row gap-3" style={{ width: '100%', padding: '11px 13px', borderRadius: 10, border: 'none', background: tab === k ? 'var(--blue-50)' : 'transparent', color: tab === k ? 'var(--blue-600)' : 'var(--ink-2)', fontWeight: tab === k ? 700 : 500, fontSize: 14, cursor: 'pointer', textAlign: 'left' }}>
|
||||
<Icon name={ic} size={18} />{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" key={tab}>
|
||||
{tab === 'personal' && <PersonalTab />}
|
||||
{tab === 'kyc' && <KycTab />}
|
||||
{tab === 'security' && <SecurityTab />}
|
||||
{tab === 'notifications' && <NotifTab />}
|
||||
{tab === 'devices' && <DevicesTab />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalTab() {
|
||||
const f = [['Full Name', D.user.name, 'user'], ['Mobile Number', D.user.mobile, 'phone'], ['Email Address', D.user.email, 'mail'], ['Allotment Number', D.user.allotment, 'building'], ['Sector', D.user.sector, 'map'], ['Plot Number', D.user.plot, 'plot']];
|
||||
return (
|
||||
<div>
|
||||
<div className="row spread" style={{ marginBottom: 18 }}><h3 style={{ fontSize: 18 }}>Personal Information</h3><button className="btn btn-outline btn-sm"><Icon name="edit" size={15} />Edit</button></div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{f.map(([l, v, ic]) => (
|
||||
<div key={l} className="field"><label>{l}</label><div className="input-icon"><Icon name={ic} /><input className="input" defaultValue={v} readOnly /></div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function KycTab() {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: 18, marginBottom: 16 }}>KYC Verification</h3>
|
||||
<div style={{ display: 'flex', gap: 14, padding: '18px 20px', borderRadius: 13, background: 'var(--emerald-50)', marginBottom: 18 }}>
|
||||
<Icon name="shield" size={24} style={{ color: 'var(--emerald-600)' }} />
|
||||
<div><div style={{ fontWeight: 700, fontSize: 15, color: 'var(--emerald-700)' }}>Your KYC is fully verified</div><div className="muted" style={{ fontSize: 13, marginTop: 2 }}>Verified on 12 Mar 2019 · Aadhaar & PAN linked</div></div>
|
||||
</div>
|
||||
{[['Aadhaar', 'xxxx xxxx 4821', true], ['PAN', 'ABCDE****F', true], ['Address Proof', 'Verified', true]].map(([l, v, ok]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '14px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<div><div style={{ fontWeight: 600, fontSize: 14 }}>{l}</div><div className="faint num" style={{ fontSize: 12.5 }}>{v}</div></div>
|
||||
<span className="badge badge-green"><Icon name="check" size={13} />Verified</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function SecurityTab() {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: 18, marginBottom: 16 }}>Security Settings</h3>
|
||||
{[['Password', 'Last changed 3 months ago', 'Change'], ['Two-Factor Authentication', 'OTP via mobile enabled', 'Manage'], ['Login Alerts', 'Email on new device login', 'Configure']].map(([l, s, b]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '16px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<div className="row gap-3"><span style={{ width: 40, height: 40, borderRadius: 10, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name="lock" size={19} /></span><div><div style={{ fontWeight: 600, fontSize: 14.5 }}>{l}</div><div className="faint" style={{ fontSize: 12.5 }}>{s}</div></div></div>
|
||||
<button className="btn btn-outline btn-sm">{b}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function NotifTab() {
|
||||
const items = [['Payment reminders', true], ['Construction approval updates', true], ['New notices & circulars', true], ['Transfer status', false], ['Promotional updates', false]];
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: 18, marginBottom: 16 }}>Notification Preferences</h3>
|
||||
{items.map(([l, on], i) => <Toggle key={i} label={l} on={on} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Toggle({ label, on }) {
|
||||
const [v, setV] = useState(on);
|
||||
return (
|
||||
<div className="row spread" style={{ padding: '14px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{label}</span>
|
||||
<button onClick={() => setV(!v)} style={{ width: 46, height: 26, borderRadius: 99, border: 'none', cursor: 'pointer', background: v ? 'var(--emerald-500)' : '#cdd7e4', position: 'relative', transition: 'background .2s' }}>
|
||||
<span style={{ position: 'absolute', top: 3, left: v ? 23 : 3, width: 20, height: 20, borderRadius: 99, background: '#fff', transition: 'left .2s', boxShadow: 'var(--sh-xs)' }} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function DevicesTab() {
|
||||
const dev = [['MacBook Pro · Chrome', 'Greater Noida · Active now', true], ['iPhone 14 · YEIDA App', 'Greater Noida · 2h ago', false], ['Windows PC · Edge', 'Delhi · 5 days ago', false]];
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: 18, marginBottom: 16 }}>Connected Devices</h3>
|
||||
{dev.map(([l, s, cur], i) => (
|
||||
<div key={i} className="row spread" style={{ padding: '16px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<div className="row gap-3"><span style={{ width: 40, height: 40, borderRadius: 10, display: 'grid', placeItems: 'center', background: 'var(--bg-2)', color: 'var(--muted)' }}><Icon name="device" size={19} /></span><div><div style={{ fontWeight: 600, fontSize: 14.5 }}>{l}</div><div className="faint" style={{ fontSize: 12.5 }}>{s}</div></div></div>
|
||||
{cur ? <span className="badge badge-green"><span className="dot" />This device</span> : <button className="btn btn-ghost btn-sm" style={{ color: 'var(--red-600)' }}>Sign out</button>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { Notices, SectorMaps, Support, Profile });
|
||||
})();
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Floating AI Assistant — global launcher + compact chat panel, on every app page */
|
||||
(function () {
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const Icon = window.Icon;
|
||||
const D = window.YEIDA;
|
||||
|
||||
// lightweight reply engine (shares the spirit of the full-page assistant)
|
||||
function reply(text) {
|
||||
const t = text.toLowerCase();
|
||||
const A = [
|
||||
{ m: ['construction', 'build', 'start'], r: "Your construction is at Step 4 — Authority Review. Next action: upload the pending Soil Test Report. Want me to open the Construction module?", a: ['construction', 'Open Construction'] },
|
||||
{ m: ['payment', 'due', 'pay', 'challan'], r: "You have ₹1,47,585 across 3 dues. Authority records and challans are on your Payments page.", a: ['payments', 'Open Payments'] },
|
||||
{ m: ['document', 'paper', 'lease', 'atl'], r: "Lease Deed: Completed. ATL: Pending — that's your next paperwork item. All documents live in the Document Center.", a: ['documents', 'Document Center'] },
|
||||
{ m: ['sell', 'transfer'], r: "To sell Plot B-47 you're eligible (7 years held). Estimated transfer cost ≈ ₹2.02 L. Shall I open the Transfer module?", a: ['transfer', 'Start Transfer'] },
|
||||
{ m: ['plan', 'design', 'tour', 'sample'], r: "I've matched 3 plans to your 300 sq.m east-facing plot, with 3D walkthroughs. Open Sample Plans?", a: ['sampleplans', 'View Plans'] },
|
||||
{ m: ['map', 'sector', 'location'], r: "Plot B-47 is in Sector 18, Block B — an east-facing 300 sq.m plot. I can open the interactive map.", a: ['sectormaps', 'Open Map'] },
|
||||
];
|
||||
const hit = A.find(x => x.m.some(k => t.includes(k)));
|
||||
return hit ? { reply: hit.r, action: hit.a } : { reply: "I can help with your plot status, authority paperwork, payments, construction, sample plans and transfers. What would you like to know?" };
|
||||
}
|
||||
|
||||
function FloatingAssistant({ go }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hi Rajeev 👋 Quick help on your plot — payments, paperwork, construction or plans. Ask away, or open the full assistant." }]);
|
||||
const [input, setInput] = useState('');
|
||||
const [typing, setTyping] = useState(false);
|
||||
const scroller = useRef(null);
|
||||
useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing, open]);
|
||||
|
||||
const send = (text) => {
|
||||
if (!text.trim()) return;
|
||||
setMsgs(m => [...m, { role: 'user', text }]); setInput(''); setTyping(true);
|
||||
setTimeout(() => { const r = reply(text); setTyping(false); setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]); }, 800);
|
||||
};
|
||||
|
||||
const quick = ['What payments are pending?', 'What is my ATL status?', 'Show sample plans'];
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', right: 26, bottom: 26, zIndex: 800, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 14 }}>
|
||||
{open && (
|
||||
<div className="anim-pop card" style={{ width: 360, maxWidth: 'calc(100vw - 52px)', height: 480, maxHeight: 'calc(100vh - 120px)', display: 'flex', flexDirection: 'column', overflow: 'hidden', boxShadow: 'var(--sh-lg)' }}>
|
||||
{/* header */}
|
||||
<div className="row spread" style={{ padding: '14px 16px', background: 'linear-gradient(120deg,#103263,#0b2548)', color: '#fff' }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 36, height: 36, borderRadius: 10, display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,.14)' }}><Icon name="sparkles" size={19} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 14.5 }}>YEIDA Assistant</div><div className="row gap-2" style={{ fontSize: 11.5, color: '#9ff0cd' }}><span style={{ width: 6, height: 6, borderRadius: 99, background: 'var(--emerald-500)' }} />Online</div></div>
|
||||
</div>
|
||||
<div className="row gap-1">
|
||||
<button onClick={() => { setOpen(false); go('assistant'); }} title="Open full assistant" style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', display: 'grid', placeItems: 'center' }}><Icon name="expand" size={15} /></button>
|
||||
<button onClick={() => setOpen(false)} style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', display: 'grid', placeItems: 'center' }}><Icon name="x" size={16} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/* messages */}
|
||||
<div ref={scroller} style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 12, background: 'var(--bg)' }}>
|
||||
{msgs.map((m, i) => (
|
||||
<div key={i} className="row" style={{ alignItems: 'flex-end', gap: 8, flexDirection: m.role === 'user' ? 'row-reverse' : 'row' }}>
|
||||
{m.role === 'bot' && <span style={{ width: 26, height: 26, borderRadius: 7, flex: 'none', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff' }}><Icon name="sparkles" size={13} /></span>}
|
||||
<div style={{ maxWidth: '80%' }}>
|
||||
<div style={{ background: m.role === 'user' ? 'var(--blue-600)' : '#fff', color: m.role === 'user' ? '#fff' : 'var(--ink)', border: m.role === 'user' ? 'none' : '1px solid var(--line)', borderRadius: m.role === 'user' ? '12px 12px 4px 12px' : '12px 12px 12px 4px', padding: '10px 13px', fontSize: 13, lineHeight: 1.5 }}>{m.text}</div>
|
||||
{m.action && <button className="btn btn-outline btn-sm" style={{ marginTop: 7 }} onClick={() => { setOpen(false); go(m.action[0]); }}>{m.action[1]}<Icon name="arrowR" size={14} /></button>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{typing && <div className="row gap-2" style={{ background: '#fff', border: '1px solid var(--line)', borderRadius: '12px 12px 12px 4px', padding: '11px 14px', alignSelf: 'flex-start' }}>{[0, 1, 2].map(i => <span key={i} style={{ width: 6, height: 6, borderRadius: 99, background: 'var(--muted)', animation: `pulseDot 1.2s ${i * 0.2}s infinite` }} />)}</div>}
|
||||
{msgs.length === 1 && (
|
||||
<div className="col gap-2" style={{ marginTop: 4 }}>
|
||||
{quick.map(q => <button key={q} onClick={() => send(q)} style={{ textAlign: 'left', fontSize: 12.5, fontWeight: 600, color: 'var(--ink-2)', padding: '9px 12px', borderRadius: 10, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer' }}>{q}</button>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* input */}
|
||||
<div style={{ padding: 12, borderTop: '1px solid var(--line)', background: '#fff' }}>
|
||||
<div className="row gap-2" style={{ background: 'var(--bg)', borderRadius: 11, padding: '5px 5px 5px 14px', border: '1.5px solid var(--line)' }}>
|
||||
<input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask anything…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 13.5 }} />
|
||||
<button className="btn btn-primary btn-sm" style={{ borderRadius: 9, padding: '8px 10px' }} onClick={() => send(input)}><Icon name="send" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* launcher */}
|
||||
<button onClick={() => setOpen(o => !o)} style={{ width: 58, height: 58, borderRadius: 99, border: 'none', cursor: 'pointer', background: open ? 'var(--navy-800)' : 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff', boxShadow: '0 16px 36px -10px rgba(11,37,72,.6)', display: 'grid', placeItems: 'center', transition: 'transform .15s' }} title="AI Assistant">
|
||||
<Icon name={open ? 'chevD' : 'sparkles'} size={26} />
|
||||
{!open && <span style={{ position: 'absolute', top: 4, right: 4, width: 12, height: 12, borderRadius: 99, background: 'var(--emerald-500)', border: '2px solid #fff' }} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.FloatingAssistant = FloatingAssistant;
|
||||
})();
|
||||
@@ -0,0 +1,101 @@
|
||||
/* Icon set — lightweight stroke icons as React components.
|
||||
Usage: <Icon name="dashboard" /> ; all share window.Icon */
|
||||
(function () {
|
||||
const P = {
|
||||
dashboard: 'M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z',
|
||||
plot: 'M4 4h16v16H4z M4 9h16 M4 14h16 M9 4v16 M14 4v16',
|
||||
construction: 'M3 21h18 M5 21V8l7-4 7 4v13 M9 21v-5h6v5 M9.5 11h.01 M14.5 11h.01',
|
||||
payments: 'M3 7h18v10H3z M3 11h18 M7 15h3',
|
||||
documents: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z M9 13h6 M9 17h6',
|
||||
notices: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0',
|
||||
map: 'M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2Z M9 4v14 M15 6v14',
|
||||
transfer: 'M7 8h14 M7 8l4-4 M7 8l4 4 M17 16H3 M17 16l-4-4 M17 16l-4 4',
|
||||
assistant: 'M12 3a9 9 0 0 0-9 9v5a3 3 0 0 0 3 3h2v-7H5v-1a7 7 0 0 1 14 0v1h-3v7h2a3 3 0 0 0 3-3v-5a9 9 0 0 0-9-9Z',
|
||||
support: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3 M12 17h.01',
|
||||
profile: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z',
|
||||
logout: 'M16 17l5-5-5-5 M21 12H9 M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4',
|
||||
search: 'M21 21l-4.3-4.3 M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z',
|
||||
bell: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0',
|
||||
chevR: 'M9 6l6 6-6 6',
|
||||
chevL: 'M15 6l-6 6 6 6',
|
||||
chevD: 'M6 9l6 6 6-6',
|
||||
chevU: 'M6 15l6-6 6 6',
|
||||
arrowR: 'M5 12h14 M13 6l6 6-6 6',
|
||||
check: 'M20 6 9 17l-5-5',
|
||||
checkCircle: 'M22 11.1V12a10 10 0 1 1-5.9-9.1 M22 4 12 14.1l-3-3',
|
||||
x: 'M18 6 6 18 M6 6l12 12',
|
||||
plus: 'M12 5v14 M5 12h14',
|
||||
download: 'M12 3v12 M7 10l5 5 5-5 M5 21h14',
|
||||
upload: 'M12 21V9 M7 14l5-5 5 5 M5 3h14',
|
||||
eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
|
||||
bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z',
|
||||
filter: 'M3 5h18 M6 12h12 M10 19h4',
|
||||
clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 7v5l3 2',
|
||||
calendar: 'M8 3v4 M16 3v4 M3 9h18 M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z',
|
||||
mapPin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
|
||||
phone: 'M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2 4.2 2 2 0 0 1 4 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L8 9.6a16 16 0 0 0 6 6l1.1-1.1a2 2 0 0 1 2.1-.5c.9.3 1.8.6 2.7.7A2 2 0 0 1 22 16.9Z',
|
||||
mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z M22 7l-10 6L2 7',
|
||||
lock: 'M5 11h14a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1Z M8 11V7a4 4 0 1 1 8 0v4',
|
||||
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z M9 12l2 2 4-4',
|
||||
rupee: 'M6 4h12 M6 8h12 M9 4c4 0 6 2 6 5s-2 4-5 4H8l6 7 M6 13h2',
|
||||
building: 'M3 21h18 M5 21V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v17 M15 8h3a1 1 0 0 1 1 1v12 M8 7h3 M8 11h3 M8 15h3',
|
||||
trend: 'M3 17l6-6 4 4 8-8 M21 7v6h-6',
|
||||
alert: 'M12 9v4 M12 17h.01 M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z',
|
||||
info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 16v-4 M12 8h.01',
|
||||
send: 'M22 2 11 13 M22 2l-7 20-4-9-9-4 20-7Z',
|
||||
sparkles: 'M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Z M19 14l.8 2L22 16.8 20 17.6 19 20l-.8-2.4L16 16.8l2.2-.8L19 14Z',
|
||||
menu: 'M3 6h18 M3 12h18 M3 18h18',
|
||||
moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z',
|
||||
globe: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M2 12h20 M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20Z',
|
||||
grid: 'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z',
|
||||
layers: 'M12 2 2 7l10 5 10-5-10-5Z M2 17l10 5 10-5 M2 12l10 5 10-5',
|
||||
zoomIn: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M11 8v6 M8 11h6',
|
||||
zoomOut: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M8 11h6',
|
||||
file: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z',
|
||||
receipt: 'M5 3h14v18l-3-2-2 2-2-2-2 2-2-2-3 2V3Z M9 8h6 M9 12h6',
|
||||
edit: 'M12 20h9 M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z',
|
||||
star: 'M12 3l2.9 6 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.8 3 1.1-6.5L2.6 9.9 9 9l3-6Z',
|
||||
user: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z',
|
||||
settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 7 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H1a2 2 0 1 1 0-4h.1A1.6 1.6 0 0 0 2.6 7a1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 7 2.6 1.6 1.6 0 0 0 8 1.1V1a2 2 0 1 1 4 0v.1A1.6 1.6 0 0 0 14.9 2.6a1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0 1.1 2.7H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z',
|
||||
history: 'M3 3v6h6 M3.5 9a9 9 0 1 0 2-5L3 9 M12 7v5l3 2',
|
||||
home: 'M3 11l9-8 9 8 M5 9v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9',
|
||||
key: 'M15 7a4 4 0 1 1-5.7 3.6L3 17v3h3l1-1h2v-2h2v-2l1.3-1.3A4 4 0 0 1 15 7Z M16.5 7.5h.01',
|
||||
device: 'M4 4h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8 20h8 M12 16v4',
|
||||
paperclip: 'M21 11.5 12 20a5 5 0 0 1-7-7l9-8.5a3.3 3.3 0 0 1 4.7 4.7L9.6 17.7a1.7 1.7 0 0 1-2.3-2.3l8-7.6',
|
||||
ticket: 'M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2 2 2 0 0 0 0 6 2 2 0 0 1-2 2H5a2 2 0 0 1-2-2 2 2 0 0 0 0-6Z M13 7v10',
|
||||
chat: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z',
|
||||
flag: 'M4 22V4 M4 4h13l-2 4 2 4H4',
|
||||
refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v5h-5',
|
||||
bed: 'M3 7v11 M3 12h18v6 M21 18v-5a3 3 0 0 0-3-3h-7v5 M7 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
|
||||
bath: 'M4 12V6a2 2 0 0 1 2-2 2 2 0 0 1 2 2 M3 12h18v3a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4v-3Z M6 19l-1 2 M18 19l1 2 M8 6h.01',
|
||||
ruler: 'M3 9l6-6 12 12-6 6L3 9Z M9 5l1.5 1.5 M12 8l1.5 1.5 M15 11l1.5 1.5 M6 12l1.5 1.5',
|
||||
briefcase: 'M3 8h18v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8Z M8 8V6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 M3 13h18',
|
||||
users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z M22 21v-2a4 4 0 0 0-3-3.9 M16 3.1a4 4 0 0 1 0 7.8',
|
||||
tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8Z M7.5 7.5h.01',
|
||||
image: 'M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z M21 16l-5-5L5 21',
|
||||
dots: 'M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z',
|
||||
megaphone: 'M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1Z M16 8a4 4 0 0 1 0 8 M19 5a8 8 0 0 1 0 14',
|
||||
arrowUpRight: 'M7 17 17 7 M8 7h9v9',
|
||||
arrowDownRight: 'M7 7l10 10 M17 8v9H8',
|
||||
heart: 'M20.8 5.6a5 5 0 0 0-7.1 0L12 7.3l-1.7-1.7a5 5 0 1 0-7.1 7.1L12 21.5l8.8-8.8a5 5 0 0 0 0-7.1Z',
|
||||
messages: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z',
|
||||
sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10Z M12 1v2 M12 21v2 M4.2 4.2l1.4 1.4 M18.4 18.4l1.4 1.4 M1 12h2 M21 12h2 M4.2 19.8l1.4-1.4 M18.4 5.6l1.4-1.4',
|
||||
chartBar: 'M3 3v18h18 M8 17v-5 M13 17V8 M18 17v-9',
|
||||
funnel: 'M3 5h18 M6 10h12 M10 15h4 M11 20h2',
|
||||
bookmark2: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z',
|
||||
pin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
|
||||
expand: 'M15 3h6v6 M9 21H3v-6 M21 3l-7 7 M3 21l7-7',
|
||||
panelLeft: 'M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M9 4v16',
|
||||
};
|
||||
function Icon({ name, size, style, className, strokeWidth }) {
|
||||
const d = P[name];
|
||||
if (!d) return null;
|
||||
const segs = d.split(' M').map((s, i) => (i === 0 ? s : 'M' + s));
|
||||
return React.createElement('svg', {
|
||||
width: size || 20, height: size || 20, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: 'currentColor', strokeWidth: strokeWidth || 1.8, strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round', style, className, 'aria-hidden': true,
|
||||
}, segs.map((seg, i) => React.createElement('path', { key: i, d: seg })));
|
||||
}
|
||||
window.Icon = Icon;
|
||||
})();
|
||||
@@ -0,0 +1,643 @@
|
||||
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
|
||||
/* BEGIN USAGE */
|
||||
/**
|
||||
* <image-slot> — user-fillable image placeholder.
|
||||
*
|
||||
* Drop this into a deck, mockup, or page wherever you want the user to
|
||||
* supply an image. You control the slot's shape and size; the user fills it
|
||||
* by dragging an image file onto it (or clicking to browse). The dropped
|
||||
* image persists across reloads via a .image-slots.state.json sidecar —
|
||||
* same read-via-fetch / write-via-window.omelette pattern as
|
||||
* design_canvas.jsx, so the filled slot shows on share links, downloaded
|
||||
* zips, and PPTX export. Outside the omelette runtime the slot is read-only.
|
||||
*
|
||||
* The host bridge only allows sidecar writes at the project root, so the
|
||||
* HTML that uses this component is assumed to live at the project root too
|
||||
* (same constraint as design_canvas.jsx).
|
||||
*
|
||||
* Attributes:
|
||||
* id Persistence key. REQUIRED for the drop to survive reload —
|
||||
* every slot on the page needs a distinct id.
|
||||
* shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded')
|
||||
* 'circle' applies 50% border-radius; on a non-square slot
|
||||
* that's an ellipse — set equal width and height for a true
|
||||
* circle.
|
||||
* radius Corner radius in px for 'rounded'. (default 12)
|
||||
* mask Any CSS clip-path value. Overrides `shape` — use this for
|
||||
* hexagons, blobs, arbitrary polygons.
|
||||
* fit object-fit: cover | contain | fill. (default 'cover')
|
||||
* With cover (the default) double-clicking the filled slot
|
||||
* enters a reframe mode: the whole image spills past the mask
|
||||
* (translucent outside, opaque inside), drag to reposition,
|
||||
* corner-drag to scale. The crop persists alongside the image
|
||||
* in the sidecar. contain/fill stay static.
|
||||
* position object-position for fit=contain|fill. (default '50% 50%')
|
||||
* placeholder Empty-state caption. (default 'Drop an image')
|
||||
* src Optional initial/fallback image URL. A user drop overrides
|
||||
* it; clearing the drop reveals src again.
|
||||
*
|
||||
* Size and layout come from ordinary CSS on the element — width/height
|
||||
* inline or from a parent grid — so it composes with any layout.
|
||||
*
|
||||
* Usage:
|
||||
* <image-slot id="hero" style="width:800px;height:450px" shape="rounded" radius="20"
|
||||
* placeholder="Drop a hero image"></image-slot>
|
||||
* <image-slot id="avatar" style="width:120px;height:120px" shape="circle"></image-slot>
|
||||
* <image-slot id="kite" style="width:300px;height:300px"
|
||||
* mask="polygon(50% 0, 100% 50%, 50% 100%, 0 50%)"></image-slot>
|
||||
*/
|
||||
/* END USAGE */
|
||||
|
||||
(() => {
|
||||
const STATE_FILE = '.image-slots.state.json';
|
||||
// 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the
|
||||
// sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB.
|
||||
const MAX_DIM = 1200;
|
||||
// Raster formats only. SVG is excluded (can carry script; createImageBitmap
|
||||
// on SVG blobs is inconsistent). GIF is excluded because the canvas
|
||||
// re-encode keeps only the first frame, so an animated GIF would silently
|
||||
// go still — better to reject than surprise.
|
||||
const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
|
||||
|
||||
// ── Shared sidecar store ────────────────────────────────────────────────
|
||||
// One fetch + immediate write-on-change for every <image-slot> on the
|
||||
// page. Reads via fetch() so viewing works anywhere the HTML and sidecar
|
||||
// are served together; writes go through window.omelette.writeFile, which
|
||||
// the host allowlists to *.state.json basenames only.
|
||||
const subs = new Set();
|
||||
let slots = {};
|
||||
// ids explicitly cleared before the sidecar fetch resolved — otherwise
|
||||
// the merge below can't tell "never set" from "just deleted" and would
|
||||
// resurrect the sidecar's stale value.
|
||||
const tombstones = new Set();
|
||||
let loaded = false;
|
||||
let loadP = null;
|
||||
|
||||
function load() {
|
||||
if (loadP) return loadP;
|
||||
loadP = fetch(STATE_FILE)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((j) => {
|
||||
// Merge: sidecar loses to any in-memory change that raced ahead of
|
||||
// the fetch (drop or clear) so neither is clobbered by hydration.
|
||||
if (j && typeof j === 'object') {
|
||||
const merged = Object.assign({}, j, slots);
|
||||
// A framing-only write that raced ahead of hydration must not
|
||||
// drop a user image that's only on disk — inherit u from the
|
||||
// sidecar for any in-memory entry that lacks one.
|
||||
for (const k in slots) {
|
||||
if (merged[k] && !merged[k].u && j[k]) {
|
||||
merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u;
|
||||
}
|
||||
}
|
||||
for (const id of tombstones) delete merged[id];
|
||||
slots = merged;
|
||||
}
|
||||
tombstones.clear();
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => { loaded = true; subs.forEach((fn) => fn()); });
|
||||
return loadP;
|
||||
}
|
||||
|
||||
// Serialize writes so two near-simultaneous drops on different slots
|
||||
// can't reorder at the backend and leave the sidecar with only the
|
||||
// first. A save requested mid-flight just marks dirty and re-fires on
|
||||
// completion with the then-current slots.
|
||||
let saving = false;
|
||||
let saveDirty = false;
|
||||
function save() {
|
||||
if (saving) { saveDirty = true; return; }
|
||||
const w = window.omelette && window.omelette.writeFile;
|
||||
if (!w) return;
|
||||
saving = true;
|
||||
Promise.resolve(w(STATE_FILE, JSON.stringify(slots)))
|
||||
.catch(() => {})
|
||||
.then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } });
|
||||
}
|
||||
|
||||
const S_MAX = 5;
|
||||
const clampS = (s) => Math.max(1, Math.min(S_MAX, s));
|
||||
|
||||
// Normalize a stored slot value. Pre-reframe sidecars stored a bare
|
||||
// data-URL string; newer ones store {u, s, x, y}. Either shape is valid.
|
||||
function getSlot(id) {
|
||||
const v = slots[id];
|
||||
if (!v) return null;
|
||||
return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v;
|
||||
}
|
||||
|
||||
function setSlot(id, val) {
|
||||
if (!id) return;
|
||||
if (val) { slots[id] = val; tombstones.delete(id); }
|
||||
else { delete slots[id]; if (!loaded) tombstones.add(id); }
|
||||
subs.forEach((fn) => fn());
|
||||
// A drop is rare + high-value — write immediately so nav-away can't lose
|
||||
// it. Gate on the initial read so we don't overwrite a sidecar we haven't
|
||||
// merged yet; the merge in load() keeps this change once the read lands.
|
||||
if (loaded) save(); else load().then(save);
|
||||
}
|
||||
|
||||
// ── Image downscale ─────────────────────────────────────────────────────
|
||||
// Encode through a canvas so the sidecar carries resized bytes, not the
|
||||
// raw upload. Longest side is capped at 2× the slot's rendered width
|
||||
// (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG
|
||||
// for photos, so there's no need for per-image format picking.
|
||||
async function toDataUrl(file, targetW) {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
try {
|
||||
const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM);
|
||||
const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height));
|
||||
const w = Math.max(1, Math.round(bitmap.width * scale));
|
||||
const h = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
|
||||
return canvas.toDataURL('image/webp', 0.85);
|
||||
} finally {
|
||||
bitmap.close && bitmap.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Custom element ──────────────────────────────────────────────────────
|
||||
const stylesheet =
|
||||
':host{display:inline-block;position:relative;vertical-align:top;' +
|
||||
' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' +
|
||||
'.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' +
|
||||
// .frame img (clipped) and .spill (unclipped ghost + handles) share the
|
||||
// same left/top/width/height in frame-%, computed by _applyView(), so the
|
||||
// inside-mask crop and the outside-mask spill stay pixel-aligned.
|
||||
'.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' +
|
||||
' -webkit-user-drag:none;user-select:none;touch-action:none}' +
|
||||
// Reframe mode (double-click): the full image spills past the mask. The
|
||||
// spill layer is sized to the IMAGE bounds so its corners are where the
|
||||
// resize handles belong. The ghost <img> inside is translucent; the real
|
||||
// clipped <img> underneath shows the opaque in-mask crop.
|
||||
'.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' +
|
||||
' cursor:grab;touch-action:none}' +
|
||||
':host([data-panning]) .spill{cursor:grabbing}' +
|
||||
'.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' +
|
||||
' pointer-events:none;-webkit-user-drag:none;user-select:none;' +
|
||||
' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' +
|
||||
'.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' +
|
||||
' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' +
|
||||
' transform:translate(-50%,-50%)}' +
|
||||
'.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' +
|
||||
'.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' +
|
||||
'.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' +
|
||||
'.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' +
|
||||
':host([data-reframe]){z-index:10}' +
|
||||
':host([data-reframe]) .spill{display:block}' +
|
||||
':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' +
|
||||
'.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
|
||||
' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +
|
||||
' cursor:pointer;user-select:none}' +
|
||||
'.empty svg{opacity:.45}' +
|
||||
'.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' +
|
||||
'.empty .sub{font-size:11px}' +
|
||||
'.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' +
|
||||
'.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' +
|
||||
':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' +
|
||||
' background:rgba(201,100,66,.10)}' +
|
||||
'.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' +
|
||||
' transition:border-color .12s}' +
|
||||
':host([data-over]) .ring{border-color:#c96442}' +
|
||||
':host([data-filled]) .ring{display:none}' +
|
||||
// Controls sit BELOW the mask (top:100%), absolutely positioned so the
|
||||
// author-declared slot height is unaffected. The gap is padding, not a
|
||||
// top offset, so the hover target stays contiguous with the frame.
|
||||
'.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' +
|
||||
' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' +
|
||||
' white-space:nowrap}' +
|
||||
':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' +
|
||||
' {opacity:1;pointer-events:auto}' +
|
||||
'.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' +
|
||||
' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' +
|
||||
' backdrop-filter:blur(6px)}' +
|
||||
'.ctl button:hover{background:rgba(0,0,0,.8)}' +
|
||||
'.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' +
|
||||
' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}';
|
||||
|
||||
const icon =
|
||||
'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
|
||||
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>' +
|
||||
'<path d="m21 15-5-5L5 21"/></svg>';
|
||||
|
||||
class ImageSlot extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['shape', 'radius', 'mask', 'fit', 'position', 'placeholder', 'src', 'id'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const root = this.attachShadow({ mode: 'open' });
|
||||
// .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius
|
||||
// on the frame (circle, pill, rounded) can't clip them.
|
||||
root.innerHTML =
|
||||
'<style>' + stylesheet + '</style>' +
|
||||
'<div class="frame" part="frame">' +
|
||||
' <img part="image" alt="" draggable="false" style="display:none">' +
|
||||
' <div class="empty" part="empty">' + icon +
|
||||
' <div class="cap"></div>' +
|
||||
' <div class="sub">or <u>browse files</u></div></div>' +
|
||||
' <div class="ring" part="ring"></div>' +
|
||||
'</div>' +
|
||||
'<div class="spill">' +
|
||||
' <img class="ghost" alt="" draggable="false">' +
|
||||
' <div class="handle" data-c="nw"></div><div class="handle" data-c="ne"></div>' +
|
||||
' <div class="handle" data-c="sw"></div><div class="handle" data-c="se"></div>' +
|
||||
'</div>' +
|
||||
'<div class="ctl"><button data-act="replace" title="Replace image">Replace</button>' +
|
||||
' <button data-act="clear" title="Remove image">Remove</button></div>' +
|
||||
'<input type="file" accept="' + ACCEPT.join(',') + '" hidden>';
|
||||
this._frame = root.querySelector('.frame');
|
||||
this._ring = root.querySelector('.ring');
|
||||
this._img = root.querySelector('.frame img');
|
||||
this._empty = root.querySelector('.empty');
|
||||
this._cap = root.querySelector('.cap');
|
||||
this._sub = root.querySelector('.sub');
|
||||
this._spill = root.querySelector('.spill');
|
||||
this._ghost = root.querySelector('.ghost');
|
||||
this._err = null;
|
||||
this._input = root.querySelector('input');
|
||||
this._depth = 0;
|
||||
this._gen = 0;
|
||||
this._view = { s: 1, x: 0, y: 0 };
|
||||
this._subFn = () => this._render();
|
||||
// Shadow-DOM listeners live with the shadow DOM — bound once here so
|
||||
// disconnect/reconnect (e.g. React remount) doesn't stack handlers.
|
||||
this._empty.addEventListener('click', () => this._input.click());
|
||||
root.addEventListener('click', (e) => {
|
||||
const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
|
||||
if (act === 'replace') { this._exitReframe(true); this._input.click(); }
|
||||
if (act === 'clear') {
|
||||
this._exitReframe(false);
|
||||
this._gen++;
|
||||
this._local = null;
|
||||
if (this.id) setSlot(this.id, null); else this._render();
|
||||
}
|
||||
});
|
||||
this._input.addEventListener('change', () => {
|
||||
const f = this._input.files && this._input.files[0];
|
||||
if (f) this._ingest(f);
|
||||
this._input.value = '';
|
||||
});
|
||||
// naturalWidth/Height aren't known until load — re-apply so the cover
|
||||
// baseline is computed from real dimensions, not the 100%×100% fallback.
|
||||
this._img.addEventListener('load', () => this._applyView());
|
||||
// Gated on editable + fit=cover so share links and contain/fill slots
|
||||
// stay static.
|
||||
this.addEventListener('dblclick', (e) => {
|
||||
if (!this.hasAttribute('data-editable') || !this._reframes()) return;
|
||||
e.preventDefault();
|
||||
if (this.hasAttribute('data-reframe')) this._exitReframe(true);
|
||||
else this._enterReframe();
|
||||
});
|
||||
// Pan + resize both originate on the spill layer. A handle pointerdown
|
||||
// drives an aspect-locked resize anchored at the opposite corner; any
|
||||
// other pointerdown on the spill pans. Offsets are frame-% so a
|
||||
// reframed slot survives responsive resize / PPTX export.
|
||||
this._spill.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0 || !this.hasAttribute('data-reframe')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._spill.setPointerCapture(e.pointerId);
|
||||
const rect = this.getBoundingClientRect();
|
||||
const fw = rect.width || 1, fh = rect.height || 1;
|
||||
const corner = e.target.getAttribute && e.target.getAttribute('data-c');
|
||||
let move;
|
||||
if (corner) {
|
||||
// Resize about the OPPOSITE corner. Viewport-px throughout (rect
|
||||
// fw/fh, not clientWidth) so the math survives a transform:scale()
|
||||
// ancestor — deck_stage renders slides scaled-to-fit.
|
||||
const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1;
|
||||
const base = Math.max(fw / iw, fh / ih);
|
||||
const sx = corner.includes('e') ? 1 : -1;
|
||||
const sy = corner.includes('s') ? 1 : -1;
|
||||
const s0 = this._view.s;
|
||||
const w0 = iw * base * s0, h0 = ih * base * s0;
|
||||
const cx0 = (50 + this._view.x) / 100 * fw;
|
||||
const cy0 = (50 + this._view.y) / 100 * fh;
|
||||
const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2;
|
||||
const diag0 = Math.hypot(w0, h0);
|
||||
const ux = sx * w0 / diag0, uy = sy * h0 / diag0;
|
||||
move = (ev) => {
|
||||
const proj = (ev.clientX - rect.left - ox) * ux +
|
||||
(ev.clientY - rect.top - oy) * uy;
|
||||
const s = clampS(s0 * proj / diag0);
|
||||
const d = diag0 * s / s0;
|
||||
this._view.s = s;
|
||||
this._view.x = (ox + ux * d / 2) / fw * 100 - 50;
|
||||
this._view.y = (oy + uy * d / 2) / fh * 100 - 50;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
};
|
||||
} else {
|
||||
this.setAttribute('data-panning', '');
|
||||
const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y };
|
||||
move = (ev) => {
|
||||
this._view.x = start.x + (ev.clientX - start.px) / fw * 100;
|
||||
this._view.y = start.y + (ev.clientY - start.py) / fh * 100;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
};
|
||||
}
|
||||
const up = () => {
|
||||
try { this._spill.releasePointerCapture(e.pointerId); } catch {}
|
||||
this._spill.removeEventListener('pointermove', move);
|
||||
this._spill.removeEventListener('pointerup', up);
|
||||
this._spill.removeEventListener('pointercancel', up);
|
||||
this.removeAttribute('data-panning');
|
||||
this._dragUp = null;
|
||||
};
|
||||
// Stashed so _exitReframe (Escape / outside-click mid-drag) can
|
||||
// tear the capture + listeners down synchronously.
|
||||
this._dragUp = up;
|
||||
this._spill.addEventListener('pointermove', move);
|
||||
this._spill.addEventListener('pointerup', up);
|
||||
this._spill.addEventListener('pointercancel', up);
|
||||
});
|
||||
// Wheel zoom stays available inside reframe mode as a trackpad nicety —
|
||||
// zooms toward the cursor (offset' = cursor·(1-k) + offset·k).
|
||||
this.addEventListener('wheel', (e) => {
|
||||
if (!this.hasAttribute('data-reframe')) return;
|
||||
e.preventDefault();
|
||||
const r = this.getBoundingClientRect();
|
||||
const cx = (e.clientX - r.left) / r.width * 100 - 50;
|
||||
const cy = (e.clientY - r.top) / r.height * 100 - 50;
|
||||
const prev = this._view.s;
|
||||
const next = clampS(prev * Math.pow(1.0015, -e.deltaY));
|
||||
if (next === prev) return;
|
||||
const k = next / prev;
|
||||
this._view.s = next;
|
||||
this._view.x = cx * (1 - k) + this._view.x * k;
|
||||
this._view.y = cy * (1 - k) + this._view.y * k;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Warn once per page — an id-less slot works for the session but
|
||||
// cannot persist, and two id-less slots would share nothing.
|
||||
if (!this.id && !ImageSlot._warned) {
|
||||
ImageSlot._warned = true;
|
||||
console.warn('<image-slot> without an id will not persist its dropped image.');
|
||||
}
|
||||
this.addEventListener('dragenter', this);
|
||||
this.addEventListener('dragover', this);
|
||||
this.addEventListener('dragleave', this);
|
||||
this.addEventListener('drop', this);
|
||||
subs.add(this._subFn);
|
||||
// width%/height% in _applyView encode the frame aspect at call time —
|
||||
// a host resize (responsive grid, pane divider) would stretch the
|
||||
// image until the next _render. Re-render on size change: _render()
|
||||
// re-seeds _view from stored before clamp/apply, so a shrink→grow
|
||||
// cycle round-trips instead of ratcheting x/y toward the narrower
|
||||
// frame's clamp range.
|
||||
this._ro = new ResizeObserver(() => this._render());
|
||||
this._ro.observe(this);
|
||||
load();
|
||||
this._render();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
subs.delete(this._subFn);
|
||||
this.removeEventListener('dragenter', this);
|
||||
this.removeEventListener('dragover', this);
|
||||
this.removeEventListener('dragleave', this);
|
||||
this.removeEventListener('drop', this);
|
||||
if (this._ro) { this._ro.disconnect(); this._ro = null; }
|
||||
this._exitReframe(false);
|
||||
}
|
||||
|
||||
_enterReframe() {
|
||||
if (this.hasAttribute('data-reframe')) return;
|
||||
this.setAttribute('data-reframe', '');
|
||||
this._applyView();
|
||||
// Close on click outside (the spill handler stopPropagation()s so
|
||||
// in-image drags don't reach this) and on Escape. Listeners are held
|
||||
// on the instance so _exitReframe / disconnectedCallback can detach
|
||||
// exactly what was attached.
|
||||
this._outside = (e) => {
|
||||
if (e.composedPath && e.composedPath().includes(this)) return;
|
||||
this._exitReframe(true);
|
||||
};
|
||||
this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); };
|
||||
document.addEventListener('pointerdown', this._outside, true);
|
||||
document.addEventListener('keydown', this._esc, true);
|
||||
}
|
||||
|
||||
_exitReframe(commit) {
|
||||
if (!this.hasAttribute('data-reframe')) return;
|
||||
if (this._dragUp) this._dragUp();
|
||||
this.removeAttribute('data-reframe');
|
||||
this.removeAttribute('data-panning');
|
||||
if (this._outside) document.removeEventListener('pointerdown', this._outside, true);
|
||||
if (this._esc) document.removeEventListener('keydown', this._esc, true);
|
||||
this._outside = this._esc = null;
|
||||
if (commit) this._commitView();
|
||||
}
|
||||
|
||||
attributeChangedCallback() { if (this.shadowRoot) this._render(); }
|
||||
|
||||
// handleEvent — one listener object for all four drag events keeps the
|
||||
// add/remove symmetric and the depth counter correct.
|
||||
handleEvent(e) {
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
// Without preventDefault the browser never fires 'drop'.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
if (e.type === 'dragenter') this._depth++;
|
||||
this.setAttribute('data-over', '');
|
||||
} else if (e.type === 'dragleave') {
|
||||
// dragenter/leave fire for every descendant crossing — count depth
|
||||
// so hovering the icon inside the empty state doesn't flicker.
|
||||
if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); }
|
||||
} else if (e.type === 'drop') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._depth = 0;
|
||||
this.removeAttribute('data-over');
|
||||
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||||
if (f) this._ingest(f);
|
||||
}
|
||||
}
|
||||
|
||||
async _ingest(file) {
|
||||
this._setError(null);
|
||||
if (!file || ACCEPT.indexOf(file.type) < 0) {
|
||||
this._setError('Drop a PNG, JPEG, WebP, or AVIF image.');
|
||||
return;
|
||||
}
|
||||
// toDataUrl can take hundreds of ms on a large photo. A Clear or a
|
||||
// newer drop during that window would be clobbered when this await
|
||||
// resumes — bump + capture a generation so stale encodes bail.
|
||||
const gen = ++this._gen;
|
||||
try {
|
||||
const w = this.clientWidth || this.offsetWidth || MAX_DIM;
|
||||
const url = await toDataUrl(file, w);
|
||||
if (gen !== this._gen) return;
|
||||
// Only exit reframe once the new image is in hand — a rejected type
|
||||
// or decode failure leaves the in-progress crop untouched.
|
||||
this._exitReframe(false);
|
||||
const val = { u: url, s: 1, x: 0, y: 0 };
|
||||
setSlot(this.id || '', val);
|
||||
// Keep a session-local copy for id-less slots so the drop still
|
||||
// shows, even though it cannot persist.
|
||||
if (!this.id) { this._local = val; this._render(); }
|
||||
} catch (err) {
|
||||
if (gen !== this._gen) return;
|
||||
this._setError('Could not read that image.');
|
||||
console.warn('<image-slot> ingest failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
_setError(msg) {
|
||||
if (this._err) { this._err.remove(); this._err = null; }
|
||||
if (!msg) return;
|
||||
const d = document.createElement('div');
|
||||
d.className = 'err'; d.textContent = msg;
|
||||
this.shadowRoot.appendChild(d);
|
||||
this._err = d;
|
||||
setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000);
|
||||
}
|
||||
|
||||
// Reframing (pan/resize) is only meaningful for fit=cover — contain/fill
|
||||
// keep the old object-fit path and double-click is a no-op.
|
||||
_reframes() {
|
||||
return this.hasAttribute('data-filled') &&
|
||||
(this.getAttribute('fit') || 'cover') === 'cover';
|
||||
}
|
||||
|
||||
// Cover-baseline geometry, shared by clamp/apply/resize. Null until the
|
||||
// img has loaded (naturalWidth is 0 before that) or when the slot has no
|
||||
// layout box — ResizeObserver fires with a 0×0 rect under display:none,
|
||||
// and clamping against a degenerate 1×1 frame would silently pull the
|
||||
// stored pan toward zero.
|
||||
_geom() {
|
||||
const iw = this._img.naturalWidth, ih = this._img.naturalHeight;
|
||||
const fw = this.clientWidth, fh = this.clientHeight;
|
||||
if (!iw || !ih || !fw || !fh) return null;
|
||||
return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) };
|
||||
}
|
||||
|
||||
_clampView() {
|
||||
// Pan range on each axis is half the overflow past the frame edge.
|
||||
const g = this._geom();
|
||||
if (!g) return;
|
||||
const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50);
|
||||
const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50);
|
||||
this._view.x = Math.max(-mx, Math.min(mx, this._view.x));
|
||||
this._view.y = Math.max(-my, Math.min(my, this._view.y));
|
||||
}
|
||||
|
||||
_applyView() {
|
||||
const g = this._geom();
|
||||
const fit = this.getAttribute('fit') || 'cover';
|
||||
if (fit !== 'cover' || !g) {
|
||||
// Non-cover, or dimensions not known yet (before img load).
|
||||
this._img.style.width = '100%';
|
||||
this._img.style.height = '100%';
|
||||
this._img.style.left = '50%';
|
||||
this._img.style.top = '50%';
|
||||
this._img.style.objectFit = fit;
|
||||
this._img.style.objectPosition = this.getAttribute('position') || '50% 50%';
|
||||
return;
|
||||
}
|
||||
// Cover baseline: img fills the frame on its tighter axis at s=1, so
|
||||
// pan works immediately on the overflowing axis without zooming first.
|
||||
// Width/height and left/top are all frame-% — depends only on the
|
||||
// frame aspect ratio, so a responsive resize keeps the same crop. The
|
||||
// spill layer mirrors the same box so its corners = image corners.
|
||||
const k = g.base * this._view.s;
|
||||
const w = (g.iw * k / g.fw * 100) + '%';
|
||||
const h = (g.ih * k / g.fh * 100) + '%';
|
||||
const l = (50 + this._view.x) + '%';
|
||||
const t = (50 + this._view.y) + '%';
|
||||
this._img.style.width = w; this._img.style.height = h;
|
||||
this._img.style.left = l; this._img.style.top = t;
|
||||
this._img.style.objectFit = '';
|
||||
this._spill.style.width = w; this._spill.style.height = h;
|
||||
this._spill.style.left = l; this._spill.style.top = t;
|
||||
}
|
||||
|
||||
_commitView() {
|
||||
const v = { s: this._view.s, x: this._view.x, y: this._view.y };
|
||||
if (this._userUrl) v.u = this._userUrl;
|
||||
// Framing-only (no u) persists too so an author-src slot remembers its
|
||||
// crop; clearing the sidecar still falls through to src=.
|
||||
if (this.id) setSlot(this.id, v);
|
||||
else { this._local = v; }
|
||||
}
|
||||
|
||||
_render() {
|
||||
// Shape / mask. Presets use border-radius so the dashed ring can
|
||||
// follow the rounded outline; clip-path is only applied for an
|
||||
// explicit `mask` (the ring is hidden there since a rectangle
|
||||
// dashed border chopped by an arbitrary polygon looks broken).
|
||||
const mask = this.getAttribute('mask');
|
||||
const shape = (this.getAttribute('shape') || 'rounded').toLowerCase();
|
||||
let radius = '';
|
||||
if (shape === 'circle') radius = '50%';
|
||||
else if (shape === 'pill') radius = '9999px';
|
||||
else if (shape === 'rounded') {
|
||||
const n = parseFloat(this.getAttribute('radius'));
|
||||
radius = (Number.isFinite(n) ? n : 12) + 'px';
|
||||
}
|
||||
this._frame.style.borderRadius = mask ? '' : radius;
|
||||
this._frame.style.clipPath = mask || '';
|
||||
this._ring.style.borderRadius = mask ? '' : radius;
|
||||
this._ring.style.display = mask ? 'none' : '';
|
||||
|
||||
// Controls and reframe entry gate on this so share links stay read-only.
|
||||
const editable = !!(window.omelette && window.omelette.writeFile);
|
||||
this.toggleAttribute('data-editable', editable);
|
||||
this._sub.style.display = editable ? '' : 'none';
|
||||
|
||||
// Content. The sidecar is also writable by the agent's write_file
|
||||
// tool, so its value isn't guaranteed canvas-originated — only accept
|
||||
// data:image/ URLs from it. The `src` attribute is author-controlled
|
||||
// (Claude wrote it into the HTML) so it passes through unchanged.
|
||||
let stored = this.id ? getSlot(this.id) : this._local;
|
||||
if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null;
|
||||
const srcAttr = this.getAttribute('src') || '';
|
||||
this._userUrl = (stored && stored.u) || null;
|
||||
const url = this._userUrl || srcAttr;
|
||||
// Don't clobber an in-flight reframe with a store-triggered re-render.
|
||||
if (!this.hasAttribute('data-reframe')) {
|
||||
this._view = {
|
||||
s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1,
|
||||
x: stored && Number.isFinite(stored.x) ? stored.x : 0,
|
||||
y: stored && Number.isFinite(stored.y) ? stored.y : 0,
|
||||
};
|
||||
}
|
||||
this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image';
|
||||
// Toggle via style.display — the [hidden] attribute alone loses to
|
||||
// the display:flex / display:block rules in the stylesheet above.
|
||||
if (url) {
|
||||
if (this._img.getAttribute('src') !== url) {
|
||||
this._img.src = url;
|
||||
this._ghost.src = url;
|
||||
}
|
||||
this._img.style.display = 'block';
|
||||
this._empty.style.display = 'none';
|
||||
this.setAttribute('data-filled', '');
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
} else {
|
||||
this._img.style.display = 'none';
|
||||
this._img.removeAttribute('src');
|
||||
this._ghost.removeAttribute('src');
|
||||
this._empty.style.display = 'flex';
|
||||
this.removeAttribute('data-filled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('image-slot')) {
|
||||
customElements.define('image-slot', ImageSlot);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Aurum Estates — router */
|
||||
(function () {
|
||||
const { useState, useEffect } = React;
|
||||
const Shell = window.LuxShell, useToast = window.useLuxToast;
|
||||
|
||||
function App() {
|
||||
const [route, setRoute] = useState(() => localStorage.getItem('aurum_route') || 'dashboard');
|
||||
const [toastNode, toast] = useToast();
|
||||
const go = (r) => { setRoute(r); localStorage.setItem('aurum_route', r); const m = document.querySelector('main'); if (m) m.scrollTop = 0; };
|
||||
|
||||
const PAGES = {
|
||||
dashboard: window.LuxDashboard, properties: window.LuxProperties, projects: window.LuxProjects,
|
||||
clients: window.LuxClients, leads: window.LuxLeads, bookings: window.LuxBookings,
|
||||
payments: window.LuxPayments, blogs: window.LuxBlogs, media: window.LuxMedia,
|
||||
reviews: window.LuxReviews, analytics: window.LuxAnalytics, reports: window.LuxReports,
|
||||
settings: window.LuxSettings,
|
||||
};
|
||||
const Page = PAGES[route] || window.LuxDashboard;
|
||||
return React.createElement(Shell, { route, go, toastNode },
|
||||
React.createElement(Page, { go, toast }));
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
|
||||
})();
|
||||
@@ -0,0 +1,165 @@
|
||||
/* Aurum Estates — Dashboard */
|
||||
(function () {
|
||||
const Icon = window.Icon, A = window.AURUM;
|
||||
const { Sparkline, AreaChart, Bars, LuxDonut, PropImage, LuxAvatar, Stars } = window;
|
||||
|
||||
const COL = {
|
||||
blue: ['var(--blue-soft)', 'var(--blue)'], navy: ['#e6edf4', 'var(--navy)'],
|
||||
gold: ['var(--gold-soft)', '#9c7b3a'], green: ['var(--green-soft)', 'var(--green-600)'],
|
||||
};
|
||||
|
||||
function Dashboard({ go }) {
|
||||
return (
|
||||
<div>
|
||||
<window.LuxPageHead kicker="Overview" title="Welcome back, Alexander" subtitle="Here's what's happening across your luxury portfolio today.">
|
||||
<button className="btn btn-outline"><Icon name="calendar" size={17} />Jun 2026</button>
|
||||
<button className="btn btn-navy" onClick={() => go('analytics')}><Icon name="chartBar" size={17} />View Analytics</button>
|
||||
</window.LuxPageHead>
|
||||
|
||||
{/* KPI row */}
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 20, marginBottom: 20 }}>
|
||||
{A.kpis.map(k => {
|
||||
const c = COL[k.color];
|
||||
return (
|
||||
<div key={k.label} className="card card-hover" style={{ padding: 22 }}>
|
||||
<div className="row spread">
|
||||
<span style={{ width: 46, height: 46, borderRadius: 13, display: 'grid', placeItems: 'center', background: c[0], color: c[1] }}><Icon name={k.icon} size={23} /></span>
|
||||
<span className={'badge badge-' + (k.up ? 'green' : 'red')}><Icon name={k.up ? 'arrowUpRight' : 'arrowDownRight'} size={13} />{k.delta}</span>
|
||||
</div>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 30, marginTop: 16 }}>{k.value}</div>
|
||||
<div className="row spread" style={{ marginTop: 4 }}>
|
||||
<span className="muted" style={{ fontSize: 13.5, fontWeight: 600 }}>{k.label}</span>
|
||||
<Sparkline data={k.spark} w={72} h={30} color={c[1]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Revenue + occupancy */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.7fr 1fr', gap: 20, marginBottom: 20 }}>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<div className="row spread wrap" style={{ gap: 12, marginBottom: 18, alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h3 style={{ fontSize: 20 }}>Revenue Overview</h3>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 3 }}>Monthly closed revenue · 2026</p>
|
||||
</div>
|
||||
<div className="row gap-5" style={{ flex: 'none' }}>
|
||||
<div><div className="muted" style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}>YTD Revenue</div><div className="serif" style={{ fontWeight: 800, fontSize: 22 }}>$48.6M</div></div>
|
||||
<div><div className="muted" style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}>Avg / month</div><div className="serif" style={{ fontWeight: 800, fontSize: 22, color: 'var(--green-600)' }}>$4.05M</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<AreaChart data={A.revenueSeries} h={230} labels={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']} />
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.08s' }}>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 4 }}>Occupancy & Sales</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 8 }}>Across managed inventory</p>
|
||||
<div className="row center" style={{ margin: '14px 0 18px' }}><LuxDonut value={92} label="92%" sub="occupied" color="var(--blue)" /></div>
|
||||
<div className="col gap-3">
|
||||
{A.subKpis.slice(0, 3).map(s => {
|
||||
const c = COL[s.color];
|
||||
return (
|
||||
<div key={s.label} className="row gap-3" style={{ padding: '9px 0', borderTop: '1px solid var(--border-soft)' }}>
|
||||
<span style={{ width: 34, height: 34, borderRadius: 9, display: 'grid', placeItems: 'center', background: c[0], color: c[1], flex: 'none' }}><Icon name={s.icon} size={17} /></span>
|
||||
<div style={{ flex: 1 }}><div style={{ fontWeight: 600, fontSize: 13.5 }}>{s.label}</div><div className="faint" style={{ fontSize: 11.5 }}>{s.sub}</div></div>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 17 }}>{s.value}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Featured properties */}
|
||||
<div className="anim-fade-up" style={{ marginBottom: 20 }}>
|
||||
<div className="row spread" style={{ marginBottom: 16 }}>
|
||||
<div><h3 style={{ fontSize: 21 }}>Featured Properties</h3><p className="muted" style={{ fontSize: 13.5, marginTop: 2 }}>Your highest-performing luxury listings</p></div>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('properties')}>View all<Icon name="arrowUpRight" size={15} /></button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 20 }}>
|
||||
{A.properties.slice(0, 3).map(p => <PropertyCard key={p.id} p={p} go={go} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lower: leads + activity */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: 20 }}>
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<div className="row spread" style={{ padding: '20px 24px 8px' }}>
|
||||
<div><h3 style={{ fontSize: 19 }}>Recent Leads</h3><p className="muted" style={{ fontSize: 13, marginTop: 2 }}>Latest inquiries & site-visit requests</p></div>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => go('leads')}>All leads</button>
|
||||
</div>
|
||||
<div style={{ padding: '4px 14px 14px' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Lead</th><th>Type</th><th>Interest</th><th>Stage</th><th style={{ textAlign: 'right' }}>Score</th></tr></thead>
|
||||
<tbody>
|
||||
{A.leads.slice(0, 4).map(l => (
|
||||
<tr key={l.name}>
|
||||
<td><div className="row gap-3"><LuxAvatar initials={l.avatar} size={34} /><div><div style={{ fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{l.name}</div><div className="faint" style={{ fontSize: 12 }}>{l.when}</div></div></div></td>
|
||||
<td><span className="row gap-2" style={{ fontSize: 13 }}><Icon name={l.type === 'Site Visit' ? 'pin' : l.type === 'Inquiry' ? 'messages' : 'phone'} size={15} style={{ color: 'var(--muted)' }} />{l.type}</span></td>
|
||||
<td style={{ color: 'var(--ink)' }}>{l.interest}</td>
|
||||
<td><span className={'badge badge-' + (l.stage === 'Qualified' ? 'green' : l.stage === 'Negotiation' ? 'gold' : l.stage === 'Contacted' ? 'navy' : 'blue')}>{l.stage}</span></td>
|
||||
<td style={{ textAlign: 'right' }}><span className="serif" style={{ fontWeight: 800, color: l.score >= 85 ? 'var(--green-600)' : 'var(--ink)' }}>{l.score}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 16 }}>Activity Timeline</h3>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{A.activity.map((a, i) => {
|
||||
const c = COL[a.color];
|
||||
return (
|
||||
<div key={i} className="row" style={{ alignItems: 'flex-start', gap: 13, paddingBottom: i === A.activity.length - 1 ? 0 : 16, position: 'relative' }}>
|
||||
{i !== A.activity.length - 1 && <span style={{ position: 'absolute', left: 16, top: 32, bottom: 0, width: 2, background: 'var(--border)' }} />}
|
||||
<span style={{ width: 34, height: 34, borderRadius: 99, flex: 'none', display: 'grid', placeItems: 'center', background: c[0], color: c[1], zIndex: 1 }}><Icon name={a.icon} size={16} /></span>
|
||||
<div style={{ paddingTop: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>{a.title}</div>
|
||||
<div className="muted" style={{ fontSize: 12.5, marginTop: 1 }}>{a.desc}</div>
|
||||
<div className="faint" style={{ fontSize: 11.5, marginTop: 3 }}>{a.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PropertyCard({ p, go }) {
|
||||
const statusColor = { 'For Sale': 'green', 'Under Offer': 'gold', 'Sold': 'gray' }[p.status] || 'blue';
|
||||
return (
|
||||
<div className="card card-hover" style={{ overflow: 'hidden', cursor: 'pointer' }} onClick={() => go('properties')}>
|
||||
<PropImage slot={p.slot} accent={p.accent} height={196}
|
||||
badge={<div style={{ position: 'absolute', top: 14, left: 14, right: 14, display: 'flex', justifyContent: 'space-between', zIndex: 3 }}>
|
||||
{p.tag ? <span className="badge" style={{ background: 'rgba(255,255,255,.92)', color: 'var(--navy)', fontWeight: 700, backdropFilter: 'blur(4px)' }}>{p.tag === 'Featured' ? <Icon name="star" size={12} style={{ color: 'var(--gold)', fill: 'var(--gold)' }} /> : <Icon name="sparkles" size={12} />}{p.tag}</span> : <span />}
|
||||
<span className="icon-btn" style={{ width: 34, height: 34, background: 'rgba(255,255,255,.92)', backdropFilter: 'blur(4px)' }}><Icon name="heart" size={17} /></span>
|
||||
</div>}>
|
||||
<div style={{ position: 'absolute', bottom: 12, left: 14, zIndex: 3 }}>
|
||||
<span className={'badge badge-' + statusColor} style={{ background: statusColor === 'gray' ? 'rgba(17,24,39,.7)' : undefined, color: statusColor === 'gray' ? '#fff' : undefined, backdropFilter: 'blur(4px)' }}><span className="dot" />{p.status}</span>
|
||||
</div>
|
||||
</PropImage>
|
||||
<div style={{ padding: '16px 18px 18px' }}>
|
||||
<div className="row spread">
|
||||
<h4 style={{ fontSize: 18 }}>{p.name}</h4>
|
||||
</div>
|
||||
<div className="row gap-2 muted" style={{ fontSize: 13, marginTop: 4 }}><Icon name="pin" size={15} />{p.loc}</div>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 22, color: 'var(--blue)', margin: '12px 0' }}>{A.fmt(p.price)}</div>
|
||||
<div className="row gap-4" style={{ paddingTop: 12, borderTop: '1px solid var(--border-soft)', fontSize: 13, color: 'var(--ink-2)', fontWeight: 600 }}>
|
||||
<span className="row gap-2"><Icon name="bed" size={16} style={{ color: 'var(--muted)' }} />{p.beds}</span>
|
||||
<span className="row gap-2"><Icon name="bath" size={16} style={{ color: 'var(--muted)' }} />{p.baths}</span>
|
||||
<span className="row gap-2"><Icon name="ruler" size={16} style={{ color: 'var(--muted)' }} />{p.area} m²</span>
|
||||
<span className="row gap-2" style={{ marginLeft: 'auto' }}><Icon name="eye" size={16} style={{ color: 'var(--muted)' }} />{p.views.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.LuxDashboard = Dashboard;
|
||||
window.LuxPropertyCard = PropertyCard;
|
||||
})();
|
||||
@@ -0,0 +1,98 @@
|
||||
/* Aurum Estates — mock data */
|
||||
window.AURUM = (function () {
|
||||
const fmt = (n) => '$' + n.toLocaleString('en-US');
|
||||
const fmtK = (n) => n >= 1e6 ? '$' + (n / 1e6).toFixed(2) + 'M' : n >= 1e3 ? '$' + (n / 1e3).toFixed(0) + 'K' : '$' + n;
|
||||
|
||||
const kpis = [
|
||||
{ label: 'Total Revenue', value: '$48.6M', delta: '+12.4%', up: true, icon: 'tag', spark: [22, 28, 26, 34, 31, 40, 38, 46, 44, 52], color: 'blue' },
|
||||
{ label: 'Active Projects', value: '34', delta: '+5', up: true, icon: 'briefcase', spark: [8, 10, 9, 12, 14, 13, 16, 15, 18, 20], color: 'navy' },
|
||||
{ label: 'Property Listings', value: '186', delta: '+18', up: true, icon: 'building', spark: [120, 130, 128, 140, 150, 158, 162, 170, 178, 186], color: 'gold' },
|
||||
{ label: 'New Leads', value: '512', delta: '+9.1%', up: true, icon: 'users', spark: [30, 42, 38, 50, 47, 60, 58, 70, 66, 80], color: 'green' },
|
||||
];
|
||||
|
||||
const subKpis = [
|
||||
{ label: 'Client Growth', value: '+24%', sub: 'vs last quarter', icon: 'trend', color: 'green' },
|
||||
{ label: 'Monthly Sales', value: '$6.2M', sub: '42 deals closed', icon: 'tag', color: 'blue' },
|
||||
{ label: 'Occupancy Rate', value: '92%', sub: 'across managed units', icon: 'building', color: 'navy' },
|
||||
{ label: 'Conversion Rate', value: '18.4%', sub: 'lead → booking', icon: 'funnel', color: 'gold' },
|
||||
];
|
||||
|
||||
const properties = [
|
||||
{ id: 'P-2041', name: 'Villa Serenissima', loc: 'Lake Como, Italy', price: 12400000, area: 820, beds: 6, baths: 7, status: 'For Sale', tag: 'Featured', views: 1820, slot: 'prop1', accent: '#6a8fc9' },
|
||||
{ id: 'P-2038', name: 'The Glass Pavilion', loc: 'Beverly Hills, CA', price: 28900000, area: 1140, beds: 8, baths: 10, status: 'For Sale', tag: 'New', views: 3210, slot: 'prop2', accent: '#7a8b7e' },
|
||||
{ id: 'P-2035', name: 'Marina Sky Penthouse', loc: 'Dubai Marina, UAE', price: 9650000, area: 540, beds: 4, baths: 5, status: 'Under Offer', tag: '', views: 2440, slot: 'prop3', accent: '#c9a86a' },
|
||||
{ id: 'P-2030', name: 'Cliffside Retreat', loc: 'Amalfi Coast, Italy', price: 7300000, area: 460, beds: 5, baths: 5, status: 'For Sale', tag: '', views: 1290, slot: 'prop4', accent: '#9c8398' },
|
||||
{ id: 'P-2027', name: 'Aspen Timber Lodge', loc: 'Aspen, Colorado', price: 15800000, area: 690, beds: 6, baths: 8, status: 'Sold', tag: '', views: 980, slot: 'prop5', accent: '#8a7d6b' },
|
||||
{ id: 'P-2021', name: 'Azure Bay Estate', loc: 'Côte d\u2019Azur, France', price: 21200000, area: 970, beds: 7, baths: 9, status: 'For Sale', tag: 'Featured', views: 2760, slot: 'prop6', accent: '#5d8aa8' },
|
||||
];
|
||||
|
||||
const projects = [
|
||||
{ name: 'Riviera Residences', loc: 'Monaco', units: 48, sold: 31, progress: 78, status: 'Construction', value: 142000000 },
|
||||
{ name: 'The Onyx Towers', loc: 'Singapore', units: 120, sold: 86, progress: 64, status: 'Construction', value: 310000000 },
|
||||
{ name: 'Palm Grove Villas', loc: 'Maldives', units: 24, sold: 24, progress: 100, status: 'Completed', value: 96000000 },
|
||||
{ name: 'Hillcrest Manors', loc: 'Tuscany, Italy', units: 36, sold: 12, progress: 28, status: 'Planning', value: 78000000 },
|
||||
];
|
||||
|
||||
const leads = [
|
||||
{ name: 'Eleanor Whitmore', type: 'Site Visit', interest: 'Villa Serenissima', stage: 'Qualified', score: 92, when: '10 min ago', avatar: 'EW' },
|
||||
{ name: 'Rajesh Kapoor', type: 'Inquiry', interest: 'Marina Sky Penthouse', stage: 'New', score: 68, when: '1 hr ago', avatar: 'RK' },
|
||||
{ name: 'Sofia Marchetti', type: 'Contact Request', interest: 'Azure Bay Estate', stage: 'Contacted', score: 80, when: '3 hr ago', avatar: 'SM' },
|
||||
{ name: 'James Okafor', type: 'Site Visit', interest: 'The Glass Pavilion', stage: 'Negotiation', score: 88, when: 'Yesterday', avatar: 'JO' },
|
||||
{ name: 'Mei Lin Chen', type: 'Inquiry', interest: 'Cliffside Retreat', stage: 'New', score: 55, when: 'Yesterday', avatar: 'MC' },
|
||||
];
|
||||
|
||||
const pipeline = [
|
||||
{ stage: 'New Inquiries', count: 184, color: '#0D6EFD' },
|
||||
{ stage: 'Contacted', count: 126, color: '#3b86ff' },
|
||||
{ stage: 'Qualified', count: 78, color: '#7AC943' },
|
||||
{ stage: 'Negotiation', count: 41, color: '#c9a86a' },
|
||||
{ stage: 'Closed Won', count: 27, color: '#102A43' },
|
||||
];
|
||||
|
||||
const bookings = [
|
||||
{ client: 'Eleanor Whitmore', property: 'Villa Serenissima', date: '12 Jun 2026', type: 'Site Visit', status: 'Confirmed' },
|
||||
{ client: 'James Okafor', property: 'The Glass Pavilion', date: '13 Jun 2026', type: 'Closing', status: 'Confirmed' },
|
||||
{ client: 'Sofia Marchetti', property: 'Azure Bay Estate', date: '14 Jun 2026', type: 'Site Visit', status: 'Pending' },
|
||||
{ client: 'Rajesh Kapoor', property: 'Marina Sky Penthouse', date: '16 Jun 2026', type: 'Consultation', status: 'Confirmed' },
|
||||
];
|
||||
|
||||
const clients = [
|
||||
{ name: 'Eleanor Whitmore', tier: 'Platinum', deals: 3, value: 34000000, since: '2021', avatar: 'EW' },
|
||||
{ name: 'James Okafor', tier: 'Gold', deals: 2, value: 28900000, since: '2022', avatar: 'JO' },
|
||||
{ name: 'Sofia Marchetti', tier: 'Gold', deals: 1, value: 21200000, since: '2023', avatar: 'SM' },
|
||||
{ name: 'Rajesh Kapoor', tier: 'Silver', deals: 1, value: 9650000, since: '2024', avatar: 'RK' },
|
||||
];
|
||||
|
||||
const payments = [
|
||||
{ id: 'INV-7841', client: 'Eleanor Whitmore', property: 'Villa Serenissima', amount: 1240000, date: '05 Jun 2026', status: 'Paid', method: 'Wire' },
|
||||
{ id: 'INV-7836', client: 'James Okafor', property: 'The Glass Pavilion', amount: 2890000, date: '03 Jun 2026', status: 'Paid', method: 'Escrow' },
|
||||
{ id: 'INV-7829', client: 'Sofia Marchetti', property: 'Azure Bay Estate', amount: 2120000, date: '01 Jun 2026', status: 'Pending', method: 'Wire' },
|
||||
{ id: 'INV-7820', client: 'Rajesh Kapoor', property: 'Marina Sky Penthouse', amount: 965000, date: '28 May 2026', status: 'Overdue', method: 'Escrow' },
|
||||
{ id: 'INV-7812', client: 'Mei Lin Chen', property: 'Cliffside Retreat', amount: 730000, date: '24 May 2026', status: 'Paid', method: 'Wire' },
|
||||
];
|
||||
|
||||
const activity = [
|
||||
{ icon: 'tag', color: 'green', title: 'Deal closed', desc: 'The Glass Pavilion — $28.9M', time: '2h ago' },
|
||||
{ icon: 'users', color: 'blue', title: 'New lead assigned', desc: 'Eleanor Whitmore · Villa Serenissima', time: '4h ago' },
|
||||
{ icon: 'building', color: 'gold', title: 'Listing published', desc: 'Azure Bay Estate, Côte d\u2019Azur', time: '8h ago' },
|
||||
{ icon: 'pin', color: 'navy', title: 'Site visit booked', desc: 'James Okafor · tomorrow 11:00', time: 'Yesterday' },
|
||||
{ icon: 'star', color: 'gold', title: 'New 5-star review', desc: '"Impeccable service" — S. Marchetti', time: '2d ago' },
|
||||
];
|
||||
|
||||
const reviews = [
|
||||
{ name: 'Sofia Marchetti', stars: 5, text: 'Impeccable from first viewing to closing. The team understood exactly what we wanted.', when: '2 days ago', avatar: 'SM' },
|
||||
{ name: 'James Okafor', stars: 5, text: 'A seamless, discreet process for a complex acquisition. Highly recommended.', when: '1 week ago', avatar: 'JO' },
|
||||
{ name: 'Eleanor Whitmore', stars: 4, text: 'Beautiful portfolio and attentive agents. Closing took slightly longer than expected.', when: '2 weeks ago', avatar: 'EW' },
|
||||
];
|
||||
|
||||
const blogs = [
|
||||
{ title: 'The 2026 Luxury Market Outlook', cat: 'Market Insights', date: '04 Jun 2026', status: 'Published', reads: 4820 },
|
||||
{ title: 'Designing the Modern Glass Villa', cat: 'Architecture', date: '28 May 2026', status: 'Published', reads: 3140 },
|
||||
{ title: 'Coastal Investment Hotspots', cat: 'Investment', date: '20 May 2026', status: 'Draft', reads: 0 },
|
||||
];
|
||||
|
||||
const revenueSeries = [3.1, 3.6, 3.3, 4.1, 4.4, 4.0, 4.8, 5.1, 4.9, 5.6, 6.0, 6.2];
|
||||
const bookingTrend = [12, 18, 15, 22, 26, 21, 30, 34, 29, 38, 42, 46];
|
||||
|
||||
return { fmt, fmtK, kpis, subKpis, properties, projects, leads, pipeline, bookings, clients, payments, activity, reviews, blogs, revenueSeries, bookingTrend };
|
||||
})();
|
||||
@@ -0,0 +1,207 @@
|
||||
/* Aurum Estates — Properties, Projects, Clients, Leads, Bookings */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, A = window.AURUM;
|
||||
const { LuxAvatar, LuxPageHead, LuxPropertyCard, AreaChart, Bars } = window;
|
||||
|
||||
/* ---------------- PROPERTIES ---------------- */
|
||||
const PFILTERS = ['All', 'For Sale', 'Under Offer', 'Sold', 'Featured'];
|
||||
function Properties({ go }) {
|
||||
const [f, setF] = useState('All');
|
||||
const list = A.properties.filter(p => f === 'All' || p.status === f || (f === 'Featured' && p.tag === 'Featured'));
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Portfolio" title="Properties" subtitle={`${A.properties.length} luxury listings across 6 markets`}>
|
||||
<button className="btn btn-outline"><Icon name="filter" size={17} />Filters</button>
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />Add Property</button>
|
||||
</LuxPageHead>
|
||||
<div className="row spread wrap" style={{ gap: 12, marginBottom: 20 }}>
|
||||
<div className="row gap-2 wrap">{PFILTERS.map(x => <button key={x} className={'chip' + (f === x ? ' active' : '')} onClick={() => setF(x)}>{x}</button>)}</div>
|
||||
<div className="input-icon" style={{ width: 260 }}><Icon name="search" /><input className="input" placeholder="Search listings…" /></div>
|
||||
</div>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 20 }}>
|
||||
{list.map(p => <LuxPropertyCard key={p.id} p={p} go={go} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- PROJECTS ---------------- */
|
||||
function Projects() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Development" title="Projects" subtitle="Active and upcoming development projects">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />New Project</button>
|
||||
</LuxPageHead>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 20 }}>
|
||||
{A.projects.map(p => {
|
||||
const sc = { Construction: 'blue', Completed: 'green', Planning: 'gold' }[p.status];
|
||||
return (
|
||||
<div key={p.name} className="card card-pad card-hover">
|
||||
<div className="row spread" style={{ marginBottom: 14 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 48, height: 48, borderRadius: 13, display: 'grid', placeItems: 'center', background: 'var(--blue-soft)', color: 'var(--blue)' }}><Icon name="briefcase" size={23} /></span>
|
||||
<div><h4 style={{ fontSize: 18 }}>{p.name}</h4><div className="row gap-2 muted" style={{ fontSize: 13, marginTop: 2 }}><Icon name="pin" size={14} />{p.loc}</div></div>
|
||||
</div>
|
||||
<span className={'badge badge-' + sc}><span className="dot" />{p.status}</span>
|
||||
</div>
|
||||
<div className="row spread" style={{ fontSize: 13, marginBottom: 7 }}>
|
||||
<span className="muted" style={{ fontWeight: 600 }}>{p.sold} of {p.units} units sold</span>
|
||||
<span className="serif" style={{ fontWeight: 800 }}>{p.progress}%</span>
|
||||
</div>
|
||||
<div className="progress" style={{ marginBottom: 16 }}><span style={{ width: p.progress + '%', background: p.status === 'Completed' ? 'linear-gradient(90deg,var(--green),var(--green-600))' : undefined }} /></div>
|
||||
<div className="row spread" style={{ paddingTop: 14, borderTop: '1px solid var(--border-soft)' }}>
|
||||
<div><div className="faint" style={{ fontSize: 11.5, fontWeight: 600, textTransform: 'uppercase' }}>GDV</div><div className="serif" style={{ fontWeight: 800, fontSize: 19, marginTop: 2 }}>{A.fmtK(p.value)}</div></div>
|
||||
<button className="btn btn-outline btn-sm">Manage<Icon name="arrowUpRight" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- CLIENTS ---------------- */
|
||||
function Clients() {
|
||||
const tierColor = { Platinum: 'navy', Gold: 'gold', Silver: 'gray' };
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Relationships" title="Clients" subtitle="Your high-net-worth client roster">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />Add Client</button>
|
||||
</LuxPageHead>
|
||||
<div className="card" style={{ overflow: 'hidden', padding: '8px 16px 16px' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Client</th><th>Tier</th><th>Deals</th><th>Lifetime Value</th><th>Client Since</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{A.clients.map(c => (
|
||||
<tr key={c.name}>
|
||||
<td><div className="row gap-3"><LuxAvatar initials={c.avatar} size={38} /><span style={{ fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{c.name}</span></div></td>
|
||||
<td><span className={'badge badge-' + tierColor[c.tier]}>{c.tier === 'Platinum' && <Icon name="star" size={12} style={{ fill: 'currentColor' }} />}{c.tier}</span></td>
|
||||
<td className="num">{c.deals}</td>
|
||||
<td><span className="serif" style={{ fontWeight: 800, color: 'var(--ink)' }}>{A.fmtK(c.value)}</span></td>
|
||||
<td className="faint">{c.since}</td>
|
||||
<td style={{ textAlign: 'right' }}><button className="icon-btn"><Icon name="dots" size={18} /></button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- LEADS ---------------- */
|
||||
function Leads() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Sales Pipeline" title="Lead Management" subtitle="Track inquiries from first contact to closing">
|
||||
<button className="btn btn-outline"><Icon name="funnel" size={17} />Pipeline View</button>
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />Add Lead</button>
|
||||
</LuxPageHead>
|
||||
|
||||
{/* pipeline funnel */}
|
||||
<div className="card card-pad anim-fade-up" style={{ marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 18 }}>Lead Conversion Funnel</h3>
|
||||
<div className="row gap-3 wrap">
|
||||
{A.pipeline.map((s, i) => (
|
||||
<div key={s.stage} style={{ flex: 1, minWidth: 150 }}>
|
||||
<div style={{ height: 92, borderRadius: 14, background: s.color, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 18px', color: '#fff', position: 'relative', opacity: 1 - i * 0.04 }}>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 28 }}>{s.count}</div>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, opacity: .9 }}>{s.stage}</div>
|
||||
</div>
|
||||
{i < A.pipeline.length - 1 && <div className="faint" style={{ textAlign: 'center', fontSize: 11.5, marginTop: 7, fontWeight: 600 }}>{Math.round(A.pipeline[i + 1].count / s.count * 100)}% →</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* leads table */}
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', padding: '8px 16px 16px' }}>
|
||||
<div className="row spread" style={{ padding: '14px 8px 6px' }}>
|
||||
<h3 style={{ fontSize: 18 }}>All Leads</h3>
|
||||
<div className="input-icon" style={{ width: 240 }}><Icon name="search" /><input className="input" style={{ height: 38 }} placeholder="Search leads…" /></div>
|
||||
</div>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Lead</th><th>Source</th><th>Interest</th><th>Stage</th><th>Follow-up</th><th style={{ textAlign: 'right' }}>Score</th></tr></thead>
|
||||
<tbody>
|
||||
{A.leads.map(l => (
|
||||
<tr key={l.name}>
|
||||
<td><div className="row gap-3"><LuxAvatar initials={l.avatar} size={36} /><div><div style={{ fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{l.name}</div><div className="faint" style={{ fontSize: 12 }}>{l.when}</div></div></div></td>
|
||||
<td><span className="row gap-2" style={{ fontSize: 13 }}><Icon name={l.type === 'Site Visit' ? 'pin' : l.type === 'Inquiry' ? 'messages' : 'phone'} size={15} style={{ color: 'var(--muted)' }} />{l.type}</span></td>
|
||||
<td style={{ color: 'var(--ink)' }}>{l.interest}</td>
|
||||
<td><span className={'badge badge-' + (l.stage === 'Qualified' ? 'green' : l.stage === 'Negotiation' ? 'gold' : l.stage === 'Contacted' ? 'navy' : 'blue')}>{l.stage}</span></td>
|
||||
<td className="faint">{l.stage === 'New' ? 'Call today' : 'Email sent'}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<div className="row gap-2" style={{ justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<div style={{ width: 44, height: 6, borderRadius: 99, background: '#eef1f6', overflow: 'hidden' }}><span style={{ display: 'block', height: '100%', width: l.score + '%', background: l.score >= 85 ? 'var(--green)' : 'var(--blue)' }} /></div>
|
||||
<span className="serif" style={{ fontWeight: 800, width: 26, textAlign: 'right' }}>{l.score}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- BOOKINGS ---------------- */
|
||||
function Bookings() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Schedule" title="Bookings" subtitle="Site visits, consultations and closings">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />New Booking</button>
|
||||
</LuxPageHead>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 20 }}>
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', padding: '8px 16px 16px' }}>
|
||||
<div style={{ padding: '14px 8px 6px' }}><h3 style={{ fontSize: 18 }}>Upcoming</h3></div>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Client</th><th>Property</th><th>Type</th><th>Date</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{A.bookings.map(b => (
|
||||
<tr key={b.client + b.date}>
|
||||
<td><div className="row gap-3"><LuxAvatar initials={b.client.split(' ').map(x => x[0]).join('')} size={34} /><span style={{ fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{b.client}</span></div></td>
|
||||
<td>{b.property}</td>
|
||||
<td><span className="row gap-2" style={{ fontSize: 13 }}><Icon name={b.type === 'Site Visit' ? 'pin' : b.type === 'Closing' ? 'tag' : 'messages'} size={15} style={{ color: 'var(--muted)' }} />{b.type}</span></td>
|
||||
<td className="num">{b.date}</td>
|
||||
<td><span className={'badge badge-' + (b.status === 'Confirmed' ? 'green' : 'amber')}>{b.status}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s', alignSelf: 'start' }}>
|
||||
<div className="row spread" style={{ marginBottom: 14 }}><h3 style={{ fontSize: 17 }}>June 2026</h3><div className="row gap-1"><button className="icon-btn" style={{ width: 30, height: 30 }}><Icon name="chevL" size={16} /></button><button className="icon-btn" style={{ width: 30, height: 30 }}><Icon name="chevR" size={16} /></button></div></div>
|
||||
<MiniCalendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniCalendar() {
|
||||
const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
const marked = { 12: true, 13: true, 14: true, 16: true };
|
||||
const cells = [];
|
||||
for (let i = 0; i < 1; i++) cells.push(null);
|
||||
for (let d = 1; d <= 30; d++) cells.push(d);
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 4, marginBottom: 6 }}>
|
||||
{days.map((d, i) => <div key={i} className="faint" style={{ textAlign: 'center', fontSize: 11, fontWeight: 700 }}>{d}</div>)}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 4 }}>
|
||||
{cells.map((d, i) => (
|
||||
<div key={i} style={{ aspectRatio: '1', display: 'grid', placeItems: 'center', borderRadius: 9, fontSize: 13, fontWeight: 600, position: 'relative', background: d === 12 ? 'var(--blue)' : marked[d] ? 'var(--blue-soft)' : 'transparent', color: d === 12 ? '#fff' : marked[d] ? 'var(--blue)' : d ? 'var(--ink-2)' : 'transparent', cursor: d ? 'pointer' : 'default' }}>
|
||||
{d}{marked[d] && d !== 12 && <span style={{ position: 'absolute', bottom: 4, width: 4, height: 4, borderRadius: 99, background: 'var(--blue)' }} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { LuxProperties: Properties, LuxProjects: Projects, LuxClients: Clients, LuxLeads: Leads, LuxBookings: Bookings });
|
||||
})();
|
||||
@@ -0,0 +1,265 @@
|
||||
/* Aurum Estates — Analytics, Payments, Blogs, Media, Reviews, Reports, Settings */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, A = window.AURUM;
|
||||
const { LuxAvatar, LuxPageHead, AreaChart, Bars, LuxDonut, Stars, PropImage } = window;
|
||||
|
||||
/* ---------------- ANALYTICS ---------------- */
|
||||
function Analytics() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Insights" title="Analytics" subtitle="Performance across revenue, leads and bookings">
|
||||
<button className="btn btn-outline"><Icon name="calendar" size={17} />Last 12 months</button>
|
||||
<button className="btn btn-navy"><Icon name="download" size={17} />Export</button>
|
||||
</LuxPageHead>
|
||||
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 20, marginBottom: 20 }}>
|
||||
{A.subKpis.map(s => {
|
||||
const c = { blue: ['var(--blue-soft)', 'var(--blue)'], navy: ['#e6edf4', 'var(--navy)'], gold: ['var(--gold-soft)', '#9c7b3a'], green: ['var(--green-soft)', 'var(--green-600)'] }[s.color];
|
||||
return (
|
||||
<div key={s.label} className="card card-pad card-hover">
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, display: 'grid', placeItems: 'center', background: c[0], color: c[1] }}><Icon name={s.icon} size={22} /></span>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 28, marginTop: 14 }}>{s.value}</div>
|
||||
<div className="muted" style={{ fontSize: 13.5, fontWeight: 600 }}>{s.label}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 3 }}>{s.sub}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 20, marginBottom: 20 }}>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 4 }}>Revenue Growth</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>Monthly revenue · $M</p>
|
||||
<AreaChart data={A.revenueSeries} h={220} color="var(--blue)" labels={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']} />
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 4 }}>Conversion</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 8 }}>Lead → booking rate</p>
|
||||
<div className="row center" style={{ margin: '18px 0' }}><LuxDonut value={18.4} label="18.4%" sub="conversion" color="var(--green)" /></div>
|
||||
<div className="col gap-2">
|
||||
{[['Lead → Qualified', 42], ['Qualified → Visit', 61], ['Visit → Offer', 38]].map(([l, v]) => (
|
||||
<div key={l}><div className="row spread" style={{ fontSize: 12.5, marginBottom: 5 }}><span className="muted" style={{ fontWeight: 600 }}>{l}</span><span className="num faint">{v}%</span></div><div className="progress" style={{ height: 6 }}><span style={{ width: v + '%' }} /></div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 4 }}>Booking Trends</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>Monthly confirmed bookings</p>
|
||||
<Bars data={A.bookingTrend} w={620} h={150} color="var(--navy)" labels={['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']} />
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<h3 style={{ fontSize: 19, marginBottom: 16 }}>Property Performance</h3>
|
||||
<div className="col gap-3">
|
||||
{A.properties.slice(0, 5).map(p => (
|
||||
<div key={p.id} className="row gap-3">
|
||||
<div style={{ width: 40, height: 40, borderRadius: 10, flex: 'none', background: `linear-gradient(150deg, ${p.accent}, ${p.accent}cc)` }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row spread"><span style={{ fontWeight: 600, fontSize: 13.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span><span className="num faint" style={{ fontSize: 12 }}>{p.views.toLocaleString()} views</span></div>
|
||||
<div className="progress" style={{ height: 6, marginTop: 5 }}><span style={{ width: Math.min(100, p.views / 33) + '%', background: p.accent }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- PAYMENTS ---------------- */
|
||||
function Payments() {
|
||||
const total = A.payments.reduce((s, p) => s + p.amount, 0);
|
||||
const paid = A.payments.filter(p => p.status === 'Paid').reduce((s, p) => s + p.amount, 0);
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Finance" title="Payments" subtitle="Invoices, escrow and transaction history">
|
||||
<button className="btn btn-outline"><Icon name="download" size={17} />Statement</button>
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />New Invoice</button>
|
||||
</LuxPageHead>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 20, marginBottom: 20 }}>
|
||||
{[['Total Invoiced', A.fmtK(total), 'blue', 'tag'], ['Collected', A.fmtK(paid), 'green', 'check'], ['Outstanding', A.fmtK(total - paid), 'gold', 'clock']].map(([l, v, c, ic]) => {
|
||||
const cc = { blue: ['var(--blue-soft)', 'var(--blue)'], green: ['var(--green-soft)', 'var(--green-600)'], gold: ['var(--gold-soft)', '#9c7b3a'] }[c];
|
||||
return (
|
||||
<div key={l} className="card card-pad">
|
||||
<span style={{ width: 44, height: 44, borderRadius: 12, display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={ic} size={22} /></span>
|
||||
<div className="serif" style={{ fontWeight: 800, fontSize: 28, marginTop: 14 }}>{v}</div>
|
||||
<div className="muted" style={{ fontSize: 13.5, fontWeight: 600 }}>{l}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="card" style={{ overflow: 'hidden', padding: '8px 16px 16px' }}>
|
||||
<div style={{ padding: '14px 8px 6px' }}><h3 style={{ fontSize: 18 }}>Transactions</h3></div>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Invoice</th><th>Client</th><th>Property</th><th>Method</th><th>Date</th><th>Status</th><th style={{ textAlign: 'right' }}>Amount</th></tr></thead>
|
||||
<tbody>
|
||||
{A.payments.map(p => (
|
||||
<tr key={p.id}>
|
||||
<td className="num" style={{ fontWeight: 600, color: 'var(--ink)' }}>{p.id}</td>
|
||||
<td>{p.client}</td>
|
||||
<td className="faint">{p.property}</td>
|
||||
<td>{p.method}</td>
|
||||
<td className="num">{p.date}</td>
|
||||
<td><span className={'badge badge-' + (p.status === 'Paid' ? 'green' : p.status === 'Pending' ? 'amber' : 'red')}>{p.status}</span></td>
|
||||
<td style={{ textAlign: 'right' }}><span className="serif" style={{ fontWeight: 800, color: 'var(--ink)' }}>{A.fmt(p.amount)}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- BLOGS ---------------- */
|
||||
function Blogs() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Content" title="Blogs" subtitle="Editorial & market-insight publishing">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />Write Article</button>
|
||||
</LuxPageHead>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 20 }}>
|
||||
{A.blogs.map((b, i) => (
|
||||
<div key={b.title} className="card card-hover" style={{ overflow: 'hidden' }}>
|
||||
<div style={{ height: 150, background: `linear-gradient(150deg, ${['#6a8fc9', '#7a8b7e', '#c9a86a'][i]}, ${['#3a5e94', '#566355', '#9c7b3a'][i]})`, position: 'relative' }}>
|
||||
<span className="badge" style={{ position: 'absolute', top: 12, left: 12, background: 'rgba(255,255,255,.92)', color: 'var(--navy)', fontWeight: 700 }}>{b.cat}</span>
|
||||
</div>
|
||||
<div style={{ padding: '16px 18px 18px' }}>
|
||||
<span className={'badge badge-' + (b.status === 'Published' ? 'green' : 'gray')} style={{ marginBottom: 10 }}>{b.status}</span>
|
||||
<h4 style={{ fontSize: 18, lineHeight: 1.25 }}>{b.title}</h4>
|
||||
<div className="row spread muted" style={{ fontSize: 12.5, marginTop: 12 }}><span>{b.date}</span>{b.reads > 0 && <span className="row gap-2"><Icon name="eye" size={14} />{b.reads.toLocaleString()} reads</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- MEDIA LIBRARY ---------------- */
|
||||
function Media() {
|
||||
const accents = ['#6a8fc9', '#7a8b7e', '#c9a86a', '#9c8398', '#8a7d6b', '#5d8aa8', '#7e9e8b', '#a98b6b'];
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Assets" title="Media Library" subtitle="Photography, renders and marketing collateral">
|
||||
<button className="btn btn-primary"><Icon name="upload" size={17} />Upload Media</button>
|
||||
</LuxPageHead>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
|
||||
{accents.map((a, i) => (
|
||||
<div key={i} className="card card-hover" style={{ overflow: 'hidden', padding: 0 }}>
|
||||
<PropImage slot={'media' + i} accent={a} height={150} radius={0} placeholder="Drop image" />
|
||||
<div className="row spread" style={{ padding: '10px 13px' }}>
|
||||
<div><div style={{ fontWeight: 600, fontSize: 13 }}>villa_render_{i + 1}.jpg</div><div className="faint" style={{ fontSize: 11.5 }}>4.2 MB · 4K</div></div>
|
||||
<button className="icon-btn" style={{ width: 30, height: 30 }}><Icon name="dots" size={16} /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- REVIEWS ---------------- */
|
||||
function Reviews() {
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Reputation" title="Reviews" subtitle="Client testimonials & satisfaction">
|
||||
<div className="row gap-3">
|
||||
<div className="card" style={{ padding: '10px 18px', display: 'flex', alignItems: 'center', gap: 10 }}><span className="serif" style={{ fontWeight: 800, fontSize: 24 }}>4.9</span><Stars n={5} size={16} /></div>
|
||||
</div>
|
||||
</LuxPageHead>
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 20 }}>
|
||||
{A.reviews.map(r => (
|
||||
<div key={r.name} className="card card-pad card-hover">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<div className="row gap-3"><LuxAvatar initials={r.avatar} size={42} /><div><div style={{ fontWeight: 700, fontSize: 14.5 }}>{r.name}</div><div className="faint" style={{ fontSize: 12 }}>{r.when}</div></div></div>
|
||||
<Stars n={r.stars} />
|
||||
</div>
|
||||
<p className="serif" style={{ fontSize: 16, lineHeight: 1.55, color: 'var(--ink-2)', fontStyle: 'italic' }}>"{r.text}"</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- REPORTS ---------------- */
|
||||
function Reports() {
|
||||
const reports = [
|
||||
['Q2 2026 Portfolio Performance', 'Financial', '12 pages'],
|
||||
['Lead Source Attribution', 'Marketing', '8 pages'],
|
||||
['Market Valuation Index — June', 'Market', '20 pages'],
|
||||
['Agent Productivity Summary', 'Operations', '6 pages'],
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Documents" title="Reports" subtitle="Generated business intelligence reports">
|
||||
<button className="btn btn-primary"><Icon name="plus" size={17} />Generate Report</button>
|
||||
</LuxPageHead>
|
||||
<div className="card" style={{ overflow: 'hidden', padding: '8px 16px 16px' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Report</th><th>Category</th><th>Length</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{reports.map(([t, c, l]) => (
|
||||
<tr key={t}>
|
||||
<td><div className="row gap-3"><span style={{ width: 38, height: 38, borderRadius: 9, display: 'grid', placeItems: 'center', background: 'var(--blue-soft)', color: 'var(--blue)' }}><Icon name="documents" size={18} /></span><span style={{ fontWeight: 600, color: 'var(--ink)' }}>{t}</span></div></td>
|
||||
<td><span className="badge badge-navy">{c}</span></td>
|
||||
<td className="faint">{l}</td>
|
||||
<td style={{ textAlign: 'right' }}><div className="row gap-2" style={{ justifyContent: 'flex-end' }}><button className="btn btn-outline btn-sm"><Icon name="eye" size={15} />View</button><button className="icon-btn"><Icon name="download" size={18} /></button></div></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- SETTINGS ---------------- */
|
||||
function Settings() {
|
||||
const [tab, setTab] = useState('profile');
|
||||
const tabs = [['profile', 'Profile', 'user'], ['company', 'Company', 'building'], ['team', 'Team', 'users'], ['billing', 'Billing', 'tag'], ['notifications', 'Notifications', 'bell']];
|
||||
return (
|
||||
<div>
|
||||
<LuxPageHead kicker="Configuration" title="Settings" subtitle="Manage your account and workspace" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '230px 1fr', gap: 20 }}>
|
||||
<div className="card" style={{ padding: 10, alignSelf: 'start' }}>
|
||||
{tabs.map(([k, l, ic]) => (
|
||||
<button key={k} onClick={() => setTab(k)} className="row gap-3" style={{ width: '100%', padding: '11px 13px', borderRadius: 11, border: 'none', background: tab === k ? 'var(--blue-soft)' : 'transparent', color: tab === k ? 'var(--blue)' : 'var(--ink-2)', fontWeight: tab === k ? 700 : 500, fontSize: 14, cursor: 'pointer', textAlign: 'left' }}>
|
||||
<Icon name={ic} size={18} />{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" key={tab}>
|
||||
{tab === 'profile' ? (
|
||||
<div>
|
||||
<div className="row gap-4" style={{ marginBottom: 24 }}>
|
||||
<LuxAvatar initials="AV" size={72} style={{ fontSize: 28 }} />
|
||||
<div><h3 style={{ fontSize: 18 }}>Alexander Vale</h3><div className="muted" style={{ fontSize: 13.5 }}>Managing Director · Aurum Estates</div><button className="btn btn-outline btn-sm" style={{ marginTop: 10 }}><Icon name="image" size={15} />Change photo</button></div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{[['Full Name', 'Alexander Vale', 'user'], ['Email', 'a.vale@aurumestates.com', 'mail'], ['Phone', '+1 310 555 0148', 'phone'], ['Role', 'Managing Director', 'briefcase']].map(([l, v, ic]) => (
|
||||
<div key={l} className="field"><label>{l}</label><div className="input-icon"><Icon name={ic} /><input className="input" defaultValue={v} /></div></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="row gap-3" style={{ marginTop: 22 }}><button className="btn btn-primary">Save Changes</button><button className="btn btn-ghost" style={{ border: '1px solid var(--border)' }}>Cancel</button></div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--muted)' }}>
|
||||
<span style={{ width: 56, height: 56, borderRadius: 14, display: 'grid', placeItems: 'center', margin: '0 auto 14px', background: 'var(--blue-soft)', color: 'var(--blue)' }}><Icon name={tabs.find(t => t[0] === tab)[2]} size={27} /></span>
|
||||
<h3 style={{ fontSize: 18 }}>{tabs.find(t => t[0] === tab)[1]} settings</h3>
|
||||
<p style={{ fontSize: 14, marginTop: 6 }}>This section is part of the configuration suite.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { LuxAnalytics: Analytics, LuxPayments: Payments, LuxBlogs: Blogs, LuxMedia: Media, LuxReviews: Reviews, LuxReports: Reports, LuxSettings: Settings });
|
||||
})();
|
||||
@@ -0,0 +1,88 @@
|
||||
/* Aurum Estates — app shell: collapsible sidebar + glass sticky header */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, Logo = window.LuxLogo, Avatar = window.LuxAvatar;
|
||||
|
||||
const NAV = [
|
||||
{ k: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
|
||||
{ k: 'properties', label: 'Properties', icon: 'building' },
|
||||
{ k: 'projects', label: 'Projects', icon: 'briefcase' },
|
||||
{ k: 'clients', label: 'Clients', icon: 'users' },
|
||||
{ k: 'leads', label: 'Leads', icon: 'funnel', badge: '12' },
|
||||
{ k: 'bookings', label: 'Bookings', icon: 'calendar' },
|
||||
{ k: 'payments', label: 'Payments', icon: 'tag' },
|
||||
{ k: 'blogs', label: 'Blogs', icon: 'megaphone' },
|
||||
{ k: 'media', label: 'Media Library', icon: 'image' },
|
||||
{ k: 'reviews', label: 'Reviews', icon: 'star' },
|
||||
{ k: 'analytics', label: 'Analytics', icon: 'chartBar' },
|
||||
{ k: 'reports', label: 'Reports', icon: 'documents' },
|
||||
{ k: 'settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
|
||||
function Shell({ route, go, children, toastNode }) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const w = collapsed ? 'var(--sidebar-collapsed)' : 'var(--sidebar)';
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg)' }}>
|
||||
{/* Sidebar */}
|
||||
<aside style={{ width: w, flex: 'none', position: 'sticky', top: 0, height: '100vh', background: 'linear-gradient(190deg,#16314c,#102A43 55%,#0c2236)', display: 'flex', flexDirection: 'column', padding: collapsed ? '20px 14px' : '22px 18px', transition: 'width .25s cubic-bezier(.3,.7,.3,1)', overflow: 'hidden' }}>
|
||||
<div className="row spread" style={{ marginBottom: 22, paddingLeft: collapsed ? 0 : 4, justifyContent: collapsed ? 'center' : 'space-between' }}>
|
||||
<Logo collapsed={collapsed} light />
|
||||
</div>
|
||||
<nav style={{ display: 'flex', flexDirection: 'column', gap: 4, overflowY: 'auto', overflowX: 'hidden', flex: 1, margin: '0 -4px', padding: '0 4px' }}>
|
||||
{NAV.map(n => (
|
||||
<button key={n.k} className={'nav-item' + (route === n.k ? ' active' : '')} onClick={() => go(n.k)} title={collapsed ? n.label : ''} style={{ justifyContent: collapsed ? 'center' : 'flex-start', padding: collapsed ? '11px 0' : '10px 13px' }}>
|
||||
<Icon name={n.icon} />
|
||||
{!collapsed && <span>{n.label}</span>}
|
||||
{!collapsed && n.badge && <span className="nav-badge">{n.badge}</span>}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
{/* upgrade card */}
|
||||
{!collapsed && (
|
||||
<div style={{ marginTop: 14, padding: '15px 16px', borderRadius: 16, background: 'rgba(255,255,255,.06)', border: '1px solid rgba(255,255,255,.1)' }}>
|
||||
<div className="row gap-2" style={{ color: 'var(--green)', fontWeight: 700, fontSize: 13 }}><Icon name="sparkles" size={16} />Aurum Pro</div>
|
||||
<div style={{ color: 'rgba(200,214,232,.78)', fontSize: 12, marginTop: 6, lineHeight: 1.5 }}>Unlock AI valuations & market reports.</div>
|
||||
<button className="btn btn-sm" style={{ background: '#fff', color: 'var(--navy)', marginTop: 11, width: '100%' }}>Upgrade</button>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Main column */}
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* glass header */}
|
||||
<header className="glass" style={{ height: 'var(--header)', flex: 'none', position: 'sticky', top: 0, zIndex: 50, display: 'flex', alignItems: 'center', gap: 14, padding: '0 28px', borderBottom: '1px solid var(--border)', borderLeft: 'none', borderRight: 'none', borderTop: 'none' }}>
|
||||
<button className="icon-btn" onClick={() => setCollapsed(c => !c)} title="Toggle sidebar"><Icon name="panelLeft" /></button>
|
||||
<div className="input-icon" style={{ width: 380, maxWidth: '32vw' }}>
|
||||
<Icon name="search" />
|
||||
<input className="input" style={{ height: 42, background: 'rgba(255,255,255,.7)' }} placeholder="Search properties, clients, leads…" />
|
||||
<kbd style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', fontSize: 11, fontWeight: 600, color: 'var(--faint)', background: '#fff', border: '1px solid var(--border)', borderRadius: 6, padding: '2px 6px' }}>⌘K</kbd>
|
||||
</div>
|
||||
<div className="row gap-2" style={{ marginLeft: 'auto' }}>
|
||||
<button className="btn btn-primary btn-sm" style={{ marginRight: 4 }}><Icon name="plus" size={16} />Quick Add</button>
|
||||
<button className="icon-btn" title="Theme"><Icon name="sun" /></button>
|
||||
<button className="icon-btn" title="Messages"><Icon name="messages" /><span className="notif-dot" /></button>
|
||||
<button className="icon-btn" title="Notifications"><Icon name="bell" /><span className="notif-dot" /></button>
|
||||
<div style={{ width: 1, height: 28, background: 'var(--border)', margin: '0 4px' }} />
|
||||
<button onClick={() => go('settings')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '5px 9px 5px 5px', borderRadius: 99, border: '1px solid var(--border)', background: 'rgba(255,255,255,.8)', cursor: 'pointer' }}>
|
||||
<Avatar initials="AV" size={32} />
|
||||
<div style={{ textAlign: 'left', lineHeight: 1.15, whiteSpace: 'nowrap' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13 }}>Alexander Vale</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)' }}>Managing Director</div>
|
||||
</div>
|
||||
<Icon name="chevD" size={16} style={{ color: 'var(--faint)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style={{ flex: 1, overflow: 'auto', padding: '30px 36px 56px' }} key={route}>
|
||||
<div className="anim-fade-in" style={{ maxWidth: 1640, margin: '0 auto' }}>{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
{toastNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.LuxShell = Shell;
|
||||
})();
|
||||
@@ -0,0 +1,150 @@
|
||||
/* Aurum Estates — shared UI: logo, charts, property image, helpers */
|
||||
(function () {
|
||||
const { useState, useEffect, useRef } = React;
|
||||
const Icon = window.Icon;
|
||||
|
||||
function Logo({ collapsed, light }) {
|
||||
const fg = light ? '#fff' : 'var(--navy)';
|
||||
return React.createElement('div', { className: 'row gap-3', style: { minWidth: 0 } },
|
||||
React.createElement('div', { style: { width: 40, height: 40, borderRadius: 12, flex: 'none', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,#0D6EFD,#102A43)', boxShadow: '0 8px 18px -6px rgba(13,110,253,.6)' } },
|
||||
React.createElement('svg', { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none' },
|
||||
React.createElement('path', { d: 'M4 21V10l8-6 8 6v11', stroke: '#fff', strokeWidth: 1.7, strokeLinejoin: 'round', strokeLinecap: 'round' }),
|
||||
React.createElement('path', { d: 'M12 4l8 6', stroke: '#fff', strokeWidth: 1.7, strokeLinecap: 'round' }),
|
||||
React.createElement('rect', { x: 10, y: 13, width: 4, height: 8, fill: '#7AC943' }))),
|
||||
!collapsed && React.createElement('div', { style: { lineHeight: 1.05, minWidth: 0 } },
|
||||
React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 19, color: fg, letterSpacing: '.01em' } }, 'AURUM'),
|
||||
React.createElement('div', { style: { fontSize: 10.5, fontWeight: 600, letterSpacing: '.22em', color: light ? 'rgba(174,187,207,.9)' : 'var(--muted)', textTransform: 'uppercase' } }, 'Estates')));
|
||||
}
|
||||
|
||||
function Avatar({ initials, size = 38, style }) {
|
||||
return React.createElement('div', { className: 'avatar', style: { width: size, height: size, fontSize: size * 0.38, ...style } }, initials);
|
||||
}
|
||||
|
||||
function Sparkline({ data, w = 120, h = 40, color = 'var(--blue)', fill = true }) {
|
||||
const max = Math.max(...data), min = Math.min(...data), range = max - min || 1;
|
||||
const stepX = w / (data.length - 1);
|
||||
const pts = data.map((d, i) => [i * stepX, h - 4 - (h - 8) * (d - min) / range]);
|
||||
const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
|
||||
const area = `${line} L${w},${h} L0,${h} Z`;
|
||||
const id = 'sg' + Math.random().toString(36).slice(2, 7);
|
||||
return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: w, height: h, preserveAspectRatio: 'none', style: { display: 'block', overflow: 'visible' } },
|
||||
React.createElement('defs', null, React.createElement('linearGradient', { id, x1: 0, y1: 0, x2: 0, y2: 1 },
|
||||
React.createElement('stop', { offset: '0%', stopColor: color, stopOpacity: .2 }),
|
||||
React.createElement('stop', { offset: '100%', stopColor: color, stopOpacity: 0 }))),
|
||||
fill && React.createElement('path', { d: area, fill: `url(#${id})` }),
|
||||
React.createElement('path', { d: line, fill: 'none', stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
||||
}
|
||||
|
||||
function AreaChart({ data, w = 640, h = 220, color = 'var(--blue)', labels }) {
|
||||
const max = Math.max(...data) * 1.1, min = 0, range = max - min || 1;
|
||||
const padL = 4, padB = 22;
|
||||
const stepX = (w - padL) / (data.length - 1);
|
||||
const pts = data.map((d, i) => [padL + i * stepX, (h - padB) * (1 - (d - min) / range) + 4]);
|
||||
const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
|
||||
const area = `${line} L${pts[pts.length - 1][0]},${h - padB} L${pts[0][0]},${h - padB} Z`;
|
||||
const ref = useRef(null);
|
||||
useEffect(() => { const el = ref.current; if (el) { const len = el.getTotalLength(); el.style.strokeDasharray = len; el.style.strokeDashoffset = len; el.getBoundingClientRect(); el.style.transition = 'stroke-dashoffset 1.2s cubic-bezier(.3,.7,.3,1)'; el.style.strokeDashoffset = 0; } }, []);
|
||||
const id = 'ac' + Math.random().toString(36).slice(2, 7);
|
||||
return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, preserveAspectRatio: 'none', style: { display: 'block', overflow: 'visible' } },
|
||||
React.createElement('defs', null, React.createElement('linearGradient', { id, x1: 0, y1: 0, x2: 0, y2: 1 },
|
||||
React.createElement('stop', { offset: '0%', stopColor: color, stopOpacity: .22 }),
|
||||
React.createElement('stop', { offset: '100%', stopColor: color, stopOpacity: 0 }))),
|
||||
[0.25, 0.5, 0.75, 1].map((f, i) => React.createElement('line', { key: i, x1: 0, y1: (h - padB) * f, x2: w, y2: (h - padB) * f, stroke: '#eef1f6', strokeWidth: 1 })),
|
||||
React.createElement('path', { d: area, fill: `url(#${id})` }),
|
||||
React.createElement('path', { ref, d: line, fill: 'none', stroke: color, strokeWidth: 2.6, strokeLinecap: 'round', strokeLinejoin: 'round' }),
|
||||
pts.map((p, i) => React.createElement('circle', { key: i, cx: p[0], cy: p[1], r: i === pts.length - 1 ? 4.5 : 0, fill: '#fff', stroke: color, strokeWidth: 2.6 })),
|
||||
labels && labels.map((l, i) => React.createElement('text', { key: i, x: padL + i * ((w - padL) / (labels.length - 1)), y: h - 4, fontSize: 11, fontWeight: 600, fill: 'var(--faint)', textAnchor: i === 0 ? 'start' : i === labels.length - 1 ? 'end' : 'middle' }, l)));
|
||||
}
|
||||
|
||||
function Bars({ data, w = 300, h = 110, color = 'var(--blue)', labels }) {
|
||||
const max = Math.max(...data) || 1;
|
||||
const gap = 8, bw = (w - gap * (data.length - 1)) / data.length;
|
||||
return React.createElement('svg', { viewBox: `0 0 ${w} ${h + 18}`, width: '100%', height: h + 18, style: { display: 'block' } },
|
||||
data.map((d, i) => {
|
||||
const bh = Math.max(4, (d / max) * h);
|
||||
return React.createElement('g', { key: i },
|
||||
React.createElement('rect', { x: i * (bw + gap), y: h - bh, width: bw, height: bh, rx: 5, fill: i === data.length - 1 ? color : '#dbe7fb', style: { transformOrigin: `center ${h}px`, animation: `growBar .6s ${i * 0.04}s both` } }),
|
||||
labels && React.createElement('text', { x: i * (bw + gap) + bw / 2, y: h + 14, fontSize: 10.5, fontWeight: 600, fill: 'var(--faint)', textAnchor: 'middle' }, labels[i]));
|
||||
}));
|
||||
}
|
||||
|
||||
function Donut({ value, size = 130, stroke = 13, color = 'var(--blue)', track = '#eef1f6', label, sub }) {
|
||||
const r = (size - stroke) / 2, c = 2 * Math.PI * r;
|
||||
const target = c - (value / 100) * c;
|
||||
// start at final value when hidden (preview/print) so the arc is always visible
|
||||
const [off, setOff] = useState(typeof document !== 'undefined' && document.hidden ? target : c);
|
||||
useEffect(() => { const t = setTimeout(() => setOff(target), 120); return () => clearTimeout(t); }, [target]);
|
||||
return React.createElement('div', { style: { position: 'relative', width: size, height: size } },
|
||||
React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } },
|
||||
React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: track, strokeWidth: stroke }),
|
||||
React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeDasharray: c, strokeDashoffset: off, style: { transition: 'stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1)' } })),
|
||||
React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } },
|
||||
React.createElement('div', null,
|
||||
React.createElement('div', { className: 'serif', style: { fontWeight: 800, fontSize: size * 0.24 } }, label != null ? label : value + '%'),
|
||||
sub && React.createElement('div', { style: { fontSize: 11.5, color: 'var(--muted)', fontWeight: 600 } }, sub))));
|
||||
}
|
||||
|
||||
// Property image: uses <image-slot> so user can drop a real photo; styled placeholder by default
|
||||
function PropImage({ slot, accent, height = 200, radius = 16, placeholder = 'Drop property photo', children, badge }) {
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
// inject a tasteful default placeholder background into the slot's empty state
|
||||
}, []);
|
||||
return React.createElement('div', { style: { position: 'relative', height, borderRadius: radius, overflow: 'hidden' } },
|
||||
React.createElement('div', { style: { position: 'absolute', inset: 0, background: `linear-gradient(150deg, ${accent || '#6a8fc9'}, ${shade(accent || '#6a8fc9', -28)})` } },
|
||||
React.createElement(Architecture, { accent })),
|
||||
React.createElement('image-slot', { ref, id: slot, shape: 'rect', placeholder, style: { position: 'absolute', inset: 0, width: '100%', height: '100%' } }),
|
||||
badge,
|
||||
children);
|
||||
}
|
||||
|
||||
// simple architectural silhouette as default placeholder content
|
||||
function Architecture({ accent }) {
|
||||
return React.createElement('svg', { viewBox: '0 0 400 220', width: '100%', height: '100%', preserveAspectRatio: 'xMidYMid slice', style: { position: 'absolute', inset: 0, opacity: .9 } },
|
||||
React.createElement('rect', { width: 400, height: 220, fill: 'none' }),
|
||||
// sky glow
|
||||
React.createElement('circle', { cx: 320, cy: 40, r: 60, fill: 'rgba(255,255,255,.12)' }),
|
||||
// villa silhouette
|
||||
React.createElement('rect', { x: 70, y: 110, width: 160, height: 90, fill: 'rgba(255,255,255,.16)' }),
|
||||
React.createElement('rect', { x: 230, y: 80, width: 90, height: 120, fill: 'rgba(255,255,255,.22)' }),
|
||||
React.createElement('rect', { x: 95, y: 130, width: 26, height: 26, fill: 'rgba(255,255,255,.3)' }),
|
||||
React.createElement('rect', { x: 132, y: 130, width: 26, height: 26, fill: 'rgba(255,255,255,.3)' }),
|
||||
React.createElement('rect', { x: 250, y: 100, width: 22, height: 30, fill: 'rgba(255,255,255,.34)' }),
|
||||
React.createElement('rect', { x: 282, y: 100, width: 22, height: 30, fill: 'rgba(255,255,255,.34)' }),
|
||||
// waterline
|
||||
React.createElement('rect', { x: 0, y: 200, width: 400, height: 20, fill: 'rgba(255,255,255,.1)' }),
|
||||
React.createElement('path', { d: 'M0 200 Q100 192 200 200 T400 200', stroke: 'rgba(255,255,255,.25)', strokeWidth: 2, fill: 'none' }),
|
||||
React.createElement('foreignObject', { x: 0, y: 0, width: 400, height: 220 }));
|
||||
}
|
||||
|
||||
function shade(hex, amt) {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
let r = (n >> 16) + amt, g = ((n >> 8) & 0xff) + amt, b = (n & 0xff) + amt;
|
||||
r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b));
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
function PageHead({ title, kicker, subtitle, children }) {
|
||||
return React.createElement('div', { className: 'row spread wrap', style: { gap: 16, marginBottom: 26 } },
|
||||
React.createElement('div', null,
|
||||
kicker && React.createElement('div', { className: 'kicker', style: { marginBottom: 8 } }, kicker),
|
||||
React.createElement('h1', { style: { fontSize: 30, fontWeight: 800 } }, title),
|
||||
subtitle && React.createElement('p', { className: 'muted', style: { fontSize: 14.5, marginTop: 6 } }, subtitle)),
|
||||
children && React.createElement('div', { className: 'row gap-3' }, children));
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
const push = (msg, kind = 'success') => { const id = Math.random(); setToasts(t => [...t, { id, msg, kind }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3000); };
|
||||
const node = React.createElement('div', { style: { position: 'fixed', bottom: 24, right: 24, display: 'flex', flexDirection: 'column', gap: 10, zIndex: 9000 } },
|
||||
toasts.map(t => React.createElement('div', { key: t.id, className: 'anim-pop', style: { display: 'flex', alignItems: 'center', gap: 11, padding: '13px 17px', borderRadius: 13, background: 'var(--navy)', color: '#fff', boxShadow: 'var(--sh-lg)', fontSize: 14, fontWeight: 500, maxWidth: 360 } },
|
||||
React.createElement('span', { style: { display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 99, background: 'var(--green)', flex: 'none', color: '#173a05' } }, React.createElement(Icon, { name: 'check', size: 14 })), t.msg)));
|
||||
return [node, push];
|
||||
}
|
||||
|
||||
function Stars({ n, size = 14 }) {
|
||||
return React.createElement('div', { className: 'row gap-1' }, [0, 1, 2, 3, 4].map(i => React.createElement(Icon, { key: i, name: 'star', size, style: { color: i < n ? 'var(--gold)' : '#dfe4ec', fill: i < n ? 'var(--gold)' : '#dfe4ec' } })));
|
||||
}
|
||||
|
||||
Object.assign(window, { LuxLogo: Logo, LuxAvatar: Avatar, Sparkline, AreaChart, Bars, LuxDonut: Donut, PropImage, LuxPageHead: PageHead, useLuxToast: useToast, Stars });
|
||||
})();
|
||||
@@ -0,0 +1,203 @@
|
||||
/* ============================================================
|
||||
Aurum Estates — Luxury Real Estate Dashboard
|
||||
Playfair Display (display) + Inter (UI) · glassmorphism · blue/navy
|
||||
============================================================ */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,500;0,600;0,700;0,800;0,900;1,500&family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--blue: #0D6EFD;
|
||||
--blue-600: #0a59d4;
|
||||
--blue-700: #0848a8;
|
||||
--blue-soft: #e7f0ff;
|
||||
--blue-50: #f2f7ff;
|
||||
--navy: #102A43;
|
||||
--navy-700: #1c3e5e;
|
||||
--black: #111827;
|
||||
--green: #7AC943;
|
||||
--green-600: #62a934;
|
||||
--green-soft:#eaf7df;
|
||||
--gold: #c9a86a;
|
||||
--gold-soft: #f6efe1;
|
||||
|
||||
--bg: #F7F9FC;
|
||||
--white: #FFFFFF;
|
||||
--border:#E5E7EB;
|
||||
--border-soft:#eef1f5;
|
||||
|
||||
--ink: #111827;
|
||||
--ink-2: #374151;
|
||||
--muted: #6b7689;
|
||||
--faint: #98a2b3;
|
||||
|
||||
--font-display: 'Playfair Display', Georgia, serif;
|
||||
--font-ui: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
|
||||
--r: 14px;
|
||||
--r-lg: 20px;
|
||||
--r-xl: 26px;
|
||||
|
||||
--sh-xs: 0 1px 2px rgba(16,42,67,.05);
|
||||
--sh-sm: 0 2px 6px rgba(16,42,67,.06), 0 1px 2px rgba(16,42,67,.04);
|
||||
--sh-md: 0 12px 28px -10px rgba(16,42,67,.16), 0 4px 10px -4px rgba(16,42,67,.08);
|
||||
--sh-lg: 0 30px 60px -22px rgba(16,42,67,.30), 0 10px 24px -12px rgba(16,42,67,.16);
|
||||
--sh-blue: 0 14px 30px -10px rgba(13,110,253,.45);
|
||||
|
||||
--sidebar: 264px;
|
||||
--sidebar-collapsed: 80px;
|
||||
--header: 72px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-ui); color: var(--ink); background: var(--bg);
|
||||
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
||||
}
|
||||
#root { min-height: 100vh; }
|
||||
h1,h2,h3,h4 { font-family: var(--font-display); margin: 0; color: var(--ink); letter-spacing: -.01em; font-weight: 700; }
|
||||
p { margin: 0; }
|
||||
button { font-family: inherit; cursor: pointer; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
input, select, textarea { font-family: inherit; }
|
||||
::selection { background: var(--blue-soft); }
|
||||
.num { font-variant-numeric: tabular-nums; }
|
||||
.serif { font-family: var(--font-display); }
|
||||
.muted { color: var(--muted); }
|
||||
.faint { color: var(--faint); }
|
||||
|
||||
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
*::-webkit-scrollbar-thumb { background: #d3dae5; border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; }
|
||||
*::-webkit-scrollbar-thumb:hover { background: #bcc6d6; background-clip: padding-box; }
|
||||
*::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
/* layout helpers */
|
||||
.row { display: flex; align-items: center; }
|
||||
.col { display: flex; flex-direction: column; }
|
||||
.spread { justify-content: space-between; }
|
||||
.center { align-items: center; justify-content: center; }
|
||||
.grow { flex: 1; }
|
||||
.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}.gap-6{gap:24px}
|
||||
.wrap { flex-wrap: wrap; }
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
|
||||
font-weight: 600; font-size: 14px; padding: 11px 18px; border-radius: 11px;
|
||||
border: 1px solid transparent; background: transparent; color: var(--ink); white-space: nowrap; line-height: 1;
|
||||
transition: transform .12s ease, box-shadow .18s, background .18s, border-color .18s, color .15s;
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn svg { width: 18px; height: 18px; flex: none; }
|
||||
.btn-primary { background: var(--blue); color: #fff; box-shadow: var(--sh-blue); }
|
||||
.btn-primary:hover { background: var(--blue-600); box-shadow: 0 18px 36px -10px rgba(13,110,253,.55); }
|
||||
.btn-navy { background: var(--navy); color: #fff; }
|
||||
.btn-navy:hover { background: var(--navy-700); }
|
||||
.btn-dark { background: var(--black); color: #fff; }
|
||||
.btn-dark:hover { filter: brightness(1.25); }
|
||||
.btn-outline { background: #fff; border-color: var(--border); color: var(--ink-2); box-shadow: var(--sh-xs); }
|
||||
.btn-outline:hover { border-color: #cdd5e0; background: #fcfdff; }
|
||||
.btn-ghost { color: var(--muted); }
|
||||
.btn-ghost:hover { background: #f0f3f8; color: var(--ink); }
|
||||
.btn-sm { padding: 8px 13px; font-size: 13px; border-radius: 9px; }
|
||||
.btn-sm svg { width: 15px; height: 15px; }
|
||||
.btn-lg { padding: 14px 24px; font-size: 15px; border-radius: 13px; }
|
||||
.btn-block { width: 100%; }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--sh-sm); }
|
||||
.card-pad { padding: 24px; }
|
||||
.card-hover { transition: box-shadow .22s, transform .22s, border-color .22s; }
|
||||
.card-hover:hover { box-shadow: var(--sh-md); transform: translateY(-3px); border-color: #dbe2ec; }
|
||||
|
||||
/* glass */
|
||||
.glass { background: rgba(255,255,255,.72); backdrop-filter: blur(16px) saturate(1.4); -webkit-backdrop-filter: blur(16px) saturate(1.4); border: 1px solid rgba(255,255,255,.6); }
|
||||
|
||||
/* ---------- Badges ---------- */
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px; line-height: 1.3; white-space: nowrap; }
|
||||
.badge .dot { width: 6px; height: 6px; border-radius: 99px; background: currentColor; }
|
||||
.badge-blue { background: var(--blue-soft); color: var(--blue-700); }
|
||||
.badge-green { background: var(--green-soft); color: var(--green-600); }
|
||||
.badge-navy { background: #e6edf4; color: var(--navy); }
|
||||
.badge-gold { background: var(--gold-soft); color: #9c7b3a; }
|
||||
.badge-gray { background: #eef1f5; color: var(--muted); }
|
||||
.badge-amber { background: #fdf2e0; color: #b9760c; }
|
||||
.badge-red { background: #fdecec; color: #cf4040; }
|
||||
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font-size: 13px; font-weight: 500;
|
||||
padding: 8px 15px; border-radius: 99px; background: #fff; border: 1px solid var(--border); color: var(--ink-2); transition: all .15s;
|
||||
}
|
||||
.chip:hover { border-color: #cdd5e0; }
|
||||
.chip.active { background: var(--navy); color: #fff; border-color: var(--navy); }
|
||||
|
||||
/* ---------- Inputs ---------- */
|
||||
.field { display: flex; flex-direction: column; gap: 7px; }
|
||||
.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
|
||||
.input, .select {
|
||||
width: 100%; padding: 12px 14px; font-size: 14px; color: var(--ink); background: #fff;
|
||||
border: 1.5px solid var(--border); border-radius: 11px; outline: none; transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.input::placeholder { color: #9aa6b6; }
|
||||
.input:focus, .select:focus { border-color: var(--blue); box-shadow: 0 0 0 4px rgba(13,110,253,.12); }
|
||||
.input-icon { position: relative; }
|
||||
.input-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #9aa6b6; }
|
||||
.input-icon .input { padding-left: 42px; }
|
||||
|
||||
/* ---------- Sidebar nav ---------- */
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 13px; width: 100%; padding: 10px 13px; border-radius: 12px;
|
||||
border: none; background: transparent; color: #aebbcf; font-size: 14px; font-weight: 500; text-align: left;
|
||||
transition: background .15s, color .15s; position: relative;
|
||||
}
|
||||
.nav-item svg { width: 20px; height: 20px; flex: none; }
|
||||
.nav-item:hover { background: rgba(255,255,255,.06); color: #eaf1fb; }
|
||||
.nav-item.active { background: linear-gradient(100deg, var(--blue), var(--blue-600)); color: #fff; box-shadow: var(--sh-blue); font-weight: 600; }
|
||||
.nav-badge { margin-left: auto; font-size: 11px; font-weight: 700; background: var(--green); color: #173a05; padding: 1px 7px; border-radius: 99px; }
|
||||
|
||||
/* ---------- Tables ---------- */
|
||||
.tbl { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
|
||||
.tbl th { text-align: left; font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: 4px 16px; }
|
||||
.tbl td { padding: 14px 16px; font-size: 14px; color: var(--ink-2); background: #fff; border-top: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); }
|
||||
.tbl td:first-child { border-left: 1px solid var(--border-soft); border-radius: 12px 0 0 12px; }
|
||||
.tbl td:last-child { border-right: 1px solid var(--border-soft); border-radius: 0 12px 12px 0; }
|
||||
.tbl tbody tr { transition: box-shadow .15s, transform .15s; }
|
||||
.tbl tbody tr:hover td { background: #fafcff; }
|
||||
.tbl tbody tr:hover { box-shadow: var(--sh-sm); }
|
||||
|
||||
/* progress */
|
||||
.progress { height: 8px; background: #eef1f6; border-radius: 99px; overflow: hidden; }
|
||||
.progress > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--blue), var(--blue-600)); transition: width .8s cubic-bezier(.2,.7,.3,1); }
|
||||
|
||||
/* icon button */
|
||||
.icon-btn { width: 40px; height: 40px; border-radius: 12px; border: 1px solid transparent; display: inline-flex; align-items: center; justify-content: center; background: transparent; color: var(--muted); transition: background .15s, color .15s, border-color .15s; position: relative; }
|
||||
.icon-btn:hover { background: #f0f3f8; color: var(--ink); }
|
||||
.icon-btn svg { width: 20px; height: 20px; }
|
||||
.notif-dot { position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 99px; background: var(--green); border: 2px solid #fff; }
|
||||
|
||||
.kicker { font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--blue); }
|
||||
.divider { height: 1px; background: var(--border); border: none; }
|
||||
|
||||
.avatar { border-radius: 99px; object-fit: cover; background: linear-gradient(135deg, var(--blue), var(--navy)); display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; }
|
||||
|
||||
/* property image slot default look */
|
||||
image-slot { display: block; }
|
||||
|
||||
/* animations (opt-in via body.anim-on) */
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes popIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
|
||||
@keyframes growBar { from { transform: scaleY(0); } to { transform: scaleY(1); } }
|
||||
body.anim-on .anim-fade-up { animation: fadeUp .55s cubic-bezier(.2,.7,.3,1) both; }
|
||||
body.anim-on .anim-fade-in { animation: fadeIn .4s ease both; }
|
||||
body.anim-on .anim-pop { animation: popIn .35s cubic-bezier(.2,.8,.3,1) both; }
|
||||
body.anim-on .stagger > * { animation: fadeUp .55s cubic-bezier(.2,.7,.3,1) both; }
|
||||
body.anim-on .stagger > *:nth-child(1){animation-delay:.04s}
|
||||
body.anim-on .stagger > *:nth-child(2){animation-delay:.10s}
|
||||
body.anim-on .stagger > *:nth-child(3){animation-delay:.16s}
|
||||
body.anim-on .stagger > *:nth-child(4){animation-delay:.22s}
|
||||
body.anim-on .stagger > *:nth-child(5){animation-delay:.28s}
|
||||
body.anim-on .stagger > *:nth-child(6){animation-delay:.34s}
|
||||
body.anim-on .stagger > *:nth-child(7){animation-delay:.40s}
|
||||
body.anim-on .stagger > *:nth-child(8){animation-delay:.46s}
|
||||
@@ -0,0 +1,160 @@
|
||||
/* Interactive stylized sector map — pan via plots, zoom, layers, hover, select. */
|
||||
(function () {
|
||||
const { useState, useMemo } = React;
|
||||
const Icon = window.Icon;
|
||||
|
||||
// Generate a plausible plot layout: blocks of plots separated by roads.
|
||||
function buildPlots(seed) {
|
||||
const rng = (() => { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; })();
|
||||
const blocks = [
|
||||
{ code: 'A', x: 40, y: 40, cols: 6, rows: 3 },
|
||||
{ code: 'B', x: 40, y: 230, cols: 6, rows: 3 },
|
||||
{ code: 'C', x: 470, y: 40, cols: 5, rows: 3 },
|
||||
{ code: 'D', x: 470, y: 230, cols: 5, rows: 4 },
|
||||
];
|
||||
const pw = 58, ph = 50, gap = 6;
|
||||
const plots = [];
|
||||
blocks.forEach(b => {
|
||||
for (let r = 0; r < b.rows; r++) for (let c = 0; c < b.cols; c++) {
|
||||
const n = r * b.cols + c + 1;
|
||||
const types = ['res', 'res', 'res', 'comm', 'inst', 'park'];
|
||||
const t = rng() < 0.08 ? 'park' : rng() < 0.12 ? 'comm' : rng() < 0.08 ? 'inst' : 'res';
|
||||
plots.push({
|
||||
id: b.code + '-' + n, block: b.code,
|
||||
x: b.x + c * (pw + gap), y: b.y + r * (ph + gap), w: pw, h: ph, type: t,
|
||||
});
|
||||
}
|
||||
});
|
||||
return plots;
|
||||
}
|
||||
|
||||
const TYPE = {
|
||||
res: { fill: '#ffffff', label: 'Residential', sw: '#cbd8e8' },
|
||||
comm: { fill: '#fff2dc', label: 'Commercial', sw: '#f0d49a' },
|
||||
inst: { fill: '#e7eeff', label: 'Institutional', sw: '#bcd0f5' },
|
||||
park: { fill: '#e3f6ec', label: 'Green / Park', sw: '#b6e6cd' },
|
||||
};
|
||||
|
||||
function SectorMap({ sectorName = 'Sector 18', myPlot = 'B-47', height = 460, compact }) {
|
||||
const plots = useMemo(() => buildPlots(sectorName.length * 137 + 7), [sectorName]);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [sel, setSel] = useState(null);
|
||||
const [hover, setHover] = useState(null);
|
||||
const [layers, setLayers] = useState({ satellite: false, master: false, infra: true });
|
||||
const [search, setSearch] = useState('');
|
||||
// map myPlot to an existing plot id; fall back to one in the same block, then a default
|
||||
const mine = plots.find(p => p.id === myPlot)
|
||||
|| (myPlot && plots.find(p => p.block === myPlot.split('-')[0]))
|
||||
|| plots[14];
|
||||
const matched = search ? plots.find(p => p.id.toLowerCase() === search.toLowerCase()) : null;
|
||||
|
||||
const W = 760, H = 480;
|
||||
return (
|
||||
<div className="card" style={{ overflow: 'hidden', position: 'relative' }}>
|
||||
{/* toolbar */}
|
||||
<div className="row spread" style={{ padding: '12px 16px', borderBottom: '1px solid var(--line)', flexWrap: 'wrap', gap: 10 }}>
|
||||
<div className="row gap-3">
|
||||
<span className="badge badge-navy"><Icon name="map" size={13} />{sectorName}</span>
|
||||
<div className="input-icon" style={{ width: 200 }}>
|
||||
<Icon name="search" />
|
||||
<input className="input" style={{ height: 36, fontSize: 13 }} placeholder="Find plot e.g. B-47" value={search} onChange={e => { setSearch(e.target.value); const m = plots.find(p => p.id.toLowerCase() === e.target.value.toLowerCase()); if (m) setSel(m.id); }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row gap-2">
|
||||
{[['satellite', 'layers', 'Satellite'], ['master', 'grid', 'Master Plan'], ['infra', 'building', 'Infrastructure']].map(([k, ic, l]) => (
|
||||
<button key={k} onClick={() => setLayers(s => ({ ...s, [k]: !s[k] }))}
|
||||
className="chip" style={{ padding: '7px 11px', fontSize: 12.5, background: layers[k] ? 'var(--navy-800)' : '#fff', color: layers[k] ? '#fff' : 'var(--ink-2)', borderColor: layers[k] ? 'var(--navy-800)' : 'var(--line)' }}>
|
||||
<Icon name={ic} size={14} />{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* map canvas */}
|
||||
<div style={{ position: 'relative', height, overflow: 'hidden', background: layers.satellite ? '#1f3247' : 'linear-gradient(135deg,#eef4fb,#e9f2ec)' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, transform: `scale(${zoom})`, transformOrigin: 'center', transition: 'transform .35s cubic-bezier(.3,.7,.3,1)' }}>
|
||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%" style={{ display: 'block' }}>
|
||||
{/* land base */}
|
||||
<rect width={W} height={H} fill={layers.satellite ? '#2a4258' : '#e6eef7'} />
|
||||
{/* greens / blocks bg when satellite */}
|
||||
{layers.satellite && plots.filter(p => p.type === 'park').map(p => <rect key={p.id} x={p.x} y={p.y} width={p.w} height={p.h} fill="#2f5e44" rx={3} />)}
|
||||
{/* roads */}
|
||||
<g>
|
||||
<rect x={0} y={188} width={W} height={34} fill={layers.satellite ? '#3a4a5c' : '#d7e0ec'} />
|
||||
<rect x={420} y={0} width={34} height={H} fill={layers.satellite ? '#3a4a5c' : '#d7e0ec'} />
|
||||
<line x1={0} y1={205} x2={W} y2={205} stroke={layers.satellite ? '#6f7f93' : '#fff'} strokeWidth={2.5} strokeDasharray="14 11" />
|
||||
<line x1={437} y1={0} x2={437} y2={H} stroke={layers.satellite ? '#6f7f93' : '#fff'} strokeWidth={2.5} strokeDasharray="14 11" />
|
||||
</g>
|
||||
{/* master plan grid overlay */}
|
||||
{layers.master && Array.from({ length: 20 }).map((_, i) => (
|
||||
<g key={i}>
|
||||
<line x1={i * 40} y1={0} x2={i * 40} y2={H} stroke="rgba(38,112,230,.18)" strokeWidth={1} />
|
||||
<line x1={0} y1={i * 40} x2={W} y2={i * 40} stroke="rgba(38,112,230,.18)" strokeWidth={1} />
|
||||
</g>
|
||||
))}
|
||||
{/* plots */}
|
||||
{plots.map(p => {
|
||||
const t = TYPE[p.type];
|
||||
const isMine = p.id === mine.id;
|
||||
const isSel = sel === p.id;
|
||||
const isHover = hover === p.id;
|
||||
const isMatch = matched && matched.id === p.id;
|
||||
let fill = layers.satellite ? (p.type === 'park' ? '#356b4e' : '#cdd9e6') : t.fill;
|
||||
if (isMine) fill = 'var(--emerald-500)';
|
||||
if (isSel || isMatch) fill = 'var(--blue-500)';
|
||||
return (
|
||||
<g key={p.id} style={{ cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHover(p.id)} onMouseLeave={() => setHover(null)}
|
||||
onClick={() => setSel(p.id)}>
|
||||
<rect x={p.x} y={p.y} width={p.w} height={p.h} rx={4} fill={fill}
|
||||
stroke={isMine ? '#fff' : isSel || isMatch ? '#fff' : (layers.satellite ? '#56697d' : t.sw)}
|
||||
strokeWidth={isMine || isSel || isMatch ? 2.5 : 1}
|
||||
style={{ filter: isHover ? 'brightness(.95)' : 'none', transition: 'filter .1s' }} />
|
||||
<text x={p.x + p.w / 2} y={p.y + p.h / 2 + 4} textAnchor="middle" fontSize="11" fontWeight="700"
|
||||
fill={isMine || isSel || isMatch ? '#fff' : (layers.satellite ? '#dce6f1' : '#7b8aa0')} style={{ pointerEvents: 'none' }}>{p.id}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{/* my plot pulse */}
|
||||
<circle cx={mine.x + mine.w / 2} cy={mine.y + mine.h / 2} r={36} fill="none" stroke="var(--emerald-500)" strokeWidth={2} opacity={.4} style={{ animation: 'ping 2.4s infinite' }} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* legend */}
|
||||
{!compact && (
|
||||
<div style={{ position: 'absolute', left: 14, bottom: 14, background: 'rgba(255,255,255,.94)', backdropFilter: 'blur(6px)', borderRadius: 11, padding: '11px 13px', boxShadow: 'var(--sh-md)', display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 11.5, color: 'var(--ink-2)' }}>Legend</div>
|
||||
{[['Your Plot', 'var(--emerald-500)'], ['Selected', 'var(--blue-500)'], ['Residential', '#fff'], ['Commercial', '#fff2dc'], ['Green / Park', '#e3f6ec']].map(([l, c]) => (
|
||||
<div key={l} className="row gap-2" style={{ fontSize: 11.5, color: 'var(--muted)' }}>
|
||||
<span style={{ width: 13, height: 13, borderRadius: 3, background: c, border: '1px solid var(--line)' }} />{l}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* zoom controls */}
|
||||
<div style={{ position: 'absolute', right: 14, top: 14, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<button className="icon-btn" style={{ background: '#fff', boxShadow: 'var(--sh-sm)' }} onClick={() => setZoom(z => Math.min(2.2, z + 0.25))}><Icon name="zoomIn" /></button>
|
||||
<button className="icon-btn" style={{ background: '#fff', boxShadow: 'var(--sh-sm)' }} onClick={() => setZoom(z => Math.max(0.7, z - 0.25))}><Icon name="zoomOut" /></button>
|
||||
<button className="icon-btn" style={{ background: '#fff', boxShadow: 'var(--sh-sm)' }} onClick={() => { setZoom(1); setSel(null); }}><Icon name="refresh" /></button>
|
||||
</div>
|
||||
|
||||
{/* selected plot tooltip */}
|
||||
{sel && (() => { const p = plots.find(x => x.id === sel); const isMine = p.id === mine.id; return (
|
||||
<div className="anim-pop" style={{ position: 'absolute', right: 14, bottom: 14, width: 210, background: '#fff', borderRadius: 12, padding: '13px 15px', boxShadow: 'var(--sh-lg)', border: '1px solid var(--line)' }}>
|
||||
<div className="row spread"><span style={{ fontWeight: 800, fontFamily: 'var(--font-display)', fontSize: 16 }}>Plot {p.id}</span><button onClick={() => setSel(null)} style={{ border: 'none', background: 'none', color: 'var(--faint)', cursor: 'pointer' }}><Icon name="x" size={16} /></button></div>
|
||||
<div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>{TYPE[p.type].label} · Block {p.block}</div>
|
||||
<div style={{ height: 1, background: 'var(--line)', margin: '10px 0' }} />
|
||||
<div className="col gap-2" style={{ fontSize: 12.5 }}>
|
||||
<div className="row spread"><span className="muted">Area</span><b>{p.type === 'comm' ? '450' : '300'} sq.m</b></div>
|
||||
<div className="row spread"><span className="muted">Status</span>{isMine ? <span className="badge badge-green" style={{ fontSize: 10.5 }}>Yours</span> : <span className="badge badge-gray" style={{ fontSize: 10.5 }}>Allotted</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
); })()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.SectorMap = SectorMap;
|
||||
})();
|
||||
@@ -0,0 +1,301 @@
|
||||
/* Construction Journey — goal selection + 8-step survey + Project Team + Progress Center */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead, Donut = window.Donut, Avatar = window.Avatar;
|
||||
const D = window.YEIDA, fmt = D.fmtINR;
|
||||
|
||||
function MyJourney({ go, toast }) {
|
||||
const [tab, setTab] = useState('journey');
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="My Construction Journey" subtitle="A guided, step-by-step path for your plot — from goal to completion certificate">
|
||||
<button className="btn btn-outline" onClick={() => go('sampleplans')}><Icon name="grid" size={17} />Design Explorer</button>
|
||||
<button className="btn btn-primary" onClick={() => go('construction')}><Icon name="construction" size={17} />Construction Module</button>
|
||||
</PageHead>
|
||||
|
||||
{/* sub-tabs */}
|
||||
<div className="row gap-2" style={{ marginBottom: 22, borderBottom: '1px solid var(--line)', flexWrap: 'wrap' }}>
|
||||
{[['journey', 'Journey Steps'], ['team', 'Project Team'], ['progress', 'Progress Center']].map(([k, l]) => (
|
||||
<button key={k} onClick={() => setTab(k)} style={{ padding: '11px 4px', marginRight: 22, background: 'none', border: 'none', borderBottom: '2.5px solid ' + (tab === k ? 'var(--blue-600)' : 'transparent'), color: tab === k ? 'var(--ink)' : 'var(--muted)', fontWeight: tab === k ? 700 : 500, fontSize: 14.5, cursor: 'pointer', marginBottom: -1 }}>{l}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === 'journey' && <JourneySteps go={go} toast={toast} />}
|
||||
{tab === 'team' && <ProjectTeam toast={toast} />}
|
||||
{tab === 'progress' && <ProgressCenter />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- GOAL SELECTION + 8-STEP ACCORDION SURVEY ---------------- */
|
||||
function JourneySteps({ go, toast }) {
|
||||
const [goal, setGoal] = useState('start');
|
||||
const [steps, setSteps] = useState(D.journeySteps);
|
||||
const [open, setOpen] = useState(() => {
|
||||
const a = D.journeySteps.findIndex(s => s.active);
|
||||
return a >= 0 ? a : 0;
|
||||
});
|
||||
|
||||
const doneCount = steps.filter(s => s.done).length;
|
||||
const save = (i) => {
|
||||
setSteps(arr => arr.map((s, j) => j === i ? { ...s, done: true, active: false } : s));
|
||||
toast && toast('Information saved · ' + steps[i].title);
|
||||
const next = steps.findIndex((s, j) => j > i && !s.done);
|
||||
setOpen(next >= 0 ? next : -1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* progress header */}
|
||||
<div className="card card-pad anim-fade-up" style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 20, flexWrap: 'wrap' }}>
|
||||
<Donut value={Math.round(doneCount / steps.length * 100)} label={doneCount + '/' + steps.length} sub="steps done" size={104} color="var(--emerald-500)" />
|
||||
<div style={{ flex: 1, minWidth: 220 }}>
|
||||
<h3 style={{ fontSize: 19 }}>You're {Math.round(doneCount / steps.length * 100)}% through your journey</h3>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 4 }}>Complete each step below — saved steps turn green and stay editable. We'll guide you to the next action.</p>
|
||||
<div className="row gap-2" style={{ marginTop: 12, flexWrap: 'wrap' }}>
|
||||
<span className="badge badge-green"><Icon name="check" size={12} />{doneCount} completed</span>
|
||||
<span className="badge badge-blue">{steps.length - doneCount} remaining</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* goal selection */}
|
||||
<div className="card card-pad anim-fade-up" style={{ marginBottom: 20 }}>
|
||||
<h3 style={{ fontSize: 18 }}>What would you like to do with your plot?</h3>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 4, marginBottom: 16 }}>Pick a goal — we'll personalise your journey around it.</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(220px,1fr))', gap: 12 }}>
|
||||
{D.journeyGoals.map(gOpt => {
|
||||
const on = goal === gOpt.id;
|
||||
return (
|
||||
<button key={gOpt.id} onClick={() => setGoal(gOpt.id)} className="card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: '14px 15px', borderRadius: 13, border: '1.5px solid ' + (on ? 'var(--blue-400)' : 'var(--line)'), background: on ? 'var(--blue-50)' : '#fff', display: 'flex', gap: 12, alignItems: 'flex-start', boxShadow: on ? '0 0 0 4px rgba(38,112,230,.1)' : 'none' }}>
|
||||
<span style={{ width: 40, height: 40, borderRadius: 11, flex: 'none', display: 'grid', placeItems: 'center', background: on ? 'var(--blue-500)' : 'var(--bg-2)', color: on ? '#fff' : 'var(--muted)' }}><Icon name={gOpt.icon} size={20} /></span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, lineHeight: 1.25 }}>{gOpt.title}</div>
|
||||
<div className="muted" style={{ fontSize: 11.5, marginTop: 3, lineHeight: 1.35 }}>{gOpt.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* accordion survey */}
|
||||
<div className="col gap-3">
|
||||
{steps.map((s, i) => {
|
||||
const isOpen = open === i;
|
||||
const state = s.done ? 'done' : s.active || isOpen ? 'active' : 'todo';
|
||||
return (
|
||||
<div key={s.key} className="card anim-fade-up" style={{ overflow: 'hidden', border: '1px solid ' + (state === 'done' ? '#bfe6d3' : isOpen ? 'var(--blue-200,#bcd0f5)' : 'var(--line)'), borderLeft: '4px solid ' + (state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : 'var(--line)') }}>
|
||||
{/* header row */}
|
||||
<button onClick={() => setOpen(isOpen ? -1 : i)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 14, padding: '15px 18px', background: s.done ? 'var(--emerald-50)' : '#fff', border: 'none', cursor: 'pointer', textAlign: 'left' }}>
|
||||
<span style={{ width: 38, height: 38, borderRadius: 99, flex: 'none', display: 'grid', placeItems: 'center', fontWeight: 800, fontFamily: 'var(--font-display)', fontSize: 15, background: s.done ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : 'var(--bg-2)', color: s.done || state === 'active' ? '#fff' : 'var(--faint)' }}>
|
||||
{s.done ? <Icon name="check" size={18} /> : i + 1}
|
||||
</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="row gap-2" style={{ flexWrap: 'wrap' }}>
|
||||
<span style={{ fontWeight: 700, fontSize: 15 }}>{s.title}</span>
|
||||
{s.done && <span className="badge badge-green" style={{ fontSize: 10.5 }}><Icon name="check" size={11} />Information Saved</span>}
|
||||
{state === 'active' && !s.done && <span className="badge badge-blue" style={{ fontSize: 10.5 }}>In progress</span>}
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: 12.5, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.done ? s.summary : s.desc}</div>
|
||||
</div>
|
||||
<Icon name={isOpen ? 'chevU' : 'chevD'} size={18} style={{ color: 'var(--faint)', flex: 'none' }} />
|
||||
</button>
|
||||
|
||||
{/* body */}
|
||||
{isOpen && (
|
||||
<div className="anim-fade-in" style={{ padding: '4px 18px 18px 70px', borderTop: '1px solid var(--line-2)' }}>
|
||||
<StepBody step={s} go={go} />
|
||||
<div className="row gap-3" style={{ marginTop: 16 }}>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => save(i)}><Icon name="check" size={15} />{s.done ? 'Update & Save' : 'Save & Continue'}</button>
|
||||
{s.done && <span className="row gap-2 faint" style={{ fontSize: 12.5 }}><Icon name="edit" size={14} />Editable anytime</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepBody({ step, go }) {
|
||||
// tailored, lightweight content per step
|
||||
switch (step.key) {
|
||||
case 'goal':
|
||||
return <Note>Your selected goal drives the recommended steps, designs and budget. You can change it from the selector above.</Note>;
|
||||
case 'plot':
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', gap: 12 }}>
|
||||
{[['Allotment No.', D.user.allotment], ['Plot', D.user.plot], ['Sector', D.user.sector], ['Pocket', D.user.pocket]].map(([l, v]) => <Field key={l} label={l} value={v} />)}
|
||||
</div>
|
||||
);
|
||||
case 'design':
|
||||
return (
|
||||
<div>
|
||||
<Note>Selected design from the Design Explorer. Browse alternatives anytime.</Note>
|
||||
<button className="btn btn-outline btn-sm" style={{ marginTop: 10 }} onClick={() => go('sampleplans')}><Icon name="grid" size={15} />Open Design Explorer</button>
|
||||
</div>
|
||||
);
|
||||
case 'planning':
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', gap: 12 }}>
|
||||
{[['Configuration', 'G+1'], ['Built-up area', '4,200 sq.ft'], ['Est. duration', '11 months'], ['Plan status', 'Sanctioned']].map(([l, v]) => <Field key={l} label={l} value={v} />)}
|
||||
</div>
|
||||
);
|
||||
case 'budget':
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', gap: 12, marginBottom: 12 }}>
|
||||
{[['Total budget', fmt(D.construction.estimatedCost)], ['Milestones', '5'], ['Funded', fmt(D.construction.spent)]].map(([l, v]) => <Field key={l} label={l} value={v} />)}
|
||||
</div>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('payments')}><Icon name="rupee" size={15} />Set up milestone payments</button>
|
||||
</div>
|
||||
);
|
||||
case 'contractor':
|
||||
return (
|
||||
<div>
|
||||
<Note>Assign your project team — manager, site engineer, architect & crew. View the Project Team tab to manage contacts.</Note>
|
||||
</div>
|
||||
);
|
||||
case 'progress':
|
||||
return <Note>Once construction starts, track every stage with photos, notes and dates in the Progress Center.</Note>;
|
||||
case 'certificate':
|
||||
return <Note>After final inspection, apply for and download your Completion Certificate here.</Note>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function Field({ label, value }) {
|
||||
return (
|
||||
<div style={{ background: 'var(--bg)', borderRadius: 11, padding: '11px 14px' }}>
|
||||
<div className="faint" style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.04em' }}>{label}</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5, marginTop: 3 }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Note({ children }) {
|
||||
return <p className="muted" style={{ fontSize: 13.5, lineHeight: 1.6 }}>{children}</p>;
|
||||
}
|
||||
|
||||
/* ---------------- PROJECT TEAM ---------------- */
|
||||
function ProjectTeam({ toast }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18 }}>
|
||||
<div className="anim-fade-up">
|
||||
<h3 style={{ fontSize: 18, marginBottom: 14 }}>Your Project Team</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
{D.projectTeam.map(m => (
|
||||
<div key={m.role} className="card card-pad card-hover">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Avatar initials={m.avatar} size={46} />
|
||||
{m.online && <span style={{ position: 'absolute', bottom: 0, right: 0, width: 12, height: 12, borderRadius: 99, background: 'var(--emerald-500)', border: '2px solid #fff' }} />}
|
||||
</div>
|
||||
<span className="badge badge-navy" style={{ fontSize: 10.5 }}>{m.role}</span>
|
||||
</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 15 }}>{m.name}</div>
|
||||
<div className="muted" style={{ fontSize: 12.5, marginTop: 1 }}>{m.org}</div>
|
||||
<div className="row gap-2" style={{ marginTop: 12 }}>
|
||||
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={() => toast && toast('Calling ' + m.name)}><Icon name="phone" size={14} />Call</button>
|
||||
<button className="btn btn-ghost btn-sm" style={{ border: '1px solid var(--line)', flex: 1 }} onClick={() => toast && toast('Message sent')}><Icon name="chat" size={14} />Message</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* team updates / milestone */}
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s', alignSelf: 'start' }}>
|
||||
<h3 style={{ fontSize: 17, marginBottom: 16 }}>Team Updates</h3>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{D.teamUpdates.map((u, i, arr) => (
|
||||
<div key={i} className="row" style={{ alignItems: 'flex-start', gap: 12, paddingBottom: i === arr.length - 1 ? 0 : 16, position: 'relative' }}>
|
||||
{i !== arr.length - 1 && <span style={{ position: 'absolute', left: 16, top: 36, bottom: 0, width: 2, background: 'var(--line)' }} />}
|
||||
<Avatar initials={u.avatar} size={34} />
|
||||
<div style={{ paddingTop: 1 }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.45 }}><b style={{ color: 'var(--ink)' }}>{u.who}</b> — {u.text}</div>
|
||||
<div className="faint" style={{ fontSize: 11.5, marginTop: 3 }}>{u.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- PROGRESS CENTER (stages + live view) ---------------- */
|
||||
function ProgressCenter() {
|
||||
const overall = Math.round(D.progressStages.reduce((s, x) => s + x.pct, 0) / D.progressStages.length);
|
||||
return (
|
||||
<div>
|
||||
{/* live view + summary */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18, marginBottom: 22 }}>
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<div style={{ position: 'relative', height: 280, background: 'radial-gradient(120% 120% at 50% 0%,#16407e,#081a39)', display: 'grid', placeItems: 'center' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: .3, backgroundImage: 'linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.06) 1px,transparent 1px)', backgroundSize: '36px 36px' }} />
|
||||
<div style={{ position: 'absolute', top: 14, left: 14, display: 'flex', alignItems: 'center', gap: 7, padding: '6px 12px', borderRadius: 99, background: 'rgba(210,63,63,.92)', color: '#fff', fontSize: 12, fontWeight: 700 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 99, background: '#fff', animation: 'pulseDot 1.4s infinite' }} />LIVE
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', color: '#fff', position: 'relative' }}>
|
||||
<div style={{ width: 70, height: 70, borderRadius: 99, margin: '0 auto 12px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.25)' }}><Icon name="eye" size={32} /></div>
|
||||
<div style={{ fontWeight: 700, fontSize: 16 }}>Live Site Camera</div>
|
||||
<div style={{ fontSize: 12.5, color: 'rgba(200,216,240,.8)', marginTop: 4 }}>Plot B-47 · Cam 1 · 1080p — drop in your RTSP/webcam feed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row spread" style={{ padding: '12px 16px' }}>
|
||||
<div className="row gap-2">{['Cam 1', 'Cam 2', 'Drone'].map((c, i) => <span key={c} className={'chip' + (i === 0 ? ' active' : '')} style={{ fontSize: 12 }}>{c}</span>)}</div>
|
||||
<span className="faint" style={{ fontSize: 12 }}>Updated just now</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<div className="row center" style={{ margin: '6px 0 14px' }}><Donut value={overall} label={overall + '%'} sub="overall" color="var(--blue-500)" /></div>
|
||||
<div className="col gap-2">
|
||||
{[['Daily Site Photos', 'image', '42 this month'], ['Weekly Progress Report', 'documents', 'Latest: 06 Jun'], ['Current Stage', 'construction', 'Slab Casting']].map(([l, ic, v]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '9px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="row gap-3" style={{ fontSize: 13, fontWeight: 600 }}><span style={{ width: 30, height: 30, borderRadius: 8, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name={ic} size={15} /></span>{l}</span>
|
||||
<span className="faint" style={{ fontSize: 12 }}>{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* stage timeline */}
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 18, marginBottom: 18 }}>Construction Stages</h3>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{D.progressStages.map((st, i, arr) => {
|
||||
const state = st.pct === 100 ? 'done' : st.pct > 0 ? 'active' : 'todo';
|
||||
const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : '#c2cedd';
|
||||
return (
|
||||
<div key={st.name} className="row" style={{ alignItems: 'flex-start', gap: 16, paddingBottom: i === arr.length - 1 ? 0 : 18, position: 'relative' }}>
|
||||
{i !== arr.length - 1 && <span style={{ position: 'absolute', left: 17, top: 36, bottom: 0, width: 2, background: state === 'done' ? 'var(--emerald-500)' : 'var(--line)' }} />}
|
||||
<span style={{ width: 36, height: 36, borderRadius: 99, flex: 'none', display: 'grid', placeItems: 'center', background: '#fff', border: '2.5px solid ' + cc, color: cc, zIndex: 1 }}>
|
||||
{state === 'done' ? <Icon name="check" size={16} /> : state === 'active' ? <span style={{ width: 9, height: 9, borderRadius: 99, background: cc, animation: 'pulseDot 1.4s infinite' }} /> : <span style={{ fontWeight: 700, fontSize: 12 }}>{i + 1}</span>}
|
||||
</span>
|
||||
<div style={{ flex: 1, paddingTop: 4 }}>
|
||||
<div className="row spread" style={{ flexWrap: 'wrap', gap: 8 }}>
|
||||
<span style={{ fontWeight: 700, fontSize: 14.5, color: state === 'todo' ? 'var(--faint)' : 'var(--ink)' }}>{st.name}</span>
|
||||
<div className="row gap-2">
|
||||
{st.photos > 0 && <span className="badge badge-gray" style={{ fontSize: 10.5 }}><Icon name="image" size={11} />{st.photos}</span>}
|
||||
<span className={'badge badge-' + (state === 'done' ? 'green' : state === 'active' ? 'blue' : 'gray')} style={{ fontSize: 10.5 }}>{state === 'done' ? 'Completed' : state === 'active' ? st.pct + '%' : 'Upcoming'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{st.note && <div className="muted" style={{ fontSize: 12.5, marginTop: 3 }}>{st.note}</div>}
|
||||
<div className="faint" style={{ fontSize: 11.5, marginTop: 3 }}>{st.date}</div>
|
||||
{state === 'active' && <div className="progress" style={{ height: 5, marginTop: 7, maxWidth: 320 }}><span style={{ width: st.pct + '%' }} /></div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.MyJourney = MyJourney;
|
||||
})();
|
||||
@@ -0,0 +1,263 @@
|
||||
/* Registration wizard: Account → Allotment → Verify → Addresses */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon;
|
||||
const D = window.YEIDA;
|
||||
const AuthShell = window.AuthShell, VerifyPanel = window.VerifyPanel;
|
||||
|
||||
const STEPS = [
|
||||
{ k: 'account', label: 'Account', icon: 'user' },
|
||||
{ k: 'allotment', label: 'Allotment', icon: 'building' },
|
||||
{ k: 'verify', label: 'Verify', icon: 'shield' },
|
||||
{ k: 'address', label: 'Address', icon: 'mapPin' },
|
||||
];
|
||||
|
||||
function Stepper({ step }) {
|
||||
return (
|
||||
<div className="row gap-2" style={{ marginBottom: 24 }}>
|
||||
{STEPS.map((s, i) => (
|
||||
<React.Fragment key={s.k}>
|
||||
<div className="row gap-2" style={{ flex: 'none' }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 99, display: 'grid', placeItems: 'center', fontWeight: 700, fontSize: 13, background: i < step ? 'var(--emerald-500)' : i === step ? 'var(--blue-600)' : 'var(--bg-2)', color: i <= step ? '#fff' : 'var(--faint)', transition: 'all .25s' }}>
|
||||
{i < step ? <Icon name="check" size={15} /> : i + 1}
|
||||
</span>
|
||||
<span style={{ fontSize: 12.5, fontWeight: i === step ? 700 : 500, color: i === step ? 'var(--ink)' : 'var(--muted)' }}>{s.label}</span>
|
||||
</div>
|
||||
{i < STEPS.length - 1 && <span style={{ flex: 1, height: 2, borderRadius: 99, background: i < step ? 'var(--emerald-500)' : 'var(--line)' }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Register({ go }) {
|
||||
const [step, setStep] = useState(0);
|
||||
const [form, setForm] = useState({
|
||||
firstName: 'Rajeev', lastName: 'Malhotra', email: 'r.malhotra@email.com',
|
||||
cc: '+91', mobile: '9876500012', password: '', confirm: '',
|
||||
relationship: 'Self', terms: true, cookies: true,
|
||||
allotteeFirst: 'Rajeev', allotteeLast: 'Malhotra',
|
||||
hasAllotment: true, allotment: 'YEA-654321', sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47',
|
||||
});
|
||||
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
|
||||
const next = () => setStep(s => Math.min(STEPS.length - 1, s + 1));
|
||||
const isAllottee = form.relationship === 'Self';
|
||||
|
||||
return (
|
||||
<AuthShell wide>
|
||||
<div className="anim-fade-up">
|
||||
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, marginLeft: -8 }} onClick={() => step === 0 ? go('login') : setStep(s => s - 1)}><Icon name="chevL" size={16} />Back</button>
|
||||
<div className="kicker" style={{ marginBottom: 8 }}>New Allottee Registration</div>
|
||||
<h1 style={{ fontSize: 25, fontWeight: 800, marginBottom: 20 }}>
|
||||
{['Create your account', 'Identify your plot', 'Verify email & phone', 'Your addresses'][step]}
|
||||
</h1>
|
||||
<Stepper step={step} />
|
||||
|
||||
<div key={step} className="anim-fade-in">
|
||||
{step === 0 && <AccountStep form={form} set={set} isAllottee={isAllottee} />}
|
||||
{step === 1 && <AllotmentStep form={form} set={set} />}
|
||||
{step === 2 && <VerifyStep form={form} go={next} />}
|
||||
{step === 3 && <AddressStep form={form} set={set} />}
|
||||
</div>
|
||||
|
||||
{step !== 2 && (
|
||||
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 22 }}
|
||||
onClick={() => step === STEPS.length - 1 ? go('app') : next()}>
|
||||
{step === 0 ? 'Create Account & Verify' : step === 3 ? 'Finish & Enter Portal' : 'Continue'}
|
||||
<Icon name="arrowR" size={19} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{step === 0 && (
|
||||
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 14, color: 'var(--muted)' }}>
|
||||
Already registered?{' '}
|
||||
<button onClick={() => go('login')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 14 }}>Sign in</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
/* Step 0 — Account creation */
|
||||
function AccountStep({ form, set, isAllottee }) {
|
||||
const pwMismatch = form.confirm.length > 0 && form.password !== form.confirm;
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: 15 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="field"><label>First Name</label><div className="input-icon"><Icon name="user" /><input className="input" value={form.firstName} onChange={e => set('firstName', e.target.value)} /></div></div>
|
||||
<div className="field"><label>Last Name</label><div className="input-icon"><Icon name="user" /><input className="input" value={form.lastName} onChange={e => set('lastName', e.target.value)} /></div></div>
|
||||
</div>
|
||||
<div className="field"><label>Email Address</label><div className="input-icon"><Icon name="mail" /><input className="input" value={form.email} onChange={e => set('email', e.target.value)} placeholder="you@email.com" /></div></div>
|
||||
<div className="field"><label>Mobile Number</label>
|
||||
<div className="row gap-2">
|
||||
<select className="select" value={form.cc} onChange={e => set('cc', e.target.value)} style={{ width: 100, flex: 'none' }}>{D.countryCodes.map(c => <option key={c.code} value={c.code}>{c.label} {c.code}</option>)}</select>
|
||||
<div className="input-icon" style={{ flex: 1 }}><Icon name="phone" /><input className="input" value={form.mobile} onChange={e => set('mobile', e.target.value)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="field"><label>Password</label><div className="input-icon"><Icon name="lock" /><input className="input" type="password" value={form.password} onChange={e => set('password', e.target.value)} placeholder="Min 8 characters" /></div></div>
|
||||
<div className="field"><label>Confirm Password</label><div className="input-icon"><Icon name="lock" /><input className="input" type="password" value={form.confirm} onChange={e => set('confirm', e.target.value)} placeholder="Re-enter password" style={pwMismatch ? { borderColor: 'var(--red-500)' } : undefined} /></div></div>
|
||||
</div>
|
||||
{pwMismatch && <div className="row gap-2" style={{ fontSize: 12, color: 'var(--red-600)', marginTop: -6 }}><Icon name="alert" size={14} />Passwords do not match</div>}
|
||||
|
||||
{/* Relationship with Allottee */}
|
||||
<div className="field">
|
||||
<label>Relationship with Allottee</label>
|
||||
<select className="select" value={form.relationship} onChange={e => set('relationship', e.target.value)}>
|
||||
{D.relationshipOptions.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="row gap-3" style={{ padding: '12px 14px', borderRadius: 12, background: isAllottee ? 'var(--emerald-50)' : 'var(--blue-50)', fontSize: 12.5, color: isAllottee ? 'var(--emerald-700)' : 'var(--blue-700)' }}>
|
||||
<Icon name={isAllottee ? 'shield' : 'info'} size={17} style={{ flex: 'none' }} />
|
||||
<span>{isAllottee ? 'Self registration — you are the original allottee (Allottee = User).' : `Invitation registration — you are registering on behalf of the allottee as ${form.relationship} (Allottee ≠ User).`}</span>
|
||||
</div>
|
||||
|
||||
{/* Allottee name — required when registrant is NOT the allottee */}
|
||||
{!isAllottee && (
|
||||
<div className="anim-fade-in" style={{ display: 'grid', gap: 12, padding: 16, borderRadius: 14, border: '1px dashed var(--blue-100)', background: '#fbfdff' }}>
|
||||
<div className="row gap-2" style={{ fontWeight: 700, fontSize: 13.5, color: 'var(--ink-2)' }}><Icon name="user" size={16} style={{ color: 'var(--blue-600)' }} />Original Allottee Details</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="field"><label>Allottee First Name</label><input className="input" value={form.allotteeFirst} onChange={e => set('allotteeFirst', e.target.value)} placeholder="As per records" /></div>
|
||||
<div className="field"><label>Allottee Last Name</label><input className="input" value={form.allotteeLast} onChange={e => set('allotteeLast', e.target.value)} placeholder="As per records" /></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={form.terms} onChange={e => set('terms', e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
|
||||
<span>I accept YEIDA's <b style={{ color: 'var(--ink-2)' }}>Terms & Conditions</b> and <b style={{ color: 'var(--ink-2)' }}>Privacy Policy</b></span>
|
||||
</label>
|
||||
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer', marginTop: -4 }}>
|
||||
<input type="checkbox" checked={form.cookies} onChange={e => set('cookies', e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
|
||||
<span>I consent to <b style={{ color: 'var(--ink-2)' }}>cookies</b> for a secure, personalised portal experience</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Step 1 — Allotment identification */
|
||||
function AllotmentStep({ form, set }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: 19, fontWeight: 800 }}>What is your Allotment Number?</h3>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 5, marginBottom: 18 }}>Enter your allotment number to link your plot, or identify it by sector & plot if you don't have it handy.</p>
|
||||
|
||||
{/* toggle */}
|
||||
<div className="row gap-2" style={{ background: 'var(--bg-2)', padding: 4, borderRadius: 12, marginBottom: 18, width: 'fit-content' }}>
|
||||
{[['have', 'I have my allotment number'], ['forgot', "I don't remember it"]].map(([k, l]) => (
|
||||
<button key={k} onClick={() => set('hasAllotment', k === 'have')}
|
||||
style={{ padding: '8px 15px', borderRadius: 9, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', background: (form.hasAllotment === (k === 'have')) ? '#fff' : 'transparent', color: (form.hasAllotment === (k === 'have')) ? 'var(--ink)' : 'var(--muted)', boxShadow: (form.hasAllotment === (k === 'have')) ? 'var(--sh-xs)' : 'none' }}>{l}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{form.hasAllotment ? (
|
||||
<div className="field">
|
||||
<label>Allotment Number</label>
|
||||
<div className="input-icon"><Icon name="building" /><input className="input" value={form.allotment} maxLength={10} onChange={e => set('allotment', e.target.value.toUpperCase())} placeholder="e.g. YEA-654321" /></div>
|
||||
<span className="faint" style={{ fontSize: 12 }}>Format: YEA- followed by your 6-digit number</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="row gap-3" style={{ padding: '11px 14px', borderRadius: 11, background: 'var(--amber-50)', fontSize: 12.5, color: 'var(--amber-600)', marginBottom: 16 }}>
|
||||
<Icon name="info" size={16} style={{ flex: 'none' }} />Identify your plot below — we'll match it to your allotment record.
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
|
||||
<div className="field"><label>Sector</label>
|
||||
<select className="select" value={form.sector} onChange={e => set('sector', e.target.value)}>{D.sectors.map(s => <option key={s.id} value={s.name}>{s.name}</option>)}</select>
|
||||
</div>
|
||||
<div className="field"><label>Pocket</label>
|
||||
<select className="select" value={form.pocket} onChange={e => set('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => <option key={p}>{p}</option>)}</select>
|
||||
</div>
|
||||
<div className="field"><label>Plot Number</label>
|
||||
<div className="input-icon"><Icon name="plot" /><input className="input" value={form.plot} onChange={e => set('plot', e.target.value)} placeholder="3–4 digits" maxLength={5} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="faint" style={{ fontSize: 12, marginTop: 8, display: 'block' }}>Plot number is typically 3 or 4 digits (e.g. 047 or 1024).</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Step 2 — Verification */
|
||||
function VerifyStep({ form, go }) {
|
||||
const [both, setBoth] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: -6, marginBottom: 18 }}>Verify both your email and mobile number on this screen. Each requires an OTP.</p>
|
||||
<VerifyPanel email={form.email} phone={form.mobile} onChange={setBoth} />
|
||||
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 20, opacity: both ? 1 : .55 }} disabled={!both} onClick={go}>{both ? 'Continue' : 'Verify email & phone to continue'}<Icon name="arrowR" size={19} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Step 3 — Addresses (registered + mailing, editable independently) */
|
||||
function AddressStep({ form, set }) {
|
||||
const [sameAsReg, setSameAsReg] = useState(false);
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: 18 }}>
|
||||
<AddressBlock title="Registered Address" sub="Official authority / allotment address" icon="building" prefix="reg" form={form} set={set} />
|
||||
<label className="row gap-3" style={{ fontSize: 13.5, color: 'var(--muted)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={sameAsReg} onChange={e => setSameAsReg(e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
|
||||
<span>My mailing address is the same as my registered address</span>
|
||||
</label>
|
||||
{!sameAsReg && <AddressBlock title="Mailing / Current Address" sub="Where we send communications" icon="mail" prefix="mail" form={form} set={set} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddressBlock({ title, sub, icon, prefix, form, set }) {
|
||||
const def = prefix === 'reg' ? D.user.registeredAddress : D.user.mailingAddress;
|
||||
return (
|
||||
<div className="card" style={{ padding: 18, boxShadow: 'none', border: '1px solid var(--line)' }}>
|
||||
<div className="row gap-3" style={{ marginBottom: 14 }}>
|
||||
<span style={{ width: 36, height: 36, borderRadius: 9, display: 'grid', placeItems: 'center', background: 'var(--blue-50)', color: 'var(--blue-600)' }}><Icon name={icon} size={18} /></span>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 14.5 }}>{title}</div><div className="faint" style={{ fontSize: 12 }}>{sub}</div></div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<div className="field"><label>Address Line 1</label><input className="input" defaultValue={def.line1} /></div>
|
||||
<div className="field"><label>Address Line 2</label><input className="input" defaultValue={def.line2} /></div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr .8fr', gap: 12 }}>
|
||||
<div className="field"><label>City</label><input className="input" defaultValue={def.city} /></div>
|
||||
<div className="field"><label>State</label><input className="input" defaultValue={def.state} /></div>
|
||||
<div className="field"><label>PIN</label><input className="input num" defaultValue={def.pin} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Standalone allotment lookup (login path — "I don't remember my allotment number") */
|
||||
function AllotmentLookup({ go }) {
|
||||
const [form, setForm] = useState({ sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47' });
|
||||
const [found, setFound] = useState(false);
|
||||
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
|
||||
return (
|
||||
<AuthShell>
|
||||
<div className="anim-fade-up">
|
||||
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 14, marginLeft: -8 }} onClick={() => go('login')}><Icon name="chevL" size={16} />Back</button>
|
||||
<div className="kicker" style={{ marginBottom: 8 }}>Find your allotment</div>
|
||||
<h1 style={{ fontSize: 25, fontWeight: 800 }}>Identify your plot</h1>
|
||||
<p className="muted" style={{ fontSize: 14, marginTop: 6, marginBottom: 20 }}>No allotment number? Locate your plot by sector, pocket and plot number.</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 14 }}>
|
||||
<div className="field"><label>Sector</label><select className="select" value={form.sector} onChange={e => set('sector', e.target.value)}>{D.sectors.map(s => <option key={s.id} value={s.name}>{s.name}</option>)}</select></div>
|
||||
<div className="field"><label>Pocket</label><select className="select" value={form.pocket} onChange={e => set('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => <option key={p}>{p}</option>)}</select></div>
|
||||
</div>
|
||||
<div className="field" style={{ marginBottom: 18 }}><label>Plot Number (3–4 digits)</label><div className="input-icon"><Icon name="plot" /><input className="input" value={form.plot} onChange={e => set('plot', e.target.value)} placeholder="e.g. 047" /></div></div>
|
||||
{found ? (
|
||||
<div className="anim-pop" style={{ padding: '14px 16px', borderRadius: 12, background: 'var(--emerald-50)', border: '1px solid #bfe6d3', marginBottom: 18 }}>
|
||||
<div className="row gap-3"><Icon name="checkCircle" size={20} style={{ color: 'var(--emerald-600)' }} /><div><div style={{ fontWeight: 700, fontSize: 14 }}>Allotment found · YEA-654321</div><div className="muted" style={{ fontSize: 12.5 }}>{form.sector} · {form.pocket} · Plot {form.plot}</div></div></div>
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-outline btn-block" style={{ marginBottom: 18 }} onClick={() => setFound(true)}><Icon name="search" size={17} />Find my allotment</button>
|
||||
)}
|
||||
<button className="btn btn-primary btn-block btn-lg" disabled={!found} style={{ opacity: found ? 1 : .55 }} onClick={() => go('verify')}>Continue to Verification<Icon name="arrowR" size={19} /></button>
|
||||
</div>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
window.Register = Register;
|
||||
window.AllotmentLookup = AllotmentLookup;
|
||||
})();
|
||||
@@ -0,0 +1,257 @@
|
||||
/* Payments module */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead, LineChart = window.LineChart, BarChart = window.BarChart;
|
||||
const D = window.YEIDA, fmt = D.fmtINR;
|
||||
|
||||
function Payments({ go, toast }) {
|
||||
const [tab, setTab] = useState('dues');
|
||||
const [pay, setPay] = useState(null); // due being paid
|
||||
const totalDue = D.dues.reduce((s, d) => s + d.amount, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Payments" subtitle="Authority challans & dues plus construction milestone payments">
|
||||
<button className="btn btn-outline" onClick={() => setTab('history')}><Icon name="history" size={18} />History</button>
|
||||
<button className="btn btn-primary" onClick={() => setPay({ label: 'All Outstanding Dues', amount: totalDue })}><Icon name="rupee" size={18} />Pay All Dues</button>
|
||||
</PageHead>
|
||||
|
||||
{/* summary cards */}
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16, marginBottom: 18 }}>
|
||||
{[
|
||||
['Outstanding', fmt(totalDue), 'amber', 'rupee', '3 dues pending'],
|
||||
['Paid (FY 25-26)', fmt(265370), 'emerald', 'checkCircle', '5 transactions'],
|
||||
['Next Due', '30 Jun', 'blue', 'calendar', 'Lease Rent 2026-27'],
|
||||
['Total Premium', fmt(D.property.totalValue), 'navy', 'building', '90% paid'],
|
||||
].map(([l, v, c, ic, sub]) => {
|
||||
const cc = { blue: ['var(--blue-50)', 'var(--blue-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c];
|
||||
return (
|
||||
<div key={l} className="card card-pad">
|
||||
<div className="row spread"><span style={{ width: 40, height: 40, borderRadius: 11, display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={ic} size={20} /></span></div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 24, marginTop: 14 }}>{v}</div>
|
||||
<div style={{ fontSize: 13.5, color: 'var(--muted)', fontWeight: 600 }}>{l}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 3 }}>{sub}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* tabs */}
|
||||
<div className="row gap-2" style={{ marginBottom: 18, borderBottom: '1px solid var(--line)', flexWrap: 'wrap' }}>
|
||||
{[['dues', 'Outstanding Dues'], ['history', 'Payment History'], ['receipts', 'Receipts'], ['construction', 'Construction Payments'], ['analytics', 'Financial Analytics']].map(([k, l]) => (
|
||||
<button key={k} onClick={() => setTab(k)} style={{ padding: '11px 4px', marginRight: 18, background: 'none', border: 'none', borderBottom: '2.5px solid ' + (tab === k ? 'var(--blue-600)' : 'transparent'), color: tab === k ? 'var(--ink)' : 'var(--muted)', fontWeight: tab === k ? 700 : 500, fontSize: 14.5, cursor: 'pointer', marginBottom: -1 }}>{l}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === 'dues' && <Dues onPay={setPay} onChallan={() => toast && toast('e-Challan generated · CH-2026-0606-18')} />}
|
||||
{tab === 'history' && <History />}
|
||||
{tab === 'receipts' && <Receipts toast={toast} />}
|
||||
{tab === 'construction' && <ConstructionPayments toast={toast} />}
|
||||
{tab === 'analytics' && <Analytics />}
|
||||
|
||||
{pay && <PayModal due={pay} onClose={() => setPay(null)} onDone={() => { setPay(null); toast && toast('Payment successful · receipt sent to email'); }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dues({ onPay, onChallan }) {
|
||||
return (
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Description</th><th>Type</th><th>Due Date</th><th>Status</th><th style={{ textAlign: 'right' }}>Amount</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{D.dues.map(d => (
|
||||
<tr key={d.id}>
|
||||
<td><div style={{ fontWeight: 600, color: 'var(--ink)' }}>{d.label}</div><div className="faint" style={{ fontSize: 12 }}>{d.id}</div></td>
|
||||
<td>{d.type}</td>
|
||||
<td className="num">{d.due}</td>
|
||||
<td><span className={'badge badge-' + (d.status === 'overdue' ? 'red' : d.status === 'due' ? 'amber' : 'gray')}>{d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'}</span></td>
|
||||
<td className="num" style={{ textAlign: 'right', fontWeight: 700, color: 'var(--ink)' }}>{fmt(d.amount)}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<div className="row gap-2" style={{ justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-outline btn-sm" onClick={onChallan}><Icon name="receipt" size={15} />Challan</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => onPay(d)}>Pay</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function History() {
|
||||
return (
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden' }}>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Transaction</th><th>Date</th><th>Mode</th><th>Challan</th><th>Status</th><th style={{ textAlign: 'right' }}>Amount</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{D.payments.map(p => (
|
||||
<tr key={p.id}>
|
||||
<td><div style={{ fontWeight: 600, color: 'var(--ink)' }}>{p.label}</div><div className="faint num" style={{ fontSize: 12 }}>{p.id}</div></td>
|
||||
<td className="num">{p.date}</td>
|
||||
<td>{p.mode}</td>
|
||||
<td className="num faint" style={{ fontSize: 13 }}>{p.challan}</td>
|
||||
<td><span className="badge badge-green"><Icon name="check" size={13} />{p.status}</span></td>
|
||||
<td className="num" style={{ textAlign: 'right', fontWeight: 700, color: 'var(--ink)' }}>{fmt(p.amount)}</td>
|
||||
<td style={{ textAlign: 'right' }}><button className="icon-btn" title="Download receipt"><Icon name="download" size={18} /></button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Receipts({ toast }) {
|
||||
return (
|
||||
<div className="anim-fade-up" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 16 }}>
|
||||
{D.payments.map(p => (
|
||||
<div key={p.id} className="card card-pad card-hover">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<span style={{ width: 42, height: 42, borderRadius: 11, display: 'grid', placeItems: 'center', background: 'var(--emerald-50)', color: 'var(--emerald-700)' }}><Icon name="receipt" size={21} /></span>
|
||||
<span className="badge badge-green"><Icon name="check" size={12} />Paid</span>
|
||||
</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14.5 }}>{p.label}</div>
|
||||
<div className="faint num" style={{ fontSize: 12, marginTop: 3 }}>{p.challan} · {p.date}</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 21, margin: '12px 0' }}>{fmt(p.amount)}</div>
|
||||
<div className="row gap-2">
|
||||
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={() => toast && toast('Receipt downloaded')}><Icon name="download" size={15} />Download</button>
|
||||
<button className="btn btn-ghost btn-sm" style={{ border: '1px solid var(--line)' }}><Icon name="eye" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConstructionPayments({ toast }) {
|
||||
const CP = D.constructionPayments;
|
||||
const C = D.construction;
|
||||
const paid = CP.filter(p => p.status === 'Paid').reduce((s, p) => s + p.amount, 0);
|
||||
const upcoming = CP.filter(p => p.status !== 'Paid').reduce((s, p) => s + p.amount, 0);
|
||||
return (
|
||||
<div className="anim-fade-up">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 16, marginBottom: 18 }}>
|
||||
{[['Estimated Build Cost', fmt(C.estimatedCost), 'navy', 'building'], ['Paid to Contractor', fmt(paid), 'emerald', 'checkCircle'], ['Upcoming Milestones', fmt(upcoming), 'amber', 'clock']].map(([l, v, c, ic]) => {
|
||||
const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c];
|
||||
return (
|
||||
<div key={l} className="card card-pad">
|
||||
<span style={{ width: 40, height: 40, borderRadius: 11, display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={ic} size={20} /></span>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 23, marginTop: 12 }}>{v}</div>
|
||||
<div style={{ fontSize: 13.5, color: 'var(--muted)', fontWeight: 600 }}>{l}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="card" style={{ overflow: 'hidden' }}>
|
||||
<div className="row spread" style={{ padding: '16px 20px 4px' }}>
|
||||
<h3 style={{ fontSize: 17 }}>Milestone Payments & Invoices</h3>
|
||||
<span className="muted" style={{ fontSize: 12.5 }}>Contractor · {C.contractor.name}</span>
|
||||
</div>
|
||||
<table className="tbl">
|
||||
<thead><tr><th>Milestone</th><th>Invoice</th><th>Vendor</th><th>Date</th><th>Status</th><th style={{ textAlign: 'right' }}>Amount</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{CP.map(p => (
|
||||
<tr key={p.id}>
|
||||
<td style={{ fontWeight: 600, color: 'var(--ink)' }}>{p.milestone}</td>
|
||||
<td className="num faint">{p.id}</td>
|
||||
<td>{p.vendor}</td>
|
||||
<td className="num">{p.date}</td>
|
||||
<td><span className={'badge badge-' + (p.status === 'Paid' ? 'green' : 'amber')}>{p.status === 'Paid' ? <Icon name="check" size={12} /> : null}{p.status}</span></td>
|
||||
<td className="num" style={{ textAlign: 'right', fontWeight: 700, color: 'var(--ink)' }}>{fmt(p.amount)}</td>
|
||||
<td style={{ textAlign: 'right' }}>{p.status === 'Paid' ? <button className="icon-btn" title="Receipt" onClick={() => toast && toast('Receipt downloaded')}><Icon name="download" size={18} /></button> : <button className="btn btn-primary btn-sm" onClick={() => toast && toast('Milestone payment scheduled')}>Pay</button>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Analytics() {
|
||||
return (
|
||||
<div className="anim-fade-up">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, marginBottom: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 17 }}>Cumulative payments</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 14 }}>Premium + lease rent paid since allotment</p>
|
||||
<LineChart data={[1.2, 1.8, 2.4, 3.1, 3.9, 4.6, 5.4, 6.0, 6.5, 6.95]} h={180} color="var(--emerald-600)" fill="rgba(15,157,107,.12)" />
|
||||
<div className="row spread" style={{ marginTop: 8, fontSize: 12, color: 'var(--faint)', fontWeight: 600 }}><span>2019</span><span>2021</span><span>2023</span><span>2025</span><span>2026</span></div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 17 }}>By category</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 18 }}>Lifetime breakdown</p>
|
||||
{[['Location Premium', 82, 'var(--blue-500)'], ['Lease Rent', 11, 'var(--emerald-500)'], ['Fees & Charges', 5, 'var(--amber-500)'], ['Utilities', 2, 'var(--navy-700)']].map(([l, p, c]) => (
|
||||
<div key={l} style={{ marginBottom: 14 }}>
|
||||
<div className="row spread" style={{ fontSize: 13, marginBottom: 6 }}><span style={{ fontWeight: 600 }}>{l}</span><span className="num faint">{p}%</span></div>
|
||||
<div className="progress"><span style={{ width: p + '%', background: c }} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 17, marginBottom: 4 }}>Yearly payments</h3>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 18 }}>Total paid per financial year (₹ lakh)</p>
|
||||
<BarChart data={[1.2, 1.4, 1.1, 1.6, 1.9, 2.2, 2.0, 2.6]} w={760} h={120} color="var(--blue-500)" gap={14} />
|
||||
<div className="row spread" style={{ marginTop: 8, fontSize: 12, color: 'var(--faint)', fontWeight: 600 }}>{['19', '20', '21', '22', '23', '24', '25', '26'].map(y => <span key={y}>FY{y}</span>)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PayModal({ due, onClose, onDone }) {
|
||||
const [method, setMethod] = useState('upi');
|
||||
const [stage, setStage] = useState('select');
|
||||
const methods = [['upi', 'UPI', 'phone'], ['card', 'Debit/Credit Card', 'payments'], ['net', 'Net Banking', 'building']];
|
||||
const pay = () => { setStage('processing'); setTimeout(() => setStage('done'), 1600); };
|
||||
return (
|
||||
<div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(8,26,57,.5)', backdropFilter: 'blur(3px)', display: 'grid', placeItems: 'center', zIndex: 1000, padding: 20 }}>
|
||||
<div onClick={e => e.stopPropagation()} className="anim-pop card" style={{ width: 440, maxWidth: '100%', padding: 0, overflow: 'hidden' }}>
|
||||
{stage !== 'done' ? (
|
||||
<>
|
||||
<div style={{ padding: '20px 24px', borderBottom: '1px solid var(--line)' }} className="row spread">
|
||||
<h3 style={{ fontSize: 18 }}>Complete Payment</h3>
|
||||
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
||||
</div>
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ background: 'var(--bg)', borderRadius: 13, padding: '16px 18px', marginBottom: 20 }}>
|
||||
<div className="muted" style={{ fontSize: 13 }}>{due.label}</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 30, marginTop: 4 }}>{fmt(due.amount)}</div>
|
||||
</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, marginBottom: 10 }}>Payment Method</div>
|
||||
<div className="col gap-2" style={{ marginBottom: 22 }}>
|
||||
{methods.map(([k, l, ic]) => (
|
||||
<button key={k} onClick={() => setMethod(k)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '13px 15px', borderRadius: 12, border: '1.5px solid ' + (method === k ? 'var(--blue-400)' : 'var(--line)'), background: method === k ? 'var(--blue-50)' : '#fff', cursor: 'pointer' }}>
|
||||
<span style={{ width: 38, height: 38, borderRadius: 10, display: 'grid', placeItems: 'center', background: method === k ? 'var(--blue-500)' : 'var(--bg-2)', color: method === k ? '#fff' : 'var(--muted)' }}><Icon name={ic} size={18} /></span>
|
||||
<span style={{ fontWeight: 600, fontSize: 14.5, flex: 1, textAlign: 'left' }}>{l}</span>
|
||||
<span style={{ width: 19, height: 19, borderRadius: 99, border: '2px solid ' + (method === k ? 'var(--blue-500)' : '#cdd7e4'), display: 'grid', placeItems: 'center' }}>{method === k && <span style={{ width: 9, height: 9, borderRadius: 99, background: 'var(--blue-500)' }} />}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn btn-primary btn-block btn-lg" onClick={pay} disabled={stage === 'processing'}>
|
||||
{stage === 'processing' ? <><span style={{ width: 17, height: 17, border: '2px solid rgba(255,255,255,.4)', borderTopColor: '#fff', borderRadius: 99, animation: 'spin .7s linear infinite' }} />Processing…</> : <><Icon name="lock" size={17} />Pay {fmt(due.amount)}</>}
|
||||
</button>
|
||||
<div className="faint row gap-2 center" style={{ fontSize: 11.5, marginTop: 12 }}><Icon name="shield" size={14} />Secured by YEIDA Payment Gateway</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ padding: '44px 30px', textAlign: 'center' }}>
|
||||
<div style={{ width: 72, height: 72, borderRadius: 99, margin: '0 auto 16px', display: 'grid', placeItems: 'center', background: 'var(--emerald-500)', color: '#fff', boxShadow: '0 14px 30px -10px rgba(15,157,107,.6)' }} className="anim-pop"><Icon name="check" size={38} /></div>
|
||||
<h3 style={{ fontSize: 22 }}>Payment Successful</h3>
|
||||
<p className="muted" style={{ fontSize: 14, marginTop: 8 }}>{fmt(due.amount)} paid · Receipt CH-2026-0606-18</p>
|
||||
<div className="row gap-3 center" style={{ marginTop: 22 }}>
|
||||
<button className="btn btn-outline"><Icon name="download" size={17} />Receipt</button>
|
||||
<button className="btn btn-primary" onClick={onDone}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Payments = Payments;
|
||||
})();
|
||||
@@ -0,0 +1,143 @@
|
||||
/* My Plot */
|
||||
(function () {
|
||||
const Icon = window.Icon, PageHead = window.PageHead, SectorMap = window.SectorMap;
|
||||
const D = window.YEIDA, fmt = D.fmtINR, P = D.property;
|
||||
|
||||
const timeline = [
|
||||
{ title: 'Plot Allotted', date: '14 Mar 2019', desc: 'Allotment letter issued for Plot B-47', color: 'emerald', icon: 'check' },
|
||||
{ title: 'Possession Granted', date: '22 Aug 2019', desc: 'Physical possession handed over', color: 'emerald', icon: 'home' },
|
||||
{ title: 'Lease Deed Registered', date: '05 Sep 2019', desc: '90-year lease registered at Sub-Registrar', color: 'emerald', icon: 'file' },
|
||||
{ title: 'Building Plan Sanctioned', date: '18 Feb 2026', desc: 'Construction approval granted', color: 'blue', icon: 'construction' },
|
||||
{ title: 'Construction In Progress', date: 'Ongoing', desc: 'Ground floor structure — 65% complete', color: 'amber', icon: 'building' },
|
||||
];
|
||||
|
||||
function InfoRow({ label, value, strong }) {
|
||||
return (
|
||||
<div className="row spread" style={{ padding: '11px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="muted" style={{ fontSize: 13.5 }}>{label}</span>
|
||||
<span style={{ fontSize: 13.5, fontWeight: strong ? 800 : 600, color: 'var(--ink)' }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyPlot({ go }) {
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="My Plot" subtitle={`${P.sector} · Plot ${P.plot} · ${P.category}`}>
|
||||
<button className="btn btn-outline" onClick={() => go('documents')}><Icon name="documents" size={18} />Documents</button>
|
||||
<button className="btn btn-primary" onClick={() => go('construction')}><Icon name="construction" size={18} />Construction</button>
|
||||
</PageHead>
|
||||
|
||||
{/* hero property card */}
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', marginBottom: 18 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr' }}>
|
||||
<div style={{ padding: 26 }}>
|
||||
<div className="row spread" style={{ marginBottom: 18 }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 52, height: 52, borderRadius: 14, display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff' }}><Icon name="building" size={26} /></span>
|
||||
<div>
|
||||
<h2 style={{ fontSize: 22 }}>Plot {P.plot}, {P.sector}</h2>
|
||||
<div className="muted" style={{ fontSize: 13.5 }}>Allotment {P.allotmentNo}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row gap-2"><window.StatusBadge code="LD_COMPLETED" /><window.StatusBadge code="ATL_PENDING" /></div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14, marginBottom: 8 }}>
|
||||
{[['Plot Size', P.size], ['Category', P.category], ['Facing', P.facing], ['Block', P.block], ['Road Width', P.road], ['Lease Period', P.leasePeriod]].map(([l, v]) => (
|
||||
<div key={l} style={{ background: 'var(--bg)', borderRadius: 11, padding: '12px 14px' }}>
|
||||
<div className="faint" style={{ fontSize: 11.5, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.04em' }}>{l}</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 15, marginTop: 4 }}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: 'radial-gradient(120% 120% at 100% 0%,#16407e,#0b2548)', color: '#fff', padding: 26, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: 12.5, color: 'rgba(200,216,240,.8)', fontWeight: 600 }}>Total Premium Value</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 32, marginTop: 4 }}>{fmt(P.totalValue)}</div>
|
||||
<div className="progress" style={{ margin: '16px 0 8px', background: 'rgba(255,255,255,.14)' }}><span style={{ width: Math.round(P.paid / P.totalValue * 100) + '%' }} /></div>
|
||||
<div className="row spread" style={{ fontSize: 13 }}>
|
||||
<span style={{ color: '#9ff0cd', fontWeight: 700 }}>{fmt(P.paid)} paid</span>
|
||||
<span style={{ color: 'rgba(200,216,240,.8)' }}>{fmt(P.totalValue - P.paid)} balance</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 18, fontSize: 12.5, color: 'rgba(200,216,240,.75)', display: 'flex', gap: 18 }}>
|
||||
<span><Icon name="calendar" size={13} style={{ verticalAlign: -2, marginRight: 5 }} />Allotted {P.allottedOn}</span>
|
||||
<span><Icon name="rupee" size={13} style={{ verticalAlign: -2, marginRight: 5 }} />{P.rate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* map + info */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, marginBottom: 18 }}>
|
||||
<div className="anim-fade-up">
|
||||
<div className="row spread" style={{ marginBottom: 12 }}>
|
||||
<h3 style={{ fontSize: 17 }}>Plot Location</h3>
|
||||
<span className="muted row gap-2" style={{ fontSize: 13 }}><Icon name="mapPin" size={15} />{P.sector}, Yamuna Expressway, Gautam Buddh Nagar</span>
|
||||
</div>
|
||||
<SectorMap sectorName="Sector 18" myPlot="B-47" height={420} />
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.08s', alignSelf: 'start' }}>
|
||||
<h3 style={{ fontSize: 17, marginBottom: 8 }}>Ownership Information</h3>
|
||||
<InfoRow label="Allottee" value={D.user.name} />
|
||||
<InfoRow label="Registered by" value={D.user.isAllottee ? 'Self (Allottee)' : D.user.relationship + ' (on behalf of allottee)'} />
|
||||
<InfoRow label="Allotment No." value={P.allotmentNo} />
|
||||
<InfoRow label="Allotted On" value={P.allottedOn} />
|
||||
<InfoRow label="Possession" value={P.possession} />
|
||||
<InfoRow label="Lease Period" value={P.leasePeriod} />
|
||||
<InfoRow label="Premium Rate" value={P.rate} />
|
||||
<InfoRow label="Total Value" value={fmt(P.totalValue)} strong />
|
||||
<button className="btn btn-outline btn-block btn-sm" style={{ marginTop: 16 }} onClick={() => go('transfer')}><Icon name="transfer" size={16} />Transfer Plot Ownership</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* authority status + addresses */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginBottom: 18 }}>
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 17, marginBottom: 14 }}>Authority Status</h3>
|
||||
<div className="col gap-3">
|
||||
{[['Lease Deed', 'LD_COMPLETED', 'file'], ['ATL (Allotment Transfer Letter)', 'ATL_PENDING', 'checkCircle'], ['Authority Verification', 'VERIFIED_AUTH', 'shield'], ['Possession', 'POSS_GRANTED', 'home']].map(([l, code, ic]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '10px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="row gap-3" style={{ fontSize: 13.5, fontWeight: 600 }}><span style={{ width: 32, height: 32, borderRadius: 8, display: 'grid', placeItems: 'center', background: 'var(--bg-2)', color: 'var(--muted)' }}><Icon name={ic} size={16} /></span>{l}</span>
|
||||
<window.StatusBadge code={code} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad anim-fade-up" style={{ animationDelay: '.06s' }}>
|
||||
<div className="row spread" style={{ marginBottom: 14 }}>
|
||||
<h3 style={{ fontSize: 17 }}>Addresses</h3>
|
||||
<button className="btn btn-outline btn-sm" onClick={() => go('profile')}><Icon name="edit" size={15} />Edit</button>
|
||||
</div>
|
||||
{[['Registered Address', D.user.registeredAddress, 'building'], ['Mailing / Current Address', D.user.mailingAddress, 'mail']].map(([title, a, ic]) => (
|
||||
<div key={title} style={{ marginBottom: 14 }}>
|
||||
<div className="row gap-2" style={{ marginBottom: 6 }}><Icon name={ic} size={15} style={{ color: 'var(--blue-600)' }} /><span style={{ fontWeight: 700, fontSize: 13 }}>{title}</span></div>
|
||||
<div className="muted" style={{ fontSize: 13, lineHeight: 1.5, paddingLeft: 23 }}>{a.line1}, {a.line2}<br />{a.city}, {a.state} — {a.pin}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
<div className="card card-pad anim-fade-up">
|
||||
<h3 style={{ fontSize: 17, marginBottom: 20 }}>Property Timeline</h3>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: 22, right: 22, top: 22, height: 2, background: 'var(--line)' }} />
|
||||
{timeline.map((t, i) => {
|
||||
const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)' }[t.color];
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, textAlign: 'center', position: 'relative', padding: '0 6px' }}>
|
||||
<span style={{ width: 44, height: 44, borderRadius: 99, display: 'grid', placeItems: 'center', margin: '0 auto', background: '#fff', border: '2.5px solid ' + cc, color: cc, position: 'relative', zIndex: 1 }}><Icon name={t.icon} size={19} /></span>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, marginTop: 11 }}>{t.title}</div>
|
||||
<div className="faint" style={{ fontSize: 12, marginTop: 2 }}>{t.date}</div>
|
||||
<div className="muted" style={{ fontSize: 12, marginTop: 6, lineHeight: 1.4 }}>{t.desc}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.MyPlot = MyPlot;
|
||||
})();
|
||||
@@ -0,0 +1,164 @@
|
||||
/* Sample Plans & Virtual Tours + First-time Journey ("What would you like to do next?") */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead;
|
||||
const D = window.YEIDA;
|
||||
|
||||
/* ---------------- SAMPLE PLANS & VIRTUAL TOURS ---------------- */
|
||||
const FILTERS = [
|
||||
{ key: 'all', label: 'All Plans' },
|
||||
{ key: 'size', label: 'My Plot Size (300 sq.m)', test: p => p.size === '300 sq.m' },
|
||||
{ key: 'facing', label: 'My Orientation (East)', test: p => p.facing === 'East' },
|
||||
{ key: 'tour', label: '3D Walkthrough', test: p => p.tour },
|
||||
];
|
||||
|
||||
function PlanThumb({ accent }) {
|
||||
return (
|
||||
<svg viewBox="0 0 320 170" width="100%" style={{ display: 'block' }}>
|
||||
<rect width="320" height="170" fill={accent} opacity="0.1" />
|
||||
<rect x="40" y="34" width="240" height="104" fill="#fff" stroke={accent} strokeWidth="2" />
|
||||
{/* room partitions — schematic floor plan */}
|
||||
<line x1="150" y1="34" x2="150" y2="138" stroke={accent} strokeWidth="1.5" opacity=".5" />
|
||||
<line x1="40" y1="86" x2="150" y2="86" stroke={accent} strokeWidth="1.5" opacity=".5" />
|
||||
<line x1="150" y1="92" x2="280" y2="92" stroke={accent} strokeWidth="1.5" opacity=".5" />
|
||||
<line x1="215" y1="34" x2="215" y2="92" stroke={accent} strokeWidth="1.5" opacity=".5" />
|
||||
<rect x="58" y="50" width="26" height="20" rx="2" fill={accent} opacity=".25" />
|
||||
<rect x="170" y="50" width="30" height="22" rx="2" fill={accent} opacity=".25" />
|
||||
<circle cx="245" cy="120" r="14" fill="none" stroke={accent} strokeWidth="1.5" opacity=".4" />
|
||||
{/* compass */}
|
||||
<g transform="translate(292,28)"><circle r="11" fill="#fff" stroke={accent} strokeWidth="1" /><path d="M0,-7 L3,2 L0,0 L-3,2 Z" fill={accent} /><text y="-13" textAnchor="middle" fontSize="7" fontWeight="700" fill={accent}>N</text></g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function SamplePlans({ go, toast }) {
|
||||
const [f, setF] = useState('all');
|
||||
const active = FILTERS.find(x => x.key === f);
|
||||
const list = D.samplePlans.filter(p => !active.test || active.test(p));
|
||||
const [tour, setTour] = useState(null);
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Sample Plans & Virtual Tours" subtitle="Architect-designed plans matched to your plot — explore before you build">
|
||||
<button className="btn btn-outline" onClick={() => go('journey')}><Icon name="chevL" size={17} />What's next</button>
|
||||
<button className="btn btn-primary" onClick={() => go('construction')}><Icon name="construction" size={18} />Start Construction</button>
|
||||
</PageHead>
|
||||
|
||||
{/* hero strip */}
|
||||
<div className="card anim-fade-up" style={{ overflow: 'hidden', marginBottom: 18, display: 'grid', gridTemplateColumns: '1.4fr 1fr' }}>
|
||||
<div style={{ padding: '26px 28px' }}>
|
||||
<span className="badge badge-blue" style={{ marginBottom: 12 }}><Icon name="sparkles" size={13} />Plot-specific guidance</span>
|
||||
<h2 style={{ fontSize: 23 }}>Plans tailored to Plot B-47</h2>
|
||||
<p className="muted" style={{ fontSize: 14.5, lineHeight: 1.6, marginTop: 10, maxWidth: 460 }}>Based on your 300 sq.m east-facing plot in Sector 18, here are orientation- and size-appropriate designs with 3D walkthroughs and virtual tours.</p>
|
||||
<div className="row gap-3" style={{ marginTop: 18, flexWrap: 'wrap' }}>
|
||||
{[['Plot size', '300 sq.m'], ['Facing', 'East'], ['Permissible FAR', '1.5'], ['Max height', '15 m']].map(([l, v]) => (
|
||||
<div key={l} style={{ background: 'var(--bg)', borderRadius: 11, padding: '10px 14px' }}><div className="faint" style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase' }}>{l}</div><div style={{ fontWeight: 700, fontSize: 15, marginTop: 2 }}>{v}</div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: 'radial-gradient(120% 120% at 100% 0%,#16407e,#0b2548)', display: 'grid', placeItems: 'center', padding: 24 }}>
|
||||
<div style={{ textAlign: 'center', color: '#fff' }}>
|
||||
<div style={{ width: 64, height: 64, borderRadius: 16, margin: '0 auto 12px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.2)' }}><Icon name="eye" size={30} /></div>
|
||||
<div style={{ fontWeight: 700, fontSize: 16 }}>Immersive 3D Walkthroughs</div>
|
||||
<div style={{ fontSize: 12.5, color: 'rgba(200,216,240,.8)', marginTop: 4 }}>Tour {D.samplePlans.filter(p => p.tour).length} designs in virtual reality</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* filters */}
|
||||
<div className="row gap-2" style={{ marginBottom: 18, flexWrap: 'wrap' }}>
|
||||
{FILTERS.map(x => <button key={x.key} className={'chip' + (f === x.key ? ' active' : '')} onClick={() => setF(x.key)}>{x.label}</button>)}
|
||||
</div>
|
||||
|
||||
{/* plan grid */}
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 18 }}>
|
||||
{list.map(p => (
|
||||
<div key={p.id} className="card card-hover" style={{ overflow: 'hidden' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<PlanThumb accent={p.accent} />
|
||||
{p.tour && <button onClick={() => setTour(p)} style={{ position: 'absolute', top: 12, right: 12, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 11px', borderRadius: 99, border: 'none', background: 'rgba(8,26,57,.78)', color: '#fff', fontSize: 12, fontWeight: 600, cursor: 'pointer', backdropFilter: 'blur(4px)' }}><Icon name="eye" size={14} />3D Tour</button>}
|
||||
</div>
|
||||
<div style={{ padding: '16px 18px 18px' }}>
|
||||
<div className="row spread">
|
||||
<h4 style={{ fontSize: 17 }}>{p.name}</h4>
|
||||
<span className="badge badge-gray" style={{ fontSize: 11 }}>{p.style}</span>
|
||||
</div>
|
||||
<div className="row gap-4" style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--line-2)', fontSize: 13, color: 'var(--ink-2)', fontWeight: 600 }}>
|
||||
<span className="row gap-2"><Icon name="ruler" size={16} style={{ color: 'var(--muted)' }} />{p.size}</span>
|
||||
<span className="row gap-2"><Icon name="bed" size={16} style={{ color: 'var(--muted)' }} />{p.beds} BHK</span>
|
||||
<span className="row gap-2"><Icon name="building" size={16} style={{ color: 'var(--muted)' }} />{p.floors}</span>
|
||||
</div>
|
||||
<div className="row gap-2" style={{ marginTop: 14 }}>
|
||||
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={() => setTour(p)}><Icon name="eye" size={15} />View Plan</button>
|
||||
<button className="btn btn-ghost btn-sm" style={{ border: '1px solid var(--line)' }} onClick={() => toast && toast('Plan downloaded')}><Icon name="download" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tour && <TourModal plan={tour} onClose={() => setTour(null)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TourModal({ plan, onClose }) {
|
||||
return (
|
||||
<div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(8,26,57,.6)', backdropFilter: 'blur(4px)', display: 'grid', placeItems: 'center', zIndex: 1000, padding: 24 }}>
|
||||
<div onClick={e => e.stopPropagation()} className="anim-pop card" style={{ width: 760, maxWidth: '100%', padding: 0, overflow: 'hidden' }}>
|
||||
<div className="row spread" style={{ padding: '16px 22px', borderBottom: '1px solid var(--line)' }}>
|
||||
<div><div style={{ fontWeight: 700, fontSize: 16 }}>{plan.name}</div><div className="faint" style={{ fontSize: 12.5 }}>{plan.style} · {plan.size} · {plan.facing} facing</div></div>
|
||||
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
||||
</div>
|
||||
<div style={{ position: 'relative', height: 380, background: 'radial-gradient(120% 120% at 50% 0%,#16407e,#081a39)', display: 'grid', placeItems: 'center' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: .3, backgroundImage: 'linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.06) 1px,transparent 1px)', backgroundSize: '40px 40px' }} />
|
||||
<div style={{ textAlign: 'center', color: '#fff', position: 'relative' }}>
|
||||
<div style={{ width: 80, height: 80, borderRadius: 99, margin: '0 auto 16px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.25)', cursor: 'pointer' }}><Icon name="arrowR" size={36} /></div>
|
||||
<div style={{ fontWeight: 700, fontSize: 18 }}>3D Virtual Walkthrough</div>
|
||||
<div style={{ fontSize: 13, color: 'rgba(200,216,240,.8)', marginTop: 6 }}>Immersive tour placeholder — drop in your Matterport / 360° embed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row spread" style={{ padding: '14px 22px' }}>
|
||||
<div className="row gap-2">{['Living', 'Bedrooms', 'Kitchen', 'Terrace'].map((r, i) => <span key={r} className={'chip' + (i === 0 ? ' active' : '')} style={{ fontSize: 12 }}>{r}</span>)}</div>
|
||||
<button className="btn btn-primary btn-sm"><Icon name="download" size={15} />Download Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- JOURNEY: "What would you like to do?" (7 action cards) ---------------- */
|
||||
function Journey({ go }) {
|
||||
const COL = { blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'] };
|
||||
return (
|
||||
<div>
|
||||
<div className="anim-fade-up" style={{ textAlign: 'center', maxWidth: 640, margin: '8px auto 30px' }}>
|
||||
<div style={{ width: 60, height: 60, borderRadius: 16, margin: '0 auto 16px', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff', boxShadow: 'var(--sh-glow)' }}><Icon name="sparkles" size={30} /></div>
|
||||
<h1 style={{ fontSize: 30, fontWeight: 800 }}>What would you like to do?</h1>
|
||||
<p className="muted" style={{ fontSize: 15, marginTop: 8 }}>Your plot is verified, {D.user.firstName}. Choose an action to get started — you can always come back here.</p>
|
||||
</div>
|
||||
|
||||
<div className="stagger" style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 18, maxWidth: 1020, margin: '0 auto' }}>
|
||||
{D.onboardingActions.map((c, i) => {
|
||||
const cc = COL[c.color];
|
||||
const wide = i === 6; // last card spans for 7-card balance
|
||||
return (
|
||||
<button key={c.id} onClick={() => go(c.route)} className="card card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: '24px 22px', border: '1px solid var(--line)', gridColumn: wide ? '2 / 3' : undefined, display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<span style={{ width: 54, height: 54, borderRadius: 14, display: 'grid', placeItems: 'center', background: cc[0], color: cc[1] }}><Icon name={c.icon} size={26} /></span>
|
||||
<div>
|
||||
<div className="row spread"><div style={{ fontWeight: 700, fontSize: 16.5 }}>{c.title}</div><Icon name="arrowR" size={18} style={{ color: 'var(--faint)' }} /></div>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 4, lineHeight: 1.45 }}>{c.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 28 }}>
|
||||
<button className="btn btn-ghost" onClick={() => go('dashboard')}>Skip to dashboard<Icon name="arrowR" size={16} /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { SamplePlans, Journey });
|
||||
})();
|
||||
@@ -0,0 +1,120 @@
|
||||
/* App shell: sidebar + topbar. Renders children content. */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, Logo = window.Logo, Avatar = window.Avatar;
|
||||
const D = window.YEIDA;
|
||||
|
||||
const NAV = [
|
||||
{ k: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
|
||||
{ k: 'plot', label: 'My Plot', icon: 'plot' },
|
||||
{ k: 'myjourney', label: 'My Journey', icon: 'flag' },
|
||||
{ k: 'sampleplans', label: 'Sample Plans', icon: 'grid' },
|
||||
{ k: 'construction', label: 'Construction', icon: 'construction' },
|
||||
{ k: 'payments', label: 'Payments', icon: 'payments', badge: '1' },
|
||||
{ k: 'documents', label: 'Documents', icon: 'documents' },
|
||||
{ k: 'notices', label: 'Notices', icon: 'notices', badge: '2' },
|
||||
{ k: 'sectormaps', label: 'Sector Maps', icon: 'map' },
|
||||
{ k: 'transfer', label: 'Transfer Plot', icon: 'transfer' },
|
||||
{ k: 'assistant', label: 'AI Assistant', icon: 'assistant' },
|
||||
];
|
||||
const NAV2 = [
|
||||
{ k: 'support', label: 'Support', icon: 'support' },
|
||||
{ k: 'profile', label: 'Profile', icon: 'profile' },
|
||||
];
|
||||
|
||||
function Shell({ route, go, children, dark, setDark }) {
|
||||
const [q, setQ] = useState('');
|
||||
const titleMap = {
|
||||
dashboard: 'My Plot Dashboard', plot: 'My Plot', construction: 'Construction',
|
||||
payments: 'Payments', documents: 'Document Center', notices: 'Notices & Announcements',
|
||||
sectormaps: 'Sector Maps', transfer: 'Transfer Plot', assistant: 'AI Assistant',
|
||||
support: 'Support Center', profile: 'Profile', sampleplans: 'Sample Plans & Virtual Tours',
|
||||
journey: 'Get Started', myjourney: 'My Construction Journey',
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg)' }}>
|
||||
{/* ---- Sidebar ---- */}
|
||||
<aside style={{
|
||||
width: 'var(--sidebar-w)', flex: 'none', position: 'sticky', top: 0, height: '100vh',
|
||||
background: 'linear-gradient(185deg,#0b2548,#081a39)', display: 'flex', flexDirection: 'column',
|
||||
padding: '20px 14px', borderRight: '1px solid rgba(255,255,255,.06)',
|
||||
}}>
|
||||
<div style={{ padding: '4px 8px 18px' }}><Logo light size={36} /></div>
|
||||
|
||||
{/* property capsule */}
|
||||
<div style={{
|
||||
margin: '0 2px 16px', padding: '12px 13px', borderRadius: 13,
|
||||
background: 'rgba(255,255,255,.05)', border: '1px solid rgba(255,255,255,.08)',
|
||||
}}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ width: 36, height: 36, borderRadius: 9, flex: 'none', display: 'grid', placeItems: 'center', background: 'linear-gradient(150deg,var(--blue-500),var(--blue-600))', color: '#fff' }}>
|
||||
<Icon name="building" size={19} />
|
||||
</span>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ color: '#fff', fontWeight: 700, fontSize: 13.5 }}>Plot {D.user.plot}, {D.user.sector}</div>
|
||||
<div style={{ color: 'rgba(186,202,226,.7)', fontSize: 11.5 }}>{D.user.size} · {D.user.category}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav style={{ display: 'flex', flexDirection: 'column', gap: 3, overflowY: 'auto', flex: 1 }} className="nav-scroll">
|
||||
{NAV.map(n => (
|
||||
<button key={n.k} className={'nav-item' + (route === n.k ? ' active' : '')} onClick={() => go(n.k)}>
|
||||
<Icon name={n.icon} />
|
||||
<span>{n.label}</span>
|
||||
{n.badge && <span className="nav-badge">{n.badge}</span>}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ height: 1, background: 'rgba(255,255,255,.07)', margin: '10px 8px' }} />
|
||||
{NAV2.map(n => (
|
||||
<button key={n.k} className={'nav-item' + (route === n.k ? ' active' : '')} onClick={() => go(n.k)}>
|
||||
<Icon name={n.icon} /><span>{n.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<button className="nav-item" style={{ color: '#f2a9a9' }} onClick={() => go('welcome')}>
|
||||
<Icon name="logout" /><span>Logout</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
{/* ---- Main ---- */}
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* topbar */}
|
||||
<header style={{
|
||||
height: 'var(--topbar-h)', flex: 'none', position: 'sticky', top: 0, zIndex: 50,
|
||||
background: 'rgba(255,255,255,.85)', backdropFilter: 'blur(12px)', borderBottom: '1px solid var(--line)',
|
||||
display: 'flex', alignItems: 'center', gap: 16, padding: '0 28px',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 17, color: 'var(--ink)' }}>{titleMap[route]}</div>
|
||||
<div className="input-icon" style={{ marginLeft: 14, width: 340, maxWidth: '34vw' }}>
|
||||
<Icon name="search" />
|
||||
<input className="input" style={{ height: 40, background: 'var(--bg)' }} placeholder="Search plots, dues, documents…" value={q} onChange={e => setQ(e.target.value)} />
|
||||
<kbd style={{ position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)', fontSize: 11, fontWeight: 600, color: 'var(--faint)', background: '#fff', border: '1px solid var(--line)', borderRadius: 6, padding: '2px 6px' }}>⌘K</kbd>
|
||||
</div>
|
||||
<div className="row gap-2" style={{ marginLeft: 'auto' }}>
|
||||
<button className="icon-btn" title="Help" onClick={() => go('support')}><Icon name="support" /></button>
|
||||
<button className="icon-btn" title="Language"><Icon name="globe" /></button>
|
||||
<button className="icon-btn" title="Notifications" onClick={() => go('notices')}><Icon name="bell" /><span className="notif-dot" /></button>
|
||||
<div style={{ width: 1, height: 26, background: 'var(--line)', margin: '0 4px' }} />
|
||||
<button onClick={() => go('profile')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '5px 7px 5px 5px', borderRadius: 99, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer' }}>
|
||||
<Avatar initials={D.user.initials} size={32} />
|
||||
<div style={{ textAlign: 'left', lineHeight: 1.1, paddingRight: 4 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13 }}>{D.user.name}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)' }}>{D.user.relationship} · {D.user.isAllottee ? 'Allottee' : 'Representative'}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style={{ flex: 1, overflow: 'auto', padding: '28px 36px 48px' }} key={route}>
|
||||
<div className="anim-fade-in" style={{ maxWidth: 1600, margin: '0 auto' }}>{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
{route !== 'assistant' && window.FloatingAssistant && React.createElement(window.FloatingAssistant, { go })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Shell = Shell;
|
||||
})();
|
||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,269 @@
|
||||
/* ============================================================
|
||||
YEIDA Portal — Design System
|
||||
Premium government-tech: navy / blue / emerald on clean white
|
||||
============================================================ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700;800&family=Public+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap');
|
||||
|
||||
:root {
|
||||
/* Brand */
|
||||
--navy-900: #081a39;
|
||||
--navy-800: #0b2548;
|
||||
--navy-700: #103263;
|
||||
--navy-600: #16407e;
|
||||
--blue-600: #1d5fbf;
|
||||
--blue-700: #16407e;
|
||||
--blue-500: #2670e6;
|
||||
--blue-400: #5a93f0;
|
||||
--blue-100: #d8e6fb;
|
||||
--blue-50: #eef4ff;
|
||||
--emerald-700: #0a7d56;
|
||||
--emerald-600: #0f9d6b;
|
||||
--emerald-500: #16b67e;
|
||||
--emerald-100: #cdeede;
|
||||
--emerald-50: #e8f7f0;
|
||||
--amber-600: #c77a08;
|
||||
--amber-500: #e8930c;
|
||||
--amber-50: #fdf3e2;
|
||||
--red-600: #d23f3f;
|
||||
--red-500: #e85656;
|
||||
--red-50: #fdedee;
|
||||
|
||||
/* Neutrals */
|
||||
--ink: #0e1b2e;
|
||||
--ink-2: #2b3a4f;
|
||||
--muted: #5d6f87;
|
||||
--faint: #8a99 ac;
|
||||
--faint: #8a99ac;
|
||||
--line: #e4eaf1;
|
||||
--line-2: #eef2f7;
|
||||
--bg: #f4f7fb;
|
||||
--bg-2: #eef3f9;
|
||||
--card: #ffffff;
|
||||
--ink-on-navy: #eaf1fb;
|
||||
|
||||
/* Type */
|
||||
--font-display: 'Schibsted Grotesk', system-ui, sans-serif;
|
||||
--font-ui: 'Public Sans', system-ui, -apple-system, sans-serif;
|
||||
|
||||
/* Radii */
|
||||
--r-sm: 8px;
|
||||
--r-md: 11px;
|
||||
--r-lg: 16px;
|
||||
--r-xl: 22px;
|
||||
--r-pill: 999px;
|
||||
|
||||
/* Shadows */
|
||||
--sh-xs: 0 1px 2px rgba(13,38,76,.05);
|
||||
--sh-sm: 0 1px 2px rgba(13,38,76,.05), 0 4px 12px -4px rgba(13,38,76,.06);
|
||||
--sh-md: 0 8px 24px -8px rgba(13,38,76,.13), 0 2px 6px -2px rgba(13,38,76,.05);
|
||||
--sh-lg: 0 24px 48px -16px rgba(11,37,72,.26), 0 6px 16px -8px rgba(11,37,72,.16);
|
||||
--sh-glow: 0 10px 28px -8px rgba(29,95,191,.42);
|
||||
|
||||
--sidebar-w: 252px;
|
||||
--topbar-h: 64px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
#root { min-height: 100vh; }
|
||||
|
||||
h1,h2,h3,h4,h5 { font-family: var(--font-display); margin: 0; color: var(--ink); letter-spacing: -.015em; }
|
||||
p { margin: 0; }
|
||||
button { font-family: inherit; cursor: pointer; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
input, select, textarea { font-family: inherit; }
|
||||
::selection { background: var(--blue-100); }
|
||||
|
||||
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
|
||||
|
||||
/* Scrollbars */
|
||||
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
*::-webkit-scrollbar-thumb { background: #cdd7e4; border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; }
|
||||
*::-webkit-scrollbar-thumb:hover { background: #b4c2d4; background-clip: padding-box; }
|
||||
*::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
|
||||
font-family: var(--font-ui); font-weight: 600; font-size: 14.5px;
|
||||
padding: 11px 18px; border-radius: var(--r-md); border: 1px solid transparent;
|
||||
background: transparent; color: var(--ink); white-space: nowrap;
|
||||
transition: transform .12s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease, color .15s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn svg { width: 18px; height: 18px; flex: none; }
|
||||
|
||||
.btn-primary {
|
||||
background: #0077FF;
|
||||
color: #fff; box-shadow: 0 10px 24px -8px rgba(0,119,255,.5);
|
||||
border-radius: var(--r-pill) !important;
|
||||
}
|
||||
.btn-primary:hover { box-shadow: 0 14px 32px -8px rgba(0,119,255,.6); filter: brightness(1.05); }
|
||||
|
||||
.btn-navy { background: linear-gradient(180deg, var(--navy-700), var(--navy-800)); color: #fff; box-shadow: 0 10px 24px -10px rgba(8,26,57,.6); }
|
||||
.btn-navy:hover { filter: brightness(1.1); }
|
||||
|
||||
.btn-emerald { background: linear-gradient(180deg, var(--emerald-500), var(--emerald-600)); color: #fff; box-shadow: 0 10px 26px -10px rgba(15,157,107,.55); }
|
||||
.btn-emerald:hover { filter: brightness(1.05); }
|
||||
|
||||
.btn-outline { background: #fff; border-color: var(--line); color: var(--ink-2); box-shadow: var(--sh-xs); }
|
||||
.btn-outline:hover { border-color: #c9d6e6; background: #fbfdff; }
|
||||
|
||||
.btn-ghost { color: var(--muted); }
|
||||
.btn-ghost:hover { background: var(--bg-2); color: var(--ink); }
|
||||
|
||||
.btn-sm { padding: 8px 13px; font-size: 13.5px; border-radius: var(--r-sm); }
|
||||
.btn-sm svg { width: 16px; height: 16px; }
|
||||
.btn-lg { padding: 14px 24px; font-size: 16px; }
|
||||
.btn-block { width: 100%; }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.card {
|
||||
background: var(--card); border: 1px solid var(--line);
|
||||
border-radius: var(--r-lg); box-shadow: var(--sh-sm);
|
||||
}
|
||||
.card-pad { padding: 22px; }
|
||||
.card-hover { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; }
|
||||
.card-hover:hover { box-shadow: var(--sh-md); border-color: #d6e0ee; transform: translateY(-2px); }
|
||||
|
||||
/* ---------- Badges / chips ---------- */
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 12.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-pill);
|
||||
line-height: 1.3; white-space: nowrap;
|
||||
}
|
||||
.badge .dot { width: 6px; height: 6px; border-radius: 99px; background: currentColor; }
|
||||
.badge-green { background: var(--emerald-50); color: var(--emerald-700); }
|
||||
.badge-blue { background: var(--blue-50); color: var(--blue-600); }
|
||||
.badge-amber { background: var(--amber-50); color: var(--amber-600); }
|
||||
.badge-red { background: var(--red-50); color: var(--red-600); }
|
||||
.badge-gray { background: var(--bg-2); color: var(--muted); }
|
||||
.badge-navy { background: #e9eefb; color: var(--navy-700); }
|
||||
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
|
||||
font-size: 13.5px; font-weight: 500; padding: 8px 14px; border-radius: var(--r-pill);
|
||||
background: #fff; border: 1px solid var(--line); color: var(--ink-2);
|
||||
transition: all .15s ease;
|
||||
}
|
||||
.chip:hover { border-color: #c9d6e6; background: #fbfdff; }
|
||||
.chip.active { background: var(--navy-800); color: #fff; border-color: var(--navy-800); }
|
||||
|
||||
/* ---------- Inputs ---------- */
|
||||
.field { display: flex; flex-direction: column; gap: 7px; }
|
||||
.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
|
||||
.input, .select {
|
||||
width: 100%; padding: 12px 14px; font-size: 14.5px; color: var(--ink);
|
||||
background: #fff; border: 1.5px solid var(--line); border-radius: var(--r-md);
|
||||
transition: border-color .15s ease, box-shadow .15s ease; outline: none;
|
||||
}
|
||||
.input::placeholder { color: #9aa9bc; }
|
||||
.input:focus, .select:focus { border-color: var(--blue-400); box-shadow: 0 0 0 4px rgba(38,112,230,.12); }
|
||||
.input-icon { position: relative; }
|
||||
.input-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #9aa9bc; }
|
||||
.input-icon .input { padding-left: 42px; }
|
||||
|
||||
/* ---------- Sidebar nav ---------- */
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%;
|
||||
padding: 9.5px 12px; border-radius: var(--r-md); border: none; background: transparent;
|
||||
color: #b9c7dd; font-size: 14.5px; font-weight: 500; text-align: left;
|
||||
transition: background .15s ease, color .15s ease; position: relative;
|
||||
}
|
||||
.nav-item svg { width: 19px; height: 19px; flex: none; }
|
||||
.nav-item:hover { background: rgba(255,255,255,.06); color: #eef3fb; }
|
||||
.nav-item.active { background: rgba(255,255,255,.1); color: #fff; font-weight: 600; }
|
||||
.nav-item.active::before {
|
||||
content: ''; position: absolute; left: -12px; top: 50%; transform: translateY(-50%);
|
||||
width: 3.5px; height: 22px; border-radius: 99px; background: var(--emerald-500);
|
||||
}
|
||||
.nav-badge { margin-left: auto; font-size: 11px; font-weight: 700; background: var(--emerald-500); color: #042e1f; padding: 1px 7px; border-radius: 99px; }
|
||||
|
||||
/* ---------- Misc ---------- */
|
||||
.divider { height: 1px; background: var(--line); border: none; }
|
||||
.kicker { font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--blue-600); }
|
||||
.muted { color: var(--muted); }
|
||||
.faint { color: #93a2b6; }
|
||||
|
||||
.avatar { border-radius: 99px; object-fit: cover; background: linear-gradient(135deg,var(--blue-500),var(--navy-700)); display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; }
|
||||
|
||||
.progress { height: 8px; background: var(--bg-2); border-radius: 99px; overflow: hidden; }
|
||||
.progress > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg,var(--emerald-500),var(--emerald-600)); transition: width .6s cubic-bezier(.2,.7,.3,1); }
|
||||
|
||||
/* Animations are OPT-IN via body.anim-on (added by JS only when the document is visible).
|
||||
Default/hidden/print state shows content fully so nothing ever sticks at opacity:0. */
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes popIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
|
||||
@keyframes shimmer { 100% { transform: translateX(100%); } }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
|
||||
body.anim-on .anim-fade-up { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; }
|
||||
body.anim-on .anim-fade-in { animation: fadeIn .4s ease both; }
|
||||
body.anim-on .anim-pop { animation: popIn .35s cubic-bezier(.2,.8,.3,1) both; }
|
||||
body.anim-on .stagger > * { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; }
|
||||
.stagger > *:nth-child(1){animation-delay:.03s}
|
||||
.stagger > *:nth-child(2){animation-delay:.08s}
|
||||
.stagger > *:nth-child(3){animation-delay:.13s}
|
||||
.stagger > *:nth-child(4){animation-delay:.18s}
|
||||
.stagger > *:nth-child(5){animation-delay:.23s}
|
||||
.stagger > *:nth-child(6){animation-delay:.28s}
|
||||
.stagger > *:nth-child(7){animation-delay:.33s}
|
||||
.stagger > *:nth-child(8){animation-delay:.38s}
|
||||
|
||||
/* Tables */
|
||||
.tbl { width: 100%; border-collapse: collapse; }
|
||||
.tbl th { text-align: left; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: 12px 16px; border-bottom: 1px solid var(--line); }
|
||||
.tbl td { padding: 14px 16px; border-bottom: 1px solid var(--line-2); font-size: 14px; color: var(--ink-2); }
|
||||
.tbl tbody tr { transition: background .12s ease; }
|
||||
.tbl tbody tr:hover { background: #fafcfe; }
|
||||
.tbl tbody tr:last-child td { border-bottom: none; }
|
||||
|
||||
/* Utility layout */
|
||||
.row { display: flex; align-items: center; }
|
||||
.col { display: flex; flex-direction: column; }
|
||||
.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}.gap-6{gap:24px}
|
||||
.grow{flex:1}.center{align-items:center;justify-content:center}
|
||||
.spread{justify-content:space-between}
|
||||
|
||||
/* Tooltip-ish */
|
||||
.icon-btn {
|
||||
width: 38px; height: 38px; border-radius: var(--r-md); border: 1px solid transparent;
|
||||
display: inline-flex; align-items: center; justify-content: center; background: transparent; color: var(--muted);
|
||||
transition: background .15s, color .15s, border-color .15s; position: relative;
|
||||
}
|
||||
.icon-btn:hover { background: var(--bg-2); color: var(--ink); }
|
||||
.icon-btn svg { width: 20px; height: 20px; }
|
||||
.notif-dot { position: absolute; top: 7px; right: 8px; width: 8px; height: 8px; border-radius: 99px; background: var(--red-500); border: 2px solid #fff; }
|
||||
|
||||
/* ---------- Dashboard polish ---------- */
|
||||
/* premium stat/status card: hairline top accent + refined hover */
|
||||
.stat-card { position: relative; overflow: hidden; }
|
||||
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--accent, var(--blue-500)); opacity: .9; }
|
||||
.stat-card { transition: box-shadow .22s ease, transform .22s ease, border-color .22s ease; }
|
||||
.stat-card:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; }
|
||||
|
||||
/* section eyebrow heading */
|
||||
.sec-eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--blue-600); }
|
||||
.sec-eyebrow::before { content: ''; width: 22px; height: 2px; border-radius: 2px; background: var(--blue-500); }
|
||||
|
||||
/* refined module switch card */
|
||||
.mod-switch { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease, background .2s ease; }
|
||||
.mod-switch:hover { transform: translateY(-2px); box-shadow: var(--sh-md); }
|
||||
|
||||
/* subtle metric tile */
|
||||
.metric-tile { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; }
|
||||
.metric-tile:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; }
|
||||
|
||||
/* table refinement on dashboard */
|
||||
.tbl.tbl-clean th { background: var(--bg); }
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/* Transfer Plot module */
|
||||
(function () {
|
||||
const { useState } = React;
|
||||
const Icon = window.Icon, PageHead = window.PageHead;
|
||||
const D = window.YEIDA, fmt = D.fmtINR;
|
||||
|
||||
const STEPS = ['Eligibility', 'Requirements', 'Fee Calculator', 'Application', 'Review'];
|
||||
|
||||
function Transfer({ go, toast }) {
|
||||
const [step, setStep] = useState(0);
|
||||
const feeTotal = D.transferFees.reduce((s, f) => s + f.amount, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHead title="Transfer Plot" subtitle="Transfer ownership of Plot B-47, Sector 18" />
|
||||
|
||||
{/* progress */}
|
||||
<div className="card anim-fade-up" style={{ padding: '16px 24px', marginBottom: 18, display: 'flex', alignItems: 'center', gap: 8, overflowX: 'auto' }}>
|
||||
{STEPS.map((s, i) => (
|
||||
<React.Fragment key={s}>
|
||||
<button onClick={() => setStep(i)} className="row gap-2" style={{ background: 'none', border: 'none', cursor: 'pointer', flex: 'none' }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 99, display: 'grid', placeItems: 'center', fontWeight: 700, fontSize: 13, background: i < step ? 'var(--emerald-500)' : i === step ? 'var(--blue-500)' : 'var(--bg-2)', color: i <= step ? '#fff' : 'var(--faint)' }}>{i < step ? <Icon name="check" size={15} /> : i + 1}</span>
|
||||
<span style={{ fontWeight: i === step ? 700 : 500, fontSize: 13.5, color: i === step ? 'var(--ink)' : 'var(--muted)' }}>{s}</span>
|
||||
</button>
|
||||
{i < STEPS.length - 1 && <span style={{ flex: 1, minWidth: 20, height: 2, background: i < step ? 'var(--emerald-500)' : 'var(--line)' }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div key={step} className="anim-fade-up">
|
||||
{step === 0 && <Eligibility next={() => setStep(1)} />}
|
||||
{step === 1 && <Requirements />}
|
||||
{step === 2 && <FeeCalc fees={D.transferFees} total={feeTotal} />}
|
||||
{step === 3 && <Application />}
|
||||
{step === 4 && <Review total={feeTotal} go={go} toast={toast} />}
|
||||
</div>
|
||||
|
||||
<div className="row spread" style={{ marginTop: 18 }}>
|
||||
<button className="btn btn-outline" disabled={step === 0} style={{ opacity: step === 0 ? .4 : 1 }} onClick={() => setStep(s => Math.max(0, s - 1))}><Icon name="chevL" size={17} />Back</button>
|
||||
{step < 4 && <button className="btn btn-primary" onClick={() => setStep(s => s + 1)}>Continue<Icon name="chevR" size={17} /></button>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Eligibility({ next }) {
|
||||
const checks = [
|
||||
['5-year lock-in period completed', true, 'Allotted Mar 2019 · 7 years held'],
|
||||
['No outstanding transfer restrictions', true, 'Clear'],
|
||||
['Lease deed registered', true, 'Registered Sep 2019'],
|
||||
['Pending dues cleared before transfer', false, '₹1,47,585 to be cleared first'],
|
||||
];
|
||||
const eligible = true;
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 4 }}>Eligibility Check</h3>
|
||||
<p className="muted" style={{ fontSize: 14, marginBottom: 18 }}>We verified your plot against transfer policy 2026.</p>
|
||||
<div className="col gap-2">
|
||||
{checks.map(([l, ok, note], i) => (
|
||||
<div key={i} className="row spread" style={{ padding: '13px 15px', borderRadius: 12, border: '1px solid var(--line)' }}>
|
||||
<div className="row gap-3">
|
||||
<span style={{ color: ok ? 'var(--emerald-600)' : 'var(--amber-600)' }}><Icon name={ok ? 'checkCircle' : 'alert'} size={20} /></span>
|
||||
<div><div style={{ fontWeight: 600, fontSize: 14 }}>{l}</div><div className="faint" style={{ fontSize: 12 }}>{note}</div></div>
|
||||
</div>
|
||||
<span className={'badge badge-' + (ok ? 'green' : 'amber')}>{ok ? 'Passed' : 'Action needed'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad" style={{ background: 'linear-gradient(170deg,#0b2548,#081a39)', color: '#fff', alignSelf: 'start' }}>
|
||||
<span style={{ width: 56, height: 56, borderRadius: 14, display: 'grid', placeItems: 'center', background: 'var(--emerald-500)', color: '#fff', marginBottom: 16 }}><Icon name="check" size={28} /></span>
|
||||
<h3 style={{ color: '#fff', fontSize: 20 }}>You're eligible to transfer</h3>
|
||||
<p style={{ fontSize: 13.5, color: 'rgba(200,216,240,.8)', marginTop: 8, lineHeight: 1.55 }}>Your plot meets the core eligibility criteria. Clear pending dues to proceed to fee payment and application.</p>
|
||||
<button className="btn btn-emerald btn-block" style={{ marginTop: 18 }} onClick={next}>Proceed to Requirements<Icon name="arrowR" size={17} /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Requirements() {
|
||||
const reqs = [
|
||||
['Transferor (Seller) Documents', ['PAN & Aadhaar', 'Original allotment letter', 'Latest dues clearance', 'No-objection affidavit']],
|
||||
['Transferee (Buyer) Documents', ['PAN & Aadhaar', 'Photographs (2)', 'Address proof', 'Acceptance undertaking']],
|
||||
['Joint / Common', ['Registered transfer deed', 'Sale agreement', 'Stamp duty receipt', 'Transfer fee challan']],
|
||||
];
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 18 }}>
|
||||
{reqs.map(([title, items], i) => (
|
||||
<div key={i} className="card card-pad">
|
||||
<h4 style={{ fontSize: 15.5, marginBottom: 14 }}>{title}</h4>
|
||||
<div className="col gap-3">
|
||||
{items.map(it => <div key={it} className="row gap-3" style={{ fontSize: 13.5, color: 'var(--ink-2)' }}><span style={{ width: 22, height: 22, borderRadius: 6, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--emerald-50)', color: 'var(--emerald-600)' }}><Icon name="check" size={13} /></span>{it}</div>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeeCalc({ fees, total }) {
|
||||
const [value, setValue] = useState(7725000);
|
||||
const transfer = Math.round(value * 0.025);
|
||||
const computed = transfer + 5000 + 2500 + 1350;
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 14 }}>Fee Calculator</h3>
|
||||
<div className="field" style={{ marginBottom: 18 }}>
|
||||
<label>Declared Transfer Value</label>
|
||||
<div className="input-icon"><Icon name="rupee" /><input className="input num" type="number" value={value} onChange={e => setValue(+e.target.value || 0)} /></div>
|
||||
<input type="range" min={5000000} max={12000000} step={50000} value={value} onChange={e => setValue(+e.target.value)} style={{ accentColor: 'var(--blue-600)', marginTop: 10 }} />
|
||||
<div className="row spread faint" style={{ fontSize: 11.5 }}><span>₹50 L</span><span>₹1.2 Cr</span></div>
|
||||
</div>
|
||||
<div style={{ background: 'var(--blue-50)', borderRadius: 13, padding: '16px 18px' }}>
|
||||
<div className="muted" style={{ fontSize: 12.5, fontWeight: 600 }}>Transfer Charges (2.5%)</div>
|
||||
<div className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 28, color: 'var(--blue-600)', marginTop: 4 }}>{fmt(transfer)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 14 }}>Fee Breakdown</h3>
|
||||
<div className="col">
|
||||
{[['Transfer Charges (2.5% of value)', transfer], ['Processing Fee', 5000], ['Documentation Charges', 2500], ['GST (18% on fees)', 1350]].map(([l, a], i) => (
|
||||
<div key={i} className="row spread" style={{ padding: '13px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="muted" style={{ fontSize: 13.5 }}>{l}</span><span className="num" style={{ fontWeight: 700 }}>{fmt(a)}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="row spread" style={{ padding: '16px 0 4px' }}>
|
||||
<span style={{ fontWeight: 800, fontSize: 16 }}>Total Payable</span>
|
||||
<span className="num" style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 24, color: 'var(--emerald-700)' }}>{fmt(computed)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-block" style={{ marginTop: 16 }}><Icon name="receipt" size={17} />Generate Transfer Challan</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Application() {
|
||||
const fields = [
|
||||
['Transferee Full Name', 'user', 'Enter buyer name'],
|
||||
['Transferee Mobile', 'phone', '+91'],
|
||||
['Transferee Email', 'mail', 'buyer@email.com'],
|
||||
['Transferee PAN', 'file', 'ABCDE1234F'],
|
||||
['Relationship to Transferor', 'user', 'e.g. Third party'],
|
||||
['Reason for Transfer', 'info', 'e.g. Sale'],
|
||||
];
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 16 }}>Transfer Application</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
{fields.map(([l, ic, ph]) => (
|
||||
<div key={l} className="field"><label>{l}</label><div className="input-icon"><Icon name={ic} /><input className="input" placeholder={ph} /></div></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-pad">
|
||||
<h4 style={{ fontSize: 15.5, marginBottom: 14 }}>Upload Documents</h4>
|
||||
<div style={{ border: '2px dashed var(--blue-100)', borderRadius: 13, padding: '26px 16px', textAlign: 'center', background: 'var(--blue-50)', marginBottom: 16 }}>
|
||||
<Icon name="upload" size={26} style={{ color: 'var(--blue-600)' }} />
|
||||
<div style={{ fontWeight: 700, fontSize: 14, marginTop: 8 }}>Upload transfer deed & KYC</div>
|
||||
<div className="faint" style={{ fontSize: 12 }}>PDF · up to 25 MB</div>
|
||||
</div>
|
||||
{['Transfer Deed', 'Buyer KYC', 'Sale Agreement'].map(d => (
|
||||
<div key={d} className="row spread" style={{ padding: '10px 0', borderBottom: '1px solid var(--line-2)' }}>
|
||||
<span className="row gap-2" style={{ fontSize: 13.5 }}><Icon name="file" size={16} style={{ color: 'var(--muted)' }} />{d}</span>
|
||||
<button className="btn btn-outline btn-sm">Upload</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Review({ total, go, toast }) {
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
if (submitted) return (
|
||||
<div className="card card-pad" style={{ textAlign: 'center', padding: '44px 30px' }}>
|
||||
<div style={{ width: 74, height: 74, borderRadius: 99, margin: '0 auto 16px', display: 'grid', placeItems: 'center', background: 'var(--emerald-500)', color: '#fff', boxShadow: '0 14px 30px -10px rgba(15,157,107,.6)' }} className="anim-pop"><Icon name="check" size={38} /></div>
|
||||
<h3 style={{ fontSize: 23 }}>Transfer application submitted</h3>
|
||||
<p className="muted" style={{ fontSize: 14.5, maxWidth: 460, margin: '10px auto 0', lineHeight: 1.6 }}>Application TR-2026-18-0472 is now with the Authority for review. You can track approval status from your dashboard.</p>
|
||||
<div className="row gap-3 center" style={{ marginTop: 22 }}><button className="btn btn-outline" onClick={() => go('dashboard')}>Go to Dashboard</button><button className="btn btn-primary" onClick={() => go('documents')}>View Documents</button></div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
|
||||
<div className="card card-pad">
|
||||
<h3 style={{ fontSize: 19, marginBottom: 16 }}>Review & Submit</h3>
|
||||
{[['Plot', 'B-47, Sector 18'], ['Transferor', 'Rajeev Malhotra'], ['Transferee', 'New Allottee'], ['Transfer Value', fmt(7725000)], ['Total Fees', fmt(201975)], ['Documents', '3 uploaded']].map(([l, v]) => (
|
||||
<div key={l} className="row spread" style={{ padding: '13px 0', borderBottom: '1px solid var(--line-2)' }}><span className="muted" style={{ fontSize: 13.5 }}>{l}</span><span style={{ fontWeight: 700, fontSize: 14 }}>{v}</span></div>
|
||||
))}
|
||||
<label className="row gap-3" style={{ marginTop: 16, fontSize: 13, color: 'var(--muted)', cursor: 'pointer' }}><input type="checkbox" defaultChecked style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />I declare the above information is true and complete.</label>
|
||||
</div>
|
||||
<div className="card card-pad" style={{ alignSelf: 'start', background: 'var(--bg)' }}>
|
||||
<h4 style={{ fontSize: 15.5, marginBottom: 6 }}>Approval Tracking</h4>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 16 }}>After submission your application moves through:</p>
|
||||
{['Document verification', 'Dues clearance check', 'Authority approval', 'Transfer completion'].map((s, i) => (
|
||||
<div key={s} className="row gap-3" style={{ padding: '8px 0', fontSize: 13.5 }}><span style={{ width: 24, height: 24, borderRadius: 99, flex: 'none', display: 'grid', placeItems: 'center', background: 'var(--bg-2)', color: 'var(--faint)', fontWeight: 700, fontSize: 12 }}>{i + 1}</span>{s}</div>
|
||||
))}
|
||||
<button className="btn btn-primary btn-block btn-lg" style={{ marginTop: 16 }} onClick={() => { setSubmitted(true); toast && toast('Transfer application submitted'); }}>Submit Application</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.Transfer = Transfer;
|
||||
})();
|
||||
@@ -0,0 +1,143 @@
|
||||
/* Shared UI primitives — Logo, charts, helpers. Exposed on window. */
|
||||
(function () {
|
||||
const { useState, useEffect, useRef } = React;
|
||||
const Icon = window.Icon;
|
||||
|
||||
/* YEIDA logo mark */
|
||||
function Logo({ size = 38, light = false, showText = true, sub = true }) {
|
||||
const fg = light ? '#fff' : 'var(--navy-800)';
|
||||
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 11 } },
|
||||
React.createElement('img', {
|
||||
src: 'assets/yeida-logo.png', alt: 'YEIDA',
|
||||
style: {
|
||||
width: size * 1.08, height: size * 1.08, flex: 'none', objectFit: 'contain', display: 'block',
|
||||
filter: light ? 'drop-shadow(0 2px 6px rgba(0,0,0,.35))' : 'drop-shadow(0 2px 5px rgba(16,49,99,.25))',
|
||||
}
|
||||
}),
|
||||
showText && React.createElement('div', { style: { lineHeight: 1.05 } },
|
||||
React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.46, color: fg, letterSpacing: '-.01em' } }, 'YEIDA'),
|
||||
sub && React.createElement('div', { style: { fontSize: size * 0.235, fontWeight: 600, color: light ? 'rgba(234,241,251,.7)' : 'var(--muted)', letterSpacing: '.02em', marginTop: 1 } }, 'Yamuna Expressway Authority')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
function Avatar({ initials = 'RM', size = 38, style }) {
|
||||
return React.createElement('div', {
|
||||
className: 'avatar',
|
||||
style: { width: size, height: size, fontSize: size * 0.4, ...style }
|
||||
}, initials);
|
||||
}
|
||||
|
||||
/* Line chart with gradient fill + animated draw */
|
||||
function LineChart({ data, w = 520, h = 150, color = 'var(--blue-500)', fill = 'rgba(38,112,230,.12)', pad = 8 }) {
|
||||
const max = Math.max(...data) * 1.12, min = Math.min(...data) * 0.85;
|
||||
const range = max - min || 1;
|
||||
const stepX = (w - pad * 2) / (data.length - 1);
|
||||
const pts = data.map((d, i) => [pad + i * stepX, pad + (h - pad * 2) * (1 - (d - min) / range)]);
|
||||
const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
|
||||
const area = `${line} L${pts[pts.length - 1][0]},${h - pad} L${pts[0][0]},${h - pad} Z`;
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const len = ref.current.getTotalLength();
|
||||
ref.current.style.strokeDasharray = len;
|
||||
ref.current.style.strokeDashoffset = len;
|
||||
ref.current.getBoundingClientRect();
|
||||
ref.current.style.transition = 'stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1)';
|
||||
ref.current.style.strokeDashoffset = 0;
|
||||
}
|
||||
}, []);
|
||||
return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, preserveAspectRatio: 'none', style: { display: 'block' } },
|
||||
React.createElement('path', { d: area, fill }),
|
||||
React.createElement('path', { ref, d: line, fill: 'none', stroke: color, strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' }),
|
||||
pts.map((p, i) => React.createElement('circle', { key: i, cx: p[0], cy: p[1], r: i === pts.length - 1 ? 4 : 0, fill: '#fff', stroke: color, strokeWidth: 2.5 }))
|
||||
);
|
||||
}
|
||||
|
||||
/* Mini bar chart */
|
||||
function BarChart({ data, w = 240, h = 70, color = 'var(--emerald-500)', gap = 5 }) {
|
||||
const max = Math.max(...data) || 1;
|
||||
const bw = (w - gap * (data.length - 1)) / data.length;
|
||||
return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, style: { display: 'block' } },
|
||||
data.map((d, i) => {
|
||||
const bh = Math.max(3, (d / max) * h);
|
||||
return React.createElement('rect', {
|
||||
key: i, x: i * (bw + gap), y: h - bh, width: bw, height: bh, rx: 3,
|
||||
fill: i === data.length - 1 ? color : 'var(--blue-100)',
|
||||
style: { animation: `growBar .6s ${i * 0.05}s both` }
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* Donut progress */
|
||||
function Donut({ value, size = 116, stroke = 12, color = 'var(--emerald-500)', track = 'var(--bg-2)', label, sub }) {
|
||||
const r = (size - stroke) / 2, c = 2 * Math.PI * r;
|
||||
const target = c - (value / 100) * c;
|
||||
const [off, setOff] = useState(typeof document !== 'undefined' && document.hidden ? target : c);
|
||||
useEffect(() => { const t = setTimeout(() => setOff(target), 100); return () => clearTimeout(t); }, [target]);
|
||||
return React.createElement('div', { style: { position: 'relative', width: size, height: size } },
|
||||
React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } },
|
||||
React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: track, strokeWidth: stroke }),
|
||||
React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeDasharray: c, strokeDashoffset: off, style: { transition: 'stroke-dashoffset 1s cubic-bezier(.3,.7,.3,1)' } })
|
||||
),
|
||||
React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } },
|
||||
React.createElement('div', null,
|
||||
React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.26, color: 'var(--ink)' } }, label != null ? label : value + '%'),
|
||||
sub && React.createElement('div', { style: { fontSize: 11.5, color: 'var(--muted)', fontWeight: 600, marginTop: 1 } }, sub)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Section header with kicker */
|
||||
function PageHead({ title, subtitle, children }) {
|
||||
return React.createElement('div', { className: 'row spread', style: { marginBottom: 24, gap: 16, flexWrap: 'wrap' } },
|
||||
React.createElement('div', null,
|
||||
React.createElement('h1', { style: { fontSize: 27, fontWeight: 800 } }, title),
|
||||
subtitle && React.createElement('p', { className: 'muted', style: { fontSize: 14.5, marginTop: 5 } }, subtitle)
|
||||
),
|
||||
children && React.createElement('div', { className: 'row gap-3' }, children)
|
||||
);
|
||||
}
|
||||
|
||||
/* Toast host */
|
||||
function useToast() {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
const push = (msg, kind = 'success') => {
|
||||
const id = Math.random();
|
||||
setToasts(t => [...t, { id, msg, kind }]);
|
||||
setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200);
|
||||
};
|
||||
const node = React.createElement('div', { style: { position: 'fixed', bottom: 24, right: 24, display: 'flex', flexDirection: 'column', gap: 10, zIndex: 9000 } },
|
||||
toasts.map(t => React.createElement('div', {
|
||||
key: t.id, className: 'anim-pop',
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: 11, padding: '13px 17px', borderRadius: 12,
|
||||
background: 'var(--navy-800)', color: '#fff', boxShadow: 'var(--sh-lg)', fontSize: 14, fontWeight: 500, maxWidth: 360,
|
||||
}
|
||||
},
|
||||
React.createElement('span', { style: { display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 99, background: t.kind === 'success' ? 'var(--emerald-500)' : 'var(--blue-500)', flex: 'none' } },
|
||||
React.createElement(Icon, { name: t.kind === 'success' ? 'check' : 'info', size: 14 })),
|
||||
t.msg
|
||||
))
|
||||
);
|
||||
return [node, push];
|
||||
}
|
||||
|
||||
/* Authority status badge — maps a backend status code to a labelled, coloured chip */
|
||||
function StatusBadge({ code, size, withIcon = true }) {
|
||||
const D = window.YEIDA;
|
||||
const s = (D && D.STATUS_CODES && D.STATUS_CODES[code]) || { label: code, tone: 'gray', icon: 'info' };
|
||||
return React.createElement('span', { className: 'badge badge-' + s.tone, style: size === 'lg' ? { fontSize: 13, padding: '6px 12px' } : undefined },
|
||||
withIcon && React.createElement(Icon, { name: s.icon, size: 13 }), s.label);
|
||||
}
|
||||
|
||||
Object.assign(window, { Logo, Avatar, LineChart, BarChart, Donut, PageHead, useToast, StatusBadge });
|
||||
})();
|
||||
|
||||
/* keyframes for bars */
|
||||
const __s = document.createElement('style');
|
||||
__s.textContent = '@keyframes growBar{from{transform:scaleY(0);transform-origin:bottom}to{transform:scaleY(1);transform-origin:bottom}}';
|
||||
document.head.appendChild(__s);
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 436 KiB |
@@ -0,0 +1,31 @@
|
||||
import type { Metadata } from "next";
|
||||
import "@/components/portal/portal.css";
|
||||
import { LanguageProvider } from "@/components/i18n";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "YEIDA Portal — Yamuna Expressway Property Management",
|
||||
description:
|
||||
"Allottee portal for the Yamuna Expressway Industrial Development Authority — manage your plot, payments, documents, construction and more.",
|
||||
};
|
||||
|
||||
export default function PortalRootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* Same fonts as the main website. Loaded via <link> (not @import) so
|
||||
they survive Next's CSS processing. */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Heebo:wght@300;400;500;600;700&family=Parkinsans:wght@300..800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Philosopher:wght@400;700&family=Gloock&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="anim-on"><LanguageProvider>{children}</LanguageProvider></body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Assistant } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function AssistantPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Assistant go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Construction } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function ConstructionPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Construction go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Dashboard } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Dashboard go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Documents } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function DocumentsPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Documents go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Journey } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function JourneyPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Journey go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { usePortalNav, routeKeyFromPath } from '@/components/portal/nav';
|
||||
import { useToast } from '@/components/portal/ui';
|
||||
import { Shell } from '@/components/portal/pages';
|
||||
import { PortalProvider } from '@/components/portal/portal-context';
|
||||
|
||||
export default function PortalAppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const go = usePortalNav();
|
||||
const pathname = usePathname();
|
||||
const route = routeKeyFromPath(pathname);
|
||||
const [toastNode, toast] = useToast();
|
||||
|
||||
return (
|
||||
<PortalProvider go={go} toast={toast}>
|
||||
<Shell route={route} go={go}>
|
||||
{children}
|
||||
</Shell>
|
||||
{toastNode}
|
||||
</PortalProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { MyJourney } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function MyJourneyPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <MyJourney go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Notices } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function NoticesPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Notices go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Payments } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Payments go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { MyPlot } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function PlotPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <MyPlot go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Profile } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Profile go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { SamplePlans } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function SamplePlansPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <SamplePlans go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { SectorMaps } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function SectorMapsPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <SectorMaps go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Support } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function SupportPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Support go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { Transfer } from '@/components/portal/pages';
|
||||
import { usePortal } from '@/components/portal/portal-context';
|
||||
|
||||
export default function TransferPage() {
|
||||
const { go, toast } = usePortal();
|
||||
return <Transfer go={go} toast={toast} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
import { AllotmentLookup } from '@/components/portal/onboarding';
|
||||
import { usePortalNav } from '@/components/portal/nav';
|
||||
|
||||
export default function AllotmentLookupPage() {
|
||||
const go = usePortalNav();
|
||||
return <AllotmentLookup go={go} />;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import '@/components/portal/site-header.css';
|
||||
import Header from '@/components/Header';
|
||||
import { LoginSelect } from '@/components/portal/auth';
|
||||
import { usePortalNav } from '@/components/portal/nav';
|
||||
|
||||
export default function LoginPage() {
|
||||
const go = usePortalNav();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<LoginSelect go={go} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
import { Icon, Logo, PageHead, SectorMaps } from '@/components/portal/pages';
|
||||
import { usePortalNav } from '@/components/portal/nav';
|
||||
|
||||
export default function PublicMapsPage() {
|
||||
const go = usePortalNav();
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
|
||||
<header
|
||||
style={{
|
||||
height: 64,
|
||||
background: '#fff',
|
||||
borderBottom: '1px solid var(--line)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 32px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 50,
|
||||
}}
|
||||
>
|
||||
<button onClick={() => go('welcome')} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
|
||||
<Logo size={36} />
|
||||
</button>
|
||||
<div className="row gap-3">
|
||||
<button className="btn btn-ghost" onClick={() => go('welcome')}>
|
||||
<Icon name="chevL" size={16} />
|
||||
Home
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={() => go('login')}>
|
||||
Login to Portal
|
||||
<Icon name="arrowR" size={17} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div style={{ maxWidth: 1600, margin: '0 auto', padding: '28px 36px' }}>
|
||||
<PageHead
|
||||
title="Explore Sector Maps"
|
||||
subtitle="Browse plotted layouts and master plans across YEIDA sectors — no login required"
|
||||
/>
|
||||
<SectorMaps go={go} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// The portal has no public landing page — entering /portal sends users
|
||||
// straight to the login screen.
|
||||
export default function PortalIndex() {
|
||||
redirect('/portal/login');
|
||||
}
|
||||