1<!doctype html> 2<html lang="en"> 3<head> 4 <title>Log in</title> 5 <!-- Google tag (gtag.js) -->
vendor: 65 bytes, lines 5-6
5 6<script async src="https://www.googletagmanager.com/gtag/js?id=
6G-NZRY30GYKV
vendor: 11 bytes, line 6
6"></script>
6<script>
vendor: 124 bytes, line 6
6window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '
6G-NZRY30GYKV
vendor: 3 bytes, line 6
6');
6</script>
6<meta charset="utf-8"> 7<meta name="viewport" content="width=device-width, initial-scale=1"> 8<meta name="mobile-web-app-capable" content="yes"> 9<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 10<meta name="robots" content="noindex, nofollow, noarchive"> 11<meta name="csrf-token" content="f3996184780c4c98d107f77b655efb58ff96d767e4fc37caa603fd04045fc06a_1790867886"> 12<link rel="preconnect" href="https://fonts.googleapis.com"> 13<link rel="preconnect" href="https://cdnjs.cloudflare.com"> 14<link rel="preconnect" href="https://cdn.jsdelivr.net"> 15<link rel="preconnect" href="https://code.jquery.com"> 16<!--<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>--> 17<!--<link rel="stylesheet" referrerpolicy="no-referrer" href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,700;0,800;1,400;1,700;1,800&display=swap"> 18<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@100;200;400;600;800&display=swap" rel="stylesheet">--> 19<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous" referrerpolicy="no-referrer"> 20<link href="https://cdn.datatables.net/v/bs5/dt-1.13.8/b-2.4.2/b-colvis-2.4.2/r-2.5.0/datatables.min.css" rel="stylesheet"> 21 22 23<link rel="stylesheet" referrerpolicy="no-referrer" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css"> 24<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/select2-bootstrap-5-theme.min.css"> 25 26<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" id="daterangecss"> 27<!-- url changes based on lightmode/darkmode --> 28 29 30<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer"> 31<!-- migrate to 6.7--> 32<!-- migrate to 7.0 --> 33<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer"> 34<link rel="stylesheet" referrerpolicy="no-referrer" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> 35<!--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/apexcharts/3.40.0/apexcharts.min.css" integrity="sha512-tJYqW5NWrT0JEkWYxrI4IK2jvT7PAiOwElIGTjALSyr8ZrilUQf+gjw2z6woWGSZqeXASyBXUr+WbtqiQgxUYg==" crossorigin="anonymous" referrerpolicy="no-referrer">--> 36<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/apexcharts/5.3.5/apexcharts.min.css" integrity="sha512-IqtQ7LKr3He47p7HjxynmqZfN07VljNkdGyGDdDJ//f1r6bT0IEKQf2CCtSgun/pvbFlNnPDMRrMSQhmSxmSSg==" crossorigin="anonymous" referrerpolicy="no-referrer" /> 37<link rel="stylesheet" href="css/style.css?722968"><link rel="stylesheet" href="css/light-mode.css?487997"><!-- 38<link rel="shortcut icon" href="https://ninjatms.com/favicon.ico" type="image/x-icon" /> 39<link rel="apple-touch-icon" sizes="76x76" href="ico/apple-touch-icon.png"> 40<link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png"> 41<link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png"> 42<link rel="manifest" href="ico/site.webmanifest"> 43--> 44<link rel="icon" type="image/png" href="https://ninjatms.com/icons/favicon-48x48.png" sizes="48x48"> 45<link rel="icon" type="image/svg+xml" href="https://ninjatms.com/icons/favicon.svg"> 46<link rel="shortcut icon" href="https://ninjatms.com/icons/favicon.ico"> 47<link rel="apple-touch-icon" sizes="180x180" href="https://ninjatms.com/icons/apple-touch-icon.png"> 48<link rel="manifest" href="https://ninjatms.com/icons/site.webmanifest"> 49<style> 50.rightcanvasbutton{ 51 position: fixed; 52 right: 10px; 53 bottom: 10px; 54} 55.dropdown:hover .dropdown-menu { 56/* this is required for dropdown hover because it was taken out of bootstrap */ 57 display: block; 58 margin-top: 0;
59} 60.toast-container { 61 position: fixed; 62 bottom: 0; 63 right: 0; 64 z-index: 1050; /* Adjust as needed */ 65 padding: 3rem; 66} 67/* 68styles for chat start 69*/ 70#offcanvasRight { 71 width: 34%; 72} 73#chat_container { 74 display: flex; 75 flex-direction: column; 76 height: 100%; 77} 78/* Left column: contact list spans full height of offcanvas */ 79#offcanvasRight .chat-tabs-col { 80 display: flex; 81 flex-direction: column; 82 border-right: 1px solid var(--bs-border-color, rgba(0, 0, 0, .1)); 83 background: var(--bs-body-bg, #fff); 84 overflow: hidden; 85} 86#offcanvasRight .chat-tabs-search { 87 position: relative; 88 padding: 6px 10px; 89 border-bottom: 1px solid var(--bs-border-color, rgba(0, 0, 0, .08)); 90 background: var(--bs-tertiary-bg, #f8f9fa); 91} 92#offcanvasRight .chat-tabs-search .fas { 93 position: absolute; 94 left: 22px; 95 top: 50%; 96 transform: translateY(-50%); 97 color: var(--bs-secondary-color, #6c757d); 98 font-size: 0.8rem; 99 pointer-events: none; 100} 101#offcanvasRight #chat-tabs-filter { 102 padding-left: 28px; 103 border-radius: 6px; 104} 105/* Sticky contact header pinned at the top of every conversation pane. Avatar + name on the left, 106 email/phone quick-actions on the right. Sized to align with the contacts search bar across the 107 divider so the two read as a unified top row. Subtle shadow conveys it floats above scrolled content. */ 108.chat-contact-header { 109 position: sticky; 110 top: -12px; /* i had to do this because 12px see-through space above this is rendering */ 111 z-index: 2; 112 display: flex; 113 align-items: center; 114 gap: 10px; 115 padding: 6px 10px; 116 background: var(--bs-body-bg, #fff); 117 border-bottom: 1px solid var(--bs-border-color, rgba(0, 0, 0, .08));
118 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); 119 margin: -12px -12px 8px; /* counter the .tab-content padding so the header spans full width */ 120} 121.chat-contact-header .chat-contact-avatar-wrap { 122 position: relative; 123 flex-shrink: 0; 124 width: 28px; 125 height: 28px; 126 line-height: 1; /* kill any leftover text leading so img/initials cases render the same height */ 127} 128.chat-contact-header .chat-contact-avatar { 129 display: flex; 130 align-items: center; 131 justify-content: center; 132 width: 28px; 133 height: 28px; 134 border-radius: 50%; 135 color: #fff; 136 font-size: 0.7rem; 137 font-weight: 600; 138 letter-spacing: 0.02em; 139 overflow: hidden; 140 line-height: 1; 141} 142.chat-contact-header .chat-contact-avatar img { 143 display: block; /* default <img> is inline â carries baseline descender whitespace that 144 makes pfp avatars render taller than initials avatars. block strips it. */ 145 width: 100%; 146 height: 100%; 147 object-fit: cover; 148} 149/* Centered loading spinner shown briefly while get-chat-messages resolves. */ 150.chat-loading { 151 display: flex; 152 align-items: center; 153 justify-content: center; 154 min-height: 200px; 155 color: var(--bs-secondary-color, #6c757d); 156} 157.chat-contact-header .chat-contact-online { 158 position: absolute; 159 right: -1px; 160 bottom: -1px; 161 width: 9px; 162 height: 9px; 163 border-radius: 50%; 164 border: 2px solid var(--bs-body-bg, #fff); 165} 166.chat-contact-header .chat-contact-online.is-online { 167 background: #28a745; 168 animation: chat-contact-online-glow 2s ease-in-out infinite; 169} 170.chat-contact-header .chat-contact-online.is-offline { background: #adb5bd; } 171@keyframes chat-contact-online-glow { 172 0%, 100% { 173 box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.55), 0 0 4px 1px rgba(40, 167, 69, 0.5); 174 } 175 50% { 176 box-shadow: 0 0 0 4px rgba(40, 167, 69, 0), 0 0 8px 3px rgba(40, 167, 69, 0.85); 177 } 178} 179@media (prefers-reduced-motion: reduce) { 180 .chat-contact-header .chat-contact-online.is-online { animation: none; } 181} 182.chat-contact-header .chat-contact-online.is-online.is-ai-support { 183 background: #a855f7; 184 animation: chat-contact-online-glow-ai 1.6s ease-in-out infinite; 185} 186@keyframes chat-contact-online-glow-ai { 187 0%, 100% { 188 box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.85), 0 0 6px 2px rgba(168, 85, 247, 0.85); 189 } 190 50% { 191 box-shadow: 0 0 0 6px rgba(168, 85, 247, 0), 0 0 14px 5px rgba(168, 85, 247, 1); 192 } 193} 194@media (prefers-reduced-motion: reduce) { 195 .chat-contact-header .chat-contact-online.is-online.is-ai-support { animation: none; } 196} 197.chat-contact-header .chat-contact-name { 198 flex: 0 1 auto; 199 min-width: 0; 200 font-size: 0.9rem; 201 color: var(--bs-body-color, #212529); 202} 203.chat-contact-header .chat-contact-meta { 204 display: flex; 205 align-items: center; 206 gap: 12px; 207 color: var(--bs-secondary-color, #6c757d); 208 flex-shrink: 0; 209} 210.chat-contact-header .chat-contact-link { 211 color: inherit; 212 text-decoration: none; 213 display: inline-flex; 214 align-items: center; 215 gap: 4px; 216 transition: color 0.15s ease; 217 max-width: 220px; 218} 219.chat-contact-header .chat-contact-link:hover { 220 color: var(--bs-primary, #0d6efd); 221} 222.chat-contact-header .chat-contact-link i { 223 opacity: 0.7; 224} 225.chat-contact-header .chat-contact-link-text { 226 overflow: hidden; 227 text-overflow: ellipsis; 228 white-space: nowrap; 229} 230@media (max-width: 575.98px) { 231 /* On narrow screens, hide the inline email/phone text â keep just icons as touch targets. */ 232 .chat-contact-header .chat-contact-link-text { display: none; } 233} 234#offcanvasRight .nav-tabs { 235 margin: 0; 236 padding: 4px 0; 237 flex-grow: 1; 238 overflow-y: auto; 239 border: 0; 240 width: 100%; 241 display: block; 242} 243#offcanvasRight .nav-tabs .nav-link { 244 display: flex; 245 align-items: center; 246 gap: 10px; 247 width: 100%; 248 text-align: left; 249 border: 0; 250 border-radius: 0; 251 padding: 8px 12px; 252 color: var(--bs-body-color, #212529); 253 background: transparent; 254 font-size: 0.9rem; 255 cursor: pointer; 256 position: relative; 257 transition: background-color .12s ease; 258} 259#offcanvasRight .nav-tabs .nav-link:hover { 260 background: var(--bs-tertiary-bg, rgba(0, 0, 0, .04)); 261} 262#offcanvasRight .nav-tabs .nav-link.active { 263 background: var(--bs-primary-bg-subtle, #cfe2ff); 264 color: var(--bs-primary-text-emphasis, #052c65); 265 font-weight: 600; 266 border-left: 3px solid var(--bs-primary, #0d6efd); 267 padding-left: 9px; 268} 269#offcanvasRight .nav-tabs .nav-link .chat-online-indicator { 270 flex: 0 0 auto; 271 font-size: 0.55rem; 272 width: 10px; 273 display: inline-flex; 274 align-items: center; 275 justify-content: center; 276} 277#offcanvasRight .nav-tabs .nav-link .chat-online-indicator .fa-circle.text-success { 278 animation: chat-online-pulse 2s ease-in-out infinite; 279 border-radius: 50%; 280 box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7); 281} 282@keyframes chat-online-pulse { 283 0% { 284 box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7), 0 0 4px rgba(25, 135, 84, 0.6); 285 } 286 70% { 287 box-shadow: 0 0 0 6px rgba(25, 135, 84, 0), 0 0 8px rgba(25, 135, 84, 0.9); 288 } 289 100% { 290 box-shadow: 0 0 0 0 rgba(25, 135, 84, 0), 0 0 4px rgba(25, 135, 84, 0.6); 291 } 292} 293@media (prefers-reduced-motion: reduce) { 294 #offcanvasRight .nav-tabs .nav-link .chat-online-indicator .fa-circle.text-success { 295 animation: none; 296 } 297} 298#offcanvasRight .nav-tabs .nav-link .chat-online-indicator.is-ai-support .fa-circle.text-success { 299 animation: chat-online-pulse-ai 1.6s ease-in-out infinite; 300 color: #a855f7 !important; 301} 302@keyframes chat-online-pulse-ai { 303 0% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.85), 0 0 6px 2px rgba(168, 85, 247, 0.85); } 304 70% { box-shadow: 0 0 0 8px rgba(168, 85, 247, 0), 0 0 14px 5px rgba(168, 85, 247, 1); } 305 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0), 0 0 6px 2px rgba(168, 85, 247, 0.85); } 306} 307@media (prefers-reduced-motion: reduce) { 308 #offcanvasRight .nav-tabs .nav-link .chat-online-indicator.is-ai-support .fa-circle.text-success { 309 animation: none; 310 } 311} 312#offcanvasRight .nav-tabs .nav-link .chat-user { 313 flex: 1 1 auto; 314 overflow: hidden; 315 text-overflow: ellipsis; 316 white-space: nowrap; 317 min-width: 0; 318} 319#offcanvasRight .nav-tabs .nav-link .badge { 320 flex: 0 0 auto; 321} 322/* Right column: messages + chat input */ 323#offcanvasRight .chat-content-col { 324 min-width: 0; /* allow flex children to shrink */ 325} 326#offcanvasRight .tab-content { 327 flex-grow: 1; 328 overflow-y: auto; 329 padding: 12px; 330} 331#offcanvasRight .tab-pane { 332 height: auto; 333 min-height: 100%; 334} 335/* Pin chat content to the bottom of the pane (chat-app style) so the typing 336 indicator sits right under the last bubble instead of floating above empty space. */ 337#offcanvasRight .tab-pane.chat > :first-child { 338 margin-top: auto; 339} 340#chat_form, .chat-form-bar { 341 position: static; 342 width: 100%; 343 padding: 10px 12px; 344 margin: 0; 345 border-top: 1px solid var(--bs-border-color, rgba(0, 0, 0, .08));
346 background: var(--bs-body-bg, #fff); 347 flex: 0 0 auto; 348 z-index: 1; 349} 350.chat-typing-indicator { 351 display: flex; 352 align-items: center; 353 gap: 8px; 354 padding: 6px 12px; 355 font-size: 0.78rem; 356 color: var(--bs-secondary-color, #6c757d); 357 font-style: italic; 358 background: var(--bs-tertiary-bg, #f1f3f5); 359 border-radius: 14px; 360 margin: 8px 8px; 361 width: fit-content; 362 max-width: 80%; 363} 364/* Live draft text inside the typing indicator. Shown when a tenant chatting with a support 365 user is currently composing â replaces the generic "X is typing" copy with their actual 366 in-progress message. Wraps to multiple lines if the draft is long. */ 367.chat-typing-indicator .chat-typing-preview { 368 color: var(--bs-body-color, #212529); 369 font-style: italic; 370 white-space: pre-wrap; 371 word-break: break-word; 372} 373.chat-typing-indicator .chat-typing-preview::before { content: '"'; opacity: 0.5; } 374.chat-typing-indicator .chat-typing-preview::after { content: '"'; opacity: 0.5; } 375.chat-typing-dots { 376 display: inline-flex; 377 gap: 3px; 378} 379.chat-typing-dots span { 380 display: inline-block; 381 width: 6px; 382 height: 6px; 383 border-radius: 50%; 384 background: var(--bs-secondary-color, #6c757d); 385 opacity: 0.4; 386 animation: chat-typing-bounce 1.2s ease-in-out infinite; 387} 388.chat-typing-dots span:nth-child(2) { animation-delay: 0.15s; } 389.chat-typing-dots span:nth-child(3) { animation-delay: 0.3s; } 390@keyframes chat-typing-bounce { 391 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); } 392 30% { opacity: 1; transform: translateY(-3px); } 393} 394@media (prefers-reduced-motion: reduce) { 395 .chat-typing-dots span { animation: none; opacity: 0.6; } 396} 397.chat-typing-mini { 398 display: inline-flex; 399 align-items: center; 400 gap: 2px; 401 margin-left: 6px; 402 flex: 0 0 auto; 403} 404.chat-typing-mini span { 405 display: inline-block; 406 width: 4px; 407 height: 4px; 408 border-radius: 50%; 409 background: var(--bs-primary, #0d6efd); 410 opacity: 0.4; 411 animation: chat-typing-bounce 1.2s ease-in-out infinite; 412} 413.chat-typing-mini span:nth-child(2) { animation-delay: 0.15s; } 414.chat-typing-mini span:nth-child(3) { animation-delay: 0.3s; } 415@media (prefers-reduced-motion: reduce) { 416 .chat-typing-mini span { animation: none; opacity: 0.7; } 417} 418 419/* Bounce animation for chat-notification badges (nav icon + per-contact). 420 Custom keyframe instead of animate.css's bounceIn because Bootstrap's 421 .translate-middle locks `transform: translate(-50%,-50%) !important` on 422 these badges, which kills animate.css's transform-driven keyframes. 423 Composing translate() with scale() here lets the bounce play without 424 losing the centering. Applied/restarted from chat.js _bounceBadge(). */ 425@keyframes chat-badge-bounce { 426 0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; } 427 20% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; } 428 40% { transform: translate(-50%, -50%) scale(0.9); } 429 60% { transform: translate(-50%, -50%) scale(1.04); } 430 80% { transform: translate(-50%, -50%) scale(0.97); } 431 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } 432} 433.chat-badge-bounce { 434 animation: chat-badge-bounce 0.5s cubic-bezier(0.215, 0.61, 0.355, 1) both !important; 435} 436@media (prefers-reduced-motion: reduce) { 437 .chat-badge-bounce { animation: none !important; } 438} 439 440/* Reusable empty-state â chat (no chats yet) and alerts (no alerts yet) share this look. */ 441.empty-state { 442 display: flex; 443 flex-direction: column; 444 align-items: center; 445 justify-content: center; 446 padding: 48px 24px; 447 color: var(--bs-secondary-color, #6c757d); 448 text-align: center; 449 min-height: 50vh; 450 gap: 4px; 451} 452.empty-state-icon { 453 font-size: 3rem; 454 margin-bottom: 14px; 455 color: var(--bs-secondary-color, #6c757d); 456 opacity: 0.35; 457} 458.empty-state-icon.pulse { 459 animation: empty-state-pulse 2.4s ease-in-out infinite; 460} 461.empty-state-title { 462 font-size: 1.05rem; 463 font-weight: 600; 464 color: var(--bs-body-color, #212529); 465 margin-bottom: 4px; 466} 467.empty-state-desc { 468 font-size: 0.88rem; 469 max-width: 320px; 470 line-height: 1.45; 471 color: var(--bs-secondary-color, #6c757d); 472} 473@keyframes empty-state-pulse { 474 0%, 100% { opacity: 0.25; transform: scale(1); } 475 50% { opacity: 0.55; transform: scale(1.05); } 476} 477@media (prefers-reduced-motion: reduce) { 478 .empty-state-icon.pulse { animation: none; } 479} 480/* Support welcome card â pinned at the top of every conversation with UID 2 (Ted). Message history 481 flows beneath it; doubles as a standing reassurance that a real human reads every message. */ 482.empty-state-support { 483 padding: 36px 24px; 484 gap: 6px; 485} 486.empty-state-support.support-header { 487 min-height: 0; 488 padding: 24px 20px 20px; 489 border-bottom: 1px solid var(--bs-border-color, #dee2e6); 490 margin-bottom: 8px; 491}
492.empty-state-support .support-avatar-wrap { 493 position: relative; 494 width: 76px; 495 height: 76px; 496 margin-bottom: 12px; 497} 498.empty-state-support .support-avatar { 499 width: 76px; 500 height: 76px; 501 border-radius: 50%; 502 object-fit: cover; 503 box-shadow: 0 4px 14px rgba(13, 110, 253, 0.25); 504 background: #1f2937; 505 padding: 6px; 506} 507.empty-state-support .support-online-dot { 508 position: absolute; 509 right: 2px; 510 bottom: 2px; 511 width: 16px; 512 height: 16px; 513 border-radius: 50%; 514 background: #28a745; 515 border: 3px solid var(--bs-body-bg, #fff); 516 box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25); 517 animation: support-pulse 2s ease-in-out infinite; 518} 519@keyframes support-pulse { 520 0%, 100% { box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25); } 521 50% { box-shadow: 0 0 0 6px rgba(40, 167, 69, 0.05); } 522} 523.empty-state-support .support-title { 524 font-size: 1.25rem; 525 font-weight: 700; 526} 527.empty-state-support .support-desc { 528 max-width: 360px; 529 font-size: 0.92rem; 530 color: var(--bs-body-color, #212529); 531 margin-bottom: 14px; 532} 533.empty-state-support .support-perks { 534 max-width: 360px; 535 width: 100%; 536 margin: 0; 537 padding: 0; 538 text-align: left; 539 font-size: 0.85rem; 540 color: var(--bs-secondary-color, #6c757d); 541} 542.empty-state-support .support-perks li { 543 display: flex; 544 align-items: flex-start; 545 gap: 10px; 546 padding: 6px 0; 547 line-height: 1.4; 548} 549.empty-state-support .support-perks li i { 550 flex: 0 0 18px; 551 margin-top: 2px; 552 text-align: center; 553} 554@media (prefers-reduced-motion: reduce) { 555 .empty-state-support .support-online-dot { animation: none; } 556} 557 558/* ââ AI Support (Jim) welcome card â distinct surface, NOT a copy of the 559 human Support card. Horizontal layout, tech-feel stripe, tag pills. âââ */ 560.ai-welcome { 561 position: relative; 562 background: linear-gradient(140deg, rgba(30,58,138,0.18) 0%, rgba(15,23,42,0.45) 100%); 563 border: 1px solid rgba(96,165,250,0.22); 564 border-radius: 12px; 565 padding: 14px 16px 12px; 566 margin: 8px 8px 14px; 567 overflow: hidden; 568} 569.ai-welcome-stripe { 570 position: absolute; 571 top: 0; left: 0; right: 0; 572 height: 3px; 573 background: linear-gradient(90deg, #60a5fa 0%, #3b82f6 35%, #22c55e 65%, #60a5fa 100%); 574 background-size: 300% 100%; 575 animation: ai-welcome-stripe-flow 6s linear infinite; 576} 577@keyframes ai-welcome-stripe-flow { 578 0% { background-position: 0% 50%; } 579 100% { background-position: 300% 50%; } 580} 581@media (prefers-reduced-motion: reduce) { 582 .ai-welcome-stripe { animation: none; } 583} 584.ai-welcome-row { 585 display: flex; 586 align-items: flex-start; 587 gap: 14px; 588} 589.ai-welcome-avatar-wrap { 590 position: relative; 591 flex: 0 0 auto; 592} 593.ai-welcome-avatar { 594 width: 54px; 595 height: 54px; 596 border-radius: 12px; 597 display: flex; 598 align-items: center; 599 justify-content: center; 600 background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #1d4ed8 100%); 601 color: #fff; 602 font-size: 24px; 603 box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 6px 18px rgba(29,78,216,0.45); 604} 605.ai-welcome-status { 606 position: absolute; 607 right: -2px; 608 bottom: -2px; 609 width: 12px; 610 height: 12px; 611 border-radius: 50%; 612 background: #a855f7; 613 border: 2px solid #0f172a; 614 animation: ai-welcome-pulse 1.6s ease-in-out infinite; 615} 616@keyframes ai-welcome-pulse { 617 0%, 100% { box-shadow: 0 0 0 0 rgba(168,85,247,0.85), 0 0 6px 2px rgba(168,85,247,0.85); } 618 50% { box-shadow: 0 0 0 6px rgba(168,85,247,0), 0 0 12px 4px rgba(168,85,247,1); } 619} 620@media (prefers-reduced-motion: reduce) { 621 .ai-welcome-status { animation: none; } 622} 623.ai-welcome-body { 624 flex: 1 1 auto; 625 min-width: 0; 626} 627.ai-welcome-eyebrow { 628 font-size: 0.7rem; 629 font-weight: 600; 630 letter-spacing: 0.6px; 631 text-transform: uppercase; 632 color: #93c5fd; 633 margin-bottom: 2px; 634} 635.ai-welcome-eyebrow i { 636 margin-right: 4px; 637} 638.ai-welcome-name { 639 font-size: 1.4rem; 640 font-weight: 700; 641 color: var(--bs-body-color, #f8fafc); 642 line-height: 1.1; 643 margin-bottom: 4px; 644} 645.ai-welcome-tagline { 646 font-size: 0.85rem; 647 color: var(--bs-secondary-color, #cbd5e1); 648 line-height: 1.4; 649 margin-bottom: 10px; 650} 651.ai-welcome-tags { 652 display: flex; 653 flex-wrap: wrap; 654 gap: 6px; 655} 656.ai-tag {
657 display: inline-flex; 658 align-items: center; 659 gap: 5px; 660 font-size: 0.72rem; 661 padding: 3px 9px; 662 border-radius: 999px; 663 background: rgba(59,130,246,0.12); 664 border: 1px solid rgba(96,165,250,0.25); 665 color: #bfdbfe; 666} 667.ai-tag i { 668 font-size: 0.7em; 669 opacity: 0.85; 670} 671.ai-welcome-prompts { 672 margin-top: 14px; 673 padding-top: 12px; 674 border-top: 1px dashed rgba(96,165,250,0.18); 675} 676.ai-welcome-prompts-label { 677 font-size: 0.7rem; 678 font-weight: 600; 679 letter-spacing: 0.6px; 680 text-transform: uppercase; 681 color: var(--bs-secondary-color, #94a3b8); 682 margin-bottom: 8px; 683} 684.ai-welcome-prompts-label i { 685 margin-right: 6px; 686} 687.ai-welcome-prompts-list { 688 display: flex; 689 flex-wrap: wrap; 690 gap: 6px; 691} 692.ai-try-chip { 693 font-size: 0.78rem; 694 padding: 5px 12px; 695 border-radius: 14px; 696 background: rgba(15,23,42,0.4); 697 border: 1px solid rgba(96,165,250,0.3); 698 color: #dbeafe; 699 cursor: pointer; 700 white-space: normal; 701 text-align: left; 702 line-height: 1.3; 703 transition: background 0.15s ease, border-color 0.15s ease; 704} 705.ai-try-chip:hover { 706 background: rgba(59,130,246,0.18); 707 border-color: rgba(96,165,250,0.55); 708 color: #fff; 709} 710 711/* Jim's small avatar in the conversation header (sticky top bar) â same 712 robot-head identity as the welcome card, sized to match other contacts. */ 713.chat-contact-avatar.chat-contact-avatar-ai { 714 background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #1d4ed8 100%) !important; 715 display: flex; 716 align-items: center; 717 justify-content: center; 718 color: #fff; 719} 720.chat-contact-avatar.chat-contact-avatar-ai i { 721 font-size: 1.05em; 722} 723 724#chat_form .form-floating .form-control { 725 border-radius: 6px 0 0 6px; 726} 727#chat_form .btn[type="submit"] { 728 border-radius: 0 6px 6px 0; 729} 730#messageContent { 731 height: auto; 732 overflow: visible; 733} 734#messageContent p { 735 margin:0px; 736} 737/* 738hijacked from 739https://stackoverflow.com/questions/71154905/css-for-chat-room-speech-bubble-position 740 741*/ 742.msg { 743 position: relative; 744 max-width: 90%; 745 padding: 7px 15px; 746 margin-bottom: 2px; 747} 748.chat { 749 --rad: 20px; 750 --rad-sm: 3px; 751 display: flex; 752 flex-direction: column; 753 /* padding: 20px; */ 754 /*max-width: 500px;*/ 755 margin: auto; 756} 757.msg.sent { 758 border-radius: var(--rad) var(--rad-sm) var(--rad-sm) var(--rad); 759 background: #42a5f5; 760 color: #fff; 761 /* moves it to the right */ 762 margin-left: auto; 763} 764 765.msg.rcvd { 766 border-radius: var(--rad-sm) var(--rad) var(--rad) var(--rad-sm); 767 background: #f1f1f1; 768 color: #555; 769 /* moves it to the left */ 770 margin-right: auto; 771} 772.msg.sent a { 773 color:#fff; 774} 775 776/* Improve radius for messages group */ 777 778.msg.sent:first-child, 779.msg.rcvd+.msg.sent { 780 border-top-right-radius: var(--rad); 781} 782 783.msg.rcvd:first-child, 784.msg.sent+.msg.rcvd { 785 border-top-left-radius: var(--rad); 786} 787/* time */ 788.msg::before { 789 content: attr(data-time); 790 font-size: 0.8rem; 791 position: absolute; 792 bottom: 100%; 793 color: #888; 794 white-space: nowrap; 795 /* Hidden by default */ 796 display: none; 797} 798.msg.sent::before { 799 right: 15px; 800} 801.msg.rcvd::before { 802 left: 15px; 803} 804/* Show time only for first message in group */ 805.msg:first-child::before, 806.msg.sent+.msg.rcvd::before, 807.msg.rcvd+.msg.sent::before { 808 /* Show only for first message in group */ 809 display: block; 810} 811/* Seen receipt â prepended inside the bubble's <small> header, left of the sender 812 name (kept INSIDE .msg.sent: a sibling div would break the .msg adjacency 813 selectors above). White-on-blue: the sent bubble is #42a5f5. 814 .seen-pending = circle-notch spinner while unseen; .seen-check = â once seen. */ 815.msg.sent .seen-check, 816.msg.sent .seen-pending { 817 text-decoration: none; 818 cursor: default; 819 color: rgba(255,255,255,.9); 820 margin-right: 2px; 821} 822.msg.sent .seen-pending { 823 color: rgba(255,255,255,.7); /* dimmer â still waiting */ 824} 825.msg.sent .seen-check .fa-check, 826.msg.sent .seen-pending .fa-circle-notch { 827 font-size: .7rem; 828} 829.offcanvas{ 830 transition: none !important; 831} 832</style> 833<style id="custom-theme"> 834</style>
835<script> 836function getQueryParams() { 837 var params = {}; 838 var queryString = window.location.search.substring(1); 839 var regex = /([^&=]+)=([^&]*)/g; 840 var match; 841 while (match = regex.exec(queryString)) { 842 params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); 843 } 844 return params; 845} 846var queryParams = getQueryParams(); 847 848// Access the id parameter 849var thispageid = queryParams.id; 850var protocol = window.location.href.indexOf("https://") == 0 ? "https" : "http"; 851var path = window.location.pathname; 852var thispage = path.split("/").pop() || 'index.php' 853 854//options to js 855var dispatchDriversByDefault = false; 856 857 858 const DOMAIN = 'ninjatms.com';</script>
858 859 860 <link rel="preconnect" href="https://www.google.com" crossorigin> 861 <link rel="preconnect" href="https://www.gstatic.com" crossorigin> 862 <style> 863 .container{ 864 height:100vh; 865 } 866 .main-head { 867 height: 150px; 868 background: #FFF; 869 } 870 .sidenav { 871 height: 100%; 872 background-color: #000; 873 overflow-x: hidden; 874 padding-top: 20px; 875 background: linear-gradient(-45deg, #ffffff, #5ba9ff, #001646, #00051e); 876 background-size: 400% 400%; 877 animation: gradient 15s ease infinite; 878 } 879 @keyframes gradient { 880 0% { 881 background-position: 0% 50% 882 } 883 50% { 884 background-position: 100% 50% 885 } 886 100% { 887 background-position: 0% 50% 888 } 889 } 890 main { 891 padding: 0px 10px; 892 } 893 @media screen and (max-height: 576px) { 894 .sidenav { 895 padding-top: 15px; 896 } 897 } 898 @media screen and (max-width: 576px) { 899 .login-form { 900 margin-top: 10%; 901 } 902 .register-form { 903 margin-top: 10%; 904 } 905 } 906 @media screen and (max-width: 768px){ 907 .sidenav{ 908 display:none 909 } 910 main{ 911 margin-left:0px; 912 } 913 } 914 @media screen and (min-width: 768px) { 915 main { 916 margin-left: 40%; 917 } 918 .sidenav { 919 width: 40%; 920 position: fixed; 921 z-index: 1; 922 top: 0; 923 left: 0; 924 } 925 .login-form { 926 margin-top: 15%; 927 } 928 .register-form { 929 margin-top: 20%; 930 } 931 } 932 .login-main-text { 933 margin-top: 5%; 934 padding: 95px; 935 color: #fff; 936 background-color: rgba(0,0,0,0.8); 937 } 938 .login-main-text h2 { 939 font-weight:800 940 } 941 942.header { 943 display: flex;
944 align-items: center; /* Aligns items vertically */ 945 gap: 10px; /* Adjusts space between image and text */ 946} 947 948.header img { 949 height: 75px; /* Adjust size as needed */ 950} 951 952.text-container { 953 display: flex; 954 flex-direction: column; /* Stacks the TMS name and system text vertically */ 955} 956 957/* ââ OAuth sign-in buttons (carrierpacket.link-style stack) âââââââââââââ */ 958.auth-oauth .btn{ 959 display:flex; 960 align-items:center; 961 justify-content:center; 962 gap:10px; 963 font-weight:500; 964 background:#fff; 965 color:#3c4043; 966} 967.auth-oauth .btn-google{ border:1px solid #dadce0; font-family:'Roboto',system-ui,sans-serif; } 968.auth-oauth .btn-ms{ border:1px solid #d0d4d8; font-family:'Segoe UI',system-ui,sans-serif; } 969.auth-oauth .btn-magic{ border:1px solid var(--bs-primary); color:var(--bs-primary); } 970.auth-oauth .btn:hover{ background:#f8f9fa; } 971.auth-oauth .ms-logo{ display:grid; grid-template-columns:8px 8px; grid-gap:2px; } 972.auth-oauth .ms-logo span{ width:8px; height:8px; display:block; } 973.auth-or{ 974 display:flex; 975 align-items:center; 976 gap:12px; 977 color:#6c757d; 978 font-size:13px; 979 margin:18px 0; 980} 981.auth-or::before, .auth-or::after{ content:""; flex:1; border-top:1px solid #dee2e6; } 982 </style>
983<script>const a = 'OTM2MzJmYTFjNDVmNmQ2YjU3ZjI3MzQ3ZDE0ZDk0OWI=';</script>
983 984</head> 985<body>
985<script> 986 var tmode = localStorage.getItem('t') 987 if (!tmode) { 988 localStorage.setItem('t', 'dark') 989 tmode = 'dark'; 990 } 991 if (tmode && tmode == 'dark') { 992 try { 993 //<link rel="stylesheet" type="text/css" href="css/daterangepicker.min.css"> 994 document.getElementById('daterangecss').setAttribute('href', 'css/daterangepicker.min.css') 995 var themeMode = document.querySelector('#theme-mode') 996 if (themeMode) { 997 themeMode.checked = true; 998 document.documentElement.setAttribute('data-bs-theme', tmode); 999 const tlab = document.getElementById("theme-label") ?? null 1000 if (tlab) { 1001 tlab.innerHTML = 'Dark Mode'; 1002 } 1003 } else { 1004 document.documentElement.setAttribute('data-bs-theme', tmode); 1005 } 1006 } catch (error) { 1007 console.log('darkmode/lightmode error', error) 1008 } 1009 }else{ 1010 document.documentElement.setAttribute('data-bs-theme', tmode); 1011 } 1012</script>
1012 1013 <div class="sidenav"> 1014 <div class="login-main-text"> 1015 <h2 class="animate__animated animate__fadeIn header"> 1016 <a href="https://ninjatms.com/" title="Ninja TMS"><img src="https://ninjatms.com/images/ninjatms-simple.png" alt="Ninja TMS"></a> 1017 <div class="text-container"> 1018 <em><a href="https://ninjatms.com/" title="Ninja TMS" class="text-white text-decoration-none">Ninja TMS</a></em> 1019 <span class="animate__animated animate__fadeIn" style="font-size: 13px;font-weight: 200;">Transportation Management System</span> 1020 </div> 1021 </h2> 1022 1023 1024 </div> 1025 </div> 1026 <main> 1027 <div class="container"> 1028 <div class="row"> 1029 <div class="col-md-12 col-lg-6"> 1030 <div class="login-form"> 1031 <h1 class="display-6">Login</h1> 1032 <div id="rate-limit-banner" class="alert alert-warning d-none" role="alert"> 1033 <i class="fa-solid fa-lock"></i> <strong>Too many failed login attempts.</strong> 1034 Try again in <span id="rate-limit-countdown">00:00</span>. 1035 <div class="small mt-1">Logins are blocked for 10 minutes after 7 failed attempts.</div> 1036 </div> 1037 <form id="login-form"> 1038 <input type="hidden" name="logmein" id="logmein" value="6d17f3e"> 1039 <div class="form-group"> 1040 <label for="email">Email</label> 1041 <input id="email" name="email" type="text" class="form-control" placeholder="Email Address" value=""> 1042 </div> 1043 <div class="form-group mt-3"> 1044 <label for="password">Password</label> 1045 <input id="password" name="password" type="password" class="form-control" placeholder="Password"> 1046 </div> 1047 <div class="row mt-3"> 1048 <div class="col"> 1049 <button type="submit" class="btn btn-primary g-recaptcha" data-sitekey="6LeEJWwqAAAAAJD9D3_2Ikgs2CMRrj0CK8jioVDm" data-callback="onLoginSubmit" data-action="submit" disabled><i class="fas fa-sign-in-alt"></i> Login</button> 1050 </div> 1051 </div> 1052 </form> 1053 </div> 1054 <p class="mt-3"><a href="https://ninjatms.com/app/forgot.php">Forgot your password?</a></p> 1055 </div> 1056 <div class="row mt-3"> 1057 <div class="col mt-3"> 1058 <hr> 1059 </div> 1060 </div> 1061 <div class="row mt-3"> 1062 <div class="col mt-3"> 1063 <p>Don't have an account? Get registered here </p> 1064 <a href="https://ninjatms.com/app/register.php" class="btn btn-success"><i class="fas fa-user-plus"></i> Sign up</a> 1065 </div> 1066 </div> 1067 </div> 1068 <div class="row mt-5"> 1069 <div class="col-md-6 col-sm-12" id="message"></div> 1070 </div> 1071 </div> 1072 </main> 1073
1073<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.1/js/bootstrap.min.js" integrity="sha512-ewfXo9Gq53e1q1+WDTjaHAGZ8UvCWq0eXONhwDuIoaH8xz2r96uoAYaQCm1oQhnBfRXrvJztNXFsTloJfgbL5Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
1073 1074
1074<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
vendor: 81 bytes, lines 1074-1076
1074 1075 <script src="https://www.google.com/recaptcha/api.js" async defer></script> 1076
1076<script> 1077 // Invisible reCAPTCHA callback â fires with a token on button click; triggers the form submit 1078 function onLoginSubmit(token){ $('#login-form').trigger('submit'); } 1079 const eld_redirect = false; 1080 const eid = ''; 1081 const return_url = ''; 1082 var javascript = true 1083 var n = Math.floor(Math.random() * (8008135 - 1337 + 1)) + 1337 1084 1085 let rlRemaining = 0; 1086 let rlHideScheduled = false; 1087 let recaptchaReady = false; 1088 let loginSpinning = false; 1089 1090 // Single source of truth for the submit button's disabled state 1091 function refreshSubmitState(){ 1092 const shouldDisable = loginSpinning || !recaptchaReady || rlRemaining > 0; 1093 $('#login-form button[type=submit]').prop('disabled', shouldDisable); 1094 } 1095 1096 function updateRateLimitUI(seconds){ 1097 rlRemaining = Math.max(0, seconds|0); 1098 const $banner = $('#rate-limit-banner'); 1099 const mins = Math.floor(rlRemaining / 60); 1100 const secs = rlRemaining % 60; 1101 $('#rate-limit-countdown').text( 1102 String(mins).padStart(2,'0') + ':' + String(secs).padStart(2,'0') 1103 ); 1104 if(rlRemaining > 0){ 1105 rlHideScheduled = false; 1106 $banner.removeClass('d-none'); 1107 }else if(!rlHideScheduled){ 1108 // Counted down to 00:00 â dismiss banner shortly after so user sees 00:00 briefly 1109 rlHideScheduled = true; 1110 setTimeout(function(){ $('#rate-limit-banner').addClass('d-none'); }, 2000); 1111 } 1112 refreshSubmitState(); 1113 } 1114 setInterval(function(){ if(rlRemaining > 0) updateRateLimitUI(rlRemaining - 1); }, 1000); 1115 1116 // Poll until reCAPTCHA's async api.js has defined grecaptcha, then enable the button 1117 (function waitForGrecaptcha(){ 1118 if(typeof grecaptcha !== 'undefined' && typeof grecaptcha.ready === 'function'){ 1119 grecaptcha.ready(function(){ 1120 recaptchaReady = true; 1121 refreshSubmitState(); 1122 }); 1123 }else{ 1124 setTimeout(waitForGrecaptcha, 100); 1125 } 1126 })(); 1127 1128 // Tiny escapers used when injecting server-supplied messages into the DOM. 1129 // Server messages are mostly hardcoded strings, but defense-in-depth. 1130 function escapeMsg(s){ 1131 return String(s == null ? '' : s) 1132 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 1133 .replace(/"/g, '"').replace(/'/g, '''); 1134 } 1135 function escapeAttr(s){ return escapeMsg(s); } 1136 1137 $(function(){ 1138 const sessionID = localStorage.removeItem("sessionID"); 1139 $('main').attr('data-k', a); 1140 updateRateLimitUI(rlRemaining); 1141 1142 // Swap the submit-button icon between sign-in and loading spinner; disabled state handled by refreshSubmitState 1143 function setLoginLoading(isLoading){ 1144 loginSpinning = isLoading; 1145 const $icon = $('#login-form button[type=submit] i'); 1146 if(isLoading){ 1147 $icon.removeClass('fa-sign-in-alt').addClass('fa-circle-notch fa-spin'); 1148 }else{ 1149 $icon.removeClass('fa-circle-notch fa-spin').addClass('fa-sign-in-alt'); 1150 } 1151 refreshSubmitState(); 1152 } 1153 1154 $(document).on('submit', '#login-form', function(e){ 1155 e.preventDefault(); 1156 if(rlRemaining > 0) return; 1157 if(javascript){ 1158 setLoginLoading(true); 1159 const kibble = atob($('main').attr('data-k')) 1160 let formObject = $(this).serializeArray(); 1161 formObject.push({ name: 'code', value: 'e9ec28a' }); 1162 let payload = $.param(formObject); 1163 $.post('api/_auth.php?bZ4765d=9z&'+kibble, payload, function(response){ 1164 //console.log(response) 1165 const r = (typeof response === 'object' && response) ? response : {}; 1166 1167 // 429 â rate-limited (too many failed attempts). Same handler as before. 1168 if(r.status == 429){ 1169 setLoginLoading(false); 1170 updateRateLimitUI(r.seconds_remaining || 600); 1171 $('#message').html('<div class="alert alert-danger" role="alert"><i class="fa-solid fa-lock"></i> ' + escapeMsg(r.message) + '</div>'); 1172 return; 1173 } 1174 1175 // 423 â account exists but is disabled. No retry guidance â this 1176 // won't change without admin intervention. Direct them to support. 1177 if(r.status == 423){ 1178 setLoginLoading(false); 1179 $('#message').html('<div class="alert alert-danger" role="alert"><i class="fa-solid fa-ban"></i> ' + escapeMsg(r.message || 'This account is disabled.') + '</div>'); 1180 return; 1181 } 1182 1183 // 403 â credentials OK but email not confirmed yet. Show the resend 1184 // button so the user has a one-click recovery path. 1185 if(r.status == 403 && r.unverified){ 1186 setLoginLoading(false); 1187 const email = r.email || $('#email').val(); 1188 const html = 1189 '<div class="alert alert-warning" role="alert">
1189' + 1190 '<i class="fa-solid fa-envelope-circle-check"></i> ' + escapeMsg(r.message || 'Please confirm your email address to log in.') + 1191 '<div class="mt-3">' + 1192 '<button type="button" class="btn btn-sm btn-warning resend-confirmation-btn" data-email="' + escapeAttr(email) + '">' + 1193 '<i class="fa-solid fa-paper-plane"></i> Resend confirmation email' + 1194 '</button>' + 1195 '</div>' + 1196 '</div>'; 1197 $('#message').html(html); 1198 return; 1199 } 1200 1201 // 200 â success. 1202 if(r.status == 200 || r.status == '200'){ 1203 // Detect a fresh-confirmation login via the URL param that 1204 // app/confirm_account.php sets on its redirect â works after 1205 // confirmation_code is no longer cleared by login itself. 1206 const justConfirmed = new URLSearchParams(location.search).has('confirmed'); 1207 if(justConfirmed){ 1208 if(eld_redirect){ 1209 window.location.href = 'settings.php?eldlogin=motive&eid='+eid+'#eld-section'; 1210 }else{ 1211 window.location.href = 'settings.php?newuser&first_login=1'; 1212 } 1213 }else{ 1214 window.location.href = return_url || 'index.php'; 1215 } 1216 return; 1217 } 1218 1219 // 401 â wrong creds. Surface tries_remaining if the server included it. 1220 setLoginLoading(false); 1221 let msg = r.message || 'Login failed'; 1222 if(typeof r.tries_remaining === 'number'){ 1223 msg += ' â ' + r.tries_remaining + ' ' + (r.tries_remaining === 1 ? 'try' : 'tries') + ' remaining'; 1224 } 1225 $('#message').html('<div class="alert alert-danger" role="alert"><i class="fa-solid fa-circle-exclamation"></i> ' + escapeMsg(msg) + '</div>'); 1226 }).fail(function(){ 1227 setLoginLoading(false); 1228 $('#message').html('<div class="alert alert-danger" role="alert"><i class="fa-solid fa-circle-exclamation"></i> Network error. Please try again.</div>'); 1229 }) 1230 } 1231 }) 1232 1233 // Resend confirmation email â wired only on the alert that the 403/unverified 1234 // branch above injects. Endpoint returns generic 200 even when the email 1235 // wasn't actually pending, so the UI just thanks the user either way; the 1236 // only branch that surfaces a different message is the 5-min throttle (429). 1237 $(document).on('click', '.resend-confirmation-btn', function(e){ 1238 e.preventDefault(); 1239 const $btn = $(this); 1240 const email = $btn.data('email'); 1241 const $icon = $btn.find('i'); 1242 $btn.prop('disabled', true); 1243 $icon.removeClass().addClass('fa-solid fa-circle-notch fa-spin'); 1244 const kibble = atob($('main').attr('data-k')); 1245 $.post('api/_auth.php?'+kibble, { mode: 'resend-confirmation', email: email }, function(response){ 1246 const r = (typeof response === 'object' && response) ? response : {}; 1247 if(r.status == 429){ 1248 $icon.removeClass().addClass('fa-solid fa-clock'); 1249 $btn.prop('disabled', true).removeClass('btn-warning').addClass('btn-secondary'); 1250 $btn.contents().last().replaceWith(' Wait '+ Math.ceil((r.seconds_remaining||300)/60) +' min'); 1251 $('#message').prepend('<div class="alert alert-warning small mb-2" role="alert"><i class="fa-solid fa-hourglass-half"></i> ' + escapeMsg(r.message) + '</div>'); 1252 return; 1253 } 1254 // Both 200 and any unexpected non-429 land here as "we tried" â the 1255 // generic-200 contract from the server means we can't tell the user 1256 // whether the email actually went out without leaking account state. 1257 $icon.removeClass().addClass('fa-solid fa-check'); 1258 $btn.prop('disabled', true).removeClass('btn-warning').addClass('btn-success'); 1259 $btn.contents().last().replaceWith(' Sent â check your inbox'); 1260 }, 'json').fail(function(){ 1261 $icon.removeClass().addClass('fa-solid fa-circle-exclamation'); 1262 $btn.prop('disabled', false).removeClass('btn-warning').addClass('btn-danger'); 1263 $btn.contents().last().replaceWith(' Network error â try again'); 1264 }); 1265 }); 1266 setTimeout(function(){ 1267 var email = $('#email').val(); 1268 if(email.length>0){ 1269 $('#password').focus() 1270 }else{ 1271 $('#email').focus() 1272 } 1273 },250) 1274 }) 1275 1276 </script>
1276 1277</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.