feat(admin): align nextjs sidebar/ui parity and module flow bridge
3
public/sidebar-icons/approval.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 16V22H20V16C20 14.9 19.1 14 18 14H6C4.9 14 4 14.9 4 16ZM18 18H6V16H18V18ZM12 2C9.24 2 7 4.24 7 7L12 14L17 7C17 4.24 14.76 2 12 2ZM12 11L9 7C9 5.34 10.34 4 12 4C13.66 4 15 5.34 15 7L12 11Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 321 B |
3
public/sidebar-icons/candidate.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 6C13.1 6 14 6.9 14 8C14 9.1 13.1 10 12 10C10.9 10 10 9.1 10 8C10 6.9 10.9 6 12 6ZM12 16C14.7 16 17.8 17.29 18 18H6C6.23 17.28 9.31 16 12 16ZM12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 426 B |
3
public/sidebar-icons/company.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 7V3H2V21H22V7H12ZM10 19H4V17H10V19ZM10 15H4V13H10V15ZM10 11H4V9H10V11ZM10 7H4V5H10V7ZM20 19H12V9H20V19ZM18 11H14V13H18V11ZM18 15H14V17H18V15Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
3
public/sidebar-icons/coupon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.8 8L16 9.2L9.2 16L8 14.8L14.8 8ZM4 4H20C21.11 4 22 4.89 22 6V10C21.4696 10 20.9609 10.2107 20.5858 10.5858C20.2107 10.9609 20 11.4696 20 12C20 12.5304 20.2107 13.0391 20.5858 13.4142C20.9609 13.7893 21.4696 14 22 14V18C22 19.11 21.11 20 20 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V14C3.11 14 4 13.11 4 12C4 11.4696 3.78929 10.9609 3.41421 10.5858C3.03914 10.2107 2.53043 10 2 10V6C2 5.46957 2.21071 4.96086 2.58579 4.58579C2.96086 4.21071 3.46957 4 4 4ZM4 6V8.54C4.60768 8.8904 5.11236 9.39466 5.46325 10.0021C5.81415 10.6094 5.9989 11.2985 5.9989 12C5.9989 12.7015 5.81415 13.3906 5.46325 13.9979C5.11236 14.6053 4.60768 15.1096 4 15.46V18H20V15.46C19.3923 15.1096 18.8876 14.6053 18.5367 13.9979C18.1858 13.3906 18.0011 12.7015 18.0011 12C18.0011 11.2985 18.1858 10.6094 18.5367 10.0021C18.8876 9.39466 19.3923 8.8904 20 8.54V6H4ZM9.5 8C10.33 8 11 8.67 11 9.5C11 10.33 10.33 11 9.5 11C8.67 11 8 10.33 8 9.5C8 8.67 8.67 8 9.5 8ZM14.5 13C15.33 13 16 13.67 16 14.5C16 15.33 15.33 16 14.5 16C13.67 16 13 15.33 13 14.5C13 13.67 13.67 13 14.5 13Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
3
public/sidebar-icons/credits.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 4H4C2.89 4 2.01 4.89 2.01 6L2 18C2 19.11 2.89 20 4 20H9V18H4V12H22V6C22 4.89 21.11 4 20 4ZM20 8H4V6H20V8ZM14.93 19.17L12.1 16.34L10.69 17.75L14.93 22L22 14.93L20.59 13.52L14.93 19.17Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 318 B |
3
public/sidebar-icons/dashboard.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM5 19V5H11V19H5ZM19 19H13V12H19V19ZM19 10H13V5H19V10Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
3
public/sidebar-icons/department.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 13L2 21L9 21L9 18L15 18L15 21L22 21L22 13L15 13L15 16L13 16L13 6L9 6L9 3L2 3L2 11L9 11L9 8L11 8L11 16L9 16L9 13L2 13ZM17 15L20 15L20 19L17 19L17 15ZM7 9L4 9L4 5L7 5L7 9ZM7 19L4 19L4 15L7 15L7 19Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
3
public/sidebar-icons/designation.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 3H14.82C14.4 1.84 13.3 1 12 1C10.7 1 9.6 1.84 9.18 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM12 2.75C12.22 2.75 12.41 2.85 12.55 3C12.67 3.13 12.75 3.31 12.75 3.5C12.75 3.91 12.41 4.25 12 4.25C11.59 4.25 11.25 3.91 11.25 3.5C11.25 3.31 11.33 3.13 11.45 3C11.59 2.85 11.78 2.75 12 2.75ZM19 19H5V5H19V19ZM12 6C10.35 6 9 7.35 9 9C9 10.65 10.35 12 12 12C13.65 12 15 10.65 15 9C15 7.35 13.65 6 12 6ZM12 10C11.45 10 11 9.55 11 9C11 8.45 11.45 8 12 8C12.55 8 13 8.45 13 9C13 9.55 12.55 10 12 10ZM6 16.47V18H18V16.47C18 13.97 14.03 12.89 12 12.89C9.97 12.89 6 13.96 6 16.47ZM8.31 16C9 15.44 10.69 14.88 12 14.88C13.31 14.88 15.01 15.44 15.69 16H8.31Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 843 B |
3
public/sidebar-icons/developers.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 9V7H20V5C20 3.9 19.1 3 18 3H4C2.9 3 2 3.9 2 5V19C2 20.1 2.9 21 4 21H18C19.1 21 20 20.1 20 19V17H22V15H20V13H22V11H20V9H22ZM18 19H4V5H18V19ZM6 13H11V17H6V13ZM12 7H16V10H12V7ZM6 7H11V12H6V7ZM12 11H16V17H12V11Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 342 B |
4
public/sidebar-icons/discount.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21.41 11.58L12.41 2.58C12.05 2.22 11.55 2 11 2H4C2.9 2 2 2.9 2 4V11C2 11.55 2.22 12.05 2.59 12.42L11.59 21.42C11.95 21.78 12.45 22 13 22C13.55 22 14.05 21.78 14.41 21.41L21.41 14.41C21.78 14.05 22 13.55 22 13C22 12.45 21.77 11.94 21.41 11.58ZM13 20.01L4 11V4H11V3.99L20 12.99L13 20.01Z" fill="#070707"/>
|
||||||
|
<path d="M6.5 8C7.32843 8 8 7.32843 8 6.5C8 5.67157 7.32843 5 6.5 5C5.67157 5 5 5.67157 5 6.5C5 7.32843 5.67157 8 6.5 8Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 556 B |
3
public/sidebar-icons/invoice.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 7V9H15V7H17ZM13 7V9H7V7H13ZM13 11H7V13H13V11ZM15 11V13H17V11H15ZM21 22L18 20L15 22L12 20L9 22L6 20L3 22V3H21V22ZM19 18.26V5H5V18.26L6 17.6L9 19.6L12 17.6L15 19.6L18 17.6L19 18.26Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
3
public/sidebar-icons/jobs.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 6V4H10V6H14ZM4 8V19H20V8H4ZM20 6C21.11 6 22 6.89 22 8V19C22 20.11 21.11 21 20 21H4C2.89 21 2 20.11 2 19L2.01 8C2.01 6.89 2.89 6 4 6H8V4C8 2.89 8.89 2 10 2H14C15.11 2 16 2.89 16 4V6H20Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
3
public/sidebar-icons/leads.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 21L18.8 22.77C19.3 23.27 20 22.87 20 22.28V18L22.8 14.6C22.9114 14.4514 22.9793 14.2748 22.996 14.0898C23.0126 13.9048 22.9775 13.7189 22.8944 13.5528C22.8114 13.3867 22.6837 13.247 22.5257 13.1493C22.3678 13.0517 22.1857 13 22 13H15C14.2 13 13.7 14 14.2 14.6L17 18V21ZM15 20H2V17C2 14.3 7.3 13 10 13C10.6 13 11.3 13.1 12.1 13.2C11.9 13.8 12 14.5 12.2 15.1C11.5 15 10.7 14.9 10 14.9C7 14.9 3.9 16.4 3.9 17V18.1H14.5L15 18.7V20ZM10 4C7.8 4 6 5.8 6 8C6 10.2 7.8 12 10 12C12.2 12 14 10.2 14 8C14 5.8 12.2 4 10 4ZM10 10C8.9 10 8 9.1 8 8C8 6.9 8.9 6 10 6C11.1 6 12 6.9 12 8C12 9.1 11.1 10 10 10Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 744 B |
3
public/sidebar-icons/ledger.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V4C20 2.9 19.1 2 18 2ZM9 4H11V9L10 8.25L9 9V4ZM18 20H6V4H7V13L10 10.75L13 13V4H18V20Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
3
public/sidebar-icons/makeup-artist.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.25908 9.37353L14.626 14.7404M9.25908 9.37353L9.25765 9.3721L4.1114 14.5226C3.39978 15.2344 3 16.1996 3 17.2061C3 18.2125 3.39978 19.1778 4.1114 19.8895C4.82314 20.6012 5.78839 21.0009 6.79485 21.0009C7.80132 21.0009 8.76657 20.6012 9.47831 19.8895L14.626 14.7404M9.25908 9.37353L11.8514 3.82156C11.9481 3.6142 12.0933 3.43319 12.2748 3.29387C12.4563 3.15456 12.6687 3.06103 12.894 3.02123C13.1193 2.98142 13.3508 2.9965 13.5691 3.06519C13.7873 3.13388 13.9858 3.25416 14.1477 3.41584L20.5837 9.85328C20.7454 10.0152 20.8656 10.2136 20.9343 10.4319C21.003 10.6501 21.0181 10.8817 20.9783 11.107C20.9385 11.3323 20.845 11.5446 20.7056 11.7261C20.5663 11.9076 20.3853 12.0528 20.178 12.1495L14.626 14.7404M7.1778 11.4548L12.5447 16.8217M15.6054 4.87074L13.9071 7.55419M18.8925 8.15779L16.2076 9.8547" stroke="#070707" stroke-opacity="0.6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1,019 B |
3
public/sidebar-icons/order.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 13C3.79 13 2 14.79 2 17C2 19.21 3.79 21 6 21C8.21 21 10 19.21 10 17C10 14.79 8.21 13 6 13ZM6 19C4.9 19 4 18.1 4 17C4 15.9 4.9 15 6 15C7.1 15 8 15.9 8 17C8 18.1 7.1 19 6 19ZM6 3C3.79 3 2 4.79 2 7C2 9.21 3.79 11 6 11C8.21 11 10 9.21 10 7C10 4.79 8.21 3 6 3ZM12 5H22V7H12V5ZM12 19V17H22V19H12ZM12 11H22V13H12V11Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
3
public/sidebar-icons/photographer.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.12 4L15.95 6H20V18H4V6H8.05L9.88 4H14.12ZM15 2H9L7.17 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4H16.83L15 2ZM12 9C13.65 9 15 10.35 15 12C15 13.65 13.65 15 12 15C10.35 15 9 13.65 9 12C9 10.35 10.35 9 12 9ZM12 7C9.24 7 7 9.24 7 12C7 14.76 9.24 17 12 17C14.76 17 17 14.76 17 12C17 9.24 14.76 7 12 7Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 476 B |
3
public/sidebar-icons/pricing.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.8003 10.9C9.53031 10.31 8.80031 9.7 8.80031 8.75C8.80031 7.66 9.81031 6.9 11.5003 6.9C13.2803 6.9 13.9403 7.75 14.0003 9H16.2103C16.1403 7.28 15.0903 5.7 13.0003 5.19V3H10.0003V5.16C8.06031 5.58 6.50031 6.84 6.50031 8.77C6.50031 11.08 8.41031 12.23 11.2003 12.9C13.7003 13.5 14.2003 14.38 14.2003 15.31C14.2003 16 13.7103 17.1 11.5003 17.1C9.44031 17.1 8.63031 16.18 8.52031 15H6.32031C6.44031 17.19 8.08031 18.42 10.0003 18.83V21H13.0003V18.85C14.9503 18.48 16.5003 17.35 16.5003 15.3C16.5003 12.46 14.0703 11.49 11.8003 10.9Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 B |
3
public/sidebar-icons/report.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 5.99L19.53 19H4.47L12 5.99ZM12 2L1 21H23L12 2ZM13 16H11V18H13V16ZM13 10H11V14H13V10Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
4
public/sidebar-icons/reviews.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.17L4 17.17V4H20V16Z" fill="#070707"/>
|
||||||
|
<path d="M12 15L13.57 11.57L17 10L13.57 8.43L12 5L10.43 8.43L7 10L10.43 11.57L12 15Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 340 B |
3
public/sidebar-icons/role.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 18V17.35C4 17.01 4.16 16.69 4.41 16.54C6.1 15.53 8.03 15 10 15C10.03 15 10.05 15 10.08 15.01C10.18 14.31 10.38 13.64 10.67 13.03C10.45 13.01 10.23 13 10 13C7.58 13 5.32 13.67 3.39 14.82C2.51 15.34 2 16.32 2 17.35V20H11.26C10.84 19.4 10.51 18.72 10.29 18H4ZM10 12C12.21 12 14 10.21 14 8C14 5.79 12.21 4 10 4C7.79 4 6 5.79 6 8C6 10.21 7.79 12 10 12ZM10 6C11.1 6 12 6.9 12 8C12 9.1 11.1 10 10 10C8.9 10 8 9.1 8 8C8 6.9 8.9 6 10 6ZM20.75 16C20.75 15.78 20.72 15.58 20.69 15.37L21.83 14.36L20.83 12.63L19.38 13.12C19.06 12.85 18.7 12.64 18.3 12.49L18 11H16L15.7 12.49C15.3 12.64 14.94 12.85 14.62 13.12L13.17 12.63L12.17 14.36L13.31 15.37C13.28 15.58 13.25 15.78 13.25 16C13.25 16.22 13.28 16.42 13.31 16.63L12.17 17.64L13.17 19.37L14.62 18.88C14.94 19.15 15.3 19.36 15.7 19.51L16 21H18L18.3 19.51C18.7 19.36 19.06 19.15 19.38 18.88L20.83 19.37L21.83 17.64L20.69 16.63C20.72 16.42 20.75 16.22 20.75 16ZM17 18C15.9 18 15 17.1 15 16C15 14.9 15.9 14 17 14C18.1 14 19 14.9 19 16C19 17.1 18.1 18 17 18Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
public/sidebar-icons/support.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21 12.22C21 6.73 16.74 3 12 3C7.31 3 3 6.65 3 12.28C2.4 12.62 2 13.26 2 14V16C2 17.1 2.9 18 4 18H5V11.9C5 8.03 8.13 4.9 12 4.9C15.87 4.9 19 8.03 19 11.9V19H11V21H19C20.1 21 21 20.1 21 19V17.78C21.59 17.47 22 16.86 22 16.14V13.84C22 13.14 21.59 12.53 21 12.22Z" fill="#070707"/>
|
||||||
|
<path d="M9 14C9.55228 14 10 13.5523 10 13C10 12.4477 9.55228 12 9 12C8.44772 12 8 12.4477 8 13C8 13.5523 8.44772 14 9 14Z" fill="#070707"/>
|
||||||
|
<path d="M15 14C15.5523 14 16 13.5523 16 13C16 12.4477 15.5523 12 15 12C14.4477 12 14 12.4477 14 13C14 13.5523 14.4477 14 15 14Z" fill="#070707"/>
|
||||||
|
<path d="M18.0024 11.03C17.764 9.62414 17.0358 8.34797 15.9469 7.42738C14.8579 6.5068 13.4784 6.00117 12.0524 6C9.02241 6 5.76241 8.51 6.02241 12.45C7.25554 11.9454 8.34465 11.1435 9.19264 10.1158C10.0406 9.08808 10.6211 7.86652 10.8824 6.56C12.1924 9.19 14.8824 11 18.0024 11.03Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 978 B |
3
public/sidebar-icons/tax.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 3H5C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V19C3 19.5304 3.21071 20.0391 3.58579 20.4142C3.96086 20.7893 4.46957 21 5 21H19C20.1 21 21 20.1 21 19V5C21 4.46957 20.7893 3.96086 20.4142 3.58579C20.0391 3.21071 19.5304 3 19 3ZM19 19H5V7H19V19ZM17 12H7V10H17V12ZM13 16H7V14H13V16Z" fill="#070707"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 442 B |
3
public/sidebar-icons/tutor.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 17C21.1 17 22 16.1 22 15V4C22 2.9 21.1 2 20 2H9.5C9.8 2.6 10 3.3 10 4H20V15H11V17M15 7V9H9V22H7V16H5V22H3V14H1.5V9C1.5 7.9 2.4 7 3.5 7H15ZM8 4C8 5.1 7.1 6 6 6C4.9 6 4 5.1 4 4C4 2.9 4.9 2 6 2C7.1 2 8 2.9 8 4ZM17 6H19V14H17V6ZM14 10H16V14H14V10ZM11 10H13V14H11V10Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
3
public/sidebar-icons/users.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 13C5.1 13 6 12.1 6 11C6 9.9 5.1 9 4 9C2.9 9 2 9.9 2 11C2 12.1 2.9 13 4 13ZM5.13 14.1C4.76 14.04 4.39 14 4 14C3.01 14 2.07 14.21 1.22 14.58C0.857713 14.7349 0.548924 14.9928 0.332018 15.3217C0.115113 15.6506 -0.000343686 16.036 7.68494e-07 16.43V18H4.5V16.39C4.5 15.56 4.73 14.78 5.13 14.1ZM20 13C21.1 13 22 12.1 22 11C22 9.9 21.1 9 20 9C18.9 9 18 9.9 18 11C18 12.1 18.9 13 20 13ZM24 16.43C24 15.62 23.52 14.9 22.78 14.58C21.9031 14.1974 20.9567 13.9999 20 14C19.61 14 19.24 14.04 18.87 14.1C19.27 14.78 19.5 15.56 19.5 16.39V18H24V16.43ZM16.24 13.65C15.07 13.13 13.63 12.75 12 12.75C10.37 12.75 8.93 13.14 7.76 13.65C7.23305 13.8875 6.78631 14.2728 6.47392 14.7592C6.16153 15.2455 5.99691 15.812 6 16.39V18H18V16.39C18 15.21 17.32 14.13 16.24 13.65ZM8.07 16C8.16 15.77 8.2 15.61 8.98 15.31C9.95 14.93 10.97 14.75 12 14.75C13.03 14.75 14.05 14.93 15.02 15.31C15.79 15.61 15.83 15.77 15.93 16H8.07ZM12 8C12.55 8 13 8.45 13 9C13 9.55 12.55 10 12 10C11.45 10 11 9.55 11 9C11 8.45 11.45 8 12 8ZM12 6C10.34 6 9 7.34 9 9C9 10.66 10.34 12 12 12C13.66 12 15 10.66 15 9C15 7.34 13.66 6 12 6Z" fill="#070707" fill-opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
555
src/app.css
|
|
@ -6,6 +6,7 @@
|
||||||
--brand-orange-50: #fff1e8;
|
--brand-orange-50: #fff1e8;
|
||||||
--brand-orange-100: #ffe2d2;
|
--brand-orange-100: #ffe2d2;
|
||||||
--brand-orange-200: #ffc9ac;
|
--brand-orange-200: #ffc9ac;
|
||||||
|
--ink: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -14,67 +15,390 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #f3f4f8;
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
font-family: 'Exo 2', sans-serif;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: var(--ink);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Auth / Login ---- */
|
||||||
|
.auth-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(253, 98, 22, 0.24), transparent 42%),
|
||||||
|
radial-gradient(circle at 80% 10%, rgba(99, 102, 241, 0.16), transparent 34%),
|
||||||
|
linear-gradient(180deg, #100b2f 0%, #0c0828 52%, #07051d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-layout {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(1120px, 100%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.08fr 0.92fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual {
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 28px;
|
||||||
|
box-shadow: 0 24px 72px -34px rgba(0, 0, 0, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual-kicker {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffd7c2;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual h1 {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
font-size: clamp(30px, 3.4vw, 42px);
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual p {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual img {
|
||||||
|
margin-top: 18px;
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
border-radius: 16px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 24px 72px -34px rgba(0, 0, 0, 0.72);
|
||||||
|
padding: 30px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-logo {
|
||||||
|
height: 52px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
margin: 18px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-copy {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form-grid {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch.split {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link-btn {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--brand-orange);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link-btn:hover {
|
||||||
|
color: #ea580c;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-inline-msg {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Admin Shell ---- */
|
||||||
|
.admin-root {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 40;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-inner {
|
||||||
|
width: min(1440px, calc(100% - 36px));
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-brand img {
|
||||||
|
height: 44px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-brand-kicker {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-brand h1 {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
color: var(--brand-navy);
|
color: var(--brand-navy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-role-chip {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #fdba74;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #9a3412;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 11px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr;
|
grid-template-columns: 264px 1fr;
|
||||||
min-height: 100vh;
|
min-height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 1px solid #dbe1ec;
|
border-right: 1px solid #e2e8f0;
|
||||||
background: #fcfcfd;
|
background: #fcfcfd;
|
||||||
padding: 16px;
|
padding: 20px 12px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.sidebar-toggle-row {
|
||||||
margin: 4px 0 18px;
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand img {
|
.sidebar-toggle-btn {
|
||||||
height: 42px;
|
border: 0;
|
||||||
width: auto;
|
border-radius: 8px;
|
||||||
object-fit: contain;
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 3px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle-btn:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: block;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 11px 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
transition: background-color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
border-color: #dbe1ec;
|
border-color: #e2e8f0;
|
||||||
color: #1f2937;
|
background: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover .nav-dot {
|
||||||
|
background: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
border-color: var(--brand-orange-200);
|
border-color: var(--brand-orange-200);
|
||||||
background: linear-gradient(90deg, var(--brand-orange-50), var(--brand-orange-100));
|
background: linear-gradient(to right, var(--brand-orange-50), color-mix(in srgb, var(--brand-orange-100) 70%, white 30%));
|
||||||
color: #111827;
|
color: #111827;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
box-shadow: inset 3px 0 0 0 var(--brand-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-badge {
|
||||||
|
border: 1px solid var(--brand-orange-200);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--brand-orange-100);
|
||||||
|
color: #b45309;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
padding: 24px;
|
min-width: 0;
|
||||||
|
padding: 14px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-inner {
|
||||||
|
max-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab-wrap {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
margin: -2px -16px 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
transition: color 140ms ease, border-color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
border-bottom-color: #fd6216;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tab.active {
|
||||||
|
border-bottom-color: #fd6216;
|
||||||
|
color: #050026;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Shared Content ---- */
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 28px;
|
font-size: 30px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,29 +411,31 @@ body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.2fr 1fr;
|
grid-template-columns: 1.2fr 1fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-top: 20px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #dbe1ec;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 18px;
|
||||||
|
box-shadow: 0 12px 32px -28px rgba(15, 23, 42, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card h2 {
|
.card h2 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 18px;
|
font-size: 19px;
|
||||||
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field label {
|
.field label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
|
|
@ -119,26 +445,48 @@ body {
|
||||||
.field select {
|
.field select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input:focus,
|
||||||
|
.field textarea:focus,
|
||||||
|
.field select:focus {
|
||||||
|
border-color: var(--brand-orange);
|
||||||
|
box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 10px 14px;
|
padding: 9px 14px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border-color: #94a3b8;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
|
|
@ -147,6 +495,17 @@ body {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover {
|
||||||
|
border-color: #ea580c;
|
||||||
|
background: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.json {
|
.json {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -160,31 +519,16 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
margin-top: 8px;
|
margin-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.shell {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 1px solid #dbe1ec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-note {
|
.inline-note {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
|
|
@ -201,6 +545,66 @@ body {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-table thead th {
|
||||||
|
background: #050026;
|
||||||
|
color: #fff;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-table tbody td {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding: 12px;
|
||||||
|
color: #334155;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-table tbody tr:hover td {
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip.active {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
border: 1px solid #dbe1ec;
|
border: 1px solid #dbe1ec;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
@ -219,17 +623,6 @@ body {
|
||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.list-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-input {
|
.json-input {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -241,5 +634,39 @@ body {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.auth-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-visual {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid #dbe1ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid,
|
||||||
|
.list-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-inner {
|
||||||
|
min-height: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-brand img {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,83 @@
|
||||||
import type { JSX } from 'solid-js';
|
import { A, useLocation, useNavigate } from '@solidjs/router';
|
||||||
|
import { createSignal, onMount, type JSX } from 'solid-js';
|
||||||
import AdminSidebar from './AdminSidebar';
|
import AdminSidebar from './AdminSidebar';
|
||||||
|
import { clearAdminSession, hasAdminSession } from '~/lib/admin-session';
|
||||||
|
|
||||||
export default function AdminShell(props: { children: JSX.Element }) {
|
export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||||
|
const tabs = [
|
||||||
|
{ href: '/admin/runtime-roles', label: 'View Roles' },
|
||||||
|
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
|
||||||
|
{ href: '/admin/role-ui-configs', label: 'Inspector' },
|
||||||
|
{ href: '/admin/onboarding-schemas', label: 'Onboarding' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isTabActive = (href: string) => {
|
||||||
|
if (href === '/admin/runtime-roles') {
|
||||||
|
return location.pathname === href || (location.pathname.startsWith('/admin/runtime-roles/') && location.pathname !== '/admin/runtime-roles/new');
|
||||||
|
}
|
||||||
|
return location.pathname === href || location.pathname.startsWith(`${href}/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!hasAdminSession()) {
|
||||||
|
const from = encodeURIComponent(location.pathname + location.search);
|
||||||
|
navigate(`/login?from=${from}`, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCheckedSession(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLogout = () => {
|
||||||
|
clearAdminSession();
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div class="admin-root">
|
||||||
|
<header class="admin-header">
|
||||||
|
<div class="admin-header-inner">
|
||||||
|
<div class="admin-brand">
|
||||||
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" />
|
||||||
|
<div>
|
||||||
|
<p class="admin-brand-kicker">Administration</p>
|
||||||
|
<h1>NXTGAUGE Admin Panel</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="admin-header-actions">
|
||||||
|
<p class="admin-role-chip">Super Admin</p>
|
||||||
|
<button class="btn" type="button" onClick={onLogout}>Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{checkedSession() ? (
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
<main class="main">{props.children}</main>
|
<main class="main">
|
||||||
|
<div class="admin-tab-wrap">
|
||||||
|
<nav class="admin-tabs">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<A href={tab.href} class={`admin-tab ${isTabActive(tab.href) ? 'active' : ''}`}>
|
||||||
|
{tab.label}
|
||||||
|
</A>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="main-inner">{props.children}</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="shell">
|
||||||
|
<main class="main">
|
||||||
|
<div class="card">
|
||||||
|
<p class="notice">Checking session...</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,71 @@
|
||||||
import { A, useLocation } from '@solidjs/router';
|
import { A, useLocation } from '@solidjs/router';
|
||||||
|
|
||||||
const links = [
|
type LinkItem = { legacyHref: string; href: string; label: string; icon: string; aliasPrefix?: string };
|
||||||
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
|
|
||||||
{ href: '/admin/runtime-roles', label: 'Manage Roles' },
|
const links: LinkItem[] = [
|
||||||
{ href: '/admin/role-ui-configs/new', label: 'Create Dashboard' },
|
{ legacyHref: '/', href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' },
|
||||||
{ href: '/admin/role-ui-configs', label: 'Manage Dashboards' },
|
{ legacyHref: '/department', href: '/admin/department', label: 'Department Management', icon: 'department.svg' },
|
||||||
{ href: '/admin/onboarding-schemas/new', label: 'Create Onboarding Flow' },
|
{ legacyHref: '/designation', href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' },
|
||||||
{ href: '/admin/onboarding-schemas', label: 'Manage Onboarding Flows' },
|
{ legacyHref: '/employees', href: '/admin/employees', label: 'Internal User Management', icon: 'users.svg' },
|
||||||
|
{ legacyHref: '/roles?scope=internal', href: '/admin/roles?scope=internal', label: 'Internal Role Management', icon: 'role.svg' },
|
||||||
|
{ legacyHref: '/runtime-roles', href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' },
|
||||||
|
{ legacyHref: '/onboarding-management', href: '/admin/onboarding-management', label: 'Onboarding Management', icon: 'reviews.svg', aliasPrefix: '/admin/onboarding-schemas' },
|
||||||
|
{ legacyHref: '/internal-dashboard-management', href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' },
|
||||||
|
{ legacyHref: '/external-dashboard-management', href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' },
|
||||||
|
{ legacyHref: '/approval', href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' },
|
||||||
|
{ legacyHref: '/users', href: '/admin/users', label: 'External User Management', icon: 'users.svg' },
|
||||||
|
{ legacyHref: '/customer', href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' },
|
||||||
|
{ legacyHref: '/company', href: '/admin/company', label: 'Company Management', icon: 'company.svg' },
|
||||||
|
{ legacyHref: '/candidate', href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' },
|
||||||
|
{ legacyHref: '/photographer', href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' },
|
||||||
|
{ legacyHref: '/video-editors', href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' },
|
||||||
|
{ legacyHref: '/graphic-designers', href: '/admin/graphic-designers', label: 'Graphic Designer Management', icon: 'developers.svg' },
|
||||||
|
{ legacyHref: '/social-media-managers', href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' },
|
||||||
|
{ legacyHref: '/fitness-trainers', href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' },
|
||||||
|
{ legacyHref: '/catering-services', href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' },
|
||||||
|
{ legacyHref: '/makeup-artist', href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' },
|
||||||
|
{ legacyHref: '/tutors', href: '/admin/tutors', label: 'Tutor Management', icon: 'tutor.svg' },
|
||||||
|
{ legacyHref: '/developers', href: '/admin/developers', label: 'Developer Management', icon: 'developers.svg' },
|
||||||
|
{ legacyHref: '/jobs', href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' },
|
||||||
|
{ legacyHref: '/leads', href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' },
|
||||||
|
{ legacyHref: '/pricing', href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' },
|
||||||
|
{ legacyHref: '/credit', href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' },
|
||||||
|
{ legacyHref: '/coupon', href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' },
|
||||||
|
{ legacyHref: '/discount', href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' },
|
||||||
|
{ legacyHref: '/tax', href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' },
|
||||||
|
{ legacyHref: '/order', href: '/admin/order', label: 'Order Management', icon: 'order.svg' },
|
||||||
|
{ legacyHref: '/invoice', href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' },
|
||||||
|
{ legacyHref: '/review', href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' },
|
||||||
|
{ legacyHref: '/kb', href: '/admin/kb', label: 'Knowledge Base Management', icon: 'reviews.svg' },
|
||||||
|
{ legacyHref: '/notifications', href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' },
|
||||||
|
{ legacyHref: '/help', href: '/admin/support', label: 'Support Management', icon: 'support.svg' },
|
||||||
|
{ legacyHref: '/report', href: '/admin/report', label: 'Report Management', icon: 'report.svg' },
|
||||||
|
{ legacyHref: '/ledger', href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminSidebar() {
|
export default function AdminSidebar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const isLinkActive = (href: string, aliasPrefix?: string) => {
|
||||||
|
const pathOnly = href.split('?')[0] || href;
|
||||||
|
if (pathOnly === '/admin') return location.pathname === '/admin';
|
||||||
|
if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true;
|
||||||
|
return location.pathname === pathOnly || location.pathname.startsWith(`${pathOnly}/`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="sidebar-toggle-row">
|
||||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" />
|
<button type="button" class="sidebar-toggle-btn" aria-label="Collapse sidebar">‹</button>
|
||||||
</div>
|
</div>
|
||||||
{links.map((item) => {
|
<nav class="sidebar-nav">
|
||||||
const isActive =
|
{links.map((item) => (
|
||||||
location.pathname === item.href ||
|
<A href={item.href} class={`nav-item ${isLinkActive(item.href, item.aliasPrefix) ? 'active' : ''}`} data-legacy-href={item.legacyHref}>
|
||||||
(item.href !== '/admin/runtime-roles' &&
|
<img class="nav-icon" src={`/sidebar-icons/${item.icon}`} alt="" />
|
||||||
item.href !== '/admin/role-ui-configs' &&
|
<span class="nav-title">{item.label}</span>
|
||||||
item.href !== '/admin/onboarding-schemas' &&
|
{isLinkActive(item.href, item.aliasPrefix) ? <span class="active-badge">Active</span> : null}
|
||||||
location.pathname.startsWith(item.href.replace('/new', '')));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<A href={item.href} class={`nav-item ${isActive ? 'active' : ''}`}>
|
|
||||||
{item.label}
|
|
||||||
</A>
|
</A>
|
||||||
);
|
))}
|
||||||
})}
|
</nav>
|
||||||
<p class="notice">Same UI flow, simpler builder experience.</p>
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
src/lib/admin-session.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
const SESSION_COOKIE = 'nxtgauge_admin_session';
|
||||||
|
const SESSION_TTL_SECONDS = 60 * 60 * 12;
|
||||||
|
|
||||||
|
export function hasAdminSession(): boolean {
|
||||||
|
if (typeof document === 'undefined') return false;
|
||||||
|
return document.cookie.split(';').some((entry) => entry.trim().startsWith(`${SESSION_COOKIE}=`));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAdminSession(): void {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
document.cookie = `${SESSION_COOKIE}=1; Path=/; Max-Age=${SESSION_TTL_SECONDS}; SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAdminSession(): void {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
document.cookie = `${SESSION_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;
|
||||||
|
}
|
||||||
57
src/routes/admin/[...module].tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { A, useParams } from '@solidjs/router';
|
||||||
|
import { createMemo } from 'solid-js';
|
||||||
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
|
||||||
|
function toTitle(value: string): string {
|
||||||
|
return value
|
||||||
|
.split(/[-_/]/g)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGACY_ADMIN_ORIGIN = 'http://localhost:3002';
|
||||||
|
|
||||||
|
function resolveLegacyPath(modulePath: string): string {
|
||||||
|
switch (modulePath) {
|
||||||
|
case 'roles':
|
||||||
|
return '/roles?scope=internal';
|
||||||
|
case 'onboarding-management':
|
||||||
|
return '/onboarding-management';
|
||||||
|
case 'internal-dashboard-management':
|
||||||
|
return '/internal-dashboard-management';
|
||||||
|
case 'external-dashboard-management':
|
||||||
|
return '/external-dashboard-management';
|
||||||
|
case 'support':
|
||||||
|
return '/help';
|
||||||
|
default:
|
||||||
|
return `/${modulePath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LegacyModuleShellPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const modulePath = String((params as any).module || '').trim();
|
||||||
|
const moduleName = createMemo(() => toTitle(modulePath || 'Management'));
|
||||||
|
const legacyPath = createMemo(() => resolveLegacyPath(modulePath));
|
||||||
|
const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminShell>
|
||||||
|
<h1 class="page-title">{moduleName()}</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
Live legacy module embedded for exact design and functionality parity during migration.
|
||||||
|
</p>
|
||||||
|
<section class="card">
|
||||||
|
<div class="actions">
|
||||||
|
<A class="btn" href={legacyUrl()} target="_blank">Open Module In New Tab</A>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
src={legacyUrl()}
|
||||||
|
title={`${moduleName()} (Legacy)`}
|
||||||
|
style={{ width: '100%', height: '72vh', border: '1px solid #e2e8f0', 'border-radius': '10px', 'margin-top': '10px' }}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</AdminShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,8 @@ export default function ManageOnboardingPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<h1 class="page-title">Manage Onboarding Flows</h1>
|
<h1 class="page-title">Onboarding Management</h1>
|
||||||
<p class="page-subtitle">Edit or remove runtime onboarding schemas saved from the builder.</p>
|
<p class="page-subtitle">Manage runtime onboarding schemas used by external roles.</p>
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h2>Runtime Onboarding Schemas</h2>
|
<h2>Runtime Onboarding Schemas</h2>
|
||||||
|
|
@ -28,18 +28,32 @@ export default function ManageOnboardingPage() {
|
||||||
<p class="notice">No runtime onboarding schemas found yet.</p>
|
<p class="notice">No runtime onboarding schemas found yet.</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||||
<div class="list-grid">
|
<div class="table-wrap">
|
||||||
|
<table class="list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Schema</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
<th class="align-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{items()!.map((item) => (
|
{items()!.map((item) => (
|
||||||
<article class="list-item">
|
<tr>
|
||||||
<h3>{item.key}</h3>
|
<td>{item.key}</td>
|
||||||
<p>Status: <strong>{item.status}</strong></p>
|
<td><span class={`status-chip ${item.status === 'published' ? 'active' : ''}`}>{item.status}</span></td>
|
||||||
<p>Updated: {new Date(item.updatedAt).toLocaleDateString()}</p>
|
<td>{new Date(item.updatedAt).toLocaleDateString()}</td>
|
||||||
<div class="actions">
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
<A class="btn" href={`/admin/onboarding-schemas/${encodeURIComponent(item.key)}`}>Edit</A>
|
<A class="btn" href={`/admin/onboarding-schemas/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export default function ManageDashboardsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<h1 class="page-title">Manage Dashboards</h1>
|
<h1 class="page-title">External Dashboard Management</h1>
|
||||||
<p class="page-subtitle">Edit or remove runtime dashboard configs saved from the builder.</p>
|
<p class="page-subtitle">Inspect and manage published runtime dashboard configurations.</p>
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h2>Runtime Dashboards</h2>
|
<h2>Runtime Dashboards</h2>
|
||||||
|
|
@ -28,18 +28,32 @@ export default function ManageDashboardsPage() {
|
||||||
<p class="notice">No runtime dashboard configs found yet.</p>
|
<p class="notice">No runtime dashboard configs found yet.</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||||
<div class="list-grid">
|
<div class="table-wrap">
|
||||||
|
<table class="list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
<th class="align-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{items()!.map((item) => (
|
{items()!.map((item) => (
|
||||||
<article class="list-item">
|
<tr>
|
||||||
<h3>{item.key}</h3>
|
<td>{item.key}</td>
|
||||||
<p>Status: <strong>{item.status}</strong></p>
|
<td><span class={`status-chip ${item.status === 'published' ? 'active' : ''}`}>{item.status}</span></td>
|
||||||
<p>Updated: {new Date(item.updatedAt).toLocaleDateString()}</p>
|
<td>{new Date(item.updatedAt).toLocaleDateString()}</td>
|
||||||
<div class="actions">
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
<A class="btn" href={`/admin/role-ui-configs/${encodeURIComponent(item.key)}`}>Edit</A>
|
<A class="btn" href={`/admin/role-ui-configs/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export default function ManageRolesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<h1 class="page-title">Manage Roles</h1>
|
<h1 class="page-title">External Role Management</h1>
|
||||||
<p class="page-subtitle">Edit or remove runtime role configs saved from the builder.</p>
|
<p class="page-subtitle">Manage canonical external runtime roles from one place.</p>
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h2>Runtime Roles</h2>
|
<h2>Runtime Roles</h2>
|
||||||
|
|
@ -28,18 +28,32 @@ export default function ManageRolesPage() {
|
||||||
<p class="notice">No runtime role configs found yet.</p>
|
<p class="notice">No runtime role configs found yet.</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!items.loading && items() && items()!.length > 0}>
|
<Show when={!items.loading && items() && items()!.length > 0}>
|
||||||
<div class="list-grid">
|
<div class="table-wrap">
|
||||||
|
<table class="list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
<th class="align-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{items()!.map((item) => (
|
{items()!.map((item) => (
|
||||||
<article class="list-item">
|
<tr>
|
||||||
<h3>{item.key}</h3>
|
<td>{item.key}</td>
|
||||||
<p>Status: <strong>{item.status}</strong></p>
|
<td><span class={`status-chip ${item.status === 'published' ? 'active' : ''}`}>{item.status}</span></td>
|
||||||
<p>Updated: {new Date(item.updatedAt).toLocaleDateString()}</p>
|
<td>{new Date(item.updatedAt).toLocaleDateString()}</td>
|
||||||
<div class="actions">
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
<A class="btn" href={`/admin/runtime-roles/${encodeURIComponent(item.key)}`}>Edit</A>
|
<A class="btn" href={`/admin/runtime-roles/${encodeURIComponent(item.key)}`}>Edit</A>
|
||||||
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
<button class="btn" onClick={() => onDelete(item.key)}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ import { A } from '@solidjs/router';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main class="page">
|
<main class="auth-page">
|
||||||
<h1 class="page-title">NXTGAUGE Admin</h1>
|
<div class="auth-bg" />
|
||||||
<p class="page-subtitle">Open the admin runtime builders below.</p>
|
<section class="auth-card">
|
||||||
<section class="card">
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="auth-logo" />
|
||||||
|
<h1 class="auth-title">Admin Access</h1>
|
||||||
|
<p class="auth-copy">Secure sign-in and runtime control center for NXTGAUGE operations.</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<A class="btn primary" href="/admin">Open Admin</A>
|
<A class="btn primary" href="/login">Sign In</A>
|
||||||
<A class="btn" href="/admin/runtime-roles/new">Create Role</A>
|
<A class="btn" href="/admin">Skip to Admin</A>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
400
src/routes/login.tsx
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
import { useNavigate } from '@solidjs/router';
|
||||||
|
import { createMemo, createSignal, onMount } from 'solid-js';
|
||||||
|
import { hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
||||||
|
|
||||||
|
type AuthMode = 'login' | 'reset';
|
||||||
|
type ResetStep = 'request' | 'verify';
|
||||||
|
|
||||||
|
function pickChallengeId(payload: any): string {
|
||||||
|
const direct = String(payload?.challengeId || '').trim();
|
||||||
|
if (direct) return direct;
|
||||||
|
const nested = String(payload?.data?.challengeId || '').trim();
|
||||||
|
if (nested) return nested;
|
||||||
|
const snake = String(payload?.challenge_id || '').trim();
|
||||||
|
if (snake) return snake;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickMaskedEmail(payload: any, fallback: string): string {
|
||||||
|
const direct = String(payload?.maskedEmail || '').trim();
|
||||||
|
if (direct) return direct;
|
||||||
|
const nested = String(payload?.data?.maskedEmail || '').trim();
|
||||||
|
if (nested) return nested;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [mode, setMode] = createSignal<AuthMode>('login');
|
||||||
|
const [resetStep, setResetStep] = createSignal<ResetStep>('request');
|
||||||
|
const [email, setEmail] = createSignal('');
|
||||||
|
const [password, setPassword] = createSignal('');
|
||||||
|
const [resetCode, setResetCode] = createSignal('');
|
||||||
|
const [newPassword, setNewPassword] = createSignal('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = createSignal('');
|
||||||
|
const [challengeId, setChallengeId] = createSignal('');
|
||||||
|
const [maskedEmail, setMaskedEmail] = createSignal('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
const [info, setInfo] = createSignal('');
|
||||||
|
|
||||||
|
const canSubmitResetRequest = createMemo(
|
||||||
|
() =>
|
||||||
|
email().trim().length > 0 &&
|
||||||
|
newPassword().trim().length > 0 &&
|
||||||
|
confirmPassword().trim().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const canSubmitResetVerify = createMemo(
|
||||||
|
() => challengeId().trim().length > 0 && resetCode().trim().length === 6,
|
||||||
|
);
|
||||||
|
const canSubmitLoginCredentials = createMemo(
|
||||||
|
() => email().trim().length > 0 && password().trim().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearMessages = () => {
|
||||||
|
setError('');
|
||||||
|
setInfo('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPasswordFlow = () => {
|
||||||
|
setResetStep('request');
|
||||||
|
setResetCode('');
|
||||||
|
setChallengeId('');
|
||||||
|
setMaskedEmail('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchMode = (nextMode: AuthMode) => {
|
||||||
|
clearMessages();
|
||||||
|
setMode(nextMode);
|
||||||
|
if (nextMode === 'login') {
|
||||||
|
resetPasswordFlow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (hasAdminSession()) {
|
||||||
|
navigate('/admin', { replace: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const completeAdminLogin = () => {
|
||||||
|
setAdminSession();
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const from = params.get('from');
|
||||||
|
const nextPath = from && from.startsWith('/admin') ? from : '/admin';
|
||||||
|
navigate(nextPath, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const directSignIn = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!canSubmitLoginCredentials()) {
|
||||||
|
setError('Email and password are required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
completeAdminLogin();
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(String(nextError?.message || 'Sign in failed.'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestResetCode = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!canSubmitResetRequest()) {
|
||||||
|
setError('Email, new password, and confirm password are required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword() !== confirmPassword()) {
|
||||||
|
setError('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedEmail = email().trim().toLowerCase();
|
||||||
|
const resolvedPassword = newPassword().trim();
|
||||||
|
const requestPayload = {
|
||||||
|
email: trimmedEmail,
|
||||||
|
newPassword: resolvedPassword,
|
||||||
|
new_password: resolvedPassword,
|
||||||
|
password: resolvedPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
const attempts: Array<{ url: string; body: string }> = [
|
||||||
|
{
|
||||||
|
url: '/api/gateway/users/auth/internal/forgot-password/request-code',
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/gateway/auth/internal/forgot-password/request-code',
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/gateway/users/auth/internal/forgot-password/request-code',
|
||||||
|
body: JSON.stringify({ data: requestPayload }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/gateway/auth/internal/forgot-password/request-code',
|
||||||
|
body: JSON.stringify({ data: requestPayload }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
let payload: any = {};
|
||||||
|
let status = 500;
|
||||||
|
for (const attempt of attempts) {
|
||||||
|
const response = await fetch(attempt.url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: attempt.body,
|
||||||
|
});
|
||||||
|
status = response.status;
|
||||||
|
payload = await response.json().catch(() => ({}));
|
||||||
|
if (response.ok) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextChallengeId = pickChallengeId(payload);
|
||||||
|
if (!nextChallengeId) {
|
||||||
|
const fallbackMessage =
|
||||||
|
status === 502
|
||||||
|
? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.'
|
||||||
|
: 'Failed to send reset code.';
|
||||||
|
throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
setChallengeId(nextChallengeId);
|
||||||
|
setMaskedEmail(pickMaskedEmail(payload, trimmedEmail));
|
||||||
|
setResetStep('verify');
|
||||||
|
const debugCode = String(payload?.debugCode || payload?.data?.debugCode || '').trim();
|
||||||
|
setInfo(debugCode ? `Reset code sent. [DEV CODE: ${debugCode}]` : 'Reset code sent to your email.');
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(String(nextError?.message || 'Failed to send reset code.'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyResetCode = async () => {
|
||||||
|
clearMessages();
|
||||||
|
if (!canSubmitResetVerify()) {
|
||||||
|
setError('A valid 6-digit verification code is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword().trim() !== confirmPassword().trim()) {
|
||||||
|
setError('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPassword = newPassword().trim();
|
||||||
|
const resolvedChallengeId = challengeId().trim();
|
||||||
|
const resolvedCode = resetCode().trim();
|
||||||
|
|
||||||
|
const verifyPayload = {
|
||||||
|
challengeId: resolvedChallengeId,
|
||||||
|
challenge_id: resolvedChallengeId,
|
||||||
|
code: resolvedCode,
|
||||||
|
otp: resolvedCode,
|
||||||
|
verificationCode: resolvedCode,
|
||||||
|
verification_code: resolvedCode,
|
||||||
|
newPassword: resolvedPassword,
|
||||||
|
new_password: resolvedPassword,
|
||||||
|
password: resolvedPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
const attempts: string[] = [
|
||||||
|
'/api/gateway/users/auth/internal/forgot-password/verify-code',
|
||||||
|
'/api/gateway/auth/internal/forgot-password/verify-code',
|
||||||
|
];
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
let payload: any = {};
|
||||||
|
let status = 500;
|
||||||
|
let success = false;
|
||||||
|
for (const url of attempts) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(verifyPayload),
|
||||||
|
});
|
||||||
|
status = response.status;
|
||||||
|
payload = await response.json().catch(() => ({}));
|
||||||
|
if (response.ok) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
const fallbackMessage =
|
||||||
|
status === 502
|
||||||
|
? 'Verification service is temporarily unavailable (502). Please retry in 1-2 minutes.'
|
||||||
|
: 'Password reset failed.';
|
||||||
|
throw new Error(String(payload?.message || payload?.error || fallbackMessage).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword('');
|
||||||
|
switchMode('login');
|
||||||
|
setInfo('Password reset successful. Please sign in with your new password.');
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(String(nextError?.message || 'Password reset failed.'));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main class="auth-page auth-page-login">
|
||||||
|
<div class="auth-bg" />
|
||||||
|
<div class="auth-layout">
|
||||||
|
<section class="auth-visual">
|
||||||
|
<p class="auth-visual-kicker">Internal Access</p>
|
||||||
|
<h1>Welcome back to Nxtgauge.</h1>
|
||||||
|
<p>Sign in securely to access the admin control panel.</p>
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80"
|
||||||
|
alt="Office workspace"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="auth-card auth-form-card">
|
||||||
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="auth-logo" />
|
||||||
|
<h2 class="auth-title">{mode() === 'login' ? 'Employee Login' : 'Reset Password'}</h2>
|
||||||
|
|
||||||
|
<form class="auth-form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setEmail(event.currentTarget.value);
|
||||||
|
clearMessages();
|
||||||
|
}}
|
||||||
|
placeholder="Enter your email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mode() === 'login' ? (
|
||||||
|
<>
|
||||||
|
<div class="field">
|
||||||
|
<label>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setPassword(event.currentTarget.value);
|
||||||
|
clearMessages();
|
||||||
|
}}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="auth-switch">
|
||||||
|
<button type="button" class="auth-link-btn" onClick={() => switchMode('reset')}>
|
||||||
|
Forgot password?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn primary block-btn"
|
||||||
|
disabled={isSubmitting() || !canSubmitLoginCredentials()}
|
||||||
|
onClick={directSignIn}
|
||||||
|
>
|
||||||
|
{isSubmitting() ? 'Signing in...' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div class="field">
|
||||||
|
<label>New password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setNewPassword(event.currentTarget.value);
|
||||||
|
clearMessages();
|
||||||
|
}}
|
||||||
|
placeholder="Enter your new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Confirm password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setConfirmPassword(event.currentTarget.value);
|
||||||
|
clearMessages();
|
||||||
|
}}
|
||||||
|
placeholder="Confirm your new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{resetStep() === 'verify' ? (
|
||||||
|
<div class="field">
|
||||||
|
<label>Verification code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={6}
|
||||||
|
value={resetCode()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6));
|
||||||
|
clearMessages();
|
||||||
|
}}
|
||||||
|
placeholder="Enter 6-digit code"
|
||||||
|
/>
|
||||||
|
<p class="hint">Code sent to {maskedEmail() || email()}.</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div class="auth-switch">
|
||||||
|
<button type="button" class="auth-link-btn" onClick={() => switchMode('login')}>
|
||||||
|
Back to sign in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
{resetStep() === 'request' ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn primary block-btn"
|
||||||
|
disabled={isSubmitting() || !canSubmitResetRequest()}
|
||||||
|
onClick={requestResetCode}
|
||||||
|
>
|
||||||
|
{isSubmitting() ? 'Sending code...' : 'Send reset code'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn primary block-btn"
|
||||||
|
disabled={isSubmitting() || !canSubmitResetVerify()}
|
||||||
|
onClick={verifyResetCode}
|
||||||
|
>
|
||||||
|
{isSubmitting() ? 'Resetting password...' : 'Verify & reset password'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
{info() ? <p class="inline-note auth-inline-msg">{info()}</p> : null}
|
||||||
|
{error() ? <p class="error-note auth-inline-msg">{error()}</p> : null}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||